From e4365b746b40d311c59b3305c39adc61dc1a0425 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 28 Jan 2022 18:18:06 +0100 Subject: [PATCH 001/918] Draft for allowing Arnold ROP render publishing to Deadline from Houdini --- .../plugins/create/create_arnold_rop.py | 50 ++++++ .../plugins/publish/collect_arnold_rop.py | 142 ++++++++++++++++++ .../plugins/publish/collect_instances.py | 2 +- .../plugins/publish/increment_current_file.py | 2 +- .../houdini/plugins/publish/save_scene.py | 3 +- .../publish/submit_houdini_render_deadline.py | 24 +-- 6 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_arnold_rop.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py new file mode 100644 index 0000000000..e0fad2a1cc --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -0,0 +1,50 @@ +from avalon import houdini + + +class CreateArnoldRop(houdini.Creator): + """Arnold ROP""" + + label = "Arnold ROP" + family = "arnold_rop" + icon = "magic" + defaults = ["master"] + + def __init__(self, *args, **kwargs): + super(CreateArnoldRop, self).__init__(*args, **kwargs) + + # Clear the family prefix from the subset + subset = self.data["subset"] + subset_no_prefix = subset[len(self.family):] + subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:] + self.data["subset"] = subset_no_prefix + + # Add chunk size attribute + self.data["chunkSize"] = 1 + + # Remove the active, we are checking the bypass flag of the nodes + self.data.pop("active", None) + + self.data.update({"node_type": "arnold"}) + + def process(self): + instance = super(CreateArnoldRop, self).process() + + basename = instance.name() + instance.setName(basename + "_ROP", unique_name=True) + + prefix = '${HIP}/render/${HIPNAME}/`chs("subset")`.$F4.exr' + parms = { + # Render frame range + "trange": 1, + + # Arnold ROP settings + "ar_picture": prefix, + "ar_exr_half_precision": 1 # half precision + } + instance.setParms(parms) + + # Lock some Avalon attributes + to_lock = ["family", "id"] + for name in to_lock: + parm = instance.parm(name) + parm.lock(True) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py new file mode 100644 index 0000000000..e5abfccca0 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -0,0 +1,142 @@ +import re +import os + +import hou +import avalon.io as io +import avalon.api as api +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Arnold ROP Render Products + + Collects the instance.data["files"] for the render products. + + Provides: + instance -> files + + """ + + label = "Arnold ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["arnold_rop"] + + def process(self, instance): + + rop = instance[0] + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + default_prefix = evalParmNoFrame(rop, "ar_picture") + render_products = [] + + # Default beauty AOV + beauty_product = self.get_render_product_name(prefix=default_prefix, + suffix=None) + render_products.append(beauty_product) + + # TODO: Implement AOVS + # num_aovs = rop.evalParm("ar_aovs") + # for index in range(num_aovs): + # i = index + 1 + # + # # Skip disabled AOVs + # if not rop.evalParm("ar_enable_aov%s" % i): + # continue + # + # label = evalParmNoFrame(rop, "ar_aov_label%s" % i) + # if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): + # label = rop.evalParm("ar_aov_exr_layer_name%s" % i) + # + # aov_product = self.get_render_product_name(default_prefix, + # suffix=label) + # render_products.append(aov_product) + + for product in render_products: + self.log.debug("Found render product: %s" % product) + + filenames = list(render_products) + instance.data["files"] = filenames + + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + + def get_render_product_name(self, prefix, suffix): + """Return the output filename using the AOV prefix and suffix""" + + # When AOV is explicitly defined in prefix we just swap it out + # directly with the AOV suffix to embed it. + # Note: ${AOV} seems to be evaluated in the parameter as %AOV% + has_aov_in_prefix = "%AOV%" in prefix + if has_aov_in_prefix: + # It seems that when some special separator characters are present + # before the %AOV% token that Redshift will secretly remove it if + # there is no suffix for the current product, for example: + # foo_%AOV% -> foo.exr + pattern = "%AOV%" if suffix else "[._-]?%AOV%" + product_name = re.sub(pattern, + suffix, + prefix, + flags=re.IGNORECASE) + else: + if suffix: + # Add ".{suffix}" before the extension + prefix_base, ext = os.path.splitext(prefix) + product_name = prefix_base + "." + suffix + ext + else: + product_name = prefix + + return product_name diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 12d118f0cc..094f791a0b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -109,6 +109,6 @@ class CollectInstances(pyblish.api.ContextPlugin): data["frameStart"] = node.evalParm("f1") data["frameEnd"] = node.evalParm("f2") - data["steps"] = node.evalParm("f3") + data["byFrameStep"] = node.evalParm("f3") return data diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 31c2954ee7..7a1ad09249 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -15,7 +15,7 @@ class IncrementCurrentFile(pyblish.api.InstancePlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["houdini"] - families = ["colorbleed.usdrender", "redshift_rop"] + families = ["colorbleed.usdrender", "redshift_rop", "arnold_rop"] targets = ["local"] def process(self, instance): diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index 1b12efa603..75ff4dcdfd 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -9,7 +9,8 @@ class SaveCurrentScene(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder - 0.49 hosts = ["houdini"] families = ["usdrender", - "redshift_rop"] + "redshift_rop", + "arnold_rop"] targets = ["local"] def process(self, instance): diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py index fa146c0d30..9613bae7fe 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -27,13 +27,12 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder hosts = ["houdini"] families = ["usdrender", - "redshift_rop"] - targets = ["local"] + "redshift_rop", + "arnold_rop"] def process(self, instance): context = instance.context - code = context.data["code"] filepath = context.data["currentFile"] filename = os.path.basename(filepath) comment = context.data.get("comment", "") @@ -42,16 +41,14 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): # Support code prefix label for batch name batch_name = filename - if code: - batch_name = "{0} - {1}".format(code, batch_name) # Output driver to render driver = instance[0] # StartFrame to EndFrame by byFrameStep frames = "{start}-{end}x{step}".format( - start=int(instance.data["startFrame"]), - end=int(instance.data["endFrame"]), + start=int(instance.data["frameStart"]), + end=int(instance.data["frameEnd"]), step=int(instance.data["byFrameStep"]), ) @@ -71,7 +68,7 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): "UserName": deadline_user, "Plugin": "Houdini", - "Pool": "houdini_redshift", # todo: remove hardcoded pool + #"Pool": "houdini", # todo(roy): Add pool options "Frames": frames, "ChunkSize": instance.data.get("chunkSize", 10), @@ -136,9 +133,12 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): def submit(self, instance, payload): - AVALON_DEADLINE = api.Session.get("AVALON_DEADLINE", - "http://localhost:8082") - assert AVALON_DEADLINE, "Requires AVALON_DEADLINE" + # get default deadline webservice url from deadline module + deadline_url = instance.context.data.get("defaultDeadline") + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + deadline_url = instance.data.get("deadlineUrl") + assert deadline_url, "Requires Deadline Webservice URL" plugin = payload["JobInfo"]["Plugin"] self.log.info("Using Render Plugin : {}".format(plugin)) @@ -147,7 +147,7 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(AVALON_DEADLINE) + url = "{}/api/jobs".format(deadline_url) response = requests.post(url, json=payload) if not response.ok: raise Exception(response.text) From 553518064efea3af8241a46164600d45b77cb3a7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 28 Jan 2022 18:28:14 +0100 Subject: [PATCH 002/918] Shush the hound --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 +--- .../plugins/publish/submit_houdini_render_deadline.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index e5abfccca0..93849502be 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -2,14 +2,12 @@ import re import os import hou -import avalon.io as io -import avalon.api as api import pyblish.api def get_top_referenced_parm(parm): - processed = set() # disallow infinite loop + processed = set() # disallow infinite loop while True: if parm.path() in processed: raise RuntimeError("Parameter references result in cycle.") diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 9613bae7fe..996c5a5d46 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -68,7 +68,7 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): "UserName": deadline_user, "Plugin": "Houdini", - #"Pool": "houdini", # todo(roy): Add pool options + # "Pool": "houdini", # todo(roy): Add pool options "Frames": frames, "ChunkSize": instance.data.get("chunkSize", 10), From 976c708d59ecb860c7b2eccd270f99cb6e0fe1ae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:11:09 +0200 Subject: [PATCH 003/918] Refactor to new houdini Creator --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index e0fad2a1cc..f9b5f0c53c 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,7 +1,7 @@ -from avalon import houdini +from openpype.hosts.houdini.api import plugin -class CreateArnoldRop(houdini.Creator): +class CreateArnoldRop(plugin.Creator): """Arnold ROP""" label = "Arnold ROP" @@ -26,8 +26,7 @@ class CreateArnoldRop(houdini.Creator): self.data.update({"node_type": "arnold"}) - def process(self): - instance = super(CreateArnoldRop, self).process() + def _process(self, instance): basename = instance.name() instance.setName(basename + "_ROP", unique_name=True) From 8bedec713d8d6e81bd6df43122ff6f8a780666e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:11:30 +0200 Subject: [PATCH 004/918] Enable AOVs + fix collected files for publishing --- .../plugins/publish/collect_arnold_rop.py | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 93849502be..dfb8f0d5ee 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -86,22 +86,28 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): suffix=None) render_products.append(beauty_product) - # TODO: Implement AOVS - # num_aovs = rop.evalParm("ar_aovs") - # for index in range(num_aovs): - # i = index + 1 - # - # # Skip disabled AOVs - # if not rop.evalParm("ar_enable_aov%s" % i): - # continue - # - # label = evalParmNoFrame(rop, "ar_aov_label%s" % i) - # if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): - # label = rop.evalParm("ar_aov_exr_layer_name%s" % i) - # - # aov_product = self.get_render_product_name(default_prefix, - # suffix=label) - # render_products.append(aov_product) + files_by_aov = { + "": self.generate_expected_files(instance, beauty_product) + } + + num_aovs = rop.evalParm("ar_aovs") + for index in range(num_aovs): + i = index + 1 + + # Skip disabled AOVs + if not rop.evalParm("ar_enable_aov%s" % i): + continue + + if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): + label = rop.evalParm("ar_aov_exr_layer_name%s" % i) + else: + label = evalParmNoFrame(rop, "ar_aov_label%s" % i) + + aov_product = self.get_render_product_name(default_prefix, + suffix=label) + render_products.append(aov_product) + files_by_aov[label] = self.generate_expected_files(instance, + aov_product) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -111,6 +117,11 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -138,3 +149,26 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): product_name = prefix return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files From 3f59f39847158975fed9467275729ba516507ce6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:16:23 +0200 Subject: [PATCH 005/918] Refactor submit deadline to use `AbstractSubmitDeadline` class --- .../publish/submit_houdini_render_deadline.py | 175 +++++++----------- 1 file changed, 71 insertions(+), 104 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 3cacc8dd16..8aabec6e30 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -1,16 +1,24 @@ -import os -import json -import getpass - -import requests -import pyblish.api - import hou +import os +import attr +import getpass +import pyblish.api + from openpype.pipeline import legacy_io +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo -class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): +@attr.s +class DeadlinePluginInfo(): + SceneFile = attr.ib(default=None) + OutputDriver = attr.ib(default=None) + Version = attr.ib(default=None) + IgnoreInputs = attr.ib(default=True) + + +class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): """Submit Solaris USD Render ROPs to Deadline. Renders are submitted to a Deadline Web Service as @@ -30,136 +38,95 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): "redshift_rop", "arnold_rop"] targets = ["local"] + use_published = True - def process(self, instance): + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="Houdini") + instance = self._instance context = instance.context + filepath = context.data["currentFile"] filename = os.path.basename(filepath) - comment = context.data.get("comment", "") - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - jobname = "%s - %s" % (filename, instance.name) - # Support code prefix label for batch name - batch_name = filename + job_info.Name = "%s - %s" % (filename, instance.name) + job_info.BatchName = filename + job_info.Plugin = "Houdini" + job_info.UserName = context.data.get( + "deadlineUser", getpass.getuser()) - # Output driver to render - driver = instance[0] - - # StartFrame to EndFrame by byFrameStep + # Deadline requires integers in frame range frames = "{start}-{end}x{step}".format( start=int(instance.data["frameStart"]), end=int(instance.data["frameEnd"]), step=int(instance.data["byFrameStep"]), ) + job_info.Frames = frames - # Documentation for keys available at: - # https://docs.thinkboxsoftware.com - # /products/deadline/8.0/1_User%20Manual/manual - # /manual-submission.html#job-info-file-options - payload = { - "JobInfo": { - # Top-level group name - "BatchName": batch_name, + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.ChunkSize = instance.data.get("chunkSize", 10) + job_info.Comment = context.data.get("comment") - # Job name, as seen in Monitor - "Name": jobname, - - # Arbitrary username, for visualisation in Monitor - "UserName": deadline_user, - - "Plugin": "Houdini", - "Pool": instance.data.get("primaryPool"), - "secondaryPool": instance.data.get("secondaryPool"), - "Frames": frames, - - "ChunkSize": instance.data.get("chunkSize", 10), - - "Comment": comment - }, - "PluginInfo": { - # Input - "SceneFile": filepath, - "OutputDriver": driver.path(), - - # Mandatory for Deadline - # Houdini version without patch number - "Version": hou.applicationVersionString().rsplit(".", 1)[0], - - "IgnoreInputs": True - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Include critical environment variables with submission + api.Session keys = [ - # Submit along the current Avalon tool setup that we launched - # this application with so the Render Slave can build its own - # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" - "AVALON_TOOLS", + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", + "OPENPYPE_SG_USER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV", + "OPENPYPE_LOG_NO_COLORS", "OPENPYPE_VERSION" ] # Add mongo url if it's enabled - if context.data.get("deadlinePassMongoUrl"): + if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) + for key in keys: + val = environment.get(key) + if val: + job_info.EnvironmentKeyValue = "{key}={value}".format( + key=key, + value=val) + # to recognize job from PYPE for turning Event On/Off + job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1" - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - # Include OutputFilename entries - # The first entry also enables double-click to preview rendered - # frames from Deadline Monitor - output_data = {} for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) fname = os.path.basename(filepath) - output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/") - output_data["OutputFilename%d" % i] = fname + job_info.OutputDirectory = dirname.replace("\\", "/") + job_info.OutputFilename = fname - # For now ensure destination folder exists otherwise HUSK - # will fail to render the output image. This is supposedly fixed - # in new production builds of Houdini - # TODO Remove this workaround with Houdini 18.0.391+ - if not os.path.exists(dirname): - self.log.info("Ensuring output directory exists: %s" % - dirname) - os.makedirs(dirname) + return job_info - payload["JobInfo"].update(output_data) + def get_plugin_info(self): - self.submit(instance, payload) + instance = self._instance + context = instance.context - def submit(self, instance, payload): + # Output driver to render + driver = instance[0] + hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] - # get default deadline webservice url from deadline module - deadline_url = instance.context.data.get("defaultDeadline") - # if custom one is set in instance, use that - if instance.data.get("deadlineUrl"): - deadline_url = instance.data.get("deadlineUrl") - assert deadline_url, "Requires Deadline Webservice URL" + plugin_info = DeadlinePluginInfo( + SceneFile=context.data["currentFile"], + OutputDriver=driver.path(), + Version=hou_major_minor, + IgnoreInputs=True + ) - plugin = payload["JobInfo"]["Plugin"] - self.log.info("Using Render Plugin : {}".format(plugin)) + return attr.asdict(plugin_info) - self.log.info("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(deadline_url) - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) + def process(self, instance): + super(HoudiniSubmitDeadline, self).process(instance) + # TODO: Avoid the need for this logic here, needed for submit publish # Store output dir for unified publisher (filesequence) output_dir = os.path.dirname(instance.data["files"][0]) instance.data["outputDir"] = output_dir - instance.data["deadlineSubmissionJob"] = response.json() + instance.data["toBeRenderedOn"] = "deadline" From 2b6ef60b654e383f9c17ec843747c30b3c1f0a76 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:16:37 +0200 Subject: [PATCH 006/918] Submit publish job for houdini arnold rop --- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 379953c9e4..552aa253f2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -116,10 +116,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): deadline_plugin = "OpenPype" targets = ["local"] - hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] + hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony", + "houdini"] families = ["render.farm", "prerender.farm", - "renderlayer", "imagesequence", "vrayscene"] + "renderlayer", "imagesequence", "vrayscene", + "arnold_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE From 58b524407b5035f592773004c312f970ca374bf1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:18:43 +0200 Subject: [PATCH 007/918] Fix style issue / shush hound --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 ++-- .../plugins/publish/submit_houdini_render_deadline.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index dfb8f0d5ee..d90530d3a0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -107,7 +107,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): suffix=label) render_products.append(aov_product) files_by_aov[label] = self.generate_expected_files(instance, - aov_product) + aov_product) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -120,7 +120,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] # stub required data if "expectedFiles" not in instance.data: - instance.data["expectedFiles"] = list() + instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) def get_render_product_name(self, prefix, suffix): diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 8aabec6e30..6b6eebba7e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -91,8 +91,9 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): val = environment.get(key) if val: job_info.EnvironmentKeyValue = "{key}={value}".format( - key=key, - value=val) + key=key, + value=val + ) # to recognize job from PYPE for turning Event On/Off job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1" From 2fdd13738066f320359a2cf7d84407bcd93e8fbd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 28 Nov 2022 09:40:42 +0000 Subject: [PATCH 008/918] Optional control of display lights on playblast. --- openpype/hosts/maya/plugins/create/create_review.py | 2 ++ openpype/hosts/maya/plugins/publish/extract_playblast.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index ba51ffa009..65aeb2d76a 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -25,6 +25,7 @@ class CreateReview(plugin.Creator): "depth peeling", "alpha cut" ] + displayLights = ["default", "all", "selected", "active", "none"] def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -41,5 +42,6 @@ class CreateReview(plugin.Creator): data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency + data["displayLights"] = self.displayLights self.data = data diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index b19d24fad7..cbf99eccaa 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -97,6 +97,10 @@ class ExtractPlayblast(publish.Extractor): refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True)) pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + + # Show lighting mode. + index = instance.data.get("displayLights", 0) + preset["viewport_options"]["displayLights"] = self.displayLights[index] # Override transparency if requested. transparency = instance.data.get("transparency", 0) From e214062047f09e6b0879cb015e6445b889b0d33a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 2 Dec 2022 12:19:57 +0000 Subject: [PATCH 009/918] Missing class data. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index cbf99eccaa..c6bbe44efc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -23,6 +23,7 @@ class ExtractPlayblast(publish.Extractor): families = ["review"] optional = True capture_preset = {} + displayLights = ["default", "all", "selected", "active", "none"] def process(self, instance): self.log.info("Extracting capture..") @@ -97,7 +98,7 @@ class ExtractPlayblast(publish.Extractor): refreshFrameInt = int(pm.playbackOptions(q=True, minTime=True)) pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) - + # Show lighting mode. index = instance.data.get("displayLights", 0) preset["viewport_options"]["displayLights"] = self.displayLights[index] From 6bc8748b99e58894479071e14b04780a3da9cd15 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 14 Dec 2022 09:18:45 +0000 Subject: [PATCH 010/918] Collect display lights list in lib. --- openpype/hosts/maya/api/lib.py | 2 ++ openpype/hosts/maya/plugins/create/create_review.py | 3 +-- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2530021eba..617e4e3d3a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -113,6 +113,8 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] +DISPLAY_LIGHTS = ["default", "all", "selected", "active", "none"] + def get_main_window(): """Acquire Maya's main window""" diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 65aeb2d76a..1935d18deb 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -25,7 +25,6 @@ class CreateReview(plugin.Creator): "depth peeling", "alpha cut" ] - displayLights = ["default", "all", "selected", "active", "none"] def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -42,6 +41,6 @@ class CreateReview(plugin.Creator): data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency - data["displayLights"] = self.displayLights + data["displayLights"] = lib.DISPLAY_LIGHTS self.data = data diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index c6bbe44efc..d8e1184335 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -23,7 +23,6 @@ class ExtractPlayblast(publish.Extractor): families = ["review"] optional = True capture_preset = {} - displayLights = ["default", "all", "selected", "active", "none"] def process(self, instance): self.log.info("Extracting capture..") @@ -101,7 +100,7 @@ class ExtractPlayblast(publish.Extractor): # Show lighting mode. index = instance.data.get("displayLights", 0) - preset["viewport_options"]["displayLights"] = self.displayLights[index] + preset["viewport_options"]["displayLights"] = lib.DISPLAY_LIGHTS[index] # Override transparency if requested. transparency = instance.data.get("transparency", 0) From c921bc14c56ec3780981e6a432a26bc32fd84235 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 15 Dec 2022 08:57:22 +0000 Subject: [PATCH 011/918] Convert enum to string in collector --- openpype/hosts/maya/plugins/publish/collect_review.py | 5 +++++ openpype/hosts/maya/plugins/publish/extract_playblast.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index eb872c2935..995bd23687 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -5,6 +5,7 @@ import pyblish.api from openpype.client import get_subset_by_name from openpype.pipeline import legacy_io +from openpype.hosts.maya.api import lib class CollectReview(pyblish.api.InstancePlugin): @@ -139,3 +140,7 @@ class CollectReview(pyblish.api.InstancePlugin): "filename": node.filename.get() } ) + + # Convert enum attribute to string. + index = instance.data.get("displayLights", 0) + instance.data["displayLights"] = lib.DISPLAY_LIGHTS[index] diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index d8e1184335..08eb754c6d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -99,8 +99,8 @@ class ExtractPlayblast(publish.Extractor): pm.currentTime(refreshFrameInt, edit=True) # Show lighting mode. - index = instance.data.get("displayLights", 0) - preset["viewport_options"]["displayLights"] = lib.DISPLAY_LIGHTS[index] + display_lights = instance.data["displayLights"] + preset["viewport_options"]["displayLights"] = display_lights # Override transparency if requested. transparency = instance.data.get("transparency", 0) From 9284e986d1f3d3131bb4b4cce1a2808cad141bb0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 15 Dec 2022 08:57:36 +0000 Subject: [PATCH 012/918] Use display lights in thumbnail --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 712159c2be..bb9cef2c5c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -105,6 +105,10 @@ class ExtractThumbnail(publish.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) + # Show lighting mode. + display_lights = instance.data["displayLights"] + preset["viewport_options"]["displayLights"] = display_lights + # Isolate view is requested by having objects in the set besides a # camera. if preset.pop("isolate_view", False) and instance.data.get("isolate"): From 67b95c218b51e5c87132e5270d0a7c47760ed7e5 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 15 Dec 2022 13:13:48 +0000 Subject: [PATCH 013/918] Update openpype/hosts/maya/plugins/publish/collect_review.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 995bd23687..d15eb7a12b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -141,6 +141,6 @@ class CollectReview(pyblish.api.InstancePlugin): } ) - # Convert enum attribute to string. + # Convert enum attribute index to string. index = instance.data.get("displayLights", 0) instance.data["displayLights"] = lib.DISPLAY_LIGHTS[index] From 9a6dc109254ab4d1aca99f4abd3464d65b4e46c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 03:02:31 +0100 Subject: [PATCH 014/918] Initial draft for Substance Painter integration --- openpype/hosts/substancepainter/__init__.py | 10 + openpype/hosts/substancepainter/addon.py | 34 +++ .../hosts/substancepainter/api/__init__.py | 8 + .../hosts/substancepainter/api/pipeline.py | 234 ++++++++++++++++++ .../deploy/plugins/openpype_plugin.py | 15 ++ .../resources/app_icons/substancepainter.png | Bin 0 -> 107059 bytes .../system_settings/applications.json | 27 ++ openpype/settings/entities/enum_entity.py | 1 + .../schema_substancepainter.json | 40 +++ .../system_schema/schema_applications.json | 4 + 10 files changed, 373 insertions(+) create mode 100644 openpype/hosts/substancepainter/__init__.py create mode 100644 openpype/hosts/substancepainter/addon.py create mode 100644 openpype/hosts/substancepainter/api/__init__.py create mode 100644 openpype/hosts/substancepainter/api/pipeline.py create mode 100644 openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py create mode 100644 openpype/resources/app_icons/substancepainter.png create mode 100644 openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json diff --git a/openpype/hosts/substancepainter/__init__.py b/openpype/hosts/substancepainter/__init__.py new file mode 100644 index 0000000000..4c33b9f507 --- /dev/null +++ b/openpype/hosts/substancepainter/__init__.py @@ -0,0 +1,10 @@ +from .addon import ( + SubstanceAddon, + SUBSTANCE_HOST_DIR, +) + + +__all__ = ( + "SubstanceAddon", + "SUBSTANCE_HOST_DIR" +) diff --git a/openpype/hosts/substancepainter/addon.py b/openpype/hosts/substancepainter/addon.py new file mode 100644 index 0000000000..bb55f20189 --- /dev/null +++ b/openpype/hosts/substancepainter/addon.py @@ -0,0 +1,34 @@ +import os +from openpype.modules import OpenPypeModule, IHostAddon + +SUBSTANCE_HOST_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class SubstanceAddon(OpenPypeModule, IHostAddon): + name = "substancepainter" + host_name = "substancepainter" + + def initialize(self, module_settings): + self.enabled = True + + def add_implementation_envs(self, env, _app): + # Add requirements to SUBSTANCE_PAINTER_PLUGINS_PATH + plugin_path = os.path.join(SUBSTANCE_HOST_DIR, "deploy") + plugin_path = plugin_path.replace("\\", "/") + if env.get("SUBSTANCE_PAINTER_PLUGINS_PATH"): + plugin_path += os.pathsep + env["SUBSTANCE_PAINTER_PLUGINS_PATH"] + + env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path + + # Fix UI scale issue + env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(SUBSTANCE_HOST_DIR, "hooks") + ] + + def get_workfile_extensions(self): + return [".spp", ".toc"] diff --git a/openpype/hosts/substancepainter/api/__init__.py b/openpype/hosts/substancepainter/api/__init__.py new file mode 100644 index 0000000000..937d0c429e --- /dev/null +++ b/openpype/hosts/substancepainter/api/__init__.py @@ -0,0 +1,8 @@ +from .pipeline import ( + SubstanceHost, + +) + +__all__ = [ + "SubstanceHost", +] diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py new file mode 100644 index 0000000000..3fd081ca1c --- /dev/null +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +"""Pipeline tools for OpenPype Gaffer integration.""" +import os +import sys +import logging +from functools import partial + +# Substance 3D Painter modules +import substance_painter.ui +import substance_painter.event +import substance_painter.export +import substance_painter.project +import substance_painter.textureset + +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost + +import pyblish.api + +from openpype.pipeline import ( + register_creator_plugin_path, + register_loader_plugin_path, + AVALON_CONTAINER_ID +) +from openpype.lib import ( + register_event_callback, + emit_event, +) +from openpype.pipeline.load import any_outdated_containers +from openpype.hosts.substancepainter import SUBSTANCE_HOST_DIR + +log = logging.getLogger("openpype.hosts.substance") + +PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + +self = sys.modules[__name__] +self.menu = None +self.callbacks = [] + + +class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): + name = "substancepainter" + + def __init__(self): + super(SubstanceHost, self).__init__() + self._has_been_setup = False + + def install(self): + pyblish.api.register_host("substancepainter") + + pyblish.api.register_plugin_path(PUBLISH_PATH) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + + log.info("Installing callbacks ... ") + # register_event_callback("init", on_init) + _register_callbacks() + # register_event_callback("before.save", before_save) + # register_event_callback("save", on_save) + register_event_callback("open", on_open) + # register_event_callback("new", on_new) + + log.info("Installing menu ... ") + _install_menu() + + self._has_been_setup = True + + def uninstall(self): + _uninstall_menu() + _deregister_callbacks() + + def has_unsaved_changes(self): + + if not substance_painter.project.is_open(): + return False + + return substance_painter.project.needs_saving() + + def get_workfile_extensions(self): + return [".spp", ".toc"] + + def save_workfile(self, dst_path=None): + + if not substance_painter.project.is_open(): + return False + + if not dst_path: + dst_path = self.get_current_workfile() + + full_save_mode = substance_painter.project.ProjectSaveMode.Full + substance_painter.project.save_as(dst_path, full_save_mode) + + return dst_path + + def open_workfile(self, filepath): + + if not os.path.exists(filepath): + raise RuntimeError("File does not exist: {}".format(filepath)) + + # We must first explicitly close current project before opening another + if substance_painter.project.is_open(): + substance_painter.project.close() + + substance_painter.project.open(filepath) + return filepath + + def get_current_workfile(self): + if not substance_painter.project.is_open(): + return None + + filepath = substance_painter.project.file_path() + if filepath.endswith(".spt"): + # When currently in a Substance Painter template assume our + # scene isn't saved. This can be the case directly after doing + # "New project", the path will then be the template used. This + # avoids Workfiles tool trying to save as .spt extension if the + # file hasn't been saved before. + return + + return filepath + + def get_containers(self): + return [] + + @staticmethod + def create_context_node(): + pass + + def update_context_data(self, data, changes): + pass + + def get_context_data(self): + pass + + +def _install_menu(): + from PySide2 import QtWidgets + from openpype.tools.utils import host_tools + + parent = substance_painter.ui.get_main_window() + + menu = QtWidgets.QMenu("OpenPype") + + action = menu.addAction("Load...") + action.triggered.connect( + lambda: host_tools.show_loader(parent=parent, use_context=True) + ) + + action = menu.addAction("Publish...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent) + ) + + action = menu.addAction("Manage...") + action.triggered.connect( + lambda: host_tools.show_scene_inventory(parent=parent) + ) + + action = menu.addAction("Library...") + action.triggered.connect( + lambda: host_tools.show_library_loader(parent=parent) + ) + + menu.addSeparator() + action = menu.addAction("Work Files...") + action.triggered.connect( + lambda: host_tools.show_workfiles(parent=parent) + ) + + substance_painter.ui.add_menu(menu) + + def on_menu_destroyed(): + self.menu = None + + menu.destroyed.connect(on_menu_destroyed) + + self.menu = menu + + +def _uninstall_menu(): + if self.menu: + self.menu.destroy() + self.menu = None + + +def _register_callbacks(): + # Prepare emit event callbacks + open_callback = partial(emit_event, "open") + + # Connect to the Substance Painter events + dispatcher = substance_painter.event.DISPATCHER + for event, callback in [ + (substance_painter.event.ProjectOpened, open_callback) + ]: + dispatcher.connect(event, callback) + # Keep a reference so we can deregister if needed + self.callbacks.append((event, callback)) + + +def _deregister_callbacks(): + for event, callback in self.callbacks: + substance_painter.event.DISPATCHER.disconnect(event, callback) + + +def on_open(): + log.info("Running callback on open..") + print("Run") + + if any_outdated_containers(): + from openpype.widgets import popup + + log.warning("Scene has outdated content.") + + # Get main window + parent = substance_painter.ui.get_main_window() + if parent is None: + log.info("Skipping outdated content pop-up " + "because Substance window can't be found.") + else: + + # Show outdated pop-up + def _on_show_inventory(): + from openpype.tools.utils import host_tools + host_tools.show_scene_inventory(parent=parent) + + dialog = popup.Popup(parent=parent) + dialog.setWindowTitle("Substance scene has outdated content") + dialog.setMessage("There are outdated containers in " + "your Substance scene.") + dialog.on_clicked.connect(_on_show_inventory) + dialog.show() \ No newline at end of file diff --git a/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py new file mode 100644 index 0000000000..01779156f1 --- /dev/null +++ b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py @@ -0,0 +1,15 @@ + +def start_plugin(): + from openpype.pipeline import install_host + from openpype.hosts.substancepainter.api import SubstanceHost + + install_host(SubstanceHost()) + + +def close_plugin(): + from openpype.pipeline import uninstall_host + uninstall_host() + + +if __name__ == "__main__": + start_plugin() diff --git a/openpype/resources/app_icons/substancepainter.png b/openpype/resources/app_icons/substancepainter.png new file mode 100644 index 0000000000000000000000000000000000000000..dc46f25d747f6626fd085818bbc593636898eecf GIT binary patch literal 107059 zcmeFZ_g|CA_dbm4s%rstZ6Hk)5d{G$BHbE^AQ&-JDN653m5#E2t`U?fT?~Q*kPgyO zun?Mb=_*FLNbm4HbCbBAzu|e_><{~T-P6vTIpw;}Onh)fezoSS z)+w!>1KR^So?md-oy<99_U_-{zpJ$BJF4)17JstCKQ@~6e{RTj*2XO+TR6R~W|3QQ zW@Tx@-84>cJqY!`zyE3Ae;W9o2L7jk|L(5qGoNnVesgem-N?){b~SIWNoo0i(>fZMo_cP)wK;%Wk!Qu;;=`&k&&ny^J6>Bd+fL6W?bw%cr6>Aq zu~2T`hpT!^M2inwukzPXCT{A&6r|O|^eMehv_?hg6c`@!qj2h9%dgHS@_35+&T${D zlw;=1ok}T5=Cn;a=VEOUK9<%wEk*anNL^)!ve+%0OgWLx~5*0PoROPPT`CcbQsj{r^PTWHS}DWv zQV_pLDW1~?y%sh#CC%;rgFYvRUkv-n7;RAH3#Z~19%@#er0<+>q3+Tfqy+g0hK$<{ zby6I!w+QLS!Xf#-u5YM%M(on+r|s!}WZapwhu-Z#br!cZf9&IlJLX$%`}YOK555lx z*ZCn6LzI6*?^6C5uO=3=n;Mv~WA+p;TZfbS=-kZDsi~v#4U8U&!^Z9BE%x*$>?r$c zW|XiHcdSyNh)2FEiG$vQ^=O&XuZiOq4<737+5PQe%FH0U#}Hln-tn!~S$oh4^LX|W zK5FI)`_2veiipv4=!2QIh3bV}PAKjQIy-*u;sL|%^^P(t%b5Do7+p5%Nmlmad3wGK zoZCLXOR>#LKY#E+DGx)y@?@Q;lL=1q5xc*6`g2L2b|#`dbkrW1d(W7m+U7=}7a%IM~Wh+J8+>-&anrv2&t_VA^qQ-) zHuqj(Onigr;;ypgcKEri%qH)`_a!EIHAyn8W-e4OkM?x;x46U|vr?}u zp2v6YsQ=&l-~3GJcsI5fMt83AzdBkXJ>0j9^KJTj|JNB}ygS)ehQ1`}@hH2gsuGWl zBWrvnf1t*6mepHHZhE8N^%i*#!!4OkJL1PFOCQ~nvZ)N+mOs`u+XLfPDtG?#KKq+~ zp`~P;!Djk6gOAAPw~Uu@Dwx%H&C`bSRaN7Rb~0KO9VWlcLj(-qO8BhA(~|+Q$e4}M zSl*I0SG@b1M*wHSYMUfS8U1@XlM+Pk0KJ6uPJMZ;Y)5nu1wntTB1dc!zVLp3f08Kj#kD=nLTDHITjK( zVBEfCD`S7v3Dk#-wBtXujYL? zr-)xgna8ub;a+nfbux`1q2OI)!JJe}P60)a;gw^7^_fESxjtn`SEg_tQ}h@+g2K{U z`sdT~&&x$KUk*mEX8@!^C6Dcz?k51X}8xjOCy70sddZ1_uq*?)Ta`9AG}5 zo^^@-{nmZTj&ujB1I)_K&XUnRS;~y>JBUwR&TP3|X1Lz`7G>g_Wr?89jd;fDOehsO zY{?w{bb5dB&<^1&;F#FoEvBQDUJeEan62D~9=+pJ+Ccxl{mI*r_La&f;=eqcOPjQe zGwI*IoI!yk`fMua9z`Gh&=1a<4F9}yWeaT+=WfM{$s>Vz{TFNs=p$I&wyM#h6>jq~ z>$YhL(?7n~wzaV_-b0Pz+^JZh!TE2v)(J+7I_EcEmcJ6>t=rxj&9f>Y3JibcpW91q zns%w(S8$d+z&x2=T3ME9vev{zLV4sA$78?JO?m$9W(?P6WpBAQKLxS*;`&C#DPjs^ zCQUHO^IP8Kb8b~6t3lSb?WWJe>T)zt_%{E6N80bUyCE}R81n??|93Piw&Xgq=cx;o6MAfKzD^61vTL z;E}s?m%N-u&^x+885oTS{R_KuKDFgBuIyOl!!$0sP=^lDXi=PhDjGKZd?yjp5=OT( z?|4aYyGuOk0qYV@924#_T4rp*)v;*loIg|J=iJY_Me~1d{cEkE{L>KH_W0h5OpT}1 zV{JFkMP=1?or2Hy@3g{ptWDt^-&8BPhim!83E{xF zd_Vev+hPFB{rV;R$YaqN41OnL-u);%g`6#}CYL2N3-uW8`Q6|sQ`+NGmau7Y9>tYv zhJaTTU6ukqc;d^B1*Yea7_%3n^m48&!GCJcFr#O=5|3Wr6aB_>9dU1KT({B#WO;ey zn1lL@D_w)oR57I@@cTOjl3U+U|1 zxMOVQwea9+V*H)BT!u=%B*>OFR+J|AU2>Oj@s;f;k8P2%zy^q(A z0r|j%AeNQ>fN!DdyXYFcOXz=Bz%_N4^X67SW*O0pUH$_Znrref=R|sG&c98^kI=tY zzX~_rb+q!;2Y0-`-nh^(L&mHW?c1$~N)swZaR2Aq#2icDTo6mPepg=Uq}f(_q%6<6 z#M3jdQ@`=6y8ndie_sb*b-SP4ay$JZ%`1V-nFyT46yjUDDRCl6EMYo!yGV9V#JkS~7$Q zO3}`alIv2ErrKmBGUh0du6!ul0pDhXlvTKF33N-*yqe6Z@S{Pmu|Yl8L{jG(y?YoS z;!RwmhdHes@9)$8{j~-m)uWaAcXGlVn*D`c#&4(mUNrgK{I%^fK?@T}0cTyZiNB>x zxDq?|M(=7|M-%V~b1lZYe>>jSa7ztuPDgR%#aH^&+dAe_Jn}d4Nj#)HKent2W1#?~c>}JY-AToS^YSE<$iO*@E%2hZ(OLHh^x3os@y zbW*-7*VR1adGMqSN~xNwQ=MBdJfIhjJZaKH!IVM|4&bu(vN6#VJ!pX ze0sW@s^JFxt|;VY(f1^tX!D^B`?lz+mG`!hv8nsgm0RWNlq@J%==&{xbNro@4=(u2 zxoM8?tHS>K>{8qXYb6e;sK(tiXQ=x;kgYqg+%<)cE3$9?@6;*S)PPqFe|P?;Vjuo; zo_$54+Hpo+d!#rzQGzy04Ba;@`1|Y2$%!q2lR=Rd0ETjM82dQrJ7wBf7)-$N}g<& z;2JnAG4)WGJL~s4iA7G6o6+ayqW`*H;#p~i@f$tIX=nelwxJzm{cF}XvFHCD&U)W^hPtmXF3XQG$n^ImOD)FpI}CvlfJ3^7g+}LOyW@IxWXO^ zV=W`(Gk_X-xm9xOcl7Z%Me68paDH4>akBp5=k1zfJN@ms=!9|d}hFEe7<-lLW5vJh_@ zDaEInf*30bUgckX+gFls$Z8DbU?s+)SuZEPDF@C51!Q<-Kc@>V7^Rb#6nL4ZXLOx@ z*H@fXf6vpzvr3oE~)Ui2`e z?L!(XAGH~?+{(+<{s63JB13p^CyTyNb{Jg++Y)wvQ*il=pu*dlkQWTu54;d@%lqJg z-!zvS?(!ep0HF1+v~bWSPyF_@E#o(TH=IuC9Iuz1P;ymVbh7#EK=EbF^Ni?{3DuNd zfI0e*q_uwoohyn^e8|*qOlQ%b_;&sOAfGw8IefTw06Sl7e zQDjRijxqLi4K(tzL4W_EX9M#%elMLeddFY29$8Wc2;lE!PO=P-%lr-LUPuee{~Xuf z--{t52JE_WNllD1aJBKXQ}7;!=r3Os&n2#BGGC&cb&uch-&b&`Lrmr)9UV}G+4~zm zhMyg`2535WEx*rFkimx!#cvt+1sI!|>{dNBmH8XtKKWFg_25{Dq|9HB-#;sM&u(F0 z~3`iO~VTF!z3A=!B^(s|$b)Sk@;rq-}Y}t@9goUya$1I!+?>Moge&*|1DqP~U>_HnQ%R$O<9 zkYGq7NaQK~^wnEDy2a%Q>Y^h`pXj~1MR=V3`2);fzbf(9XB#lY$z7{6^o%|}y`Mr3 zw@lkdZ!yveo4Au~_=mY!;c(t`%n=<1IsTYs1$>U^KPR_oL!-#G(mD&eQ03`(s6Qp_ z7_+~2fZ0JG;J`oc=mzBNugFvS?#5m5jP4i!-2ah8GE^+D-j_Tj@_1v0Q*rqv##Dy} zYy>-3XRH5aVNN)cyk-0$6g0|Lvz%AYIK`OW_>j(9owvL16t4=0*6p^5lKO&)me|?r zy5&U5-dy5-zh{km_nLID?c9hvZJu(4dcQ8m$4sd~;q-+~+70Xeog+{NuWY;=f9+F9 zzH~3V7=*&MN_`pGd=dbv&Dh+ zYZ7`7i~RdzjQ!-}u*<47Q?9cGq72C$S@X4raRkqb^1BYzML`X{!xXwvGf5hlJTEvx zjuyLfpt}%9oDYA+aJ*l~NR*W}qmX{)1B#MmH$oI5xwf4w{7vyw#7Ps|xA7=S7A;hL z;O-bdsd0dQN_QS*sd>ITaL=KsP*$g<_Y0M72uu&ZSS>y&5~H%M-`sflC1rcz5Pd}X z!TsvfJ{A1o+xQffI&N#Q)4yx}!Dj4?bLqKtFK6!2b6MBC?$V0Tjr7K_k1v~wJ0IW6 z8K|wr74+`;{r$5DRh9MHd-G^=DsevtKIJmQC}qp1rV9l8Zkh<1uxe z&(?9arkbskf4MhBDT-`x_fMhI5~i*{OgjU+VhAhEOg;{M%resow;8O@TW8>Ak(VuQ zKI6>B!Hs@;&sW$q7$p@X zXx}bWLYnyJ#-?PFf5TGbkrIYtx7d|ZQb-f8-)u@79a2qb2?+G*;z5`~lr zEuTHwKbFS`Exq5^>GXif{fX5qcO5^s==FHx&n>CchQT)mWLpN@`IQy>a=48-Wn$~b z<2mk>xEhstzFrmjvR?|f$xP=Pg{ZO>emb6}wz6myvgmP$t~+n`pc4hh$xEv1SqnGF z$&nU{Jhn2LG_1QU(yGl^j6#x_4F~w9{j;}^S}o4do0%ToOzc!P3h5OKi9b9Nks3Km z(n*^xy-i=W{P})xa8UOVvU|s4zrFbjlJ(Z?vR`xcaWR7b7aAsGt_;%8d z^4Ai%ZzLJ!t2u+~9Ol#t?G>HMOkDh&(X{kwq*XV{{SbY7-g`_xTld5gR{VJ$OSHTr zgm}s38)vjj(wmty$LlVZ8HMz*(JuJ#q|9y>)(Snhk>OF^L~|#%rmB}-LS$LnD@H9- zpI@q#I-A`5rV_`}d}rh?mI?g*gZ4q&fa~brWfy*j6N$ zVseZuA4+(&pI>XngAX?4w0a}%_hK8`e=0;1Jf+F9#^pxs)BY7N%N9}4^SH`5Nxe5+ zVQ+riNSZHVqZ#4mqvL~hT75}B^yAWps7Ze_%xI(TfK%_&m+6m% zEXBZ=EkE&Ojk?Sb>4!{~+G_JlwVo(~e6F_Nbw7fTadVO%aXCFpcWiOs+;r4(3u6`a z%o{|7Q>2M}f8u;UTdlBhND~|FTb8FTA*tAzb-JEJSy(hFtc@*TEw0jM=+Ij|rNyGw zn$0S9R7EA=`Mi#n#8?%-f_y4f*eE2J%pYhH5OsZ?|NH;{H1IzS{7(b_Kh%I=X?t4HN!s`C z>_T~qvnI+mi;aGxjrBjje|boWEDLGqUA*8J>1@=ZtgA}4ap@0V?kA6|G_J0#k&(&Y-np#Hwp>!7GB2YQY73BR z8+m>ul^Rc)AhXdv0=TfkE!(rvDgma}zEnl2q}IWSdQp@*PnxJAO+=7H^$yEr|Ed=c zCe)dTH)*wuMXj2qdx?>wKbozabE1}2^IycpObBNk^UqRSxq-7c*iGy7k6F+WU2yY< zvNIEn26qv%Y%Fm;HU5c2Z}!%6 zV%$_vK+@7Br=d71g=0Uce!EO<5stWODU>suX4*L_<6gl~GP|hQiQE1AVm~(aF8U^A zh8)tr^S6>%iAedacOw2_D}$o7Q%b5~5A43bNTQzi8taea?~7s}z?3cw^O*L}jEIl1 zI#QiYP<{=GtEVj4;FkN@XlxaGwV%`~5C$?y3)gV-??^v>Wl+j2n~#sxIIVtFD(TUE z#qgF4<;&YQvR?#ELW0$jP-~NYL93KdH7l@CJZrhMS+swzpW%-lLKl-`YkW zC-_9Hq-V50rEtGfq_Z)RyGCVMO)GR&lr(YCPdnkl@J*b{DnHfMC~01j%&+ix)U`Jw z+kUa#X^};Pk+|<{Fm)~I6a!W_HCw0f;5kPX|B?QpCje8ic0C%NS<(j?8>-;Z_lnb zcCh*#y(l`g3`76P?KUFRlN>CWQq7!8Edf(pIe;nPvR%sTQ=Kt*Y9!@7k22b3-;)Q66 zIH?#ytfS^}yi)5!VdjeS_T`pAbU9x&z0ezbMapvfTDAU&D0LT0{Ncf{*}?fFO9}sM z-LaxiqoobPd)GQ={4j=a_Ez9wH-B24Ht^D-bRHSj^1U?% zQNCXs*w07E7m*tKT%JGVH8+;u$wz%cTIs>`{B>=E2BFlaU11FzANk?bj&MA^6r`?I zZ`7QGLbi<5uWEXYM&L7cyemEPUb{*ooDLkrYa8lDbI(|@Y|G!?W5GcD)t{@2O9$*( z3|m6f~98Rd)o&*9L1bW>CiudYVD-lBE_vS7x=N!Vu;`TZK)`ylm< zyreX}dBKZ+*yu}I=*JP4CHo;w=+Jq5`N4U_d&Gv8x2ih2bbL+V#Vd8#PpbMPZ;u5A zU4Ds9`fRR15=pV+Cr8)uYjB>u6y#7X9S&Ip4z4G=|6~gNDr4?%L{z}o`nR`IH~5o+ zc#nS=f{0bFJ{A{Y_A~PyU0Jn}gC}>f)!rr9iNbk_5P}l+w9rU)(y&73vVWS@+R19R zK#gbtBAY0fZf%YRD?TWPs`p?wO*3!U1BIS6cY4QhL#0Y~O9b`jkH}`24fitu~|jAxl3m zzOj+zjX0o$ukE9kKU`W1>E7%!bLV7wPLd{kFeaJ8@)bI!C9edNCMv|bP2O%}lNV;0PduTC+MPFeVZ3S6P|DrZ{6g9kDYO{$TxZe?kPdA1DLK~!qsIMwO- z-9|tf6Utk{s4KGLF~P|+#E^|iQ^ZS7mJ8`n? z*T>31ylHQtS`lDc)FX-6hQiupdxRiPHgMGOn{xVr3M86+eed>mtN*j6BY}^7+eh!{ zs87EknRLX5^Wv{YRPkRFa!}Gdx^{4a=frl^%0c>pgOes-cPu}Q51Ptaz6~P~2j8-G zY+v@G2qs4Ti3(4$eFv0+cn1sJc*xN;ln7Kz*4A2^t&4XuUG$j0DAmPZJ%|^dUQhrv zw|0w*)-TpdP#J%k@bUeGK>$}=LZ5(wuC_op;f@}iF74Q|X6;`jVA}>?i8xi6`!GkL zxm-9w#b|04m2lKwtZl2!{I$ogRxCrEV~OF#2idql$Wwg({iFbKO)cTKQHclaao#KY zb@6~V9+cefco-bbj71g#TlT#*3u!F|&M!t)HEmXv1|8$IPsoK5=V!BH7Iq4(U4$E4 zM(|85#l&Kzm(}SOESB@uO%Pn_$dO)y7i&p3ReN9IVNvQ%RcF|fcBotoA%MB_g)NQV zx&?Vvf85Wj;5r(de}OD%zK~pgl!Gsr2;9UZHh35D$j(chb_EyV`0EfXV&ZlT)uaZs zc-A6jb;s)#?YM^CD9}d8KF%pcID<$P(-sUxrA1>Qz}vWQ$w#Iq>z70LB#Q%xI<-1@ z6DK9wWX+eFu+~F|5Aq6fQun%b0Z4Nh0&LUC0CjF#TKKd`V6tRP>ABB;yZp?Q*>dtL`Y=kA)o=~}q<7$j^p z%S~9wVVBIPgd#S$oLQ#RruPY93~DuEn}m?sS<=r$qEL2qtpcsr0FI@Pvp+?4?^^c| zMu%ysg1Z3f%d(^Q2!hHCPAZll-+%f7%urn{fby_cdZ;Y6Mk=9C-O>Xh8r$?y|08IL zc#i{QDG`(q1=a$b=K4Bo_F{SPVhHAE9>6N*Z~1Bl*0~A7O&qt59W=+{wN35#el5<%Jrd$XplI} zur7m}MQB%X_IL8y7A<7f9PG6(M#sec#UNYWM7<=NO|y<&6b2UHw1#hP?W%=J7Wic< z)s#~cbxBX&CrTZ^VcPE`vli<#&t`X5RUsn*&pC^9*Zc}9ZDOMf!W}M{H01}W{BZvR zeOm7RgEsv=2AGg7RKBy>*X&H=1}cLCJ4#vbVBvx;2~oWwk=sq07e4@;HSu2y7;UIm zJ>pPH$rT%slWIB0lcfxp)}Wo<8;SM8+_y3gK+ExJlWpZ|i%Xyc1|~~dsORKGI}k8p zN~CO$way>QAY;oLTwZuvuH;hl<}MQMAp3rA`sV_ZFSH7lEl8dgF(xr!LP?zTqi*cq zq~}LiN%vWY0^vz14AEwocTz zf(JtA_%=vTU(NMLa3x$k(z&3%-KV$F3+ONkH$#OHs^0)Lbqqxz9(^7;tw;waqQ0%0?yKTK*$YD*q?u&{_{MxJi*hSkk?N z*&0-UcYPOI%YPB0uPT^De%Z?oLZZq)8|@zZXC#F~x7?Ek4pu1Te_l)BU4OL4a2OHG z>X|LEIa=hxVt#NsJwQTqa3gv6p;`&ED?7 zL+-Ktum!dK+G4`p(H`p+k{3nAgyUcbi!dz;4t83jNLw*$MA6gUh8LSYYwR6NkLv#U zWK6);9ILiPQR=5hhDbl!<}IowL~ahy>!=N~R&R;J0JSg1Oz27ei>>clw!%*(i^U{n zHeut{OAt!jr)N1;%wCBJQK;uN^_8-4OhzBcXh4j#o-LCn%l`1o00r}=b#T>FR@7w- zu*Q~Ct6u`(R*&I`Yz&u<%|9zY zK*EFT$g!*| zIW!@FxJFhXM8JjqMj4Esd`XOiTON}T1&3^`$JGACa;uQ^?54HFyJ{*cV=Bx%1p&X* zYM~`o150EA!zSgo%IXKrO5_=uJ_)^LO_Uqk^I~zOC*>ZiX~~Xk-&5Njva&S&l{ly# z=Qp_bRwSWL*_qtr&=h%BjAXL0P;N>*>0QKn!@^BwyUrR0;s4;_;a|6Fx6G3?9o0}l z;oH%79vbUht>wPmq*ZKIpmaR0UaEK1!(l9~chc?pbXd;Wn2##UN*_*63pq|=a;m?x zD_Th;*?l~VZ6fCqXVuHzpBu4AmHPDSk0{|Pez2m=N}8Ask2xPBcR8sNb-&`UF44g& z?vw9Hc7g9w&>5LOD)PG0GG-@+MXS2cnU_nH;=HKU_-b`*xU|z^MrX63gx{*zu=}JQePh ze>s$MJ`}C{$Yr}R(W@*9LsXv%$KLk{h*GWDXd$#@Osso_hF6Z;*kvQJyxCd;e{ixH zn`s?G%SL|Usx@;e_=;97-&W=8n*stqJiIn;3#HdSN7vS=4;y$ioVX13%A-WUU((P2g4$~hHvLSd2v}jU*x>E9{(XBF8oG?Dj$%`fhxw1b%WuaR zNco?v$B)Ov5!-_av6)@i;AE00wJMdmQZYJn;UsnlB8dw`k8`;B$G(YZcD(SFBzL=l zCh?Ql{30fzLosZ$%@37PTaaE>pH`?Gxiw(Y3VUg95W`t;qcbJ@*lNSKtEfYQ*iePf zOy2R67-7gzS%_Hx>o7eofH{$L3Rkt6Q#*z^%J)lml@3x#c2OIk`T%1E2+Of6tv#03 z7)&$>HEw~d++}(c+gIbiGoPMOdW|QMzL-+J+$!=QC{R~}2r61Q6dZzbEV`Yn)W)tx zVPb3YTLS~89lT6=LB^J~?0|ZD(Z#gZjh$=F=Vq_9 z>|vv|9p8`1tX)6UD;cGv-h3p_@7$wi!2%Wh!C^V(!jALmUs@1rdH`{<^dD%xIG6GX6-*m3xWH+d>Dl&| zs4?ReF0B<_WyO;0-Zs+P&p@GWar=zxzRy^G&G->eS6^ngqjpTEc}=$L^mzl!qp^qU zsS(lo1I8hT!tAl>2d@ubbU9@>zLy<)?W3-X%N8KZ8lHf<5lF6{0i~`ULP~DK3`(rt zduDItySXS(Z8a;`<|d8=T-SWv!G{dUdyiOGhl41Ur&m2O;#Fw1o z^|iXu1QyMVwWX(RV2{G}5*qEF(GL$)3C^uCr$5gG;HA5R$+2o(CLp*0~D>)05b%Q!y)68bC19Fk_f) zgX_tZtksm&P0c=z9$L6;NwVx{FVL#ATeE^6-WD?>5L~{EN^wzxqYpxrsB$&EpEWbz z+RBD#9oy$6;mC zwsJYuO9{dEF z=OxgL%Ryax4HPyuBQJbVH$YR?0Ol_s?P^PB3q57b6o^7ql{QA{8P>(T% zn=Nl&ugM|gKq4C!GJip6KcNJi$03Xv;=JXFe50aL2{OmHEl>~jXML6u70{C@<5CvI zSz$Hw&%JuUj%p}WHLzD&LBYc|J5QiW zycMQ%*c(IT&+mnkn;w>`-$r?N88$b$D(4ARlfjW;@iIpgzS&XM^WOY;>sAeTL#Jg0 zDi=#%nM{q8eAHwmcK-)^Hf6QB9v8$LW9XIFHCp=q2a5LwB<7CJJfo1Jo-}Hf5u>j61n@G*A>Coy)@l71ZR!0L_1dTf0#_bFuBM;Fv_hBtJ0*3c zHc!?uOowAO8!5AWmk-4={4xqy<*b7mG$@al3IgD4WOKOq{_UgSQ;Xa3t>9zl^0veH zMlGjySqp8XM=E#CJUk3#+R)bAnK~z@~&WQflw=s(_k^EyK)= zbXj|*8WN?$eyB0p$N&yXa*^NfRMcifoB?j^`2%L$CgYw!5nw-p){?lzyH^Rvrg;hU z+cXg9H@S6dwz$A?=SV)Fb#W!UcH{1xe?_^gFRC+we?0JYGGGk*~6}>z2=@juH?JL4<@(l zw<(NE zHk!BOPS|}@`n>Zdx00>OSS0Vqpa5?Eay59&)ZfeP09W~fo2uDyE)@r;7nkZ3VMI#l zBtnaq31~B>Jp1;(f~S;OMsW1PjSVE%qz}L1A|U>rAooNNxTa(vbgGcc_TvhAcB6oN zYh!?w?ouIXK`jiJ)Zm7No49rqq6?A-N94Lj^!lTH-zEk@S%&~p@G^mbz;ZU zN_|RtHUOk4*{KDGb^gdg!_q%h$Wp#k%dRSDs%Ue;RQ+x!QkJs2rHdR*DABaO44`S}v}UU0j?>evZ8(>$fNgq)Ry(X6Kg`R~pE{7Mwd zJYRN}d}>C1Uz32)B^zxulNYwaX51V&G3c2zFQnEuSLNhkiPM^K17xlDY}Vxxx>^>o z_v`*C!A1)_?hjRc+`PNGaiHqf!1<*-$mLnb+~AVa(-gc#qxoo@Y{x6fC`94CBQj)r zYmC9;{QytT1I=1LYm=;Rd^VB;OZ)2q+xOl8i!kQoRPVHCLhdWlvM}oE29aMBY$9T8 z`QpvYrvZFz0N+|k6Z<^FVU6(V0@8#@FxERXb~Wx$pw=J2Et*htjPMhJXZ;xXB9%WW%fhqqAEaC5 zoHODIu>=+E8O0#+hZ38zEqaU&M#`~01S)-QWg}8xyQwrv34B*Uk2ENZi?wHxOxjRX zJ{YNs90yFrG(>nV>M^hRkHy{^Kj>Uu$qFfWBf+!b>P65EBHBO^qF#yb9FOCSiSsg6 zH}=9weYHIgjX%pFAGg_S+Xj0z2R_K;m5k_K8*%3Go0y*GAD8WckFQlGi`Fy& z1H3aGz)VDBYHhW?!v@r_s3@}ec43xoZzN%1#R(3xy~hjq;|=UbHSeh?8mrS3w0fYs zn1@h`YyO88`;!6IL%Na3?ewSeArvdmWEwUUwO&ISBKZPVf(tFxi5#ZfS+W+%H(M_1eI@)!39Zy!F~`A|QOIIp_Gg~r5w)GU6t8|^)na;6z# zNTAMp&{nQTYOh{#dvnEWXg^Hmn8-g0nwqM4W>MPtC)WGl8*by)`U_K!E|7TfYM&tz z_9Fx{bfb%7WLoGWr6-2KHJN`5-#Aq^{{k|Ado*D<`CJvVd6qiLh|rKHsfA7 zqNDEv|J%-ju5Bh*!dY z&0Tw!L2Ydhu;`m>|rrG<^u8be#PW4o~+Yf>diZ<9aq8Hrbi%DXwq> z`^RhQJ{}l?YHhvND^S)RxhP`0SMQ?|ji+``)n{$nX9`Jn<-J9`_@?363h{|zJp(WxvGMbGLJZjZ!~Iy zFgOz89BK3P0;D`cT!|Q2_WSf{7!qYKL*tHeLL1)~5;HMdJP6xWwfr)@jBa7eHCV4r_tpamQUP=S{5ue4-A#8$pro7;FB@|_ z3re(zJBmcYeq~UmwtA=<5(nSh?J?r=T|kP;62BUe3pR8OJDvlHk}0dEyHEkA=5;ES z>mll!cLP^q4@5zvrF>B8^RVt?Vu=jY_9P%A9KEyaPIH+R7w{8rHri!jID(-gI@Jgz zq5{#0hVu47+tHGf&KeO_`9O(So?A_jijC=>R355c(W5ChY-DD0|0XdrJ>UsyGgnTS+tKr}`C(%>dNAQiB&hQj4 zV-Vr2_l{qCgnGjWv3Zq{nrh^=_S4Eyz-!u~JTI$v+!Kma8EtY%+FiiF&ha5G%_`Pz zUfL3+2U;qUZ?zTfvATM<{RG1OEh8WK*Qeiw+TU1|{zk&;vnp(j^Ej8u@KX6V-@qwO z44TnxI=S4+HRF|VHG~QKLbSzf1p1TFh}QN4L-}{GjW_Mv1h3vu?yyO=5Zm7zD$4mp8VXrRtn#h@G; zRhI&>&KH)))~R338`cq{?o7`F4PWpNM>rk9UISmOmV}!;Q@si9j!Hd4R85nLo6i4{ z8g{Pxr!T~1F>0Tl^I^R9*nC-m02uShc%-r|SWE%L^X&}0{rE5T@s?||UV9iNN`<#T zR77}g}N4kF4th;mq617xhvffMX|S%A%(9}%P8JtS%6mTUf}=5`;m(i4CKO1PAQ za$YUROTK{!UM@vdnJm)=1eoI&D3Lz2UC=r5{q8?f^HFNS;Qy}$L44za`27iPgl!^} zVUaI_M&b&#Zy@eQO4Ph`4gi*txWgb?R_y1M?cdiaQxIQdt9?b9 z&`KYKsc2FCQDv6s_mSTFo)F69$Q=3S!L?eX!x~2R=Q>53PB10Dfr2|h(|!c+p2s^vUa=;;Lg3tIXsFruE>g3iTZ#Z6)r) zfbvN{oxJ=-VAbNV3bD}eQ%~Nz%5^pOtlIKTLJsLI!l~0J6U~UCHKA-ImR^=S+d;~* zx&gZV*V$QNZ)sD`x|&GrT7fbGNJeHWR252qsB_s^ODCfGjY%t@jSIN>e=fj{xIhOP zWqK$xM4j~3I!Utgz)3*}lwyZbX_0PY-B-(~-(maZHJEwDOl%5U4|>t8HxB9KG85J_ zZDg7g_9OH_k#%Q3T>q!E#>v5{Mxf04lnoVgaq|*NaE-2>x)veXn1o$7wZr>{8vgCl zVz5f+&tJ`WBo+=gIT^MNCRg+acKlR`Nu|D``zH-@AVfhn0093wN1=Z&&^tN2VI=F% z$Gn5l2nZHIi8OQYk1sXM`6pvYc14qrPxp*<^P%?(iRw;NARG33nSd)hf?3?9+007t zZS~&snp}y|!WhW4Y2RaB%NGpXRxyL z2Gd8NwZ?28r}>j>L(m}|1|-r5hp*-6MGuHXrqtN~SYfmr2w)#L+iBeJ57K5O$}GB0 zRMYElg$alC%0*YHhP#eB9Pb*LL%Rw)0?)Ca0r zAlV%tN86d8Vw7WKuVXLl#o#8w9zyVPAV$3&z$>bCMjT*z-{9x@<;eY zx-w$Jq!Lf!>eGkCy!fPP-Ptjzt1Y2}>kP6;K)vB+W11{GA{Y!Mvi;}hk2azBWN5i# zS_ja*5Ij97`JTj~5F9h161=|LNQVLZZ}&&RWbg#2#XTR7ww@WBdCL$tpITi6<%y2| z>|<`ImKedol=z`x{%xH0-g;mbgby0mDipZiu_^VQk2zZw1ivy>1TNEUYeueIzvZ1_# zwooRuY416SD)|`}$lwyVX=I_VK?z!`yf-0dex13j5y`@IYIuisYw$V@6DV{; zMY#kRRsJtKP{9TGbvsQbNW8E6gqdyUO;N~=KegD`2%>2!+9r1Z6!s*KqII5&M#ec0 zhVjuz(53QTOY*VTEv=I^B=2rROQ3yU+Acl1{51*+&D z{QGQv zeYKgLfc#W=T`9=q?Fk7hkBPhZA6_ixpRY5vDoq{T}1eF3orJ%%@^~S_8F39Ws37ZVmIg^Ku zkv#(oP6M@;D2iTy>BX$zIK`;JLLY&&4Yrf)Ix`1!heI}b3uA>j`*f)@LE>q%a485|assCRKPsef43$tw{ebBH)A2Jw|GJON<; z#R2X6AjatD&xPyZ8_!R_ND3Ksd)*0F&MZupWc^UvpBL@Jway2LfAtul?~cjf5@5=) zECGCE#?Mn(j^o48-dD-#}Q)1l3>J?WuTLE14LiG&j{Ja5fr}NgEhS6qv#9)#=Nhl^TFbS z_{j3u8mB?&S~TL*SEt42@Dfqi<}lu{C!2mHGB~x-dpr!Q^vBI#2ol%Qcey4%S_cJX zkq29W?`a6-0h99jJ{7ZEHRw5}Vxm-h_9I^NzxSV?>{Ee~fcNHGP`rRwa@DE4=G)xFQ?mNe1;Q?N zro@V8+Dm?zkU=KPeYrdH%~8dY>8U&d za_Y4KBvFxB#h~}aZ;;hb0bpW#sXOK6&c54_B$cyir3oS-KED0Vfm$QC^rzJqB!io4sY4nQ56RJ^d zGBNACSFPa6&=Xx5ySD=H*q_aXt|rW3&l#~>E7dLg4n+1k<3oAVCTGd3E?%WB-q506l0D>tpPQt-|X4j#uPc{n>3@97kuL2`IXM=R2iN7q4!cDN% zS0H<#X%jyD@v#fEkn(HrF;D>?bPN#M7Y_?gk)QS_Cd?3=f6B%twWe}u314r#&H0m%2&WdDXmyD$pAD>Y%< zxifL5YYEx_z6>0POhrjidgwA7H#BQTXQM5qy&sF_1+{>g%?59nFrB!Y@ErJQZ3gLQ zzrGN`;KdXmro-|1Fg((i!k9eKQ1s}T;3F9Pop=gTYF6}iap(pH9~|-#3WJL12D$#t zHqF}CV?SdJ4@MM}ok*-YJcR785lY)o@RYF!2WFzExRfCj3QhWKd~D!p$>3=|K};lY zqrh{1!fnF?(>CWajsrJU-ts#(!N~8~^Teh?Ysgmc!IX?+n8-N75`#Rg0g#F3hiQ9t zNI)q9P{n-R6gd)`ElMt|PvIUro`f1<2WtE7|BQW&zrLC$4KN;Fyur=z>(9-907t%;M&Wu zi%igHi`49!bOMTkw;rUa^YLLM>S$U0WdL}bGoaG3_3a~|x2D=fIVgH`z5WZDuyQdU zCyW#0Dm$R)y}Np14hs*5jI*KSzQ_qR#(CNX%cwl|8*sf z*q8}@(#rSiN$SwKHsiGjhmH@c&@EzW9(9e;1{*EjjInXPQuonTAS*Ry!{fQe{nRgz z5T!5V8VD? zpr|wTWG(8{BhHoVl4%T)IRRYHocZ1Bn@q@=NN#BQf6wY%;O*WUnlN&nSd+zvqEA9N zX!G3hmsvBM#`D}Z>0SWvxj`pJ2Cjrkum6#&?HGpKB#Oo-zX;4%h3j3WE51NKXhG?A zE=`i}C`NLT{Z|2Y9*3@D3Zxtd`M4ppe#A!vJ$iUFh*tUPOR9Q}yEBepq@c-7U9@?a ze-kB5Q*)j?578d4`(O?y>A_`^M003>jN-Lzc5E#)Jj1Dh8$GA!gK z3iO1P4Lq1}h>{`1W~#%TGM--o?DO1y-h_R)T4KP`Mzg$~wPK{WlUmyZ`k6UI~YRuUC$ZndHG>og=IHJ@|8^$YrQCXGdAm5AWSNp!41@ZE}1T%w< z^@J!NLG0s3w`*YYW^7<+Cuk*j1{LRGXv4ZMli?I*M=dBT3qse4;zPPaP*6=nrQ^g0 z4QuPqDxA@o?pEP<8`_BChP0HW1L}Xm^EuG6{p3~2V?(V(y?$qCGF=m5IE)&Y2Rvv< zv`Qa=x8yH=nmdYQ*$;0bx$gv#b00Ao&y=Q{ha`>^C0<{&-O9jq!l( z4+8FZm(@4#Q-dX@o$; z!>LFQ#$iYyN{Dc1IdMv9Rv1MpcPAjGo{<Eo=u$)lrs=FOU|x z;}#E}g}S_hRtmLzqhw9pg!zlP!@yW`kd-5Zoz*{yb!7m73B&Z_#20WVOI4 zVK}&bkl0ir0Kr&MdLZ!#VbEx`NFDNk0i#HTkjjhjpv$RjtGg{w*XWtl9_#N385UaD zO*mBlZ~@vUGP$5MVv(Bb4^<+#SJ&aE;L3VOj60U{ES3@%_l{VdQ-7FNoix4QaCzA~ zMC!$!?e7TJ-~D}$B9NjbGqke^u2%f;$)#fJY0IwHPtrKba`dmA7xrt zE50S{m;Xr|(fm_1n3Y{$X1OSYTkrvgY@M53eRCpb&i3-fqY2|RRVKF8>rCVE@14In z&-cw84$*nfX)pe)Sd-KI>j6CM(4Izkk;fFezg6aKb8`&Y;TKYC{RmN7?g z+g}~w8hV}Pwp^megJB(~K92RUS(@Ane=ifI-29i%(8nBIh~>}uGqR~&${z3jI8T(! zRnWhx<5+kU_88VbpP(l^#3GZV-2CCRx5wyAcoj^s6v#`zv>&3~1rHFoY3cS{vVYSb#Gq z{JnI&a`SMj%F%3PYn_iy^O$}aYjiPeh zPVTtO11EW4lGQ&6$m%#J%kb>-%Od1k%;f!n%6T_H&IZ%R>6VyOE-B zncOSG>jM*TtTv@>=L%jwfOoKR$1ch3K0_?#rE%#N>4EV=KsH;Fl4(S~Z+vl)4gK1m&#_%VGN- z-vo5qChzaH`s*6I{~#8BZc-xO1@71Mp`IY!0;wfX@ zcEv2Pwv6~U+%`kBda75>F*vQ&Rxz<|&&;);Ga=l@E_b`qeFD@XmI0AN8ynvDhUA=?p@1TD#z^LNSm> zioRiXfkstc+H;qK;Q8_J!@^khh6cMLC!qY*Wf8HW6Iwq1(^^Z0lG0d9L#Tu*fUZjp zx@i*AKmLe|Al%WZyfl#6>XY^>NRLggesW$gxnW1>Oi|NyqD!d6cz^LN*yLWIUIx2B zlJncp)RD=BNW+AO6yO2duhql$l_?FiP;U_qT&Ruczl5@bJrev6a;w7?hOcoE`ddHp zkFV;1cQMmBm(q1WfYZGuEHx^N-V+LGl^|efL61$ceBVA%B-T88!$p}@Vh!7tx4ZKNFd)A`Z9M~5;vthm)~@PY7=C0hbZd0&H1I-6z1HPq7dH*wB2VfKwCdj6>H zdd8Wc)a_I6l$V}~P0rWn05POgnSa%Rm1nG+4%VXw|H0-|A3RnjO;U~>*ewhq$p-n7 z{;o{!brsW+|5C+IIB<1%;UbuxnX>UBf49b|D&<&`JjKzi?`t-eUh4B@B z!cq+vbQ|?R+1bcDf>xM!pCIyP|Le-&X;PqU^Q6gPONj#l3e?jAHp#U!{FL=vUDz|A zp}DnHceU{6yZZ^O&0?Cvf1Arzwj09#U_y=e^~TfrXa7uH$(c<{CFDfS99%dLN4215 z(zkDe=Vy>F<0@101vXN~BxYPv=`8VLJZe|m^+ChTluzepd%C8sSZ@23V7?Z0YGEOk z<{pR8Xj?(#4?Ny=YB%4ZxzrX|N9n>}13oz=H>`IZIoHg#pd7x`6c(4$US;o|>fMGGxd~~Rc!Mci#j#~`h8mpIlAks@ zX0u7A-(r)loBV;x&1KeJowygOsCJ z0BGCpX-SLvO_e)Xe^r^1vmM-R{?gWS1rk~kU=D635#qxkrfbgHP##DQety!BKfpWMM_8cGn`Bpqs-#KA<_SojQb<2KwvHL>oowR*9W zXfF`NL5o|8;*jSK9&qPg*Gp|u*ZvKNKYcE{B{*Dxy7Rcr&Z&Ym769t8ws-gS)I(?V z@3IiMT1TSlVXhWpAj#q$N*1Q1SyT=UMINHWsk%Vn5fXS?w6_1rndW!{l zIMS=HZa;0pYFQ(uwdOoxPDs&rEd!(-v(3T7yLaEsGtH0OCAH)i&CKb#yijG3EMz}E zbvsKAoGRuH9vzID7Jmms?>d(?<@Vu`_!plm<&?4L4$vg;+@ZR;Zn}V9SGj)kJ=!<$ z{U=|eN9W=a%495*4}E^9s6Q>03#xMC2>60SnX$p)wFAGMu+ag1%8MQVz1W%z0p;x@ z@jG&dGZS$MYk_R8os3MUvL78n-XtNLC;0b;T@aRI`e4D&t2*ny-rYes)3&7SaXquw zH&Q?qspPr zp7P(IYS8oknrFpvjC&X1yU&sV67nM-x>QCf={KGuo4TzWXlM9a6$!>IX@oVWC#FJOv7n!-c1gTEiK36t#+ z%r;bmS_qSzGs)5H-7l~n>Rg_B34&sh$P*@P;Aq0LEL$%4y((UAd#xm@l&qsfFfgy< zI52OoWwio`JVWIW@vyN&ndzlJLHo{)W>EQlJIZbEO*x~f=Y0F}x+O1oZS2)R>->~N zD0NABNw8^>81Q(#(_$;;AC)hz+y9Gz!UT52hS`gkTy@RPdcjj0c;J7@C0TRwUig88 zPdxTc8bTQ>GTPwjP0otXDmdSGRD2WgJ9lxX?zt_Xsr!zXP-9H86b4TV+Z0jLs0UQd zLBdScy}fZM4H~CTD;umcd)6pGPQxn<#H3h3Dgb#+=*)rmZ#?#O8GH-uUT!gUP|2K5 zj`0JSuUQ6nUML?;7lIXBK2dFuYzy@NDxfe~C2VthSr-vm^X+k>3^+9F-`Kcwwcjho ztkZ4>k6nm)4oiY4)dpN;T)OVE%PL)@rW0i{9IR-GVqU|v>%~XKLZ-Hm<4Y%|7ZCuP zB%KOG7PRVLi4oYfiq_P^07DfzRpUk9B~R3CmMQ)%GnNDDfiOtL{0Y}myQ@!=uL^0c z6?!od#(S2AoHQ(FHetVO?pqCCzi@Xm!B#IO>7LGe;pq;qlI`gI+|@0MF<$s+A2aGmcWWA+^^5 zIr$%OyXv(Nt4#AwUHNrkJHYKclqQMz_F7%H`N*(loETHsdX%R|7+QD z+xI6zXOb(o5NvgYDbr$btweMybE$?`Xfbg&F1J*mIp3*Tw6;2m`u@4~qc7T~pc7k= zBrpBm%rn1aLanObjP@@%3End_tJ8#R-6N}Ppxd}&n+|Cm>)^|b>eE9a{~J5HthM2( zwxh^oD=d@1o`Uiy{e@qMdL2b7nJuE4K)_GcI;*$N_@@Sr+NFAbMTHdpS;s{`cz~L= z@`w9gcsef@C=06Yo0@72oeA5G5lxa=kQ!*C&a$M5c$QD-%*h2-q7Y5n^kI~)4P?L~ zZBUj}HP)sc7`qZq^KF3)3X^QFo}B*`j45;`s`r9&#UgdR5l&-fw^V!pkk>&lDwJ@l zs=%{shgY3Ey;8kJwB$$D{%n5jO|1uijlNJZ7a&)ewFr-~%Ww(^3zc-|N$`Lno86$* zk2wa1ucuV*BpmoyD!(boXPJr~0h(9OIA%DTL#45+M!di$tAC=3B9319O)@&B zbAP7%F9LoaeeR|1kfR7^l*K&)xfGj-tux1Y*&JPu9sFK0hW!GR^S*(S>;X!dyxUgq zrSfO{IRM5fQW%+@{_H_GXK{HHZx&M0H*RiuQh)_wW=<#4)CD1{yA z%~ahnB>Vu^(Cn0M>btD|2EXoon=!LgNGu9tqUuUPjXr-fT&Su;&>kkFt3YuKDy(?J z5h$+7fl3ZB0Abm-SGRgZsa1)m<4#KdYFh~b5(q4(?t9w2t+!m6btY3-z%OZR8&ESE zK~b{%oK|2fttBDL|KLPABoJ7>-VYrRSP*XRQ1D(VUIxwt^f{6Ht(b*_H7$LQYRT22 z#E=(XV6st5mxqMbt$b7A@{76;Z~tpNZKYds8ZaJtEqNF3g*kRrNqD_k%2`GwRUQ{w zSIQCtImgaiC;}D9BX&J$ntAej1!`X-cn*_%1<8kDD*gA=m2(T?&5I?iPx~yI0YnS; zwDiqWs}yfec%Z|{wr^Qj>?7tM@guWJ3SwY1bEaFci}#1zmVbrZT6sx-L9+RxPc?m6 z?Gzkl`j^<5aYzg7yV61@V)$oYeyp2pkFml?SI@@QaA~bgNKHtfv}3_^8+I|%WVWSqNyZw{c(E4jD<3sNlF6}a`M)nmt^CYUn-5Vu1i+NkH935PF zt}jS8U-a&!*PetjiY<}ZnxXo^72;YxHBmCtqAwto|F?t{a$<3Ubn3=2SJScD!&v^_ zdf9ms4$GPrpH%S~hCy5f8ve!&!o|jd{@n+T!H%szB!1yJpGQX`w0DAcm{!-Y2_(7q za|K_qq`g;zma$|nN|UpnPec}9PY4cYk?)B1jd%y^6&drysVw6^ww zUKu@aCzkUM&@`__I|K2%hnbKvpD9cWiMtGM7{2MGl1g=!9BKaGCQkOO1spS{?uYj@ zAFp!vkbxWnIUGM%%#7zh>iK8$TbIwu@KxaUtr#i&qENPbSGdZV&L7_O7-l8`2}xjh zi2YZSVfdM45)eU$Agy=V!@V~soTZ$LsF>nQ?zaYTHebL~!0$u4(V;AWHv*A6RpW>C zky;rgOUtWN)@r@g$u$YkveJ>!6!)fSt;&b}4CMx@%HJEG?haCk z>j?XvPx1iyU@Kn2l%#{9a(^jkNsOEBf}cs~%LvWjqWBFs5Drx|CH9nz?AJ!`*aOVYxxOuCaiM4XRm2 z+vlGvTf>Bu+vH3^Ih@u;1y#uTRxr~MP2B0Q5iQ9Th|w#^-}!}1o3Yj zkoZNTNw(|xs*J)ec*SwBq_brAa{!$e5hYhbHYu3Fne&I~Lv!=WOE&BdfCh4ty+=MO z4xVQPXB3Pok1Tm>zld$2GaFbLEsO~a3@`F7Eg!QvBinD=SK7T94x<~nAS zT6JTkO)l@G4fvTGQ+{L}&iq=>8Y-7&o8!DTj2pH(hXl@APiChEXGUe2ig-XVD26*7 zh{9He27|8*^?fWd_1n{TV)=(mXHTxkKl#S9c2#`CyRWoD7vr0RDc{o5>WJc|h^70i zxov8^5D{&UYWa{94Gmf%A$><+&P#X_;Z%^+DKG7qzYyhi6UsImfz*JZ%Dw+qyz>{D zJ*RrdIi;=xHLTS-es#Qt29@jY3i0T(0|-H24o!QlSgtLLs+485Dh~QWvDGO%NA|m7 z%9Crzhxi@`ktgT8_;{_I^NFR4KA735_$xZHM~7bTSl+RmBk{Dqq1NxTnze#4$OA{p z7AA62U6;&X)>@GvB37I+ELy9zdzMDg;T$WWI!dV_S6x!=rIxy)4O!y+h4JPh09W(W ziw&ffVBlQuGbnVoA(o?IJ03pjKBWj^=G^4ANk=d-ihb86awRK3BMroRYx z8rmzPgrKUdx8|h=IQ!&kL-Kl_TtsmxN;S*FoWLjFdL>^_;=oJU+E0(^x2ej0#^gG?XeYrK**D`2>6_K%||EIp9tIETYrP z5j^Jj4UEO;IE!hj>sY}ud1i;Of3GHeFEjZc=xg_YMJ??d zK3y=-K9GE3x*~UvKvG#unfae6H zUjT;Xs}RNgf+f8U{lSR)b7`w3j3w)w(`3?d)xzh z{y7Q0($#WWCQ!e1a|SOf)cMSlItf z9y<@H>5PDq4wg-M1ov;kqEEvb_HP5HYkP}fpDnvBDZ-uNI4^AT%N%r;j}Dp7pJDCg z0=zCn^3Vl(FjSot=CrTpy8L}#p(SxW7b1lhqMqXNr5;aH&yJeeHhFuFvT8FB4x;v+O| z8Y}N^4BKsiKYa|LEzks+(~>uzP6=G9Z-qRoAfb1BbsqEg(j#QwSk%%xiJUwY)vxC? z=d@`Yl4N{s+E~QWB(nEJCBenHsJr0yjar#Wi{IcdZ!6pBmFT*v2g&>oeDzl=)C!Sc zuU%K4>T*^|t-6wzA~CAHYBiGUn~j-yg9bqd3x8fabD0cShU8m(50#08GWWduI`6v` zY~_R|P>-u$6l3#2T<(%yzJhv3MQbPH%-_8_fDQb;>%nD})D`B?8NxDqt|3Z-X<-?q z?s?w~w_z#upaQq2P3x%{YKNgPR=B5|R3wcnwuxD74_=vfwO?VD-NQfxYHwm94;FIK zPSt*gQ3~q=HH=Q)$?Ep>i%1V{Bv#MhCFl#~rFgku$xOT@IMe+Tmhu|58XPa1kyYh{ zo#gK5)>>%e+P3mHf}rc?t+HIy_CWh~|?=^}x zSu7ouS4M4z6#@(UrpxwY0rWl!P$xEZNqd!x_ZN)xg@Yt-$5;_&X9J&aYt;y9`F7Qu z<7%6xxnJ;F+0RV+tFO`%M0B3E2kmhSP@&8n{l7XgRo9(L2KoaH8(ktZqY*nWvASYy= zG6UKI2GmQ}PPPDTs(?vGtK}gZ3X1!oLMii{%{$!WwM>kzmYLdKg?2aC-k}Fmvzd1R zJ{Y~BA0b(Z%t*xHs9OIX0l&7IGZmw#KQ64MW^8#2CN?KD&^Axf6luJy+kpjh^>k}H zH=G4Vj5_Ja@-Ca#9KnTsjOE%T(HxFa1vHQ_EoM&r}MbI>=rEI!jmJU3^E=|j|xb(dk zDS8x@spa@dvgnz<=HV2%`Tflm3*XUi$o~WfKLv^#B{^I<#su9vl-7jKv`9Y3*IbR% z_B9rt%{xaPnG{Uj)$=NyE(WMp@4ij`DkHE=Cjke;;KKycy>Y48{?hUY2zUOPPL7p6 z=NNs~)jP$R91mMNZH_UKUe5v(O7mp1{CX@lVQfpc^;Wk@mf<^zF1O>&eJjo*yVQR4 z2<~k14=fJ>#W_^C){2%~LP-V>lp^LhA6_{?^Bfw2SJwZx%bt&mf$ROV$^XDG5O+ty zsoMqCP|oP-uRmAIMJlk9mRQnKev(}Ei$GF|`y<6CjLIkd09Hj>YXj>Zr9vtarw)j$ z*IT-FoB^($inn88>_e%{&>8FyQ|4+1$oK=e`t~?0?5mo4Avn5M?F9(ge*zebYMw{8 zL!3UokX(H=roT^xW%h9sw2|ZlJ0EHpMJQ54?B-jXM}g(>~QtAa&0nC4o2m*}+l1gvl zTG03bAA*p-@kyI=VjDMJnZ!KARDTA)`(&yBIcdpEKr4*UcC*KjRkKe*z3YK1j-?ky zc;A8-BQ1OVuOn^y5<&BYdeCf?MURr#QH9f0H}yxx^gSJ)rY>Q)7zn}j#n+czdHJxx zN~w9!n_)CmLD+(c{Ida;m3ZL91z$^!&`zZ?w|NtyIyMRo2?7(F6fodSe+%kX!*|jC zm*jLJmhw7_@x43^e{|bQ`&0}(Xra*dkIMpFog)_*{UNX@N8SWW1Q&ondzoUn6&1D!m^%ADJ}c<_eu*zN3^` z6>V(<;s=QW^Fl{^0EG}M0N$wz>nd0j;k>h*1kbw%1{D@{wvJb((w-sR@nK@qg%hP{ zDh$>bAApxd&0$Ay~e|*r_<=Y>5|wP-wWfQ4>HU3Au$FRcE$`3zP??&y2yfrC@=vq zgHQfwj+dr>%!Ev?rT_8+W|1I>c8XcE0F#an*}*`aaO3p`XGNY;3t^k-D)&_ zXEjutX*7&ZzYF-#-3D80OtI@>A zpo9Um%zZ=TeLlp%&@UduI{1-Zbavt?(@y9b(_)wMx`J?Sv{yiVcNBGid>dqr6AsLO zv808jSs5GhU>!dp71W0To5uiU<-ed?(#9oIkPd#O?3ib`{nZGV|M*_T7hDFgWZ4N+ zU2`%YExCzCdhS6+w+H87TB{92^~bIkh(cLP$qnv@(a_p0EdMZ)Z)pbJUf#!yb^H&? z)?UdcXeZUhzdgU>Rkn>0sVY4g8Qy*XU*zy&IZN`rz>Pb#Q%~u>aFK=C?K$#_N!(wE z@w_enLR4)A1Ot0Y@5i_H**h>>l&RhkoLyEs3Fy)?C&yCY#!iVa0fT&gP65n8{ZOTd zt^O301B~4j)`*7|rkW_!TneYu`Y$mfX^||XH`+=iy2LhGi|OG9FmD67bI}ZzKeKGe zV#>Wx4%2RnFEXKtG_39P*CsPjhTP;h1^ixD93!x|WzZX&40Hd)az^o3Ry^&j4G0<> zhy&7l+f;Azur>(Tzo37?9wyNGg|qV$&o`&L-pJI|(2u~L??ck$&EO~RpD)~{^biR$iXs8TxaX7KU}gm(K&$|Pi&72~I*YkpCgwcM191zBu*6(5 zyG7z7^q%P?oi@LAE5<(G-T!&Qp|KxxkA+NMDPBgPcGs6GQj)~cq7{-(uYC%>(Ut$W zfA#at(X9=B`z@>ck8LA4FCf~?o8PF3!z8c^ia={4_YmV!Y=30`F&uIb%PgFG>ev5$d+YV8TFHSO_aZnd-T&yu%kq)iFt`eUq~QONDd zyH{G$%2WZYm5@EITYGt9)xRmeEvz$QQDV3w{XI;)lS1vFtVYy~6~pvH@GaGiDf4f$ zHgT#*!4+R+PWIY@5h;@W98QF%67uMHKz7|6{Tn(IWf0c~JNSI56;kYy@^HxT%+3mx zv*V*ShxiHf031^lnd$<;E3?V_b{bEf>;;_ssj1$2uv!`t)GN18IUM*N zkLR}8^}Qom@^Vv9y~hh6aOX0oniL>aoJ1%K|3V&5I8J@oqGlfn$6R zc8E~jD-h>8IAfrZBDlmUh&MNUUk2dRHM;)m{<~qsu?L@HGxh&C1hS2k^Fn$2OD4=H z*;n5uq%jo{7l-OZ#-L!EIH7s#!jmDr40DOCY;tHHgP8U7?c1=JG=;N3DLo3@+q;%a_bM-+8CG=3px00Q;_ME2x1)8VPIDpw*VGDHG?^5+*ZNW>H!^!G^W1K12?sM{suCzKaPv9fE@m8lB>}d}HM( zYxPHm%3z53=ulW2Tu@%~Oc*6c0GM@3U^y(X;r-D9REzT-#z<19VBn-N8PREr^gM+! z26L`n0{xEgn-^o*c-hc-0?UzuW$&KCY^=yPm|6KFPk)s~G7y*8;1(AcYXN>xx!U;g zVbSeRlzo9gLnbCr6mlF2`?W*l5Y zY`g~9i`2@D@jv*iL#+{oGW#~J3Rh4cIxxI{FB%W?UZV ztim8A-_fDjwsVB$Xj%)AtcGL1~dhFBhi5-SO4!aG!(oIg2zn@%>4oVw@!2H(S3HC>AAXEt zXWk60SVu-bu?!PU!m57)_De$;<{Hk`r^4euJlwA5xMe-*zS3wkjE3tFCWA6aC z{G0dMKXn>NSrx6eH zo5%w+p0`%6Btq$h$u`?+ZW~sas@ANG95N{%@&YZ?qK02H@$!4NqP|1cucCXQywm*s z7LE589XRDoCEn0|YYLwxT>r~W_Ma`CTZZ(~ECuU-{;Bd=zTNrF4{m6yY&lW+X!rI@ z>`(nK@!ylwzjfxpO_Rru?^>FLUpk^@Ce?lTeQ{-)($zA##5lQ{h8q)8MGDI4r*Bt| zD9b42rp$Wk!go^Lt4s1pAxV+(0T`*KL{hWn#RoopP@~m>m;d}Td64D}KU1h_GSeHy zJ$YFezT4JtmvNjsw{`wdpfIuU>Mp+=U2bnZZaU;1-`B+9HPaOH4X(2TU0KetU( z#Usu4?uNO{HFYGDOW$A&GCb+pp-tHGQuFMkGf5WTwqWx`v{wuL@splz&fABJhM5S> zLUVToC+R*lFG+_zm`fo+p~qblUk`yHu8t`9jY=u2aA7ulLy==mE;3R%7igyM_*tyO zHKKQM6PPEo6laS<`n79&53oItJYJ(r=8w{=i#1HR2WKp5e%5c@5q2ER`YE;4V4E0O z6@FE2aAi2>PkL86sRHGbfYKm=!Zi*<7_vrAyIP zT@f>TDhK=_^XIWmAd@5Ym5H|X#OjJgX-0~YovjX{&(~$gS;N9=qDd4a=Am3AkEj+H zL^85G==x>g zFSpg5)!2zYjU6zjKMVqg5Z}Nr921HN&-c>@vMU0GHT$OT0O8+m&HnK!9+!e`?tdI# zyaxg8w>)KxOtd}j$Xdtt?by!2EhAw8(zs3u>MMA!H+DX=mWnb@Ys6fSPs$p_BnS?f~tF?9lF zb|pu*(Nydo^dFd=5|~1?rWF`SkW(A{io$+V|4+#KMi{U=)-bSxWl@rGFF3R{H3PYrig;jgZi z%mdE2y!P!!Z*Cc~YQ*@EwTUQ?gsAtH6l~UppEMDUtHzng*B1clQla(U%`o@2`gzRk zeWl3B>y@9>kVws>jDyBxMbU4Iil$Uq><&XREkTr13~EPeO2a?{ZUr{BG6zU8jMI%T zCcf}WEWbf%+m;S%DEq>8vR+F;t$E^Bz2y$_uYo4@s+XmfBj2K{mCiac^Py0*R@ zgzeabM>bsXKER9ni*^@7=ewe0RlOm&6Sp~s7jbf}2z+4H^lMGu7I=K;tJlLPU&Y~S zVA5t^vfC;=2K|<1yhIt2>to01SD=x`xf-VgnEd4?Gb`YV-g1j;xK!DAJB1R0tWKu* zw(jIZujXjbg@=98qy|%3OoxiWq2y5R4-2z(;j{34@!pr~F)~&w_cP={kxYo!%gnz}E)`s1OtB=W~Ol*27>_ zyrZZsxvKvII{YKvd7)k^4gS`w1=TyqmiOM z{Ps)CGNQ609!y`EPEK$(*o1jCkByCui;+im+2JcvEKroNu1xGTy5&)~lOK32LWakw zPwJ(mJ>uX7kPhdjH##SCa5WApbg-R*U&wuz75|oBBX%1Pyj~Hxr|ROqMEd*qXpD5Y z>VtUy)r&Yc&CY<0T(L}+j@0x$ostu?3x9qv0a>Am)q~;61Xz#=4k77K8_v%gK0nwW zk4VXPv_1@q<9X!-d3od6Mn^Wrvx{WW|LA=W{h{E# z)9os-vd%Mn{hvqp>wjPW^5u)YzJ&8ZOZcTx>r(vn4-VGBK@S_+M&-HV&v)IJwGR+S zc7koiqw{yZ#|xmtUI3eIT{imYU52QUjf%-y+jAxs=`nV@UCwQyV#CiTiaR3`Co;<} zqtiZHJD9r=3}-R}Iq|;{i+$6WghbuGmW%kFxa_X3hM5T)Tj}k* zf~$bhV5NSgi)ifYAi8GJQ)J%~o`R57WVXwXw}Qgo33ONQCu#~E+<3R_fB9~A3{fsC zb`L2*nVwqj*q6=Nv+|OwRNI}_`0FQ(kPql z8AC+Zp0l;MTeQxi-x2nB*MQ#qp??9Lu~gKsbqCyBV%(^KMYx2_BO{Ps8ulsxx(&x( zZM?hbU;~4H3V2qlD)$J*)kN`j#BaM#)rf()a|hu%p=)Z!RnGLtAg6a5#ILp5H*%2q z#ekl1D?w-%j?ZXsHp zt<}RFKm!IQm)F>tV{qYfdGgsssp}0rVYnGT9?%HTT>J8GGwunA7^$S{pB1h%jAxOS zU(lny9NrsbdLO-4aiI~XTBv3vyoD8e?iLmOZTP}2OI$Wz4gQ~!2f~|4KUE)uuva`3 z+wbhT32UF(>)iG`E+6bdm6W=AUn%4#%8DL+LC4|k^Q&<<2Xof&m-C|R-y;0NP7D0? z9|ALE6Qrzk*Xo02^1$ab)Fb(?7l4Rlkzb2{jFFznno=SzsiIhel@8if`k2?XGV4dM z!UfJnMMy@RNL|#&2Wegj=y5|1$C{5K41#1S|MGnJ3aE=n6?(W$)`_yxuKpKhwjeB` zTW!XMYk+@Ezf^Zx*vI2m;UTe^=%kN)-az2%(xb7Q`h&^e_WI4G>$jtEj_;hC0!NfX zK6B7gI>kuRTm>S5+`0pbNp1#Cw>Dvw4ku#!MegBFk>sEcX^f;FeH5}Di#{lDFs~30 zWp~_3a6|Cs-da*nR0w+=u37^IHwxuwr5lNiskj&Z?iu{u^YlBc`>+mOJg1u>{=wkN zvy%AuDtjo!Lg7b2rHw!njb2R`q75dXASpX_j>+G+s!H|n$vX^kOp1IL*W!wd-&>)g zFDC}An3(TIa_aV0acEzVMk|WigiIZOVsNH<;{Wk|wj4Rf(@1$0tRZ4+VrK0*=Vtra zU*ab)yJ)vY?u%*UiFPfjKu_*OPnxc>#3zG%^n+(owu<07&Zf8F&AnTDgg{y7i@{J@ z*~I0Z^QhH^bH1)uS!FuI;P^zKtV5+s;V~9VJdOEH7@l{VaYG!J>?dZ^&mbzKgoW3W zjxe|~9~W`o>Ug&2C14CO#st~t(j?OKNw*gQKVuUQpbGn6O4SvNal}%DGuRNfWFy%x2oX`Tl zQ3gK;ta5Nj-?>&tewDhnq0?>)$atDQr^I>9BVq|)2^_%^7{29r2t`^`2#Ahc%sw* zx%HL>rp&5E*s`3f#qs)i(%ULdWa76Mz6>YH5}5p7m>Si^`I&F*TJv(XoKcf4J*~W{ zWAcu_UZ1irrDO+pG$|{9_-CePKqVMsZ=^2A?g^OV0P@msp z-8BDt8R@MoUEq2~Mn;}{EO4j@>Z_}x3=Yu;-_5snZ=|Q*A36R!W(|QXK04m*PtvtQ zP_~Q&aEMVY`!Kd>+WmKgGYn!y{AJxUX4^o8l|;$2Q>A{ZJp=HhVeJgM(8aOq(-`HZ?Y zW@ZMudSrvzYxazs7Z;`b?o4m1!S>sfSp@yKoF|o7!?Gqnislj_LjV(bt5Q#VvcX@j zg*mq|3-Lg>@^*nGvnpCs-Kurgx_jEEh;`GmPIlx&WLNJC*A9zs!M0l2SFJqzzS0Y- za{UO%vkNQl>C`;ZOd@BQobp?!p1$@cNys+@d07v?&uG~V8=J#Y*Kp5+d3BS|ibd*H z=a+AG?(99w)h7u46HF9LFa8&iQ7H?=SA$gnMCUJHcci-h$dfLNwXZ&JNHW-zNxNn= zF?r9lmZGyvihh-d=F9C&t8hv!1<$N!{R3yWVDzb#-i4Z1`3(EonTM=%BN6D;Pj%vj zdUd|_g6xsU3P34UX(YSCDR~p-1Nn(h@TCpraQ-XFaLq(lzff{{4*7nNXbV&fr+Cb~ z&`J%+QZ$+TYYc;Dq;?wMF^#lVBr55q4M8eyy=7g%{a|K}!-_|==VPyl4p;T*6G|2h!UPaK_~J2g7%{;a=Zqm?E6 z>#dI&2MmgY_^((Lt$En_O(+>q7A?KT9&Ojvt$drSn%rN(D*595L#ygKnAtA}B27i@ z7%b&lk^Ha`#X5bIxJ-3T-tzN_QJ0D)7iv1UGx^Ow1iREys41!)LOk`mlt~R?E5&_0 zxE?)Q%jARID+ML~S(2i> z7!l5Q&~GJ5xMGLRKVCd3$E|gRa}Fo09fP%tjEz0fC6sfz3mpaH0H|#}fm@h=w*ItB zU&Tg}QLW6?mj>n1%m=#MLP^3eL7PB@=xvbqsq12o9TA=ra)cpJGr|8aP$eQcCdlf# z>qE6o`6>rZEX3NQ_?qWrj5rO;P5dh0L^L!sOsDUdjW)R;a!VFPO?*F~wX=mz zJ&zc=Ao5k}o`T^m8}&+U!ca?|(2IEcGDnv`MYuP}yql1P{jMM^sNhIqxX2E*Y+WDs z?|4~nfq1j-(BG(icrXjHQ+bqCsm0mF1K!=(>L3u++|rxUOUE9cP6OCYy&v(p(A(6}UQ%RyJ#lxPQ!h*h4`3#o!<@b$MNu(t98{JyERT-sN zbpaqD$VyQv%(uCH?F6pru1$=y6*_z;)%O33kfr6G`xxxEW0IcFddqe$66tHLXCGR2 z4q=Oq8Vr6;xC)w^OW6y(I32Gk|4E%ULhNAU=cTM?@xM&$N1W`$jYSa4pA95~?gR)j zRP9nTBY(ux&0EVc#RZ$gAVpXIX)8SuH;w+NY`Ozn#+q*qE=1I5LwMPcT8QxRW&m^; zG!tDJtJCBt^mn5RVCw4I>5%!S`j!HyJz@dT&d%O@MnPg@;lu&Rhd`N!i@oA>{u?&# zn7y&iL4Lz7_v%_~*soZ}(z&#&!#gi9q_F%EaN7jk>PMQ9Vr0vOQ^9Vu4DbOEgv1h> zcB%hVO2er&wOx}%w9amO%)fs7#d$o&n$>!JPW0+e_;?z5|8U`o7Mp*TRZ$v~=vUF= z@qc7}c|6qH|NmPmpKi&$S5Z`MBovJzlF&wE%N}C1%3j&mac{S}kz89smKX`yvSl4D zT1Ha#ERABaZ)0m1=Jz`9p}ODSe?2tkea?Bkp67L5ujlJ^&Pn0_%Nj+Uv>7^MUe6S> zdDZ-~hEFJ)V+1tb_`oiC%O?N~*6I;Yp>gYuZ}}(c5}hbBwR-={M$xV6OgA)35)q8L z!E4GH?*DOjJXJ!xh_4^&pEn`gqs)Es+_%*$e&!sm6@rpESKwwat~*z9G_vUNC`8+A zc_*28X|&;=nd3n7W>}vLW$cnzo^=NaDS((gz=vAK6OA)w#TJ!`RT}?9$Vqf??fqg! zy{Km^A1t{2_`a?=J#eikk%kFDby2hz(w0qEQzxmSP zp}EER(Q*BlVK(gu*TMHWDhV70GP?}8f1D;A%6|Bpi~HGC7Y6xNmfLhz}X6`H&K;2lukE zs-mod!I_OqjVT-tmIdyhV8_sA1qmF;2L9pE^vG`U=`)@dmAsaG*zhv7t5_e`UU=iwoLG~5cfT{aC+JFk5lo)TH=(jn!i zOa%W(3_y?LhGrg1qfl4n9iv4ez4+E9H^6FnDxPXCe}c0*FAci`jj6O-DgJ|Wwg&UA za+*fDwy?G={XTLwmA6jUa|Y~OzOb9qFV$=Q?DZYEMZl)J%5=-z6c*|YUEAi3a3C~| z&)wd?IeYx$IWRu7Ow!YSh%UOq4cUJ!&9lO(Fk=)z`dSj-Dj!Nejf7W+7O^nR>5k~f z3|t)QLf}%v&*LvtW-66ayk!xoO>|Ri`lmSDMCtUZ3yPfiO$fW1kX}QFj_vmFj}x3N zpz2a75VXD;DiqiySIiiPObV+to`A>xCXGaRZ!+;ZwR3Ikd*DUaZ-JdPPEQ?^%X_8%R z+ehWNzO{Cqr$W4=Em3r#a; zN0tcPvI+Ya%0L(-z$LOO}z$fxRj zfQ~=8nLuiQd0j&{dsySV(;|=M_pSn3S!j6gYoBsNev}4%Q!dElkI}a#^P9KJU^X={ zA>}nj9Lt=$`R8r&_M_j}>o-Fp0C^sDK->)Vs?DJ(_QWeuXJWIlKp8O271yOWOQSWN z!~K3_t(Vn*LU4F7=lFda=?<8iaE37C4~tAH`waNKMUD_nB%S6M0&F%N?Bd&-Z~DO> z+e>}b5f2#P1S7q-btvc6SR0GcpdpVF@NC@*%(3?`M)y*;8~TKeIqW2Cn*|!>gmaB) zkYajA^B+~d{>=)3?$9J9E*1+MK6T>G8pFkz`i>g>NJR}EjiJ3|<0z@d#U0Zo5!V1uvl_>Bu| zFG*r4FSQyHM~s7b`_$0aoc@J-WyKohuE-t1%}1*k2XjfY21dVPx$iAz|u$+6fd zTxJIw^XDc>{jwVlk(QYQ9Vh>a;RGTrW1Jof>=|633=sN~WMtPY1|bt^!~>u8vVR+m z07ex0M|lCyiMnL4=G@-4w;2U?JXq_y73XUgR}}UfSu~CocfYE6)mitSO{Dc2M=~E= z;Yt%5VZq2*qm_%u*)Z#w{e3aIhq#$|Zv}hzu(z&=Huv>u`A+aY<^SCUW}6!dhix(i zBL#Vv)JXYM{u9aHX(iUtt9ZvImPdq=7T_yFi-Ju+NYl1`SQskZ57wdZU68a0*E$p& z=xa=Ly=1cpgtAu-e=kKLCPtnkJ#|rNq7GLW3UE5%eLmv2&-}yj2m%c*pFZrmQ z2i$;U-==vMM60Qx#pO6ozbNMn2cLYVk@0W;brEmwq_A{zi!#;Dk0%wDVEmQ$ELtVhRNU+gHBP^le z*jM=(q;siOl+i+MS=9Rw!5s4Iig3_T#d4#I5PU+_Ghi<@)OjH!>8c(abHT2gqnwS* z0OfA?ROtCglbL`JufdJ^HBxIRP5%(U|DqZQXnxYy73%S0(yul7m+*jo)gL|R^ zTl`cyFneVbsC39;nntVutrk9H@8pgoJ9MO?#qWJ_{-WH-xs`B*POH`3yH8XgK>7A2 zjV|s9J^8qxKusP-=vcUe1ZuGhbw@nH%{O*w@-Us}&Og;*)RoL=hr3OgrugNoT88y~ zw3nxy|0lfQhVoA|61uAAx#N%Q8uO7a3@s-gg1Sl?-1IQ%=w8lL1>tS|$RO0ZV*0gkOR48*c7KEN3T%fULo0`pEE$6aH}lMGMEtn z_n5rT2PW?=$WFu)WOwT3n^h93NDTj}oIxY=Z>yUd;70hlCwov!C|>+7Bxv8F5DJPe z06Y1s`T2`&mr_9$&_$>PBb5j9K~*cfC&D#yn?kB7={%)~b#s6gRkD^F9h{*wqXu!T zN;3ga_QuXg$yhL}90AL4rJNPH^?3b7Gtv%@q)|;#4X53Y%)jd3CjwO21-ud5jP;po z%*(Bm)M|UQgW$;er_X1>ajwE;IttqS&`ypr1!+ep+_Q~1y&eru!8+jGui{!c?Sgv- zj23BA|J~4N!-elDIoEQ(4m2e*6!}zk!0`6Ol1~2vk3F>hgwsJ#PStzL>P4o&wgVpB z@*R?>WW^HpV(;fxc$TxYk3>Qi2x8qcdOCV@;n0ihq|4vCy|-ohYz4w4vIdFT;iH8- z<@4Yg0Nh?GK`42f-LZHY%c@@v#*;(ylbRUIb^>@RP4kUyY{IMg{?IpxYlhO1L~4y=|(5Z?0__z(_DSpv?CbE#kaGBuOOiX=B$GTN9L#T1m5Ltg8vh+20mFz z!#jbO2w>h~D5D0xStjoA+5+TZ@C<<Rx93>=u9`t>x86jnMx0u3x<;oq&t z!>4`l=zfvmBO(|2`bQw{M_~&}2O}HZ2|ys$doe>R?;z+<(aM%Tx$Dh_ML1Mnxyjm)|I5;i~cz|aO(sh4iZk?_Bso_8*A&b;U;`o4%7ZSD+6FctQUe>LO0*p zqPm(GP|EujKGaDQ(6UQ~9PaSEr5YqiHe7ZvSg69pwZx|8E+=v*G~~#B`+XV0z+z?D z{6hfRaGc`XP$nFCsUWUN@oNQ0i?svIC$@2lb=G8RI}xsD!>-R|x&Eju12Nm3%oLe2 z;f7^x(Im3xpjP-EDqab;h|Sfj5H^*6uIPYyTz@#u5b=M5EOjJp_fa^Q@Msa8z@@de z{EjArdZnf|hdnf;&+F2Q*;=NZL0k>U*3vdl+qq1uoeU z5ye2K;B~h5z!=?(-c+^ z;eW{N%-X`I6Uo1RxTzJtS(a99=4^kVaNBhG`y#%@!QwqRir#Z`d zIeOe>%K?tDbiRyG95vNOagSCj;)ceanjl-@ZNfs|XxCIYPh$fpP?m`LF1LY$3eHg9qX z*bDEQ#}n%-Gj^yAlVQWYW3X@^jIR7&6;H+$dj^<@+CC!+t!z&Ir(mQTWY8y^Lwei4 z_Pgh{?MGq*LJD|V6|K3>v#+viB;`0244wg6&s*)~)q0L1-$1C=6AY-$6ZfPfyCX09V(mp!JCjiJHQ1J*HY%x2IAR-bI9?x zIk{M~7UJvd{RV`+HysMoUjx=mC1NSaf~31FI`NklMto`LmMCaM|541-!4w}h1-?;n z|1~tDTZ$o|BJ!V;94^=k5iL#wc%%PMM@hj16Q;JFHNXyd-wnyzhI7-v6Wd4+UNF}* z8@i&wOu5BS%YM=G_bSAuoOUnjth@}4_{$K*tss+m7YLT>>{erwLpOa#{ENY7d}ps$ zY!RonP-OpAf&H~Ef1yOvZ1{(%)!7CESrd1Q!7im~3mqM7swjkuefvIMyLq!6Ov`NE zH--gbUC!&MGfM}+v5nxeejnd9lvG>Yr;juW!;lB){1c58?a2Tf-z7vTr-HCij z)hGk&={m#GGn`0Z1*scsaiGdL&Z7kGN6vI4Fk(K3nB+sp>kCi%d1rlh;8E2IDZ74s z8OJWynYo`2ht=ODRKW@SDcD!Urnw$6m($cLZ4M^n6#iVlV$Qymn(2YtE!_EnBfnUn zm5&*!^ZHcRhqS#J6h*SLDYTieXZy}ORu$c4<2*&|Lj68n=xX>?3 z`*a8BKUUp5Bb-F2Uq9!k;lBeC=BW+dX#s1Fbpb! zWHK2t=21j=3~hXE2@1B>ErTK0vz;qt$z`8@s)8a*Yf3SHw? zpT$*k6a1k-f;X-j+0k0|Gq{1NJWoR81GIYg@Jt7TM2WiD_)r)BeME<~Uu~r)D^))ir9um@6?s+JN-~Ajh zf<(llEY*jvUg*>|gJUqLo&iP04Fa(67S7^~DU=oP&kUsby>YYq^ESHF`b|Ukdm>4@-v*`9h%Iu{o%mQdv3MLYYIt>m7h9@ zxZ!(Sp!M@v0a)*bY{dR>27IpCLpW_y1sr77>B{MLWGX^9+$BR>08p!IbmDQ?o`m%<5WMl*K#l-OO@ZY^E0AW_cO|K^Q27|6Nbo*S6TRqYRCT&Afod>Xay3a z5_hB2&7PA>R&w0Ini{dt9+utOEB8Hyjz@#{Wh`b35TERZR$>MDo+4`f*jItn)(j_y zDj`p>pd6mf>IwsQW(QxEh|^0>+pmiW3iHLx2I(97W7(WU*+_ZrgxO>1_U^piRdqHW z-%e`I1n386s?r)t*F3v*(&yl|%ZCVMzh2{A9nZV&_~AS^%`n5fxPL#bJ9ls0wu`5? zUMRNd&!8XL`){ND|BNn=?b5lCX7V`q{h_PUQ}Z3onlBUb#?PIrYYiV+_=DH$Wv8K9 z$4Dh#j#<;3+9lGv-c6)Nr~BbOW9NB#FR3V|yp|B7!%fTA+1X?hOSr?f`rFZ(9+-LS z2N&vE$d;7`KY+YUOmt+LjB)s;%E=qSJQ6g1btU0+za54FwWPE410qM9=lgS0IxU={ z(R=o}S+vN_kScR$e{frYDLjf`J>xPmLgk%_^P zy(!ju1cf+XU8Jup`j~pXz+?eRL7gmnt#%fTHI*WxHrwiNKu8m#!!1?n>|w3U>1VZP z^R9d?xiC@t0Q}8^qe_rS8r(0%4#Jx%(?tr6wXS^Iv#+$U8*XYn<}x1>t-YWeX*x{T zWa^LyUdZuKJ!6rQD?Y7R-${E7BKfDDDu&%{~}{mO1RmsZdB>RJ|k`3ejgQGGMM7@9^pK&!tG(E(%v86&J2d z>dng$;8ogG&_p(Yh`iM0x)-vhAr!b+TJa>_Bg0&6_jr*cqgXvmZ5=$!K`N`ykSR1; z=#4iHFe&z6?P@?BUmIP6v|8z+2+es1$piiZXr>yk>RVeY!nTL1hv}7h&)3yV}y zVg=LoIW=ZtN{kN)xk}O%)puLzM0`Xd9{NgQ6{EI?vjnC(Ziyz|iiA`3!VALDrbBXJ z)U;$t+vwO@f`SsvIczSV3&b2c5dvpms)MQ80y|;EUKRfbh@VT=#XSw-!W$YS;R zK7J0R=|iD9P~Fe~fP7qiJp8#}bEc(ofEn3uvYFjM>9IX*QQ|?ZQ>RV-sy#CPcGZYr+^YnoT!t*G7)Yqqa}I zdQsaOdox7jpuj)>}9GG0b$xf?WQqx>DIahS+u( z?fV=w+VkM!ZUXohC}(8SsE?Qg&&ux94K9 zY5RE}5U(;z<>O~KMYMj?dP^5Psx8`0Kb>-EJzaIdmqEq^Ji+n4COS$^KpGa%2IJ1Qad>4X1&6P=S|XSWjf}5n z{F2YZO(mExXRJ9EvQVOx&`)Mj%yVIwr!T5LYeF{BbArT}^Nup@|B`h!583`=cq#~c z)F0oDw9}-;jMjy^w;P0=O=@}P5S}inc5k0PX7Emfx;l)(t5#Mo?2yJf1@Q2i#6w?+bUdZ%)6b zWVVhEnd7Qne{EzNJ!yA)7aOa)ci(!psOM~}N!V~T6s;x`BB~9bKz8+-i zA;n%oOMrG)|1L9w@TTigRueV7l?gj3$Y zbU)4a({H{)^RU7$+P?|o+Pg>}qxu->F-WBG)v)@zoO-Tn9?06A6tN|C_jv}?$vqS_eN&-}Cc zqKh!(WtnAOml!Gl8Ze@w^p>|=cGpN*CXv5MscoFPGV7S>j;ra&%x1KRtS6=Tp>`rj z@J^^j0Vwy+!V-Dah1;5R9kSnk!-ayFKeAfEJzz>^bl2UX&m%CcsrEdFmcR#*n1>*% zLZhh(0U1D(&HNn^|Dno<(G9R<$gE`MA6X~Dax|#6U||EIARV!07iz{3K5I1PqGOCD z%@CwRSo$)SXueC=7365r_pKy#Kzf|JQNHvECnmdc! zChD&1ZW+|m9Y$;1rWd&(5wvfHJ$!eOgsk+k2#+0+YLv%5Tsrf`$-wv(?!<^ss9WTL zd1Qjjf@{B8BOH44ZEi-(r@*i4Qs+^KNWP|~zBZFNg@p9ZTu8V~vA!oSjzSkD?sg!r z_l4Kmep&#zA+&$N?`rc?M5<6lnvIRV1N+_TXfVp}MgFr%$0*kaRa}|SziuM2t8(Uj znGV>O)-Ntd%1)JPdv&=2v;6Fso&vi-w@Lbm4L@ztU;mxFzNnw*0eZ><2#+1#C?Zt& zS+2xpXH)66Fyiui2igAzi`(p~1;k8T4H8*~Mle~QXY|dXJ3eLdB4uHm;OtkKVyTMI-u#+JLh4!t_qVEipNn0T)by);V=FKHr>m8#` zgfW6IL=~Kg*JQo{8QWTBl_4v7vJM}s0#ld>-dY^$*nAh2a;6{5_=@XCz*B-}L;JF5 zS1!Nl2sa&y`0p0rTn9t0ZPsF1>KI`*gVq)UC3c*aOeG?`9us`wZowJCF&y`T)5V|8 z*u!%DW%Le`>$GC!Ei^4ux<$|SQpSSECucErk{S_-aMt*^ii{^qdHj{?nlRpg1~%_5Jass=-6b078Ry}+&I1IUF$mOn)Uxp=_At_$$4f?``zCI#&SFPwD zTr$_63#Jq7kHNMQd!RW!w(Tq=m2%&VzC@!T@7xYw=O@UD{Pg2dxONzU(ZAA~@+uPE z;^3x*Ae*WwviA@asgnM*GgiCsTLf5&)-^W>h9YJ?naH80zh%yb@#sp!&5Dzqt*~Br zO%_Jc=jREsAuO8Z3taTi;hq@Tir*n{4!Yuz{*iSC>VN`GD8fjTf3~SF+(0dc+w8tv zuDOer%7f`uaNo`!9x{g3|A;1@QoZ!V8F=miM2vGd|h=1zH_}-QN?Se+^eu3jF7h7Lj9Aks$~IQS2_o18<3;CYm+1 zU)y!zFypZTqlLSipFc1A8&KxOR`p#9TIz}{Jmb8;1A2<)JDCP-TJBjCe*gHsSi@=* zUD4m7C7`PY*=Q8puk>k~T#k9@lu43aAI35E+3FdHCa7W)?5f7^q(e+x_O9Z>t_K4Q z2D2g<`gh^T>1{j3441-4T&-TwfPxU87E$^&Fr4&Ak=?*Pu1rKfSMy7+Y)7AeYnYND zeI6|*Vbtkh>6Y~U!s)S=$N9UrZ^No=t^nREWJ1i0@NwUP1z0haBQIl%S`0#rQ14`M zD>Ct)$WRZ5LaXiDp#}75S7DW|uX@gB8KyiZ+VGlmW;YIn|AF0TKB){&B5cnijg4F2 z*lSWKMGz^rGC@&^D^S%dOp0AtjZ0n(WNbmA!<}Ae>+c<$^3^c}pJUD%VuSiN2SGa9 z=?WYj8WS~|=dX!DK+pii7NiIH?KM>82Zxua#z;#41$)m&3kGajy!x!2GOHB<2e_Gn zF`}e6`uAQ2PUjrmkr#xAgEML2HZ-ud z*z98vbo4}^s0y+Yx&COhy@~s0g-{QdYeH2$h-AOozi`D_BPbdLgUhuc@nTFlhFOAn zAy2uDSKRn?9!-L=$4KSuAta=e1Q3!((vM)dX4^`vZ>snVlAQ%}l@UMImlz!*%1#uJ zM+I<$p>1(j}S6%QKRM6!2as>wX#4wXq=pw^Y0plD+PEB>?K}VAULF}?=utHM-M_8or*Ie^D{Y?0LaVoM zOJyX@!5-@GC-%sxet7(&1^0W7Y4cE`AJcILKE`h+GMu$haN9mix0_Mf94AU2yu+CS zLJWpTHN^YCORrraNSF?IJg18#(X21-vS>u6p&3jAnQQd$I@p{13M?~+_cU>#)obBO zh`kGkYbxLSwI<^d-vScn)s9cT6?aTF;#H}Uk*|&yJk?C$M_9WfpreB&v8^u(F%y6K z@YZ?dDL^u!*%EVFMt2W$1%}P);b`-ze*t0{z4qWW?ScWWCO*)|^-573SzQL!o{S-qtxzJgh1RyJE#XYe$6{ABeK%*&%4o%Emnr55%$e#rxBJ z5>s)kWr1qyOMQOCpI$-m^VjIdP3gUqa~1#v2b{-Xngrv+w}2H)=H+ao?q6cK>B=ew zu;zhytdB4K_UXvz9yEo3Ji8vXf+zSCuE7)rw0@YlaW^i^V{$GR{#-m7jxHT}7F=6i zjgLnLjpt6mBE>QZ588Niaeg^?3w}9Sp$D&}UB;EQ8l+f2w}Sly;w@y_5)NQhroheh zd$e6AnWQfOfwJs3%AFIF>Xj z1138p(&c0qsA{|DB?*wTUD^)tBXKXINP2YU=rYWw8U)l{C~qAS6NyzNm3N#fsDhQQ zbEHcT#&0NMytJ2Io6jot+Ruf>u@n$U?JN59bKw+jN(6&oiF$aj74IQ^)CcX1iVE-($n4MqTHq3*mE$XVuL>xE zvsN{X#{GFg5Z-`CEMM@b%H%G66>LMy=rj0CMV=^In;*VkE_g4Qm3ik!ldyhH%;B4E ztvUc#5guV#A?Qa@1WD1WKsxu(d_`ftTu(N`P~{&Zrl>9CIgbk5@AL%iPKGtz{QR)7 z%0470MN7M($u_~?x2aQq3f$l_sWmJ`G}+2reRqiY;YHEFr+#mw7&m@uQcS7e9(B;n0D4YPiejG^0gK=f84Ple@w=l` zv$wih^+II=QiP)h?~3?}woJ>f2weDr`trcHy#33ts|(2!A~GJhw!L9Ep~esuChk@f z*Bb2eArCkLjSIY7!b9x6Anhuel)s>k24>S$DBPrPF$Czw9P%LgS20v|x_&%dVTqP7 zT90{vH>;JC6Q7Z&xREPxtJjD0h3j;Lk=)Ap`>eK$J!)7AUgNZ~Myz;P1~}3a7vTEL2)JoubCLs*eXs$Y7HYkZ9!$LRbc_4a`jv z+{nGF6Og`)=8krcJ-yfydKWI<+%kO}g%$2!zWDN0)If$(0R`7}z#SIr!qsyBSbJnB zX`^OI629sc_ijfLV)LPuCuh}H7!yRMUchy?0jpsx^qYn-h_D6@P1y0W2K5=<>?Pd;R$P1*j>5fYhG}7J!sDDV&j1BMbN>?k|96)+1==D^E$ z)%viE-V2JH8-&D;-_y5uwoREQ-u~_Z_1)yHI#^)~Qq`ysHXDfGi-E!BQogZ#gV7X| zh|-X~#FKr|Z=KA9U>Es{dLAxcFpC$Xd@k9iLQcoguKR?Np!qRmo^?tzGGB< z4Qfm6B`w3mpJKg{e%^zL0c;>5LoN*)FA%nG#bjA*BPrbj&P*JnnR>gKaM$QxO1rd# z0+)z2{Y%&^xDIv&gJhNrYBKq5eKmI&!$II6i1i?405XZm4{v)0?Ro18w_UnzMa;Hp z8-UO|mOIx7v?vzv%@E>t80T-^Oewa=>LfHpn$@QkAGjPC-odCo6}^r3#80f6$_K92 zPc9T73O>xs9b=w-@CPQl2W_n4CqS&SlBxk+@1u`WCoYt2v%}^V0gm#v2?LnpUI=kD zN&E{$bV2Mnx1a%GnLxBK4mIpz@BnSZEoU7rpY&5`ICYN4W+!Yd8l^7qD0E=h;>vYi z6$vn16APSxvY}Fg9#bhF8dh1sduouCQLgS$)%U_(5cyK81N)d;;CR#6vTX=zNke-O z)KIXe=I~WJd}a9}jU23Q?nUM9D$61BHtyzvt{SwMuVnthN9UO*8LUaqiJc24Z?zw9 zzTAtG5%Ai{6u1>3Lo;28@Vs;22Vbro8QlVhP&Z?K9q|VQ(pmanq15~JPozZmO}?sl z8VU_kP6CJlJC(6!4g0^W_2D{l% z5=!NFyV#kj;|$R z8c*r|S&Hh2$d6VLgM009TMc%r2!sfmxzJMxH=wvlY(dmmNZ#=6uc=@K$GkSq=IPbIPj^r)0k@2y%w zP3%Lz*>V8P51~6z_kOm{bnS)(e+5U{Id#p zYwNc0y8|tbl^?HTvRyP^*v(Vif%qSlWr9$Ofh^l=Sb55e=p zxFe?68fk9w)q^cqmCSH5YwPM{P+ef1zhuGz@KAj2!1eaMR-wl6A)NTLec3@ZN*!7_ z57{5#HuNXwOH`Rn0!IOx80jFhsqk$T_p2>Q+sE*HlPlII0wVb>!&)y6 z60pU{)``*Cjs|F6W6WJ4p%k$ozk%jN-!bs_`a=p`RCh{*BBS5B1@l1M64W=ZPC>FPECGOqpd7Yvf->}0 zWR~(ejrCP9(24nS5J0(LJ%rLeaB;tBfYhuxr_q$TbL26wez$Eyw`uM4cwnUFR`n8S zOqv(qOz-wQCd(mXdC8)zOs$pn(-%3+CDB@28rSiHDYcOlk^Aww|4a@*q5&FmVr> zeWS=%3ISsWX|ERkElCuB?)6d!oSWPa;akO~dCS#gq^7J7*}ZQxn_Rq1wZd2C3IpW` z8TZlYqda}>E@c4S)ALRQ&1 z80F_7rt|POs|TB~s{L+;3zviOxeXWIV?=uzE;!Zg)~a9YiK8iEqs7%F=T2*t9Ua^8Kxh6BHHwOGkLasuF1B-ER(lCxpcb5Y1{bz3cYFGckj}OC2!bx*h?h z<(q}-$-O$zO>Kqbd_px+>p_yavjS}k2S;xsw&Bqpm_WeVpw=(4jWkzShtHdo&tA~+8M-G!j5+=H@(nUbaszem5V;PI6RCKc?;y3|&1_`dg5LP!o4I@45iOw->k!ez z{fG;e5UqL`P=l710p(%_oF^S3B^c8#K1BnRwNXBm@kvmfX_;^{Tz*e{e%t+9EhXhSQx7B=CfX0sYI=HQqR#Y=To_}0oracB+dE{65`3^mdnAK_HhSZ% zs1Yxglt$2@=UB)i4Xt?q1u_eeb|ABYY@te7mvA&pe;3SAr62niJo*GRw0nA#8|%x1 zTxEMQ4%A#3k$tB^BlUgZ&8dT)DzG*nv;I1QH;~W+lDk6{k^KNE;-PMywfzE>Ru5lmX2g77u= z0+8y=FO3fa-hROc`Q$rzq7N8}+qIW&O5gth+;`vKxpF^`7Q@sIjoPAD*kqcu=w{{z zkR)zh|LzC6*O_XyJ^le5J}eIHF=IkZVI8~?e2T_0yC`r1S%#EkBb@+qa(NeXhnC8y zPFg@;pad^g0gQ)GpK77Jf=7-PpnEexKZIqp*di5?CCU3gNm0k{MUizqv*WjKnQ`Mv z>ZLiuy5qJ~Hc@j40=c)>H1zD&eu%{I!OfdEsEco8>+3AFEIhyC#{fzgT+p2URPnQm zq&cF75x7>F-oc!D#5hQyp1C7bzi~}|MI{rgroOCep4uvUI)+SX@FmF>%1|T z2y9rCloX$#HZ~MRHu3c-*=OtnPxEaC5B#b*+Ml074swokNj@W|_!hDaNv4@icoq^0 zV_0bkPrU5YGw!jjbd-0+jGP9!e zIv)XTAv)0r9yP*uvR z%^k2MT?A^^CDs~>=Xy8^3Vll*P8-DMqGBP2c0yhF_1qeugTPc3-{d=}25}7UsT@?k zvZn$nQyA(QuSFi;MdIkv`w_JsNE7Ehlmmr3@i`*i@BijW%BgJG?-#Z*i{x;H3meKs zX-WYjgr!F&kd=ftiF{18g}UCk4cu6Jw-1}7R}O=k9>MrxPQmzwpf2K9U^Na!f$gfY zK${s}FH>Y&MCKYer3}Gu5cm5!&=hym6aqCoUU`sIt`nb^ghpmlv1`N0LwjAoC-J;6Jph+gDW{a|Ozj$dJd@F46C#HSDh0#wBV&&8)2C{^I~ zAr;%U^+!O>Gfby9{4xwd8Gtl9MwVGKLUg)M=nfzcQG$GQy9Ipks3>_qB7wDcp? zQERM~E8`@&yl*2RJzCinjFIk1?D~)}bdt^{wb$gtn8Hjlj9_cA!)Id3VThE3rPwjH z6=Yqv#eGb{oNI#u5+Q1!lR~`LQF`WbHh`(&)NP^EY;XM zpA6jp`(CuyR^!@imqPWqV$!+(yiXYY^8cA_y2;}!_A zo?#495sxNtPAK7y%0lxq|t{so4ErEZ<=$X_De# z`<`+FyHo{L2ao`Gq#pbT;mf1GR_(btaL;vZyyW}YkpA60g;l|{U-_3qJ#n-YJV<+j za1y+j*SMt;LGh>(S^%V%xBuWZQ2*{iler-_j7Hs@1-QQW8@iE2l7wQ+)nF^(in40P z(~RGw67wW9q3S`CIfE&#Q88~uou{~qs)J>B@`Y#2l2qagWdhil5i zPcGqHRT;E%u*&95#n?Cdm!oumx%!l+Od*csW8VVKO75X*?sZ>yUjiE)=V zjBqj*LmLhC2f$(00Yf4KKxVTx)=f;!s-QN%+NBNLkSRr~+2Do>JUOxCG_Tze_P~@q z+1?p)nAZGIUJ7wdqlh~MCFo7~KIbca8EKNPXL@Pw)6EbUTIFx3u;^F}_>r~G?+;f9&(jrr%jNo1B( za1Ijp^$%C_nbSCRA8Q47!!E^K@*6Q5Us#_W<#pDmoI?n}lji}4Kfd6jyZ^}>cmoYF z(hJq(--dCgM+>$l=&7KAGAvGdy$zPZ6*yH9MaPAm@&__Eeow8Ff9pp()QE%#=HbNy z@EN{1tT8pU{Wo}bq}(q)AJax#)Vf8oc>aw(@8u;=XZ{8EL86~8YICa21GtmBM}cTd zf>`y~>RdDz4SSwl+W~QiDIS&*3I_sy0x{9dRXl+UVAI<^B*#4}i5 z_h>el^Z0a0Qq6Lh`|kKiO7$L2gK03ZN{5D>yPxF66a0oG~Gi>_ElE|L$s$bSlNG&m8~N70WovaIR0W9 z>z==caw9zUd=nke(BFow=~6TUoE5bIzpMl8euHAp8JU-O2NTucuHl8IEkk?T9(=bo zi3hO_T`Ki}fKC}DAs%eQlwe-+y}}Jhp@C5{q53*d*XLaP;m*S9rbxC4@{<$y!80YJ zSs=IJ5gy$w&<}1xg8Z7UAjx5-f>T%a8&=x9G6=z!3eZ}bbxK;3tO2o~P z9eAWWXEO*J=NnCCY5cLlAs_MqQmQKR-^6X$20a*>(<1}__;hZpYS8+1M!vv##xPu%z2uE&=mYh{tv*xS8&HLug=xxQ&x*@R2OMqJ%j7> z$1)os(E4TvaALk20BGKBgX%|JDA8A~_!2-Widxiygt~oFD%taKT2B4W7Df~Y0Hec zLjy0Dw|_X1rpHU6S+J{b_#IZA^Z(@8l5LfrFIWoC#`$=Gr*xcej(GyZ5>6=6i(6&2 zw}9f_ecEg(VEwDqXpvy-fZkE6?^;J^B0UHT?C*XmXeT*b)CM)XfA0@*_^RJYF1&o_ z0O;Xf&8O1WIV0o&x#^<~#XPr?ePVWE*n?;uh>TfpKb)GXH5G=4Cm{v+xZH=FitB=tI|QU?4m5cMfAOGl zf7Cz42J3nQg0`sGKgAHrgv&6}py02}i7d`n3CBSOFWD2fsDIihrFMZW{}_%Fd5WYL zXOF?nHhyAgHfhdOk^EFeQIT#9X`HGZUgkaVT>C?_cgL-W`eHO6D6@rr3b!= zX0^>XE1I8%(TUBsF=jPO&dSMngKgc=yro=$U%k*#MD=%fu_()VlVzAK^(vu0!>#Uf z$Z&9$EC`@>>#(5HLws1z-M!AvebH>Xt;sy(o3Yibhey;z+4O=fVKVxE*gal82r_aa z7;#ySPgKXLTbj%Vn#|wkvhqhxuJi6)m@SNE1~0=_o^~now_Zmt(qv{%Tht#^ zZq1R&DW>8dh_ZCbO_dwDuSq9t{?uC8x?>r(#v<)uzE@D4zNE5ha42@LL)<4FKXL1!ADGU-t%ClN65`nVGLE@Y?oH2 z_=0cyNxPwLXp=Nm)R3t+CvDd>+xBPeaGE9Lq&D1zjz3=1?roIVKh^AR%8%K+FSlSz z%^hFoJy&^Q!jB|{RsHee%NM!%NwR!o@!rahL8#l!0H=M-1{sBpdL=he!%wcS?pzZ* zX#zgGvb^HV5G)@}=I*&{zXOJ!{K=n6pAlC=&Gx*7Q)hbO-|X;;I|Z|I-f9NFU{x7? z>zI6wPg4JMBa!W2QHO@EnF>Hv$K14!w=S9WRO*vM1;tD{a^ZJ4t~Z<$1R1Cd)$W8l z^7l%m*q%G#XlG`!GI7^EpR?C$i{*9NJJ(>c7jGyQ5pIjRghr0Wway!diHyO~ zH=8cshsMKW4Wsq3lf|Z6Aq8Pl{P2@aw@lW%(Pj;58J~5<*eS3KvKK2Na}G>7(Y1W8 zcEb_SI*@TD3$U~DJ~6ZQsDX@U@Scv$@aBvGT~jgm%G_A-E<^XS+TsHHPJZ=Il@x1A z707mtMUH`WyB4#X|B{@a%etl-mLNF5sg?!So1{oDDx~am=fV}0mSM8%3?kbdt5Qv} z!zdeI4Z^KmD~EfY57C?96vl>;h_%NuoQJ>`vZA3d=e}iu0t{8JBBVtHzOa+@CSy5P zb)v&`oqlj6#h!l`Z|cnSR{_B+C~8aU{OD*cl->qjVu@pHxd z+RoW$0_;@4$4wvaBV@lS6$v}H0Z{!T2wT=PQBUm^23vJx7jI$CQ3K~ef{arx_*hp3 z(%t>Ztdif_<~o|Sf^#MSMjIld3ih>VF*`?gYo)XtPsw;>2w$+0psN*ajcy(|k~Gks z1%-A=!(cd-r>Jb-=IPTkj?rX`0+uIZYi-rTJQI8}vwgMe`=$bRuE#KkGhiqlL;J^! zI}gE4#QvU8}jpP(K5_*DSYH52Ox|a!JRdgobGHHFjsZXBa$wWX(RV3~erN?#d41Y;Z z?zljQI(uC-Ps>!{uJCu+lwoAd+@Wz+aerxW^6PdndQSXY?J(=$;@Vm`&lPxRpT(W% zqZuAYseEN>+d_0bX`pZW6>#{Z79tfpVK=*;(h{RcGt2v+t-(~k^W<`6L)FUyzLET4 zj0Enub~7dpDFo^r#TwLO9em+0vx!B(G0Og~7Z8`sdaPS2e?{eY^-tcG1_x@7uK-Ld znh^RQu<{ycamKzW53Rov*Mc~#sWi6>bH8b&Oz~ z{xR-s&1q^j&w`QcuXQ;oQX~i{@83i_C}AfL_grrTU!OiLLw6OW^Efr98f3GCX^&T8 zrqACLA2d}KrQZ-KX73YF|5QQ=7(Tyn4ZhOnR63F@$(Yt;s?K$<dvCD!&XA19r68+Po)5T;UGl8!PH+LmvcL+a1stzlX)Tnq z;FD@y*|-A^kYSHfD~6}N!4!znb4rO~y4Ib>mCugzz=WWX#aYAL68H()LhHkgIS&~6^Diay|mUU1<5i$(fD_aPWu?~LEtNXq8_h)+Nyyu+vInVQX zKA-2DQ*T;$gIhAhef|>FBjaO}xt$97tbX_Nl6b)(B~NYD*q=o-o*|`Nkd>vRUmQ`g z7m^HFIgbORm1zf0R@ocIWG3zu>*or2Da zmOGXTyGqqiSEW8#rq-&%6iJ8uD+3;P5~h91>B{bC*jRo#BddP~p8j$(^P`@6Z-ml> zc@R#IId?j4!~}yuc;yoW*Pe0EaL)g_&!e!>5{b`}e|@a{kWSx3Um))OZicquCU{ug zr+ea8sS2O;9DNW9<+ioF&n^PR7GU7e&)T_~FzSJ@nTwNYYJ|>SPwBWQV-EO@3Gh8# zwdS#d^(LjihW=!5!sBhjg>Ht0k`-tFvew=RYna6)j%-!@rN(`+4DpY8}*{OeQg~q4b z_Y&>p|F=x^FTkVG`^AaQSpX<|m!Hfk&u^qnY(fGa@r1A(q*?Sf4+Fq5q+4Dz#twM4 z)*!pnSD(krmcn|qe1M#oqq=$^T^PnVih@wFe%k>_O=p$W8%4Pbm&IZ1NO08ubEM8{ z-qa#m#z(z;eN6-r(dQY;?p{z2jBuLd8Di+Gz~j94e7w_qZvk^We+rvWySJazK}Ea$ zt9>s%bo>oKuI|8ZMEy(XqY5;Mjy9nog~@Aba=k^gg*?a(2+!kYoX#L>-lm?)ze8fi z6L>af?(x$h;q~ucJ!zsQw^HAq54(M&c5^~J?>REV#)%ymRxXJ{ zX+OdGWv6dq5tOdd15uBB$*I+Suh9uGLQCeXPQ^*E`LTv0lXx|`^dg$FanDVaO^D@~ za`F-v=SFhHc^a%2ustirMlh^5qyNv531Ab{dsSDp_4U9c=$G>s%K>1sv*G(+Y1Io% zovwGtjEC;uo_t~6w?izuc;pZfQD~3L=GO;aI%WQe*5);l?$d*ZfdBJ_pv>p*mFxVyU@llH2VNa(Cmm$#5NTP*5} zOCU&FIa7r$J6~a*d&O82ZFEY--b_HuzsZjcpU!+8bx*~}@D?Ea2-I-Tp9kFeKX4iN z2egWl$3;Pr%E;tyHRB-<6K8nR*;~wF6neY)Q*X6+nEDa+$9i?uPUOj)|#D=oilPQekrIgYbHWZ9PvM z%H-rBKWO_3m{{vsWGJJXU zhD>C3&IWi%j>vrVZ?9$RhELW_eXnG9AG(wODVt#fk)`L&1vfKgn69AUo{Hgm{{_tHNR8t@5n?-N+kjGp`N$2tRabY$Fnqc|&eXY~uo;5`irA3b^D0!2AK4v} zIri{XH6Eg$JrGT`Du+#v5rcicfAJl^ukQ}2ne|oOwR(&){e8!Ki(qgX>(4LHsOyQt zCnH08aR;7}Z45iTEVrOw01_R~9bq^TT9Hgqu^kge8+|On+fjw8_@zgjcwvc-=HW)Y z@VHuO8cUIVC*GH2+`ox@|WZh#Rv zpoR@GvUnuu)ZcM4(McFk2CpQFCte&WQmMCauZYAMMHbCPJ(V^bg}1!d2ciK2IHxeV zHm4hA#X}snXII_%yRB4FXEpv;1_hrHzQ@8A4#^HEEFJyyYW4jaWje2a-0<(*18rXO z;jjw?K@Ubd0O)%XYg%{255z-Pf_eAI9LJHpAOA5Z_wvz*W@ zk*)~HFjuR7tq-F@-&npf)bKwC;sN6=YynUWm{Yah!bK|*x2U()QV(ws`Z6eUyIxYB zq89sP^|gT$t$qv;kCZ9Rdt47iU#3TimrJux^^+c%X_m?U`;#+TQV7aURN>@Y1EV`KfA15@T zjVH7}Pqy}Y1iAqtloCGUDVL)b`|002U;eR~nsk3RZMdXna$#368c%=Dqv1ELw729c*+of* zgd5A;8-vcT2v~?7J0A@F;dRGcDE38=R=8^QxN`lJyH*6HGCOX@L<3G3{|pnBXtX9FW2oix#=N!maBC6Q%_I6<{w{7nP8M?v@;l{cB%!%aWxL{ zxCTu`o7{vGZ$eRf9AAP;gUKV*p(8W%iL6S73vOn7t!}MPlc5g1@slcaq12!s*b^Lm zzeVNT$+ddq=b2tzQW9GTMQj49mmWuUc5c&zv0Kv7x1kt|;+y*G8CKycY$MuJ#oY;u zroY=o(+?%%aP?D@fOtLjF`C_VNUrNpS7{1TLp4-@VjRvisS_NkZh7zDAy!`r)Lp;e zkXeH6ioMJb++afk%C;goYDVTjx%bzSRq+i{)1@J-D9+R5lw#D?5n=8$s9IMmL||{i zuOzGj9&l&n>+-&L0i(Nvf|`R%YUa{pa&Dp#>!{PC2rlLNP|Ygi`JW%wEb2|*O;-me zAWVw(pUW>Uf&6ws9I-5a=%pY->(eLCEOVvkOYa7|!neR^6mxcEnfHth7IdjtAZx%|GDpb zM(IdfWp5T??Wv>^1cHTaY?e1<5Ym7V`Cw6uqPR&SzJ z;hy8}heF;(^-Y$Vvp~uxL?}6)QKu_;4^2q)F1oy-!01&qC5Oa;ae+W$D}$FyUREfY z1cu`j-f|YQz$Q;O)sBrJCHVNhI=oIP5!uKuC~;nADoGx8eTytyly z#y$JfN>2b2I%Dgap~v0#M$HTA`Df7BQv%vwad%Nf^CP%d&`o$$Y}r>-qN1q{hWp^< zZ%2HVmFe@T$c?|_V;oQfRra#~aj&*gc)21umDNq*o7z=@Q-Z=CR$Ikz&S|_hy#f|3 z&=nI@%$FW%0d@6#;DLg_9>u3JP~8I<9&MTPVCLsL|HJ97QP>0@prfkyb?=Rnk}|#8 z+_=tlb2@}IJ%hyIVe*k8cW@D&!kF9orD?t@6yLX&k(5@_azhmS7djx#o#10lm49P; zS=cVJ_(h$$nS1ktf~Yj~l(tKhF^}=k&5JueLI!izfETg76D~Y;LPK~oc)8orYC19n zoZ#&gPHlH9|KFz+wUMj|bLfa9yI6zuBvUSx6EbD&m9FhM%m%&ZZ)B zJJ2zzX3v!8`Z)tfn3kmME={KBWyMiFV-#=C$^DubN$M)|t%-dxoyfk}3iLXPdrUi# zR+6hop(GIHC*e@ubGsMsf=!{2?xZyY0v8l)LQGLB890W;SC>uE#p=^F51(-b?n{p8 zQEJuBt!a1(#2++nWw`~2LCGZ-UmzK`&>#C)*O6JO0)sjWs~HyfV2=>~;+n^rcFcv_ zOY4pd)ixSQkFOFwb-Z{l&xNXpW9-pgA=;$MyGQWpGjXi~7HLtErvYYLvJzOtfO2u5 z(!4H_ANYm7TnbLg4`^NKvGoFCSyFJwuboTtS{+3B3i6(jjLisP7*+IUsvkVZW?n|A zU(~!bV0G~-{fqeK<@(j=T|Fk#HG&JZXqP~jL6?yG#d`RK0vNOs7c&?@d{bL z5Y3fQnF|N#*7EGSG~r(MD;S$eth5$E; zBydQlq7jLp=sEfGF!DY?luaIo4nn5Uu&6?AFRw;3Nez)Z*TalKT3o9&KAh0bCRGNB zFL4NozAf&`(1DfsGo12#7EP>U0T)Wi{XevDj9f71&OhWCr}cBA&(jDCC_k;t(^9kT z5dwgWN^ttS0`PsufaT;+!1Iqy1y5G^sW0y5hNal91)0@F%2=&dy{BBN8r`l9|{=-1tjOv{V>%ABP;?_+H0+3hTbzU`L!X z_~a#TldQB$^*-?`SR&T{+xNATY0vckr5Y+ax(;R9FxIi*fq<=a$Q}j3=3K}H`s>NN zCKf(%Ibcsb>WNrS@*iSDZY1@B2eiF@V&Wg@7JG;z6D7g#{mR^Hd>0Y1xfmG-it&SZ zY^D*wmi2f;0I=88`$2Lk0yJo$rhTyK`@mG2Nk@#aM*cs_MLQdDg5)TvQ!(0P^Gip3 zSJlSvlraO2GkD`)laF8Yw1YY*69dljhZ})OK2jelX92U@p}8oLvZs~WaZ^#8f6-HL zs4jz@zU3q3Meq@-bvk!du~VM3nP*tXcRc+)i`9}zd%==!g}!K66xrPhj_^$KzE$FS z9~jHriYxIb6vhg3E}V5>_s=hI)Miqco&;ar0FL|Dd}fa8XBN>e?QnbN*Iv zPuf|ipa9_nPP`?JY{BzKB|ZI_>7Al&Td~4AG9#aXq4Tm>SHp^YPRUpC2I*EXu7p#< z#{MUQ#^?b4b)P?>MY39Y=6Vks!e9^dxB2sg=D)zC==3@Mbj4lF_MnXGh}Cz{c3WxJ zwaKb^WsqZe)x-L;FNE9_&FSI~;jKChe89rVCnSD<^C`~>bt_g)TZB`WMYe^d3LEb! z;CE%-V~IB^cQZdY9<{h;3`}&0cjItAxD278x_B-uald#`tc}J!l#En~*9>;O|Uur4kE_dta{g*G(z> z;TZrp{0btCn-EH&H+q~x!{c&io=ch~7ZZy1P}_b)vmor+G91zF_z z*h5Wekxo>e?uG?uIt~o+Z5}TlbggXK$jf$h(32N7bV~cZIA!3MKMJouWAdA-AMB=+ zftBx%a*w_N$=NAjuscd|A(2IHLG7yIhpYaU*Ksmy!7pmvQzzG^pmvWMNeoaO!QrIL z*T;2A7}}ER^o@Fh?4{tbQuc?x`Yk)jn?WZ$4N#R-ZzHdB*i5BV0hY*eJO&KFy0}1w zF^qXL+tWcm?`%+~-vCD5BGyIY1(2{}Hw3?O zZ0X_^^RKBQQ%#N@aFlb6Su_)p4o5+~^O2#cJ zIrA`ka4XRp*ZDZKIN>h`6T0l#9p1L7@3UW1Hv~s(QAr=6l}BSVT1kxxHz(k}uHU$? zQHCN+^d3u@vDG{$^5$z>#aSJ%pmAe*fHy9%ym#ymsS7aI!5m3g%`dg`LuQx8GQ~AO zOls0Ys}Jf;Ch~=eu?M|BB=wf9xWU-I*gtR98=KyTbr9OB5`{0ZkE~28^%r(SD+Z1ip3teykR)ycTVIYgKt*Yezoi zu-hdN=Jd@2cA{J&q%u)ovG{OPFZDJP$&*Z=c*#C&Xep7rSU+*<7{1lGutR(QiT33f z$O5ho_Pa}{^SjKu>o6zuuR#;)i}h-Yj@5XwNGZzjf)tSC2=doYCAZ&23s#lK@Prjt z4lWdL>fSKz1?~OUCJVg7BH?cD3s!$-Ez~V`A6YZFhbO0en~=2lUnE!olD^}ArBg~z zv5Ps6ojt~aXh|;6fxGN@Pqe;C-zMNRsCkwA8f4sQuBx7{gob|mB@Chw^83~oADe4$6+oVmSjrW*f1~#u)93qLp#h5z}N)1 z&MI^~5g3keu@7g)nug%)n#y6hr#NpXl9-Y=TIX?@BjCENDkQA&{NfB{S;O__l*8{N6Lndopr-B~Hk&`0xZpLIg z79U%W7BbArLvG0JQXy%)57N4@8D}3eJiVi^n{wmWyi5oyc}^M>@kmF%YeA9wEJIKq z1B;{S9GODQ?1GJwJ<1yqUTw5?0df&9aJ!>;IUe8xa3}a=0IX+O;H~DZTfH5P-(cF+ zQq9e|#RN`0!4j!uUT8wL8-vt-XM6seel*u7&teZu0i~;_ zntOuY0ZR$#2s9r9UR#QcRjL1%K-vnh?Z4>t!$=sfZRX{GX45f=!Hu>&079YCuGm9Z zQ*HjzQ40rmc0)EOxp)dXceNVXjaoT(Lk_9xT8Beunp)9tP><}!uh0cNkH(c+6(C6z zbr$tIvM6|;YHw6HILei?89?m``1x!xWD8{mv5g}!WH@YU?|M;&?6MA?KH9E1)ExUP zIOul}l{W?UwBRA6UrvSWymKP?s8l{?wi&iJ3slDDcIBRw_jyj-iefc1fc04k9L3A! zJ4HnaOmsZ$h@%957}+_FGHzXwxUULysQAbqW7}?o0vKIV=OHr4Y`sw9)Vd3?bnp4M zO6BI17+$VbGIMqqPF|}&v#xc(^6b4F`!q>m3@wRZvh(I?3He8z$Kt&M`~0N}^B%#3 z=5YD{WhTh`f}ipCvetlzy!ltV@# z3Y>%&UjYk=uwGnoO&dn=&>K~hZsnci1PzGdpihsOe+humRuI{K6>M_Avql)7swd=s zBGA5bb|Z4f{=Z_S{Llz&!g#kKa0dLXk+}=yhxL?(V zLdGVX4}}t<$89DC(XbfTeuYp4^0eV@x*RT4zScBSHG_b^Lu~!U{)VD@Nu4NN7{0+$ z4gHOBy(jMlGUe2d|M4g(VgShg30I0g!ax}FL4%qd{p1)Bk07i~!2m?l!ukhRWvh0e zbz3D@Me%YCiv4&-QCtdARq*+L^h8C6`4uwmd#PAuOM!J8N)v_6?Ci+E`c;>#E~jj+ z+M!av)`X70Z%E>*6Yah7CPZZUM-Q-{Ug1cT`R$( z7%A*;e(2J60Xztf#{$cLq0kgP0iFE6dO8I>{peiDdhYAaXN@L_f{{hV=0ID>?NnJE zN!e2Mqz8p~po45b1g%v3$Hv3a`ch5*uk!Q`q~1TCqj_3+a2{G`kmj2{u19X7UXoKG z6%sc{WyE=_EGj$*#S+i0+(HHQ*4+qiHJ1l!#~wNrt%+Jd0qDuZRs^$b+0$6U9aL;W z_g+tVsL9EXi$jOcZmt~>4cIw{DvfvPR zmJwcVgtG^g7C&r|+PH3Qx_$NwYQWqatplP6mEGauYN}D8TTNL3iUl3-dgN){vr_lgM#IN~8A$6ZXvr z%~_Etg=tIYP|jG{hN863E~0YO7=AtR2FkfK_6HnBD=}_npNzm}y3Bj>FOK-cWtF`j z+=S>8)_KdK1aG86QG;BtroVE{7vY-u(=N zR{kgJFToL51=*cvmC*cxb=v1RoS5ZNE+19NxB(sEiZ(F~j+pYKU8a931!_7I?(}^C zo2Es#AB2NOmc}E!7j;@;JIZ^n*Qm%ZkW)v-X8H|D#(WIUL%&nl%!pY!8P|gny(v4Y zavb-hl-I%gInn^v{}vLYklWH|^>uPszm%z) zGF`Lu@W!o^ia8~Ik&iY?1vgIlmh4a=RiS+;SwV^A6UfgSbFaHcRH%+H;yh0>Hsng( zi&y40I;ZIKD?#l+LPJ(r29PT&4stjn7wNDY@#7bm$*WUSySKfBdxp=N0{{gD!8RKU zyS@1r>oo0EJHf)haQ2#JlX3j+gvI<6?y66YN1s;Of<3p7&1(VEQ{*I;WF^Q}<+fh+ z=3iur zoI*4zey3P)bg8`nH1qYKDP)8n8jq}33#BVf{D{tH7PDSN7Z9waTEuD(RZ z2_6WF@I0V3(V%IEL~s9{VHHW5sgY|3MIV#uW^D$iM)!J^!Ha8Otk>>ayI{7TUk46e zLC|~t=j-GJT4kZbR&_@XD8=bCF6+tsB6$6WqCBPEZns{Qlrza0>xqtlbK_XuV*x{JJE(A-Q4~5E5cdN4g<~!cGELSPs#8 z5#1Um0uGNdOtJ$b1WpIQAvmAc%S~JP5!LQAr$hdq`c%j`n}qKvV@a`t!KG>p&W?0! z$iO|xMGV*H4cpxZOjjKsHsW3!2ojv?L8BJ;K13BtSW0X!g?WPZ-kA@x@KbclY9_Ji zl?gt`6$nBwLH=ervC3{78!s94)nffO5j@MwrK9d zgQuTqzcpC-1NDatzUk$;ap0iJY=ns)1xRAlDH#Ux;09_c5I|5Txzk@^P_!(~_x=YC z#6jqH*`D7cEHSt&8R~gs$AVcE=~q{ipeM0^)l2rTd+N@aX^&fo&A^V z-jC>CI(A&~lum^h_^duS!e>?K^b`M2ZnnqfEP-r?V2QhSdS+bnOu#iqDxt6%B5g>k z1NEPs7b$0u?dt+WS`WXN{|9sU{Z1|#Kw$1f=T9(bPcQ@J_LBV0@-_+^Zy!ffjGeCk z4Ycza+HS*CC+QEEFQ!U2Km6VWvIp|rF+VgYr<_fmL{(0OJIC+`-)*uyH+fT9G9*ID zb_@Db@M^!=iecoo4O)crr92Pi?)Ei@BHt$)YH`=Za6 z0jc(hzp!{KgD`==|EaZ5W)Dd9C^#l5%M35#>b#^z7Rg|@0;RU=6sH`s2aW?Ra}*|i zTfU7Vz(wT2E0gB_T~N%7_M^ZYRLOfA5tbGBP?13U5|fB)&4=)@?_~b;AT0WcC=Lkf zFfo9*G#mEHG)$pCulRa=t?)3c_z`dh6VvlCp3&DkN;?cy>z^R`ZU33+mPv z`PkPrF{lTF-j}Xt`R4-)Kl|=o3VxfPy@lVqm+-U(cJIjxvGlC zPscR}3Qh$_2%H)a#uM5kyEjScbJ0FZycQZ7$TGvnPL_)KqvTt3Fyb)%Qym09$h|Zz z|E>4vVv}-Qf~-tXPv$|=t4OS-NeC+uEr0Aqte9Wa>jj)wE|awsSTm~OJ1sf>Nj~4A zZP=|>kwq^c!OiTVziYp@!zUP6oIp|J#AaU61YN%}2Ls5k9^R?^jJnPX8#3;$Q&sbj z{L%;Y`*s0=!^H6F{jQ}ykq<-Y>V@i%JR!>-<$C{#rAVw_FYSA#2)2rUw5T3Pk`@sjUs#pJ&e8<7rerXEnFJ}rpHU;-8Fe55knc{fwKlRZ{8aTz)q428H zAbO|BJ~&Yczb0}Gsyr(9aa9>ZDa`MERmnE_SYo~Zt}mBwt4ycVo26CJUyHt zQU4juimtew&EYUJ#fM#;3*4i8Pdajl1Yne8yoRG}C~>ipz;>mQ$6 zOBddVVDR({o|f+O<)*c}WosJeOp(8@X-$8)sOMfGc7DK6Yg*Fp0p`qBcFv5+MB9=5 z$#Vt9wo~Hic}lyRKUlNuj*oZDH7pF7m#rqmf75m?BROa>d>#k-zfbFnB>4X5UNfKx zr502BAIl7Tutv|nDJ7!CSG9=QsLL-}Hz3?+Oq~_n>$)*+-0k?s ziKS)li7|IshX0*ek*CETj}EphGvdF6mjug<=&oGPJ|H<+B%LL&r{;2YaDiIVB2K~m zN4~A=2Ftf?WiElKF`KdYRO4*DTlHd!mfD*Frfzp1sv&6n!Qai7onb!ey1nY`{+Wm} z>&dFINiSCd^Jeyewb9SqMipmYI6aMX4dh}rEOjhduQX>FStxX+i%OW@YyWu4B~bpR z2BPt*s8N&Qpt+Gu3BR@0)+Xe_C+`wiHj#|{Jn(pyz;bQn^o8uqEJi5g(Uq~x=ADtgi`c6)Ah}L+T8&HP69KNY+2zi#h%hi7nz9(sv()pWYG0*GXz?db;uz8A~ExOqOKzqGEPo(>_?- zd3VYuZM8sw3(=}ifWb?jf3=lKU_FHvw@vd*oBOnGmLLD%%a;J(Vq#(3D%5;_SXLov zJaT%VOg1-8=fv(6>aOe_@R6U>GZMEq=NJW38BNWk)$c*2h! z&z9P{=sj4XuRQv_wV1EtzwMioMxvuEPU2sQF}s<~iyCY>0gg$qL<_3>meohvKMu!J z=kMG*_DEKyO0BMBI?HV30TY8f9v_Y$yX9k?nXkE>j8o(Yx~>A|vyxXocM^$C^(ZGA z@`!8j*k3@0{7ce%aM zN84WQvtZS1LiUV5_?qYa^_^o5&0|Hl!=pg)ahjRCz48Au5kMu_M#>>ia8d+k&O7Y%QFfkr+=BH zY{46sZ%$~ccIA_wX;G55ox%jPx)yglhH9R+cS~ucu;w#}`ZJ|tuj5-ja}>hgM|Aya zwkh<5iqwm}o&s-&6SQ~H=A=g1__OUsh6kEVYahev=Q@E`qrY{Z-CM>SlC5FR@gs(Z zCKiZ<2eHOl40+!sjNul2;KeE_WjGw`OR`jP(`Dkyx`dg)+K$Nd*bEj$PkarovZsM)U+(0-WWz%L zWX-PX7uIe`!#jK$J$p8F;ZL;ThC*3Jln?nCws*i(sD`j4rg$?H2~aszjIQZ8xwcvF zi6u|pK8)d0pDkT+(!ss8UhYPUucE%0nO8k}?Vn}VQLCnDylldVjlXWm0XqQnrs~BK zZAzcuzRHxJJ)ZhGCUn*7nhzUt>~S_%N=mBcL3{7&>kWLeHHVYDkf$Fcg9oB2l777n zBlyw`YEOlP#})^*5?WhjYdG4Wy8hNBm%#M7ThU%GwqB$wClE3^eI1Vv1jc`}@xdU+ zUYvBCNFDRh6o~!Hg_=0w%2jo8>Fwv6u>cLa;>R4y?Ru&wR&*=htED|EyV9F=+#`%1 z*)w_FO)Gb$RBL1d;Q^)A|8NWSc%#^G#u}3Ieewr<-WWxktW3qbNX2>NSh~KODyh21 zK;L?9M4>o|`)}}L!I81$+>c_^T>`r^K~1&X3~>S+<$e4`oz}6!>msQO0fh&Vr|-U! z0{tB~v`}#1vjK;7+6n|%R1u5m&n;|S+2mQP(8s#VwQsTt={iYD|}!|ZKhwv^~I zdqp_#ns)zSl*frOCIcADt@}TuRrK(@z*7PBO?R?eZZuVRyVp)|h=-k~7XQ&~7IkyM z(Xj; zpWiBmchfRmBe+ksv#VG!wYYuTyYfWVx8{SyT}~uf@AKWTGh*A+QwR;N4jqYB-=5$U zW$Kzu$%G%TAFx&Fr28|lFCLYDm8;=UTtkBYG^hGoQ>5!xGVL|Ba+`yy#fp=`Y3-9a z?X|bas;pTFY=LAn6i;o?fb9n*R@#kzW$ycsr!@lGVHzJPhwEpXRaT7%1nkW7ydnMJ zeCn^C@!#wv|43au^P$-N$u}`oElnMOFJTRL?wv-EJq#cAq1yVv!^DVK6BDJ`&mliO z4O20ee2x*?(@(GE4s9nq63LCH_7`#@Qabsa59|l>oSyuSV3oiA%t5jQGWy$2df2~p zBpG$8Pm97Ba$a#lJj+H=*l+VH?W8TOOzob_z*HLzpl?Y+B6Y0<838BH4a&!5I~wBJ;e0qcS> zKmD?IqgX|BgLlwYWx9?VipuAc4h{NR;isvh8Q!|7h5k!jIHLVooW>pSvJCyo) z;!FOXZe54O%lymmO^PFnQtDHCJ^?+#@{OP@&!pvC5Tl-MW(cy zKpep|hOqzj;|*2si*upb4-~-z%OuDwTA{5Yty_mmkU88hXHTyM-+B@$B8ai7wYOoE$93~ZbY?SVnx?4o2XJ+#~kMI$>cyl zun9~u`jf-i+e{1Ys&1O;>D61?W|bzAYo6W}LGWe3vTgYGZ`h1fD{Xcs>$|5(av_%3 z-gfbH+ZcU{kEyMrQKjGtGrU$=@{TyeZ!~7BdO&S8`v9cm& z{g`0uBOD)xt*yb3u0g;Lw^yUOQq5DA{slw9#(3PEEF z9PLi_MxqF>07HJy*KO?!ME0j)gxa_eYV3FJ$UJXHNNSi+@ADQ7ivR*%imBN5M6nqd z4Iq|-2b!!|uSQk8pT(7oN12GjW(y0GPY#(hj@~+03$i)E6c1K_vtGEVWDVCakEbzw zv>T*HKd1Y2R}@lsh~@ISdLtvcPOizaNbr3jctM;cg}ZlQmjUD(yyBVPj}59WuyY1H_(hOjlq`{I{HY^&kl>7zw3|tWsVEM-B3Ln!q6*e?>Su zK(_D(H2YY6N{bV?RiWDzZ}M}t#U#jDs`NvtV|kD}gE^tVe843zp)RrgM&pKn>5>-KAK}4nE=-*j-|@}R z%G9XSpO@C$9^Cs7eU|xhNkG*g?TEKEt2ho$@4AtsyAZY>0QV4;9q$LnEMO1ayKv{=fNbuJv0c%z{Q8jiXMADi$-_C{9y39EkD7o?Pvs^jJ z9yU!_lN?BQ`*DF}bgNJVsZuKwPWA3ycl$vFf9++NoCqhZkQ{a(!K2~k^-CQZI68}e zPfdOD$g}nxNbMD)l_)|FQWPIXFr7Gb^1)=Ef&R-A`b)!&lFs)b<4`q~uV@4QCDxQu zGZD*VB)d}A4CZXlV=QMXW}k?ZuMomA4kN+Sa8#z^OOBlWEQ_zm$y#l{#fw%MvlHH8 zr1nw-vHCi-8N%MIpu|lyw1+BiOpmwoA-v;~XIJw-sLB<(7_H>EZwxr$Vc8dlrpB{6 zD@69t6CNXd?!BVC)-QefKJhAr5%%XQ!Z`r)kVlr=_%ZFcgA0(o3#}i8Wot$)FWcUQ$1Uk~TJ>TqO?^*CIoC{TjT|Ec z{;QeuqC0tJN<-nWD7m z3H4K7pS47Xzu;MGKO^2-L@HYX+Tl33jNtGwg_7L|!2Vb$LumX)Zhx|SycCO?bNl5wvndNQp@*1&1mwEdYB>+xZS$iL zRhK+a_DQKGHDf38DL&5JTg=?5K5S2lxr0r<`=)@+3!m|~d>>NBSdc}s2aCkSQEfvC z=_@@fItn%&i}aDI&n0L)L*WT`h)9V;hm)@_z=v1_32`Yls11#CqiibY?0IZpVuIKO zbZd=y+o2SgWy_g~JK?$d-T{Tc7adAncmStI)L#9abt@<1$5CcXmeKt&7x{F?b40hY zr$9*J<_b^cEkDe&t-yQ8O-Ch(DSoqTIalPvQ$Ab8Q9o8vfrHV_y^c@mh_P>*g_#nJ zVm?K46d%Oyi>4$vy+@af7hD3l46P8mKh41-akIYpw{B(+tR*2jD+E=O(YtXlMAyK$ zOg=>F^?%IfzSA?;SK8bFwLF283`b88W$n61AMII~u^!)!G!{<_ueLstrdhZ=v&-S) zz4em2Lz^)VzSQUxVwrK^eOd*Jvf7u4+x%ago?F};VAEICtIZ73P@sQv0}N5rgnZ(G zA$tC~&w|=4e+BpX>-oSsKIfO=#mki$NwI_;yIv&Vgh8$7QrePvYe;QjZ$gt1M8#N5 z%(E89sVK%aO+~uk>SyPRg)PQx=kBq3R~)_XxmXP$Ev#*841J+2H?ru(p^xD%f%`HJ zAfL84!Du^TZ{zf3tk#_hO|cUB^nG8UQzHWR?Z#VUw)n*PrtO!^<}t|MeL*aZ%>$P2!C?FlY!DnhzcSHLUfi> zQx3MMGu9w9&&X~Hs9mrVAR}ZhWfwOyAzvY`5Fdhx~7RjMVx%9F;7CC)M(A0Gr09{u8_8jZ^kzBw5!& z8z*HxUn$Z&Pp3a^Son*{9w0JWG;Yx2S?$o_Km0}N{5nsZoaOe1s9I|)*LdmzxHE)* zW4@mH{9}FTFT}D{EB9jb0pGn6 zPcZ(yHk~ry>Or*?0Qd0B+qUGIIN@>lC=-fKh}2b(h`HvT;ijor;iw^Hx}mJ!xe(OLS}nsia=v3tc)QZc_3{SFCkK4t9D>l^}1Mz zUm@QomUg!bA^hJ{rpl*|TU@>)A8SYyHM3w+GaK5%VI20gNB5J`<4=JZ#NIb_-iVR)F zZ*;JW)cQ93|LZjz%VbBrE;J*jysqERrBB1rtpW>$BN3azM;`YoC=6km5XntltA zN&~`fw@`LLl}u#>6VfsH%oDI!JcoD;4&H1$TfdR=;z*#mxQz?7e<~k?a8IZy)ig&3 zQ?8axzg#jK=SDPVGX+p~$%Or{t#i`QkX_{rt&yJ%;r^Ol5-jfFDQ6q=r%3(^fe;GP z3@n{FFF4P{!xPjJ8#%ISHYu>kFd4BMym!3*&Twwo*#WNoJ46hd--}CV{gxT_WuAgF zRk9{uE-j`!`#>}(^07Ido+(??L0iLEe(UXXs0-3mqyN2nr6rsXV@J(f`s(E9Q3j1H zw#2zQj6dY}d8s1zJT`W6PRG zAu}vT5B6sPo-=Pn0zn8qg71FInehCKN_ikUrO*5UUz@s(G; znPg2_c^xLeigVWZgwS}G&#ddKd)@wk0#kB^FZ};xj2{258X>>vV^Yl^EXjq-6 z*%6V+-{hM+NL$s)xZz(tT}#iwgT;UIm~ueAL@V55#uyax*XK+p3t-jNSW4Bn0PO?k0Z;3>HXX-hUQp{7=pWz*0lAM6|nD> z8Jg2yQM3{B6p$&36EaK?+~+0;O?%k?eAMxHjW`?m^m|Y^BtNoxl@1e_fJZ=lLkJ73 zO+N21%(udmG*ht&nPjGSRq_IDltskzheGaM*nS*|^`+&0FHG-{-GN4+uJ7U!quCD( z26dhCSCOiBr9x4;R@&p=*yDm#^M8{Vr&7;+TloX&8OKXj4mBHIw(=NIk4?-YVLq8M zd;-k_Ej$n1&fAl8a_O?VMRu@2Gh{@Zn5uVjAYd$l7l;j3O-3{817+tIrKhx!FXoTo zWv5Qy2Rfeh7#dY0c3Dd+!04@c96GdF)q?0X1~CcMAs!-@FSND5#I;ZHNmu~Rb^f7! zc)nZ!6EL8Vn{Ci8dFnLY%9}%)q5b+X=94asM?lOaut=VFN59n4<%Wfzdpz2sFN>Ru zHzMJy+rAas_dkdcjeY#|_1V+Cjbe&}3R{q#&^UH$rYRvDS9{9eNQq&*`P@Cfyzeg; z{$|6RI135OKL3~2EXH|(yL#qO+@zrvWa4yjJ{Rb7*_vxp3dk4I z{_lm?YX`9*i$5&%oAVMepDfF^S)6=(m1J~O!hiXA*FrRKu_ob2K>T%oU@&1s*Df1> zUYAAlx*$@2_uGt|{j_(BPltPwr8l{@5>*poH=lE5bnwX56mo8Sm($T#@Mb|=!XI_v zKbn7Hkp8(A6nBXcuhpZD1bDw}B!HdthXpf~9Vmzt(KJ40!`S_HX@oz+Vvwgpe#ee# zKcYx*CEX4OR5nT@6>de&3S{v`9evH2P~ z&)|&knPl`ChEz>$!8Fq^0utzud{n6Jcb}sBj=Z6UH0>>_dOn z)>C5-mYbIIz4>j(?OmMn1Xi%vW&5!Z$?1W1S%m+`Yr)hrKhQ>~iMLphs?6ail%dQv zN<{*Eyt0*yyvu{S7@*lS-|_oMwO0b zNEY?M%y{l82{t*fg+7RcdU!KPhcuAyp6SlMk56zmbI~3aJhBo?OEk!$$gYuJI_>CeOM>29Wh89BuImKT??BtdhWsiY9ctzRseI&eI8IEaLRabf1lzMWjz@sNLqGkSw%LMd1O6lz zAVV*$72G|&9Xa!OOkstCQCn+Xti=p>HCjpT;6tK|PI>?#`7&LdKG+7y;f)n&g>#Rr zO8?ye-bwLvI(+r7TjP{E*^!<~P_MmY=6_~zYJK@ z9K43^@N;v%W;bDc?8l|d1=9E-1Pb$~YhD=W`%hV(+uzBUNF+jQvq8^5hf3?SmS97) zzHFo6R?2gi-EGoKajBn204i#=B$nV5hW;Sq-Y6Z6r`l%TM{xS`iYBv!@|rgR3#Bav z{8L#V6~0efQXl6-ZeM5kcu#2VQ}wGDMd|IO6?2eBalSct|GBI5v_zASH(wvz@X3^! zC74=FzZDK;+(dof?!lG6eM&*f)-CD$@hI1 z>mUchd3$Yc9i>~&Sw%9G_Rw_K0`Fm_uO?@=J+qLCU;y7EpLq+Usj=XR9k1&c;d;gVN1=9c5-j)ATnZEz0RHi9>nu<`&){xyyQ%xMHV>xyel6^~-V{1Wwgdp+c9)Mao?7R)%`F8piY8Vw)CfnVCFqExF+Rd)ita!79TCS%`$|;hyZ6 z{5LDL`s6tjPp2gq!QgZo4oo!ojX5pX;t=Kb{61km`>;Jv-wZRiecGH{`aWArPC_Ij z4Da?a0?H6y%E;bk(@2-V?~G0r`hR>r_;=y!KnI-yWvCr3?87)~RF?s$oYr`pI+`4m ztz56T|GbAmwb{X)AS|yBMG$q=3EqITaak~EZZn~R8z(SH{+{uxRw|+)Q3vFxfS6^9 zLybwH3WLhBJE&2#$axCi%Y+fmX+eyQxgFq9r1aHVvdvU_*mfo=&PvrlqS|zKXFcw`?+1gd)(b~UHfD6$rGcv z3Pizh51H*gk9|JE=AJ2zqL+;(?thZt8NCs#=7Ljwyw3POb7HwS$Yp`&kzj%fTY*f` zx33mS`W+Ume>QC1yR_*mkcWSB+TV6ls`~9dOy}Es#agv-(=P9zX`@KB);!g(pP+^^ zlMBIg?ie)Ta}U{e7Tf#uA*AGI;4P7ZiPs;Pd@A{HAGl+ULhj2*;zXerN^9xlHK)XTe-?i9=x$T@t4HyBx+=H=%Sp?~^`JfBeX)#of&1<#*%? z3JSuzT2-v zK-BGd_e=?hfbw-vmzu_ftSX;_UFNs>ijJZLli8<|ZgN!+%#&%9 zM@HZ;k`hLOi+_S)#uwzqyfy=Jo77Fe?gM|5U4Y^L7JfKiK0lZ=yGFVD4p)W>l)T=8 zgH}7m#}S)uO*GF1iN2oX7LsqGh!ll01tt0rnoNsfVR6sGSGg?Lg##PkT>Hbn<)z5v zrCD%8Ew(fHc4(rZU&GmfcaZyD-L%O)=Dncx~uJ)qHY&5 z>FZE=73&UPe-|kns-JUb*3e-=C};|q}$)M^G*cKVrO?b4|O;u-fD`% z4yG zq9rnc#YbUaX%KT`gVE$@bOk3i4`O<`+Xiq7a>}B`~QJ$|gu3lbIZHDdF zWz-i+6Gkp}qn3Fw4#_jXPV-KKu4030?YYB_Vf&tPXWA#P)lsFACeOFwH>Co5jYUWW zN-G~e&MMR%a*}{yS_z3edT|dP*DWzh&FR4KZxBF}yrxNmDrNtIAS-#F!iW(kC3*!{ z=nK?78Q5(Y_@~SA6=t6Kp(l7cL(1}c<-Vu8*jwzEXm59={JK@AE<(ycAy^q%*Z3|p zaC+M2wJLX&*JA$@{1g*P@y12FVqVT~u_C@kh}m&`+*5=Fq(;3uzQa4q7y{toH6JvC zI6pdr0(si*lceK3oMtVsK1E(Ek}d1PThkS?HITe}eRc@W0xQrC&s2b&rod9laKjXN zzSIGlddTP;E^OG7?n{+99=u>!?Qa=xm6t6}7oPL_d~7X+cK_YhJmAh6@q+vr2@Im!5G7L@I5dmPzmA>PF zrgfMNLtWga9_TdJIb?ZeU5)d;rXfyUxGt* zF7SdCu2C;j2J9O$y>j1QGE8~|Mopd0VK(dUsoSo;@z%60Y1Sua-K~+Qsxu;vw(lh8 zTMg^zRRz9G0cc@f@VN5>ua*AV5n$(;?|$#sf6LRE8Tg;(9+}n+&N%?4NbcIDZE9y+i+}cU`Ngk+Dj4La~#Mz zzNkH|=F|(6d_?UhhE;FHOr17c+I370az~RGrSm%!J@fgeQhmC98P%viVy3MV`;m=eyn`3JBnpxS@c=c_t$Rkpp#5ASHjZ89l$ls|&#vx$^39pLng>mzQRsO#VbUwD1>13St*e zd7eC?&!={V^b;Ma!FB6dH0s-QS}D*Vxys)D+`utA^@u0Fq`lo43 zdiPulL<2UNsYKFFaXmyNXB#FgnHO(4)FokWNGtrASRZ(!2MA02*f9=f7U!*tGbJM% zfW@&(HQuBxTy4LU6cf#iPRPvr1WyIJ8`S3jK+46bw^!y#)bY+72J!B0@)lbhe7ZGu zsL{85{UP#jb^64JK3D^=EM9)d+7B41);?h6oq8?u1LgMcIsj*tyMxcVIsjdX%Ts>< z{K_EkGg;#qYiV^Li&AkKaXYYo`uZXSgc~5y*n7$JQfVl9<$rR&FTzT>{rw-KOPl4! z$gkHqs5fkya=wloDN*<=y1@J0pzaE>ygm@%7se6(LRIwIU%XaLyG|MXh?{&5T%=D` zyJ{R%YYTiQ+i#I=nGx)YnqPIpQebvF@fWwz@lD(KO_+P|Lj)BsqB2JAf7II>wqkPE zFK@770yA?6m^1F9UFcsRuqFw*GI!{4k2Kmh<^?KPCWV<+Z;lE{_mTfPfUF4Lh>qr5j*4-eC0Z2Xa&H^OHa+TeiHgN2YMq!w-T*%- zEP-JAFJvnU=V$OZft^*mYknvXgiWJ*M$Dv7rGdwh*muCAL$jjot1$6^>re3`B$S|+ zp!I&ieuo~46T68Zk3dP*nh&J9Lv#4VvxOht{sR6b)mN7v`#34dnB+;0hC57k=V-McJzzr+tX{sbfcYX^vrdy}TJ=b!`_+0Wd?gYK7JQwKn3U2UMAM{|CPXAPe7}nfnI58Knc{7?o z)&Lr?`?)GD5g=HKjILG!e{C*Mp{P$?m?+@&ygx)oXF+e{2&`M^nDnOmII*Qhkc6Kq z9#!p)$BYTybHL$$|LJDJPCa~K+ERXf_Kp=$%VXTGPzau_L+*q;i%M~-TUNjUc!j<0 zemV;WQ0+L;2flI8iMY1m@EUU`)R{{4OD)iNKFcP&aQTAH3)Mu}wc{zNxUfK=xK5|v z39UqT=+LjOMp_j68W(1#PcK4@`vs1#>G%UZ z1mF`((9@c80W#_9?mc@1Nn=n`SzQsuqTigb*`{6}@AFL5pIN%D^PWwjWCdbvf*O%$ zcqMmQ1@{kZFocE!9ZuM52lnSK#)Z8LljxX!lry*@iAA^f^AtQmte?+l|NfBeOZv5} zKuT8Qiu6=*rnZ6se@Qh2Q?BN-Ygcrnsq%NDQ{8Ca1XZm0S*(SbeO(+fb^<3L1>E5s zT$6C_xH^AqL+vn;GDq`nMPy18q%>H9eXcp{J$|Hr70zNPc|@lcR4g#8`Whi|>5*F; zc!rK<2($bC&imyNoXhZr9U)zfOdXenaMQdleiq68;VsSP!u1UV#b}vO>eft$frkOK zqT;+=`8fJiHQq}fG32j0_qZ@GgcnOJgEmDGq8x$U!MIP-DBcD)Z`#?d)r%p z3k!lGsI3c&HPJcA#ij4#S8Y$Q9qV9i0g$WwjAE)$HWaE#^Juh>dG4^UP;I^cFf7Qa z&!*NPiO6`g{3V#7zy`IMSvO3y_OQTdJCE6@uFHT@_6 z|Kn&fZZdUczR=fDXFwq=127e?`Jqt6$-WC$VB*efY>qX<-uqGBv4SB&<0D~rOS+%y@!dujlij1rz#I| zIb@WJLad-&_!qrY28s*_{~NV{h6RJZyyfA5u#O*5n<;{Q6_~sQ1Orq1d~l_Q)(@90_oFFcxZ2vOl zWHKKvbqb*pNSs>*>8}T!g+LDEZ$0q%SC`}kiAyB3%`oYAmOs`A_mSi*#&&ouwv}l{ zb@LNPSb0|Vl`EuANo+tJFM!uB>1$Us@O3xRPcrp4b?*KGf|-#yTrJW3r?eKaJ)_&D zo_97KOF*U0nN-bBU|pMvr^|bCEvfoUD(qTV&v0#s*#?;YYO>W_R96w&J0Al=C!!dlMt7A^amIzn{O*Ndr2JT-dST0b#!b(iMYkcw|cOLt5@C z3+L_VkdzOo9IRn+^QeNOoupTQa4&sL{)s%zMg=>`GDy+CSq(+18t$h!k^&ut;16aS zuz}&^YsF@8Z?W@t0a5;^Dtqx1hJD14DDJ4+8jjcq#oy#l@w?=c;7D7Kjxdq(>ako6 zUbP(^qWef*-_rTRk zS%}1{*v{*mok?3eKudv9KgsMc<)H?A*8Uju6C`|KLRlW2f~b#;hfaJ`#nZzDK`f8; z2f7fvFO6Q;ZHLGGkKk$S!h&sYdq94PinmJo5x6k@oz*a~Dxk7xy#}D;wFnwygN_P~ zN#z0%f`1B|rTO&{93?{Ng_qqaB2SOINp*J;yJTCH@6Vq6BsF+@THfP`qeE?~B7Dh3)ht>sy_haD_bJd`pu2clP6fF1Qt73QAEBkJ z=u=YG7GHtlh+4A8blji>k)P>WK z?k@z1j*h5i9>^hSg79Ibr?6hf@G)^oDqbw)Ur~G4&)kI%b9fDP?~-XE9OU-_7x@GQcU`oDWgwI4w_ibr9H3{I{F-tc{YB09|oIi@tsGUPhME5PiH zJA}n$Bxh3lX5o)KrVyr$T@4uc3?ZlUohP`>;gva+y4+>)nKg_4jp#w`!d}rmHb2$- z&Ci>!t|v_locP&u)x?nDa1h)iLsn(o2$+P;WQwUvB@NL_3bvDCWP~^}@gDKceu0ISpF__qnjrIQtvS z@K|0PNt?O$ZgqA|(OVu1_AD;tpJ@@=?w8!T^mj!@>&-j6g0sAz_t0!f?_Mfgpmbk& zC!{(yw-3wLu|4R;k>cNJjjq*Rx=-!im&J?S@!?crnn}_3O5ek2Z@!?0uOrwzhx}$v zFEacXMGy>9##epW3ao~C=agaK8}na?`h!Ki;>zC8&QRQHW}e5B&d(E#t}`hAlIA!L zD2}$F1m-%+mocp*MJGw$$7wq?SI_{p5fw>!Svy7PKkdFR%0<5J9}E@{H`auqh{QZ}D`D^^V>pxv9rF|YWNazHd) zyEti^fdqv}y(GEc(4~!iwPg1j&n&iYi5$X8&+6QY_t7SpNIp=9Zk3~RmKauX^1z^- zlOf|xHeG+86D-GM`N~a#0)A$odXUb7Iaf3^)N?iApuLgRUO#`kaXW8u49jgOTlqXh zwo^&*Z+R>RgDmuQZy4~M>&PuJCSf&n9z&*XoHmYRusRm&yx&_CM#L{w4kem&ZAjBU z)a;B1cqcR=9rvx)a^4fZJeaRYwzroHd+z@}`y?@|czk0p2Lc<;dV4$PmY1t`k%#xW zpt&`ADQ5X@_clxC6;iO^uG1;iVkzgG?Vh9>7#ak=v-lCLm7H_2>TJ5rZv=zQEN+GZOF0@r?pV5WQFV3kbDP<-N|{e3TZlbk0ywg&8?aC@f_*5G2ntZ}s_ zMarv_%7w=mPJ<6&W`i8;&;R=_6)>|G3;hpFWIRT@2M8U?ST@7nxH&Jz4>x&#<@?Ky z?Ni=ALyN<@k$}~bqB9(Jk*C>~L1Ii9%JF*a5`=Grh;vwWguRiO*HDt)*8AficQ17| z-_1cQW39A^*^&y_iXZ|v&m0K+EBd+?!KX&AAInz}8}bfPO7S1k0_p3QjI;9Sfp2q6 zg_-5B_2#bL=u~4{M^E{p@}zCXY@l$-D?$;=cIg!!j@1*7g9cl@>zx9g0S}Y_Rdt&F z{X&UqgdV$XE7*+nOMHB4QKAQFzR>L49E*`cMB$`I^S^X8*lL((|k#Ty~<9CrD zovf$gwCiVBW`^&_I6d~H+$s7VcFeq?w5#v2-ga&ok&^2~y{}i*Zvlfmzs!us8nI<9 zCXpA9_RO=)^2YaUn7?3X<YPR z=$(jMQd23glxxu2(bXdPvH`#G5ydoYR!%7d`PaF>zmn4>FCh@&yRT>1;{BzW~;7=+`GA2sN)sSbZeQH+Cn&@WZjm1z z!D0;Gd2wrv^DHH%GEEFrTtU6NPC)!?_`92vvrV0~H$mZ?g5B`*%OFhaUT%}`D}Coj zq7zHv&+iH@w*GW$XsWo6(oL#I-edrF*!n0vaC?qyXGQpSIFt((!^!CwWBUevDZPYa z=tlbLnZvC@Tut{V-HQ*C4pa{DnlFI*_wip}-te~X^pl=S;buJF8x9s= zBJo4!@xT8*j6Ln*@r?8C{eF@Zwn9yD%Cp^#_M> zt`fNuhoPrwsI5U&&%-yAZsQ=80T8|1zhR^6*pyaXS{=rV^tbZvfqI$V6|*zoYZ9Vq z1;QuBz9{J7=bMDABrj^1vRL`At;IU8@)>!7*|aaT@a1OwPIhm3izLx4+%EDWC>Yty zgIC)Je6>q~@*OpT&w!RN$=7MCzG218ayA6))AXkugW_cyd6%1U_5j#H{9ffmy+=2? zni9VO@@(TrXI;R1HE3A=w2UpV|^q}yJuE{yrK!eVE zplpQ7Rb+R@_F=A@2m5U$$?iiRk(lr z>FRk+N-EXBAZdNXjtW{X9&r?_Ipa5en9yOd%vPHW% zAzFvT%@?0?oSqr$mzp~-`Rgt$d)!_*CtfPfM>;z-DrYN8uhYfb0CHI8<-1SVsJH%5 zuyH#@Y+nylj|B;*4S7H~Sev>BleCbE^WX;*neCkvMUTGUW!s$rk~QeG+}r=uds zt+A!w!Uybc*LF>6kDrBMvpEXk_=;af;7D;*0Y*tEK8XuJx^V->uOtqs-02XaC&xVy z6w{|7KUk#&hD0k384^No@v`>9cb)0M`OcM zr`s;}@_iRGG6VzE> zoKHaRFZU%#VEI`vvIqO`Ccbmn8XZe)r;4%*63fT8&M7g{)%2k%LJrkktUGKJWB`t73?K7kFC@EGW!EB?G}FuR8z{<6dAJ+= z%h{yFL0CvhC(ZJ88PcX$ZW2WFMvvQuWqsz3H06gteqQ!#$N-!Axzth&8Gj3)YWI`?G?(aM~YE; z>~>vE%2y4%V*yGlJp@+eor4OAkGf0|w=>vodT2S%X`}(>*XXNOiC?R2-euGLPtee^ zfbZMLbSP(ve~uEA`9v6?dAP_)SY;{R))r?TNDbJt7U!kttd-%Wlvh7) zTp)bAOSTIHqgKBV8yi0eU#>HrZK-RTmYy0u3sh8BHnXk`3;1{|X8560rsOPo)2Ypf z!`D>o3(gSB-zdkzVmXXU@1t>nN;jY20f!Q$HxJ)SAyr-t`mR;-rwBhh{fv}z^Q|VY zNsAT7)K=3P2!%YttN)&QE=1E=_AM!80YQ!yy;L1<@Xe;E`js6m(!l4VFZg(CjT_4) z+Oltt(}gY`FeA1%&Pa1J)D0Quy*8vr4V~`NL_k^xVKJli(!XCK__V~GH`Q0hhpf-$ zy2I}1-y{iWRmvfWJT2-aNR*faWlAup3@U76obuKM$;oP4@vRr4t7RWf&6xj2UJUM8 zLVo*K#wQT^_cN1bR_DDYSRl@l&fFLO zKrgVr{6ZWhbMY#~fUwYQM?q|A?j|;)I2peLQ3vE?oUIi6(R%r0W8Y$nO1b{DiQ>y3 zoU?_F@!iM z7#o3*fG0ML)fMY?F=rP=&7!K}@K;*OPuiMx zV(nTGcS?m1a&nvtOxySb(@FVIQ86@#s{`O%m}~``e80ua)VmREXT#0bG)?y^Z#>sG z&$JWoPm==;xy^(egh>LELORk6S=fV|*i@&Ct-*Ub9i;$^!FS5R)q4|qqrf)%T3iXZ z4SB_b-IVBO_PG^r(u&`p&ySW;4p$C|L1IC6?~c>(H928+lVj-18IbCla?dojv)l}6 zJ|6Y*5WYr-NBy7S&*l>La;+7PK++WGHgBiS2C+eMYk|l}Y`p-) z1d{O4Gq9#u&*yvEXEoLSqbjK|`{fS7=Pk<*fNqGVB*Z1G-ImO9|2fM}98{)OOv%N& z3&hZn4jb8XgQdyJj7Caa&B%@`bIbCK5P^STqA8$5EzUuwU`$GE^-cXI8@}JvMf#*A zh6b+8TlLj+56kW>D!im&QPJ6@b+B}>V$8&D6aE&j%6b5hz)kPfL6MGqSA|&0w=ME| zJ-E03$Jar_NBn;yNSAyx8X8+p+cywAm-v`x6A=)L5{0=>v#y3P`G+uuH!wOILX#d_j~ ze&aqPxK*$S@9SwmUw&!bgRc!Zl@hDkF*`JR7?BRlz1+1RW|+-um^^FXBbjo%MX3JN z=@0%xcaMtcw`$22CB*n|{QdDj`m0M|ftX2LI*zf4gJb#t9=gt+YTX_9OUn0R&x{wT zx(6YNj+kA^QF#Nmtr2lYin>s>(f`hQsap>pZ)!GZ`4zNSrp)gU^zlBRTBcKmqq=_K6(aA zuVLzqX4z9Nt(mcnnX$Ztr55Ve0)*2+lUDZ6I`Br{{U8bSn^f&bu1Xi2NHDK$=a8Ef zVM(9c_D$VEv5O#ttPc#9ICjE<({RPk&zMs)@2vBh0^3kx#c|lm9!~ zx{Ie|T-Gkq;$7xe19|)oq3p(Jefcy}h9-x=&swyh5&i$(ODoM|_Dv;h4}`^#@aunw zo9%3WsCswsv~Q)^_i zwv|wr8#a9Izt`JJLQqiz`DEsCW5uXcaQ$32t-pmRC$$}K6?zb@)5Sfb(!)0}EeFB; z&d++Um~*es#!DXmO~@o0GPh#M?(!-AqAC7fiMO16oEPJsbA&zr-Yzu7f5L$24KAWN zQ1joh*o&rbmh&wL|CsgF^_t>u0ju;*s5((kvM^E1a8JV?&drQW{@`2 zu6#a$Vk|ru=;gzpT01}E(+DQFw;5-Go}pkBf(+@YNN>nYszL3@Fo7E>O%I|ik9HnU zuCSc!Ol`G|RFkGhSY0oo*EaA6!R~y2n`{)N+T!rY-e6k~xhRLgTY&y>H-DF;bD%gr ztdC*#U{*>V`K$0w=7~Vl|GtN476PUb&b(p;KI4DC|E<9PR^Wdt@V^!K|FQz5lx^1B Wwf31l8gz< Date: Fri, 6 Jan 2023 04:04:32 +0100 Subject: [PATCH 015/918] Add substance mesh loader --- .../plugins/load/load_mesh.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/load/load_mesh.py diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py new file mode 100644 index 0000000000..7cc5e35912 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -0,0 +1,98 @@ +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.pipeline import legacy_io + +import substance_painter.project +import qargparse + + +class SubstanceLoadProjectMesh(load.LoaderPlugin): + """Load mesh for project""" + + families = ["*"] + representations = ["abc", "fbx", "obj", "gltf"] + + label = "Load mesh" + order = -10 + icon = "code-fork" + color = "orange" + + options = [ + qargparse.Boolean( + "preserve_strokes", + default=True, + help="Preserve strokes positions on mesh.\n" + "(only relevant when loading into existing project)" + ), + qargparse.Boolean( + "import_cameras", + default=True, + help="Import cameras from the mesh file." + ) + ] + + def load(self, context, name, namespace, data): + + if not substance_painter.project.is_open(): + # Allow to 'initialize' a new project + # TODO: preferably these settings would come from the actual + # new project prompt of Substance (or something that is + # visually similar to still allow artist decisions) + settings = substance_painter.project.Settings( + default_texture_resolution=4096, + import_cameras=data.get("import_cameras", True), + ) + + substance_painter.project.create( + mesh_file_path=self.fname, + settings=settings + ) + return + + # Reload the mesh + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=data.get("import_cameras", True), + preserve_strokes=data.get("preserve_strokes", True) + ) + + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: + print("Reload succeeded") + else: + raise RuntimeError("Reload of mesh failed") + + path = self.fname + substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + + # TODO: Register with the project so host.get_containers() can return + # the loaded content in manager + + def switch(self, container, representation): + self.update(container, representation) + + def update(self, container, representation): + + path = get_representation_path(representation) + + # Reload the mesh + # TODO: Re-use settings from first load? + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=True, + preserve_strokes=True + ) + + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: + print("Reload succeeded") + else: + raise RuntimeError("Reload of mesh failed") + + substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + + def remove(self, container): + + # Remove OpenPype related settings about what model was loaded + # or close the project? + pass From 3cb797b10a04726183ca740a5f10b593be45aea1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 04:05:13 +0100 Subject: [PATCH 016/918] Add some fixes to stylesheet to avoid very odd looking OpenPype UIs in Substance Painter --- openpype/style/style.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index a7a48cdb9d..ae1b9d2991 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -127,6 +127,7 @@ QPushButton { border-radius: 0.2em; padding: 3px 5px 3px 5px; background: {color:bg-buttons}; + min-width: 0px; /* Substance Painter fix */ } QPushButton:hover { @@ -328,7 +329,15 @@ QTabWidget::tab-bar { alignment: left; } +/* avoid QTabBar overrides in Substance Painter */ +QTabBar { + text-transform: none; + font-weight: normal; +} + QTabBar::tab { + text-transform: none; + font-weight: normal; border-top: 1px solid {color:border}; border-left: 1px solid {color:border}; border-right: 1px solid {color:border}; @@ -368,6 +377,7 @@ QHeaderView { QHeaderView::section { background: {color:bg-view-header}; padding: 4px; + border-top: 0px; /* Substance Painter fix */ border-right: 1px solid {color:bg-view}; border-radius: 0px; text-align: center; From e710a8dc70496e042e000da50c5ad2181376c84a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 05:03:19 +0100 Subject: [PATCH 017/918] Fix bug if file wasn't saved yet, file_path() would return None --- openpype/hosts/substancepainter/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 3fd081ca1c..31c87f079d 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -112,7 +112,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return None filepath = substance_painter.project.file_path() - if filepath.endswith(".spt"): + if filepath and filepath.endswith(".spt"): # When currently in a Substance Painter template assume our # scene isn't saved. This can be the case directly after doing # "New project", the path will then be the template used. This From 8468dbce679cc5dfee58e99e4015bb812f47080d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 05:04:53 +0100 Subject: [PATCH 018/918] Implement managing for Load Mesh (draft implementation) --- .../hosts/substancepainter/api/pipeline.py | 47 +++++++++++- .../plugins/load/load_mesh.py | 71 ++++++++++++++----- 2 files changed, 97 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 31c87f079d..4d49fa83d7 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -123,7 +123,16 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return filepath def get_containers(self): - return [] + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata("OpenPype") + containers = metadata.get("containers") + if containers: + for key, container in containers.items(): + container["objectName"] = key + yield container @staticmethod def create_context_node(): @@ -231,4 +240,38 @@ def on_open(): dialog.setMessage("There are outdated containers in " "your Substance scene.") dialog.on_clicked.connect(_on_show_inventory) - dialog.show() \ No newline at end of file + dialog.show() + + +def imprint_container(container, + name, + namespace, + context, + loader): + """Imprint a loaded container with metadata. + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + container (dict): The (substance metadata) dictionary to imprint into. + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + context (dict): Asset information + loader (load.LoaderPlugin): loader instance used to produce container. + + Returns: + None + + """ + + data = [ + ("schema", "openpype:container-2.0"), + ("id", AVALON_CONTAINER_ID), + ("name", str(name)), + ("namespace", str(namespace) if namespace else None), + ("loader", str(loader.__class__.__name__)), + ("representation", str(context["representation"]["_id"])), + ] + for key, value in data: + container[key] = value diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 7cc5e35912..519ed3ad4e 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -2,12 +2,27 @@ from openpype.pipeline import ( load, get_representation_path, ) -from openpype.pipeline import legacy_io +from openpype.hosts.substancepainter.api.pipeline import imprint_container import substance_painter.project import qargparse +def set_container(key, container): + metadata = substance_painter.project.Metadata("OpenPype") + containers = metadata.get("containers") or {} + containers[key] = container + metadata.set("containers", containers) + + +def remove_container(key): + metadata = substance_painter.project.Metadata("OpenPype") + containers = metadata.get("containers") + if containers: + containers.pop(key, None) + metadata.set("containers", containers) + + class SubstanceLoadProjectMesh(load.LoaderPlugin): """Load mesh for project""" @@ -33,6 +48,8 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): ) ] + container_key = "ProjectMesh" + def load(self, context, name, namespace, data): if not substance_painter.project.is_open(): @@ -49,25 +66,34 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): mesh_file_path=self.fname, settings=settings ) - return - # Reload the mesh - settings = substance_painter.project.MeshReloadingSettings( - import_cameras=data.get("import_cameras", True), - preserve_strokes=data.get("preserve_strokes", True) - ) + else: + # Reload the mesh + settings = substance_painter.project.MeshReloadingSettings( + import_cameras=data.get("import_cameras", True), + preserve_strokes=data.get("preserve_strokes", True) + ) - def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): - if status == substance_painter.project.ReloadMeshStatus.SUCCESS: - print("Reload succeeded") - else: - raise RuntimeError("Reload of mesh failed") + def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa + if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa + print("Reload succeeded") + else: + raise RuntimeError("Reload of mesh failed") - path = self.fname - substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + path = self.fname + substance_painter.project.reload_mesh(path, + settings, + on_mesh_reload) - # TODO: Register with the project so host.get_containers() can return - # the loaded content in manager + # Store container + container = {} + imprint_container(container, + name=self.container_key, + namespace=self.container_key, + context=context, + loader=self) + container["options"] = data + set_container(self.container_key, container) def switch(self, container, representation): self.update(container, representation) @@ -78,9 +104,10 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # Reload the mesh # TODO: Re-use settings from first load? + container_options = container.get("options", {}) settings = substance_painter.project.MeshReloadingSettings( - import_cameras=True, - preserve_strokes=True + import_cameras=container_options.get("import_cameras", True), + preserve_strokes=container_options.get("preserve_strokes", True) ) def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): @@ -91,8 +118,14 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): substance_painter.project.reload_mesh(path, settings, on_mesh_reload) + # Update container representation + container["representation"] = str(representation["_id"]) + set_container(self.container_key, container) + def remove(self, container): # Remove OpenPype related settings about what model was loaded # or close the project? - pass + # TODO: This is likely best 'hidden' away to the user because + # this will leave the project's mesh unmanaged. + remove_container(self.container_key) From 30764456afa4f92053b61d6a3e39576874c235a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 05:22:59 +0100 Subject: [PATCH 019/918] Add launch with last workfile support for Substance Painter --- openpype/hooks/pre_add_last_workfile_arg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 3609620917..d5a9a41e5a 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -23,6 +23,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "blender", "photoshop", "tvpaint", + "substance", "aftereffects" ] From bcac4d1fafde2a3a2b7ce6f426d603d586b4df05 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 12:17:49 +0100 Subject: [PATCH 020/918] Add draft for workfile Creator --- .../plugins/create/create_workfile.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/create/create_workfile.py diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py new file mode 100644 index 0000000000..cec760040b --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating workfiles.""" + +from openpype.pipeline import CreatedInstance, AutoCreator +from openpype.pipeline import legacy_io +from openpype.client import get_asset_by_name + +import substance_painter.project + + +def set_workfile_data(data, update=False): + if update: + data = get_workfile_data().update(data) + metadata = substance_painter.project.Metadata("OpenPype") + metadata.set("workfile", data) + + +def get_workfile_data(): + metadata = substance_painter.project.Metadata("OpenPype") + return metadata.get("workfile") or {} + + +class CreateWorkfile(AutoCreator): + """Workfile auto-creator.""" + identifier = "io.openpype.creators.substancepainter.workfile" + label = "Workfile" + family = "workfile" + icon = "document" + + default_variant = "Main" + + def create(self): + + variant = self.default_variant + project_name = self.project_name + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + host_name = legacy_io.Session["AVALON_APP"] + + # Workfile instance should always exist and must only exist once. + # As such we'll first check if it already exists and is collected. + current_instance = next( + ( + instance for instance in self.create_context.instances + if instance.creator_identifier == self.identifier + ), None) + + if current_instance is None: + self.log.info("Auto-creating workfile instance...") + asset_doc = get_asset_by_name(project_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 + } + current_instance = self.create_instance_in_context(subset_name, + data) + elif ( + current_instance["asset"] != asset_name + or current_instance["task"] != task_name + ): + # Update instance context if is not the same + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + current_instance["asset"] = asset_name + current_instance["task"] = task_name + current_instance["subset"] = subset_name + + set_workfile_data(current_instance.data_to_store()) + + def collect_instances(self): + workfile = get_workfile_data() + if not workfile: + return + self.create_instance_in_context_from_existing(workfile) + + def update_instances(self, update_list): + for instance, _changes in update_list: + set_workfile_data(instance.data_to_store(), update=True) + + # Helper methods (this might get moved into Creator class) + def create_instance_in_context(self, subset_name, data): + instance = CreatedInstance( + self.family, subset_name, data, self + ) + self.create_context.creator_adds_instance(instance) + return instance + + def create_instance_in_context_from_existing(self, data): + instance = CreatedInstance.from_existing(data, self) + self.create_context.creator_adds_instance(instance) + return instance From 1c4ff746adaee6e2ac34f765d57f64bda967765e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 6 Jan 2023 16:10:26 +0100 Subject: [PATCH 021/918] Remove 'fix' which didn't originally fix the UI issue - it was a styleSheet issue --- openpype/hosts/substancepainter/addon.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/substancepainter/addon.py b/openpype/hosts/substancepainter/addon.py index bb55f20189..6288ef1559 100644 --- a/openpype/hosts/substancepainter/addon.py +++ b/openpype/hosts/substancepainter/addon.py @@ -20,9 +20,6 @@ class SubstanceAddon(OpenPypeModule, IHostAddon): env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path - # Fix UI scale issue - env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None) - def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] From 82639e8634587b7f63c703903c947c13f5e6f327 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 16:18:07 +0100 Subject: [PATCH 022/918] Avoid trying to import blessed terminal coloring in Substance Painter --- openpype/hosts/substancepainter/addon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/addon.py b/openpype/hosts/substancepainter/addon.py index 6288ef1559..2fbea139c5 100644 --- a/openpype/hosts/substancepainter/addon.py +++ b/openpype/hosts/substancepainter/addon.py @@ -20,6 +20,9 @@ class SubstanceAddon(OpenPypeModule, IHostAddon): env["SUBSTANCE_PAINTER_PLUGINS_PATH"] = plugin_path + # Log in Substance Painter doesn't support custom terminal colors + env["OPENPYPE_LOG_NO_COLORS"] = "Yes" + def get_launch_hook_paths(self, app): if app.host_name != self.host_name: return [] From c101f6a2cbce65bdf97d8ccc7812f85895f38bdc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 16:19:47 +0100 Subject: [PATCH 023/918] Cleanup OpenPype Qt widgets on Substance Painter shutdown --- .../deploy/plugins/openpype_plugin.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py index 01779156f1..e7e1849546 100644 --- a/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py +++ b/openpype/hosts/substancepainter/deploy/plugins/openpype_plugin.py @@ -1,13 +1,34 @@ + +def cleanup_openpype_qt_widgets(): + """ + Workaround for Substance failing to shut down correctly + when a Qt window was still open at the time of shutting down. + + This seems to work sometimes, but not all the time. + + """ + # TODO: Create a more reliable method to close down all OpenPype Qt widgets + from PySide2 import QtWidgets + import substance_painter.ui + + # Kill OpenPype Qt widgets + print("Killing OpenPype Qt widgets..") + for widget in QtWidgets.QApplication.topLevelWidgets(): + if widget.__module__.startswith("openpype."): + print(f"Deleting widget: {widget.__class__.__name__}") + substance_painter.ui.delete_ui_element(widget) + + def start_plugin(): from openpype.pipeline import install_host from openpype.hosts.substancepainter.api import SubstanceHost - install_host(SubstanceHost()) def close_plugin(): from openpype.pipeline import uninstall_host + cleanup_openpype_qt_widgets() uninstall_host() From ccb4371641b79275702bc5557fefdf3c8d39c0a6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 17:42:43 +0100 Subject: [PATCH 024/918] Refactor metadata code to allow more structure for future Substance Painter plugins --- .../hosts/substancepainter/api/pipeline.py | 54 ++++++++++++++++- .../plugins/create/create_workfile.py | 27 ++++----- .../plugins/load/load_mesh.py | 58 +++++++++---------- 3 files changed, 91 insertions(+), 48 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 4d49fa83d7..e7dbe5e5eb 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -36,6 +36,10 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + +OPENPYPE_METADATA_KEY = "OpenPype" +OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key + self = sys.modules[__name__] self.menu = None self.callbacks = [] @@ -127,8 +131,8 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): if not substance_painter.project.is_open(): return - metadata = substance_painter.project.Metadata("OpenPype") - containers = metadata.get("containers") + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) if containers: for key, container in containers.items(): container["objectName"] = key @@ -275,3 +279,49 @@ def imprint_container(container, ] for key, value in data: container[key] = value + + +def set_project_metadata(key, data): + """Set a key in project's OpenPype metadata.""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + metadata.set(key, data) + + +def get_project_metadata(key): + """Get a key from project's OpenPype metadata.""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(key) + + +def set_container_metadata(object_name, container_data, update=False): + """Helper method to directly set the data for a specific container + + Args: + object_name (str): The unique object name identifier for the container + container_data (dict): The data for the container. + Note 'objectName' data is derived from `object_name` and key in + `container_data` will be ignored. + update (bool): Whether to only update the dict data. + + """ + # The objectName is derived from the key in the metadata so won't be stored + # in the metadata in the container's data. + container_data.pop("objectName", None) + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) or {} + if update: + existing_data = containers.setdefault(object_name, {}) + existing_data.update(container_data) # mutable dict, in-place update + else: + containers[object_name] = container_data + metadata.set("containers", containers) + + +def remove_container_metadata(object_name): + """Helper method to remove the data for a specific container""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + containers = metadata.get(OPENPYPE_METADATA_CONTAINERS_KEY) + if containers: + containers.pop(object_name, None) + metadata.set("containers", containers) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index cec760040b..8b010ebe2c 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -5,20 +5,10 @@ from openpype.pipeline import CreatedInstance, AutoCreator from openpype.pipeline import legacy_io from openpype.client import get_asset_by_name -import substance_painter.project - - -def set_workfile_data(data, update=False): - if update: - data = get_workfile_data().update(data) - metadata = substance_painter.project.Metadata("OpenPype") - metadata.set("workfile", data) - - -def get_workfile_data(): - metadata = substance_painter.project.Metadata("OpenPype") - return metadata.get("workfile") or {} - +from openpype.hosts.substancepainter.api.pipeline import ( + set_project_metadata, + get_project_metadata +) class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" @@ -71,17 +61,20 @@ class CreateWorkfile(AutoCreator): current_instance["task"] = task_name current_instance["subset"] = subset_name - set_workfile_data(current_instance.data_to_store()) + set_project_metadata("workfile", current_instance.data_to_store()) def collect_instances(self): - workfile = get_workfile_data() + workfile = get_project_metadata("workfile") if not workfile: return self.create_instance_in_context_from_existing(workfile) def update_instances(self, update_list): for instance, _changes in update_list: - set_workfile_data(instance.data_to_store(), update=True) + # Update project's workfile metadata + data = get_project_metadata("workfile") or {} + data.update(instance.data_to_store()) + set_project_metadata("workfile", data) # Helper methods (this might get moved into Creator class) def create_instance_in_context(self, subset_name, data): diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 519ed3ad4e..3e62b90988 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -2,27 +2,16 @@ from openpype.pipeline import ( load, get_representation_path, ) -from openpype.hosts.substancepainter.api.pipeline import imprint_container +from openpype.hosts.substancepainter.api.pipeline import ( + imprint_container, + set_container_metadata, + remove_container_metadata +) import substance_painter.project import qargparse -def set_container(key, container): - metadata = substance_painter.project.Metadata("OpenPype") - containers = metadata.get("containers") or {} - containers[key] = container - metadata.set("containers", containers) - - -def remove_container(key): - metadata = substance_painter.project.Metadata("OpenPype") - containers = metadata.get("containers") - if containers: - containers.pop(key, None) - metadata.set("containers", containers) - - class SubstanceLoadProjectMesh(load.LoaderPlugin): """Load mesh for project""" @@ -48,10 +37,12 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): ) ] - container_key = "ProjectMesh" - def load(self, context, name, namespace, data): + # Get user inputs + import_cameras = data.get("import_cameras", True) + preserve_strokes = data.get("preserve_strokes", True) + if not substance_painter.project.is_open(): # Allow to 'initialize' a new project # TODO: preferably these settings would come from the actual @@ -59,7 +50,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # visually similar to still allow artist decisions) settings = substance_painter.project.Settings( default_texture_resolution=4096, - import_cameras=data.get("import_cameras", True), + import_cameras=import_cameras, ) substance_painter.project.create( @@ -70,8 +61,8 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): else: # Reload the mesh settings = substance_painter.project.MeshReloadingSettings( - import_cameras=data.get("import_cameras", True), - preserve_strokes=data.get("preserve_strokes", True) + import_cameras=import_cameras, + preserve_strokes=preserve_strokes ) def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa @@ -87,13 +78,21 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # Store container container = {} + project_mesh_object_name = "_ProjectMesh_" imprint_container(container, - name=self.container_key, - namespace=self.container_key, + name=project_mesh_object_name, + namespace=project_mesh_object_name, context=context, loader=self) - container["options"] = data - set_container(self.container_key, container) + + # We want store some options for updating to keep consistent behavior + # from the user's original choice. We don't store 'preserve_strokes' + # as we always preserve strokes on updates. + container["options"] = { + "import_cameras": import_cameras, + } + + set_container_metadata(project_mesh_object_name, container) def switch(self, container, representation): self.update(container, representation) @@ -107,7 +106,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): container_options = container.get("options", {}) settings = substance_painter.project.MeshReloadingSettings( import_cameras=container_options.get("import_cameras", True), - preserve_strokes=container_options.get("preserve_strokes", True) + preserve_strokes=True ) def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): @@ -119,8 +118,9 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): substance_painter.project.reload_mesh(path, settings, on_mesh_reload) # Update container representation - container["representation"] = str(representation["_id"]) - set_container(self.container_key, container) + object_name = container["objectName"] + update_data = {"representation": str(representation["_id"])} + set_container_metadata(object_name, update_data, update=True) def remove(self, container): @@ -128,4 +128,4 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # or close the project? # TODO: This is likely best 'hidden' away to the user because # this will leave the project's mesh unmanaged. - remove_container(self.container_key) + remove_container_metadata(container["objectName"]) From cf92213dd1fde6efb5ab117a1d4e4b7a96b188d5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Jan 2023 17:42:55 +0100 Subject: [PATCH 025/918] Cosmetics --- .../hosts/substancepainter/plugins/create/create_workfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 8b010ebe2c..4b34f4cc8c 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -10,6 +10,7 @@ from openpype.hosts.substancepainter.api.pipeline import ( get_project_metadata ) + class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" identifier = "io.openpype.creators.substancepainter.workfile" From ae496b9712bafc77a0d8350b92b0e84505eee512 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 9 Jan 2023 07:30:29 +0000 Subject: [PATCH 026/918] Use project settings by default. --- openpype/hosts/maya/api/lib.py | 4 +++- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4b8b6b1949..9aa2325e25 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -113,7 +113,9 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] -DISPLAY_LIGHTS = ["default", "all", "selected", "active", "none"] +DISPLAY_LIGHTS = [ + "project_settings", "default", "all", "selected", "active", "none" +] def get_main_window(): diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index a1e6b2d503..7542785152 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -100,7 +100,8 @@ class ExtractPlayblast(publish.Extractor): # Show lighting mode. display_lights = instance.data["displayLights"] - preset["viewport_options"]["displayLights"] = display_lights + if display_lights != "project_settings": + preset["viewport_options"]["displayLights"] = display_lights # Override transparency if requested. transparency = instance.data.get("transparency", 0) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 80e94303a6..de6bc3895e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -107,7 +107,8 @@ class ExtractThumbnail(publish.Extractor): # Show lighting mode. display_lights = instance.data["displayLights"] - preset["viewport_options"]["displayLights"] = display_lights + if display_lights != "project_settings": + preset["viewport_options"]["displayLights"] = display_lights # Override transparency if requested. transparency = instance.data.get("transparency", 0) From c34f8fed24a7c84ce22a615b5f438798b2f461c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 10:29:44 +0100 Subject: [PATCH 027/918] Bypass silently if a project was not open when querying metadata --- openpype/hosts/substancepainter/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index e7dbe5e5eb..70353039f5 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -289,6 +289,9 @@ def set_project_metadata(key, data): def get_project_metadata(key): """Get a key from project's OpenPype metadata.""" + if not substance_painter.project.is_open(): + return + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) return metadata.get(key) From 2c544246fd855de080387e1f86a053e5fd31e12f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 10:30:18 +0100 Subject: [PATCH 028/918] Do not auto create workfile instance if project isn't open. --- .../hosts/substancepainter/plugins/create/create_workfile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 4b34f4cc8c..22e12b4079 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -10,6 +10,8 @@ from openpype.hosts.substancepainter.api.pipeline import ( get_project_metadata ) +import substance_painter.project + class CreateWorkfile(AutoCreator): """Workfile auto-creator.""" @@ -22,6 +24,9 @@ class CreateWorkfile(AutoCreator): def create(self): + if not substance_painter.project.is_open(): + return + variant = self.default_variant project_name = self.project_name asset_name = legacy_io.Session["AVALON_ASSET"] From ec2f10caf383a769fd90a3777ee47568054b6d41 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 10:30:32 +0100 Subject: [PATCH 029/918] Simplify logic --- .../hosts/substancepainter/plugins/create/create_workfile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 22e12b4079..729cc8f718 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -71,9 +71,8 @@ class CreateWorkfile(AutoCreator): def collect_instances(self): workfile = get_project_metadata("workfile") - if not workfile: - return - self.create_instance_in_context_from_existing(workfile) + if workfile: + self.create_instance_in_context_from_existing(workfile) def update_instances(self, update_list): for instance, _changes in update_list: From c3fca896d48f82026aea0f81055a996c366ea920 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 11:16:23 +0100 Subject: [PATCH 030/918] Implement plug-ins to support workfile publishing --- .../plugins/publish/collect_current_file.py | 17 ++++++++++++ .../collect_workfile_representation.py | 26 +++++++++++++++++++ .../plugins/publish/increment_workfile.py | 23 ++++++++++++++++ .../plugins/publish/save_workfile.py | 23 ++++++++++++++++ 4 files changed, 89 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/publish/collect_current_file.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/increment_workfile.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/save_workfile.py diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py new file mode 100644 index 0000000000..dac493bbf1 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py @@ -0,0 +1,17 @@ +import pyblish.api + +from openpype.pipeline import registered_host + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.49 + label = "Current Workfile" + hosts = ["substancepainter"] + + def process(self, context): + host = registered_host() + path = host.get_current_workfile() + context.data["currentFile"] = path + self.log.debug(f"Current workfile: {path}") \ No newline at end of file diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py new file mode 100644 index 0000000000..563c2d4c07 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py @@ -0,0 +1,26 @@ +import os +import pyblish.api + + +class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): + """Create a publish representation for the current workfile instance.""" + + order = pyblish.api.CollectorOrder + label = "Workfile representation" + hosts = ['substancepainter'] + families = ["workfile"] + + def process(self, instance): + + context = instance.context + current_file = context.data["currentFile"] + + folder, file = os.path.split(current_file) + filename, ext = os.path.splitext(file) + + instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] diff --git a/openpype/hosts/substancepainter/plugins/publish/increment_workfile.py b/openpype/hosts/substancepainter/plugins/publish/increment_workfile.py new file mode 100644 index 0000000000..b45d66fbb1 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/increment_workfile.py @@ -0,0 +1,23 @@ +import pyblish.api + +from openpype.lib import version_up +from openpype.pipeline import registered_host + + +class IncrementWorkfileVersion(pyblish.api.ContextPlugin): + """Increment current workfile version.""" + + order = pyblish.api.IntegratorOrder + 1 + label = "Increment Workfile Version" + optional = True + hosts = ["substancepainter"] + + def process(self, context): + + assert all(result["success"] for result in context.data["results"]), ( + "Publishing not successful so version is not increased.") + + host = registered_host() + path = context.data["currentFile"] + self.log.info(f"Incrementing current workfile to: {path}") + host.save_workfile(version_up(path)) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py new file mode 100644 index 0000000000..5e86785e0d --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -0,0 +1,23 @@ +import pyblish.api + +from openpype.pipeline import registered_host + + +class SaveCurrentWorkfile(pyblish.api.ContextPlugin): + """Save current workfile""" + + label = "Save current workfile" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["substancepainter"] + + def process(self, context): + + host = registered_host() + assert context.data['currentFile'] == host.get_current_workfile() + + if host.has_unsaved_changes(): + self.log.info("Saving current file..") + host.save_workfile() + else: + self.log.debug("Skipping workfile save because there are no " + "unsaved changes.") From 564e8f4d40febfb08b65fc31e10b710d38cbddc7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 11:17:25 +0100 Subject: [PATCH 031/918] Cosmetics --- .../substancepainter/plugins/publish/collect_current_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py index dac493bbf1..9a37eb0d1c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_current_file.py @@ -14,4 +14,4 @@ class CollectCurrentFile(pyblish.api.ContextPlugin): host = registered_host() path = host.get_current_workfile() context.data["currentFile"] = path - self.log.debug(f"Current workfile: {path}") \ No newline at end of file + self.log.debug(f"Current workfile: {path}") From f9d3c9f77227fef2ddcf43649e69d0fb88d4e2bd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 18:13:49 +0100 Subject: [PATCH 032/918] Early prototype for Texture publishing in Substance Painter (WIP - not functional; doesn't integrate yet) --- .../plugins/create/create_textures.py | 149 ++++++++++++++++++ .../plugins/publish/extract_textures.py | 71 +++++++++ 2 files changed, 220 insertions(+) create mode 100644 openpype/hosts/substancepainter/plugins/create/create_textures.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/extract_textures.py diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py new file mode 100644 index 0000000000..af2e23b3bf --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating textures.""" +import os + +from openpype.pipeline import CreatedInstance, Creator + +from openpype.hosts.substancepainter.api.pipeline import ( + set_project_metadata, + get_project_metadata +) + +from openpype.lib import ( + EnumDef, + UILabelDef, + NumberDef +) + +import substance_painter.project +import substance_painter.resource + + +def get_export_presets(): + import substance_painter.resource + + preset_resources = {} + + # TODO: Find more optimal way to find all export templates + for shelf in substance_painter.resource.Shelves.all(): + shelf_path = os.path.normpath(shelf.path()) + + presets_path = os.path.join(shelf_path, "export-presets") + if not os.path.exists(presets_path): + continue + + for fname in os.listdir(presets_path): + if fname.endswith(".spexp"): + template_name = os.path.splitext(fname)[0] + + resource = substance_painter.resource.ResourceID( + context=shelf.name(), + name=template_name + ) + resource_url = resource.url() + + preset_resources[resource_url] = template_name + + # Sort by template name + export_templates = dict(sorted(preset_resources.items(), + key=lambda x: x[1])) + + return export_templates + + +class CreateTextures(Creator): + """Create a texture set.""" + identifier = "io.openpype.creators.substancepainter.textures" + label = "Textures" + family = "textures" + icon = "picture-o" + + default_variant = "Main" + + def create(self, subset_name, instance_data, pre_create_data): + + if not substance_painter.project.is_open(): + return + + instance = self.create_instance_in_context(subset_name, instance_data) + set_project_metadata("textures", instance.data_to_store()) + + def collect_instances(self): + workfile = get_project_metadata("textures") + if workfile: + self.create_instance_in_context_from_existing(workfile) + + def update_instances(self, update_list): + for instance, _changes in update_list: + # Update project's metadata + data = get_project_metadata("textures") or {} + data.update(instance.data_to_store()) + set_project_metadata("textures", data) + + def remove_instances(self, instances): + for instance in instances: + # TODO: Implement removal + # api.remove_instance(instance) + self._remove_instance_from_context(instance) + + # Helper methods (this might get moved into Creator class) + def create_instance_in_context(self, subset_name, data): + instance = CreatedInstance( + self.family, subset_name, data, self + ) + self.create_context.creator_adds_instance(instance) + return instance + + def create_instance_in_context_from_existing(self, data): + instance = CreatedInstance.from_existing(data, self) + self.create_context.creator_adds_instance(instance) + return instance + + def get_instance_attr_defs(self): + + return [ + EnumDef("exportPresetUrl", + items=get_export_presets(), + label="Output Template"), + EnumDef("exportFileFormat", + items={ + None: "Based on output template", + # TODO: implement extensions + }, + label="File type"), + EnumDef("exportSize", + items={ + None: "Based on each Texture Set's size", + # The key is size of the texture file in log2. + # (i.e. 10 means 2^10 = 1024) + 7: "128", + 8: "256", + 9: "512", + 10: "1024", + 11: "2048", + 12: "4096" + }, + label="Size"), + + EnumDef("exportPadding", + items={ + "passthrough": "No padding (passthrough)", + "infinite": "Dilation infinite", + "transparent": "Dilation + transparent", + "color": "Dilation + default background color", + "diffusion": "Dilation + diffusion" + }, + label="Padding"), + NumberDef("exportDilationDistance", + minimum=0, + maximum=256, + decimals=0, + default=16, + label="Dilation Distance"), + UILabelDef("Note: Dilation Distance is only used with " + "'Dilation + ' padding options"), + ] + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes + return self.get_instance_attr_defs() diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py new file mode 100644 index 0000000000..93e0c8cb31 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -0,0 +1,71 @@ +from openpype.pipeline import KnownPublishError, publish + +import substance_painter.export + + +class ExtractTextures(publish.Extractor): + """Extract Textures using an output template config""" + + label = "Extract Texture Sets" + hosts = ['substancepainter'] + families = ["textures"] + + def process(self, instance): + + staging_dir = self.staging_dir(instance) + + # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa + creator_attrs = instance.data["creator_attributes"] + config = { + "exportShaderParams": True, + "exportPath": staging_dir, + "defaultExportPreset": creator_attrs["exportPresetUrl"], + + # Custom overrides to the exporter + "exportParameters": [ + { + "parameters": { + "fileFormat": creator_attrs["exportFileFormat"], + "sizeLog2": creator_attrs["exportSize"], + "paddingAlgorithm": creator_attrs["exportPadding"], + "dilationDistance": creator_attrs["exportDilationDistance"] # noqa + } + } + ] + } + + # Create the list of Texture Sets to export. + config["exportList"] = [] + for texture_set in substance_painter.textureset.all_texture_sets(): + # stack = texture_set.get_stack() + config["exportList"].append({"rootPath": texture_set.name()}) + + # Consider None values optionals + for override in config["exportParameters"]: + parameters = override.get("parameters") + for key, value in dict(parameters).items(): + if value is None: + parameters.pop(key) + + result = substance_painter.export.export_project_textures(config) + + if result.status != substance_painter.export.ExportStatus.Success: + raise KnownPublishError( + "Failed to export texture set: {}".format(result.message) + ) + + files = [] + for stack, maps in result.textures.items(): + for texture_map in maps: + self.log.info(f"Exported texture: {texture_map}") + files.append(texture_map) + + # TODO: add the representations so they integrate the way we'd want + """ + instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] + """ From 0741c9850861779974e95cf764c3a7d2f0b097cc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 9 Jan 2023 18:15:06 +0100 Subject: [PATCH 033/918] Cosmetics --- .../hosts/substancepainter/plugins/publish/extract_textures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 93e0c8cb31..d72d9920fd 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -55,7 +55,7 @@ class ExtractTextures(publish.Extractor): ) files = [] - for stack, maps in result.textures.items(): + for _stack, maps in result.textures.items(): for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") files.append(texture_map) From 87f23c978d44d587e74adfb2d517da798dfecafe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 00:52:07 +0100 Subject: [PATCH 034/918] Add the built-in `export-preset-generator` template entries --- .../plugins/create/create_textures.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index af2e23b3bf..41de2ad946 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -48,7 +48,20 @@ def get_export_presets(): export_templates = dict(sorted(preset_resources.items(), key=lambda x: x[1])) - return export_templates + # Add default built-ins at the start + # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa + result = { + "export-preset-generator://viewport2d": "2D View", # noqa + "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa + "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa + "export-preset-generator://sketchfab": "Sketchfab", # noqa + "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa + "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa + "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa + "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa + } + result.update(export_templates) + return result class CreateTextures(Creator): From 9a4f5650199000658e93e189810cca7b1482e9ed Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 01:21:08 +0100 Subject: [PATCH 035/918] Shorten label --- .../hosts/substancepainter/plugins/create/create_textures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 41de2ad946..c1d907a974 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -153,8 +153,8 @@ class CreateTextures(Creator): decimals=0, default=16, label="Dilation Distance"), - UILabelDef("Note: Dilation Distance is only used with " - "'Dilation + ' padding options"), + UILabelDef("*only used with " + "'Dilation + ' padding"), ] def get_pre_create_attr_defs(self): From 139eafb5c7e951dcc08fa1c1a8e7e5bf2a4928d1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 01:21:31 +0100 Subject: [PATCH 036/918] Debug log used Substance Painter export preset --- .../substancepainter/plugins/publish/extract_textures.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index d72d9920fd..8ebad3193f 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -14,12 +14,15 @@ class ExtractTextures(publish.Extractor): staging_dir = self.staging_dir(instance) - # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa creator_attrs = instance.data["creator_attributes"] + preset_url = creator_attrs["exportPresetUrl"] + self.log.debug(f"Exporting using preset: {preset_url}") + + # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa config = { "exportShaderParams": True, "exportPath": staging_dir, - "defaultExportPreset": creator_attrs["exportPresetUrl"], + "defaultExportPreset": preset_url, # Custom overrides to the exporter "exportParameters": [ From 391ba1ada24ffb275443a47f008b6afce2feba52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 11:21:55 +0100 Subject: [PATCH 037/918] Remove unusued imports --- openpype/hosts/substancepainter/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 70353039f5..aae1f39a3e 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -8,9 +8,7 @@ from functools import partial # Substance 3D Painter modules import substance_painter.ui import substance_painter.event -import substance_painter.export import substance_painter.project -import substance_painter.textureset from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost From c1abd00bba43cb98501efd649462c990414f720c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:33:17 +0100 Subject: [PATCH 038/918] Store menu and callbacks on the SubstanceHost instance --- .../hosts/substancepainter/api/pipeline.py | 120 +++++++++--------- 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index aae1f39a3e..db4bb47401 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -34,14 +34,9 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") - OPENPYPE_METADATA_KEY = "OpenPype" OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key -self = sys.modules[__name__] -self.menu = None -self.callbacks = [] - class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "substancepainter" @@ -49,6 +44,8 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def __init__(self): super(SubstanceHost, self).__init__() self._has_been_setup = False + self.menu = None + self.callbacks = [] def install(self): pyblish.api.register_host("substancepainter") @@ -59,20 +56,20 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Installing callbacks ... ") # register_event_callback("init", on_init) - _register_callbacks() + self._register_callbacks() # register_event_callback("before.save", before_save) # register_event_callback("save", on_save) register_event_callback("open", on_open) # register_event_callback("new", on_new) log.info("Installing menu ... ") - _install_menu() + self._install_menu() self._has_been_setup = True def uninstall(self): - _uninstall_menu() - _deregister_callbacks() + self._uninstall_menu() + self._deregister_callbacks() def has_unsaved_changes(self): @@ -146,74 +143,71 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def get_context_data(self): pass + def _install_menu(self): + from PySide2 import QtWidgets + from openpype.tools.utils import host_tools -def _install_menu(): - from PySide2 import QtWidgets - from openpype.tools.utils import host_tools + parent = substance_painter.ui.get_main_window() - parent = substance_painter.ui.get_main_window() + menu = QtWidgets.QMenu("OpenPype") - menu = QtWidgets.QMenu("OpenPype") + action = menu.addAction("Load...") + action.triggered.connect( + lambda: host_tools.show_loader(parent=parent, use_context=True) + ) - action = menu.addAction("Load...") - action.triggered.connect( - lambda: host_tools.show_loader(parent=parent, use_context=True) - ) + action = menu.addAction("Publish...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent) + ) - action = menu.addAction("Publish...") - action.triggered.connect( - lambda: host_tools.show_publisher(parent=parent) - ) + action = menu.addAction("Manage...") + action.triggered.connect( + lambda: host_tools.show_scene_inventory(parent=parent) + ) - action = menu.addAction("Manage...") - action.triggered.connect( - lambda: host_tools.show_scene_inventory(parent=parent) - ) + action = menu.addAction("Library...") + action.triggered.connect( + lambda: host_tools.show_library_loader(parent=parent) + ) - action = menu.addAction("Library...") - action.triggered.connect( - lambda: host_tools.show_library_loader(parent=parent) - ) + menu.addSeparator() + action = menu.addAction("Work Files...") + action.triggered.connect( + lambda: host_tools.show_workfiles(parent=parent) + ) - menu.addSeparator() - action = menu.addAction("Work Files...") - action.triggered.connect( - lambda: host_tools.show_workfiles(parent=parent) - ) + substance_painter.ui.add_menu(menu) - substance_painter.ui.add_menu(menu) + def on_menu_destroyed(): + self.menu = None - def on_menu_destroyed(): - self.menu = None + menu.destroyed.connect(on_menu_destroyed) - menu.destroyed.connect(on_menu_destroyed) + self.menu = menu - self.menu = menu + def _uninstall_menu(self): + if self.menu: + self.menu.destroy() + self.menu = None + + def _register_callbacks(self): + # Prepare emit event callbacks + open_callback = partial(emit_event, "open") + + # Connect to the Substance Painter events + dispatcher = substance_painter.event.DISPATCHER + for event, callback in [ + (substance_painter.event.ProjectOpened, open_callback) + ]: + dispatcher.connect(event, callback) + # Keep a reference so we can deregister if needed + self.callbacks.append((event, callback)) -def _uninstall_menu(): - if self.menu: - self.menu.destroy() - self.menu = None - - -def _register_callbacks(): - # Prepare emit event callbacks - open_callback = partial(emit_event, "open") - - # Connect to the Substance Painter events - dispatcher = substance_painter.event.DISPATCHER - for event, callback in [ - (substance_painter.event.ProjectOpened, open_callback) - ]: - dispatcher.connect(event, callback) - # Keep a reference so we can deregister if needed - self.callbacks.append((event, callback)) - - -def _deregister_callbacks(): - for event, callback in self.callbacks: - substance_painter.event.DISPATCHER.disconnect(event, callback) + def _deregister_callbacks(self): + for event, callback in self.callbacks: + substance_painter.event.DISPATCHER.disconnect(event, callback) def on_open(): From df5300ed32a0a4cff5af52a930c535773238deda Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:33:33 +0100 Subject: [PATCH 039/918] Cosmetics --- openpype/hosts/substancepainter/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index db4bb47401..48adc107e2 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -204,7 +204,6 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # Keep a reference so we can deregister if needed self.callbacks.append((event, callback)) - def _deregister_callbacks(self): for event, callback in self.callbacks: substance_painter.event.DISPATCHER.disconnect(event, callback) From 3b4f9feaadfaaee4ae763a78744a274cd467e744 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:34:20 +0100 Subject: [PATCH 040/918] Remove unused import --- openpype/hosts/substancepainter/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 48adc107e2..df705bb010 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Pipeline tools for OpenPype Gaffer integration.""" import os -import sys import logging from functools import partial From 5a7c5762847ed22f89a26d09f062a0948c34397b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 16:44:09 +0100 Subject: [PATCH 041/918] Remove debug print message --- openpype/hosts/substancepainter/api/pipeline.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index df705bb010..3a68a7fa86 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -210,7 +210,6 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def on_open(): log.info("Running callback on open..") - print("Run") if any_outdated_containers(): from openpype.widgets import popup From 24b6583c63ea14920bc6a56649c7db6ed1e3176c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Jan 2023 17:58:47 +0100 Subject: [PATCH 042/918] Set explicit defaults for creator --- .../hosts/substancepainter/plugins/create/create_textures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index c1d907a974..6d4f816961 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -123,6 +123,7 @@ class CreateTextures(Creator): None: "Based on output template", # TODO: implement extensions }, + default=None, label="File type"), EnumDef("exportSize", items={ @@ -136,6 +137,7 @@ class CreateTextures(Creator): 11: "2048", 12: "4096" }, + default=None, label="Size"), EnumDef("exportPadding", @@ -146,6 +148,7 @@ class CreateTextures(Creator): "color": "Dilation + default background color", "diffusion": "Dilation + diffusion" }, + default="infinite", label="Padding"), NumberDef("exportDilationDistance", minimum=0, From 61710d614d5753b2287c9c5be5110147bd4612b0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 13:23:51 +0100 Subject: [PATCH 043/918] TODO was already resolved --- openpype/hosts/substancepainter/plugins/load/load_mesh.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 3e62b90988..00f808199f 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -102,7 +102,6 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): path = get_representation_path(representation) # Reload the mesh - # TODO: Re-use settings from first load? container_options = container.get("options", {}) settings = substance_painter.project.MeshReloadingSettings( import_cameras=container_options.get("import_cameras", True), From 2177877713f538f70217a944014212fc183c7412 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 14:47:38 +0100 Subject: [PATCH 044/918] Load OpenPype plug-in on first run of Substance Painter through OpenPype --- .../startup/openpype_load_on_first_run.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py diff --git a/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py new file mode 100644 index 0000000000..90b1ec6bbd --- /dev/null +++ b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py @@ -0,0 +1,43 @@ +"""Ease the OpenPype on-boarding process by loading the plug-in on first run""" + +OPENPYPE_PLUGIN_NAME = "openpype_plugin" + + +def start_plugin(): + try: + # This isn't exposed in the official API so we keep it in a try-except + from painter_plugins_ui import ( + get_settings, + LAUNCH_AT_START_KEY, + ON_STATE, + PLUGINS_MENU, + plugin_manager + ) + + # The `painter_plugins_ui` plug-in itself is also a startup plug-in + # we need to take into account that it could run either earlier or + # later than this startup script, we check whether its menu initialized + is_before_plugins_menu = PLUGINS_MENU is None + + settings = get_settings(OPENPYPE_PLUGIN_NAME) + if settings.value(LAUNCH_AT_START_KEY, None) is not None: + print("Initializing OpenPype plug-in on first run...") + if is_before_plugins_menu: + print("- running before 'painter_plugins_ui'") + # Delay the launch to the painter_plugins_ui initialization + settings.setValue(LAUNCH_AT_START_KEY, ON_STATE) + else: + # Launch now + print("- running after 'painter_plugins_ui'") + plugin_manager(OPENPYPE_PLUGIN_NAME)(True) + + # Set the checked state in the menu to avoid confusion + action = next(action for action in PLUGINS_MENU._menu.actions() + if action.text() == OPENPYPE_PLUGIN_NAME) + if action is not None: + action.blockSignals(True) + action.setChecked(True) + action.blockSignals(False) + + except Exception as exc: + print(exc) From d1d15683983db8d3d9ca9e1a121b794b9b0acf3e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 14:54:07 +0100 Subject: [PATCH 045/918] Fix logic --- .../deploy/startup/openpype_load_on_first_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py index 90b1ec6bbd..04b610b4df 100644 --- a/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py +++ b/openpype/hosts/substancepainter/deploy/startup/openpype_load_on_first_run.py @@ -20,7 +20,7 @@ def start_plugin(): is_before_plugins_menu = PLUGINS_MENU is None settings = get_settings(OPENPYPE_PLUGIN_NAME) - if settings.value(LAUNCH_AT_START_KEY, None) is not None: + if settings.value(LAUNCH_AT_START_KEY, None) is None: print("Initializing OpenPype plug-in on first run...") if is_before_plugins_menu: print("- running before 'painter_plugins_ui'") From d2baa5ec4d9f92c143172f95719bb7b319ae79a2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jan 2023 15:38:22 +0100 Subject: [PATCH 046/918] Allow to configure custom shelves for Substance Painter in project settings --- openpype/hosts/substancepainter/api/lib.py | 57 +++++++++++++++++++ .../hosts/substancepainter/api/pipeline.py | 28 +++++++++ .../project_settings/substancepainter.json | 3 + .../schemas/projects_schema/schema_main.json | 4 ++ .../schema_project_substancepainter.json | 18 ++++++ 5 files changed, 110 insertions(+) create mode 100644 openpype/hosts/substancepainter/api/lib.py create mode 100644 openpype/settings/defaults/project_settings/substancepainter.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py new file mode 100644 index 0000000000..d468f6cc45 --- /dev/null +++ b/openpype/hosts/substancepainter/api/lib.py @@ -0,0 +1,57 @@ +import os +import re +import substance_painter.resource + + +def load_shelf(path, name=None): + """Add shelf to substance painter (for current application session) + + This will dynamically add a Shelf for the current session. It's good + to note however that these will *not* persist on restart of the host. + + Note: + Consider the loaded shelf a static library of resources. + + The shelf will *not* be visible in application preferences in + Edit > Settings > Libraries. + + The shelf will *not* show in the Assets browser if it has no existing + assets + + The shelf will *not* be a selectable option for selecting it as a + destination to import resources too. + + """ + + # Ensure expanded path with forward slashes + path = os.path.expandvars(path) + path = os.path.abspath(path) + path = path.replace("\\", "/") + + # Path must exist + if not os.path.isdir(path): + raise ValueError(f"Path is not an existing folder: {path}") + + # This name must be unique and must only contain lowercase letters, + # numbers, underscores or hyphens. + if name is None: + name = os.path.basename(path) + + name = name.lower() + name = re.sub(r"[^a-z0-9_\-]", "_", name) # sanitize to underscores + + if substance_painter.resource.Shelves.exists(name): + shelf = next( + shelf for shelf in substance_painter.resource.Shelves.all() + if shelf.name() == name + ) + if os.path.normpath(shelf.path()) != os.path.normpath(path): + raise ValueError(f"Shelf with name '{name}' already exists " + f"for a different path: '{shelf.path()}") + + return + + print(f"Adding Shelf '{name}' to path: {path}") + substance_painter.resource.Shelves.add(name, path) + + return name diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 3a68a7fa86..f4d4c5b00c 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -10,6 +10,7 @@ import substance_painter.event import substance_painter.project from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost +from openpype.settings import get_current_project_settings import pyblish.api @@ -25,6 +26,8 @@ from openpype.lib import ( from openpype.pipeline.load import any_outdated_containers from openpype.hosts.substancepainter import SUBSTANCE_HOST_DIR +from . import lib + log = logging.getLogger("openpype.hosts.substance") PLUGINS_DIR = os.path.join(SUBSTANCE_HOST_DIR, "plugins") @@ -45,6 +48,7 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self._has_been_setup = False self.menu = None self.callbacks = [] + self.shelves = [] def install(self): pyblish.api.register_host("substancepainter") @@ -64,9 +68,13 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Installing menu ... ") self._install_menu() + project_settings = get_current_project_settings() + self._install_shelves(project_settings) + self._has_been_setup = True def uninstall(self): + self._uninstall_shelves() self._uninstall_menu() self._deregister_callbacks() @@ -206,6 +214,26 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def _deregister_callbacks(self): for event, callback in self.callbacks: substance_painter.event.DISPATCHER.disconnect(event, callback) + self.callbacks.clear() + + def _install_shelves(self, project_settings): + + shelves = project_settings["substancepainter"].get("shelves", {}) + for name, path in shelves.items(): + # TODO: Allow formatting with anatomy for the paths + shelf_name = None + try: + shelf_name = lib.load_shelf(path, name=name) + except ValueError as exc: + print(f"Failed to load shelf -> {exc}") + + if shelf_name: + self.shelves.append(shelf_name) + + def _uninstall_shelves(self): + for shelf_name in self.shelves: + substance_painter.resource.Shelves.remove(shelf_name) + self.shelves.clear() def on_open(): diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json new file mode 100644 index 0000000000..a424a923da --- /dev/null +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -0,0 +1,3 @@ +{ + "shelves": {} +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index 0b9fbf7470..b3c5c62a89 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -114,6 +114,10 @@ "type": "schema", "name": "schema_project_photoshop" }, + { + "type": "schema", + "name": "schema_project_substancepainter" + }, { "type": "schema", "name": "schema_project_harmony" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json new file mode 100644 index 0000000000..4a02a9d8ca --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json @@ -0,0 +1,18 @@ +{ + "type": "dict", + "collapsible": true, + "key": "substancepainter", + "label": "Substance Painter", + "is_file": true, + "children": [ + { + "type": "dict-modifiable", + "key": "shelves", + "label": "Shelves", + "use_label_wrap": true, + "object_type": { + "type": "text" + } + } + ] +} From 42b207445ed49dab7d5ce23556d7cbd0e7316ba3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 12:32:38 +0100 Subject: [PATCH 047/918] Implement working WIP draft for Texture Publishing --- .../hosts/substancepainter/api/colorspace.py | 157 +++++++++++++ openpype/hosts/substancepainter/api/lib.py | 139 ++++++++++++ .../plugins/create/create_textures.py | 71 +----- .../publish/collect_textureset_images.py | 207 ++++++++++++++++++ .../plugins/publish/extract_textures.py | 87 +++----- 5 files changed, 548 insertions(+), 113 deletions(-) create mode 100644 openpype/hosts/substancepainter/api/colorspace.py create mode 100644 openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py diff --git a/openpype/hosts/substancepainter/api/colorspace.py b/openpype/hosts/substancepainter/api/colorspace.py new file mode 100644 index 0000000000..f7b9f7694a --- /dev/null +++ b/openpype/hosts/substancepainter/api/colorspace.py @@ -0,0 +1,157 @@ +"""Substance Painter OCIO management + +Adobe Substance 3D Painter supports OCIO color management using a per project +configuration. Output color spaces are defined at the project level + +More information see: + - https://substance3d.adobe.com/documentation/spdoc/color-management-223053233.html # noqa + - https://substance3d.adobe.com/documentation/spdoc/color-management-with-opencolorio-225969419.html # noqa + +""" +import substance_painter.export +import substance_painter.js +import json + +from .lib import ( + get_document_structure, + get_channel_format +) + + +def _iter_document_stack_channels(): + """Yield all stack paths and channels project""" + + for material in get_document_structure()["materials"]: + material_name = material["name"] + for stack in material["stacks"]: + stack_name = stack["name"] + for channel in stack["channels"]: + if stack_name: + stack_path = [material_name, stack_name] + else: + stack_path = material_name + yield stack_path, channel + + +def _get_first_color_and_data_stack_and_channel(): + """Return first found color channel and data channel.""" + color_channel = None + data_channel = None + for stack_path, channel in _iter_document_stack_channels(): + channel_format = get_channel_format(stack_path, channel) + if channel_format["color"]: + color_channel = (stack_path, channel) + else: + data_channel = (stack_path, channel) + + if color_channel and data_channel: + return color_channel, data_channel + + return color_channel, data_channel + + +def get_project_channel_data(): + """Return colorSpace settings for the current substance painter project. + + In Substance Painter only color channels have Color Management enabled + whereas data channels have no color management applied. This can't be + changed. The artist can only customize the export color space for color + channels per bit-depth for 8 bpc, 16 bpc and 32 bpc. + + As such this returns the color space for 'data' and for per bit-depth + for color channels. + + Example output: + { + "data": {'colorSpace': 'Utility - Raw'}, + "8": {"colorSpace": "ACES - AcesCG"}, + "16": {"colorSpace": "ACES - AcesCG"}, + "16f": {"colorSpace": "ACES - AcesCG"}, + "32f": {"colorSpace": "ACES - AcesCG"} + } + + """ + + keys = ["colorSpace"] + query = {key: f"${key}" for key in keys} + + config = { + "exportPath": "/", + "exportShaderParams": False, + "defaultExportPreset": "query_preset", + + "exportPresets": [{ + "name": "query_preset", + + # List of maps making up this export preset. + "maps": [{ + "fileName": json.dumps(query), + # List of source/destination defining which channels will + # make up the texture file. + "channels": [], + "parameters": { + "fileFormat": "exr", + "bitDepth": "32f", + "dithering": False, + "sizeLog2": 4, + "paddingAlgorithm": "passthrough", + "dilationDistance": 16 + } + }] + }], + } + + def _get_query_output(config): + # Return the basename of the single output path we defined + result = substance_painter.export.list_project_textures(config) + path = next(iter(result.values()))[0] + # strip extension and slash since we know relevant json data starts + # and ends with { and } characters + path = path.strip("/\\.exr") + return json.loads(path) + + # Query for each type of channel (color and data) + color_channel, data_channel = _get_first_color_and_data_stack_and_channel() + colorspaces = {} + for key, channel_data in { + "data": data_channel, + "color": color_channel + }.items(): + if channel_data is None: + # No channel of that datatype anywhere in the Stack. We're + # unable to identify the output color space of the project + colorspaces[key] = None + continue + + stack, channel = channel_data + + # Stack must be a string + if not isinstance(stack, str): + # Assume iterable + stack = "/".join(stack) + + # Define the temp output config + config["exportList"] = [{"rootPath": stack}] + config_map = config["exportPresets"][0]["maps"][0] + config_map["channels"] = [ + { + "destChannel": x, + "srcChannel": x, + "srcMapType": "documentMap", + "srcMapName": channel + } for x in "RGB" + ] + + if key == "color": + # Query for each bit depth + # Color space definition can have a different OCIO config set + # for 8-bit, 16-bit and 32-bit outputs so we need to check each + # bit depth + for depth in ["8", "16", "16f", "32f"]: + config_map["parameters"]["bitDepth"] = depth # noqa + colorspaces[key + depth] = _get_query_output(config) + else: + # Data channel (not color managed) + colorspaces[key] = _get_query_output(config) + + return colorspaces diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index d468f6cc45..b929f881a8 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -1,6 +1,145 @@ import os import re +import json + import substance_painter.resource +import substance_painter.js + + +def get_export_presets(): + """Return Export Preset resource URLs for all available Export Presets. + + Returns: + dict: {Resource url: GUI Label} + + """ + # TODO: Find more optimal way to find all export templates + + preset_resources = {} + for shelf in substance_painter.resource.Shelves.all(): + shelf_path = os.path.normpath(shelf.path()) + + presets_path = os.path.join(shelf_path, "export-presets") + if not os.path.exists(presets_path): + continue + + for filename in os.listdir(presets_path): + if filename.endswith(".spexp"): + template_name = os.path.splitext(filename)[0] + + resource = substance_painter.resource.ResourceID( + context=shelf.name(), + name=template_name + ) + resource_url = resource.url() + + preset_resources[resource_url] = template_name + + # Sort by template name + export_templates = dict(sorted(preset_resources.items(), + key=lambda x: x[1])) + + # Add default built-ins at the start + # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa + result = { + "export-preset-generator://viewport2d": "2D View", # noqa + "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa + "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa + "export-preset-generator://sketchfab": "Sketchfab", # noqa + "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa + "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa + "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa + "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa + } + result.update(export_templates) + return result + + +def _convert_stack_path_to_cmd_str(stack_path): + """Convert stack path `str` or `[str, str]` for javascript query + + Example usage: + >>> stack_path = _convert_stack_path_to_cmd_str(stack_path) + >>> cmd = f"alg.mapexport.channelIdentifiers({stack_path})" + >>> substance_painter.js.evaluate(cmd) + + Args: + stack_path (list or str): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + + Returns: + str: Stack path usable as argument in javascript query. + + """ + return json.dumps(stack_path) + + +def get_channel_identifiers(stack_path=None): + """Return the list of channel identifiers. + + If a context is passed (texture set/stack), + return only used channels with resolved user channels. + + Channel identifiers are: + basecolor, height, specular, opacity, emissive, displacement, + glossiness, roughness, anisotropylevel, anisotropyangle, transmissive, + scattering, reflection, ior, metallic, normal, ambientOcclusion, + diffuse, specularlevel, blendingmask, [custom user names]. + + Args: + stack_path (list or str, Optional): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + + Returns: + list: List of channel identifiers. + + """ + if stack_path is None: + stack_path = "" + else: + stack_path = _convert_stack_path_to_cmd_str(stack_path) + cmd = f"alg.mapexport.channelIdentifiers({stack_path})" + return substance_painter.js.evaluate(cmd) + + +def get_channel_format(stack_path, channel): + """Retrieve the channel format of a specific stack channel. + + See `alg.mapexport.channelFormat` (javascript API) for more details. + + The channel format data is: + "label" (str): The channel format label: could be one of + [sRGB8, L8, RGB8, L16, RGB16, L16F, RGB16F, L32F, RGB32F] + "color" (bool): True if the format is in color, False is grayscale + "floating" (bool): True if the format uses floating point + representation, false otherwise + "bitDepth" (int): Bit per color channel (could be 8, 16 or 32 bpc) + + Args: + stack_path (list or str): Path to the stack, could be + "Texture set name" or ["Texture set name", "Stack name"] + channel (str): Identifier of the channel to export + (see `get_channel_identifiers`) + + Returns: + dict: The channel format data. + + """ + stack_path = _convert_stack_path_to_cmd_str(stack_path) + cmd = f"alg.mapexport.channelFormat({stack_path}, '{channel}')" + return substance_painter.js.evaluate(cmd) + + +def get_document_structure(): + """Dump the document structure. + + See `alg.mapexport.documentStructure` (javascript API) for more details. + + Returns: + dict: Document structure or None when no project is open + + """ + return substance_painter.js.evaluate("alg.mapexport.documentStructure()") def load_shelf(path, name=None): diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 6d4f816961..9d641215dc 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -1,74 +1,27 @@ # -*- coding: utf-8 -*- """Creator plugin for creating textures.""" -import os from openpype.pipeline import CreatedInstance, Creator - -from openpype.hosts.substancepainter.api.pipeline import ( - set_project_metadata, - get_project_metadata -) - from openpype.lib import ( EnumDef, UILabelDef, NumberDef ) +from openpype.hosts.substancepainter.api.pipeline import ( + set_project_metadata, + get_project_metadata +) +from openpype.hosts.substancepainter.api.lib import get_export_presets + import substance_painter.project -import substance_painter.resource - - -def get_export_presets(): - import substance_painter.resource - - preset_resources = {} - - # TODO: Find more optimal way to find all export templates - for shelf in substance_painter.resource.Shelves.all(): - shelf_path = os.path.normpath(shelf.path()) - - presets_path = os.path.join(shelf_path, "export-presets") - if not os.path.exists(presets_path): - continue - - for fname in os.listdir(presets_path): - if fname.endswith(".spexp"): - template_name = os.path.splitext(fname)[0] - - resource = substance_painter.resource.ResourceID( - context=shelf.name(), - name=template_name - ) - resource_url = resource.url() - - preset_resources[resource_url] = template_name - - # Sort by template name - export_templates = dict(sorted(preset_resources.items(), - key=lambda x: x[1])) - - # Add default built-ins at the start - # TODO: find the built-ins automatically; scraped with https://gist.github.com/BigRoy/97150c7c6f0a0c916418207b9a2bc8f1 # noqa - result = { - "export-preset-generator://viewport2d": "2D View", # noqa - "export-preset-generator://doc-channel-normal-no-alpha": "Document channels + Normal + AO (No Alpha)", # noqa - "export-preset-generator://doc-channel-normal-with-alpha": "Document channels + Normal + AO (With Alpha)", # noqa - "export-preset-generator://sketchfab": "Sketchfab", # noqa - "export-preset-generator://adobe-standard-material": "Substance 3D Stager", # noqa - "export-preset-generator://usd": "USD PBR Metal Roughness", # noqa - "export-preset-generator://gltf": "glTF PBR Metal Roughness", # noqa - "export-preset-generator://gltf-displacement": "glTF PBR Metal Roughness + Displacement texture (experimental)" # noqa - } - result.update(export_templates) - return result class CreateTextures(Creator): """Create a texture set.""" - identifier = "io.openpype.creators.substancepainter.textures" + identifier = "io.openpype.creators.substancepainter.textureset" label = "Textures" - family = "textures" + family = "textureSet" icon = "picture-o" default_variant = "Main" @@ -79,19 +32,19 @@ class CreateTextures(Creator): return instance = self.create_instance_in_context(subset_name, instance_data) - set_project_metadata("textures", instance.data_to_store()) + set_project_metadata("textureSet", instance.data_to_store()) def collect_instances(self): - workfile = get_project_metadata("textures") + workfile = get_project_metadata("textureSet") if workfile: self.create_instance_in_context_from_existing(workfile) def update_instances(self, update_list): for instance, _changes in update_list: # Update project's metadata - data = get_project_metadata("textures") or {} + data = get_project_metadata("textureSet") or {} data.update(instance.data_to_store()) - set_project_metadata("textures", data) + set_project_metadata("textureSet", data) def remove_instances(self, instances): for instance in instances: diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py new file mode 100644 index 0000000000..96f2daa525 --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -0,0 +1,207 @@ +import os +import copy +import clique +import pyblish.api + +from openpype.pipeline import publish + +import substance_painter.export +from openpype.hosts.substancepainter.api.colorspace import ( + get_project_channel_data, +) + + +def get_project_color_spaces(): + """Return unique color space names used for exports. + + This is based on the Color Management preferences of the project. + + See also: + func:`get_project_channel_data` + + """ + return set( + data["colorSpace"] for data in get_project_channel_data().values() + ) + + +def _get_channel_name(path, + texture_set_name, + project_colorspaces): + """Return expected 'name' for the output image. + + This will be used as a suffix to the separate image publish subsets. + + """ + # TODO: This will require improvement before being production ready. + # TODO(Question): Should we preserve the texture set name in the suffix + # TODO so that exports with multiple texture sets can work within a single + # TODO parent textureSet, like `texture{Variant}.{TextureSet}{Channel}` + name = os.path.basename(path) # filename + name = os.path.splitext(name)[0] # no extension + # Usually the channel identifier comes after $textureSet in + # the export preset. Unfortunately getting the export maps + # and channels explicitly is not trivial so for now we just + # assume this will generate a nice identifier for the end user + name = name.split(f"{texture_set_name}_", 1)[-1] + + # TODO: We need more explicit ways to detect the color space part + for colorspace in project_colorspaces: + if name.endswith(f"_{colorspace}"): + name = name[:-len(f"_{colorspace}")] + break + + return name + + +class CollectTextureSet(pyblish.api.InstancePlugin): + """Extract Textures using an output template config""" + # TODO: More explicitly detect UDIM tiles + # TODO: Get color spaces + # TODO: Detect what source data channels end up in each file + + label = "Collect Texture Set images" + hosts = ['substancepainter'] + families = ["textureSet"] + order = pyblish.api.CollectorOrder + + def process(self, instance): + + config = self.get_export_config(instance) + textures = substance_painter.export.list_project_textures(config) + + instance.data["exportConfig"] = config + + colorspaces = get_project_color_spaces() + + outputs = {} + for (texture_set_name, stack_name), maps in textures.items(): + + # Log our texture outputs + self.log.debug(f"Processing stack: {stack_name}") + for texture_map in maps: + self.log.debug(f"Expecting texture: {texture_map}") + + # For now assume the UDIM textures end with .. and + # when no trailing number is present before the extension then it's + # considered to *not* be a UDIM export. + collections, remainder = clique.assemble( + maps, + patterns=[clique.PATTERNS["frames"]], + minimum_items=True + ) + + outputs = {} + if collections: + # UDIM tile sequence + for collection in collections: + name = _get_channel_name(collection.head, + texture_set_name=texture_set_name, + project_colorspaces=colorspaces) + outputs[name] = collection + self.log.info(f"UDIM Collection: {collection}") + else: + # Single file per channel without UDIM number + for path in remainder: + name = _get_channel_name(path, + texture_set_name=texture_set_name, + project_colorspaces=colorspaces) + outputs[name] = path + self.log.info(f"Single file: {path}") + + # Let's break the instance into multiple instances to integrate + # a subset per generated texture or texture UDIM sequence + context = instance.context + for map_name, map_output in outputs.items(): + + is_udim = isinstance(map_output, clique.Collection) + if is_udim: + first_file = list(map_output)[0] + map_fnames = [os.path.basename(path) for path in map_output] + else: + first_file = map_output + map_fnames = map_output + + ext = os.path.splitext(first_file)[1] + assert ext.lstrip('.'), f"No extension: {ext}" + + # Define the suffix we want to give this particular texture + # set and set up a remapped subset naming for it. + suffix = f".{map_name}" + image_subset = instance.data["subset"][len("textureSet"):] + image_subset = "texture" + image_subset + suffix + + # TODO: Retrieve and store color space with the representation + + # Clone the instance + image_instance = context.create_instance(instance.name) + image_instance[:] = instance[:] + image_instance.data.update(copy.deepcopy(instance.data)) + image_instance.data["name"] = image_subset + image_instance.data["label"] = image_subset + image_instance.data["subset"] = image_subset + image_instance.data["family"] = "image" + image_instance.data["families"] = ["image", "textures"] + image_instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': map_fnames, + }] + + instance.append(image_instance) + + def get_export_config(self, instance): + """Return an export configuration dict for texture exports. + + This config can be supplied to: + - `substance_painter.export.export_project_textures` + - `substance_painter.export.list_project_textures` + + See documentation on substance_painter.export module about the + formatting of the configuration dictionary. + + Args: + instance (pyblish.api.Instance): Texture Set instance to be + published. + + Returns: + dict: Export config + + """ + + creator_attrs = instance.data["creator_attributes"] + preset_url = creator_attrs["exportPresetUrl"] + self.log.debug(f"Exporting using preset: {preset_url}") + + # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa + config = { # noqa + "exportShaderParams": True, + "exportPath": publish.get_instance_staging_dir(instance), + "defaultExportPreset": preset_url, + + # Custom overrides to the exporter + "exportParameters": [ + { + "parameters": { + "fileFormat": creator_attrs["exportFileFormat"], + "sizeLog2": creator_attrs["exportSize"], + "paddingAlgorithm": creator_attrs["exportPadding"], + "dilationDistance": creator_attrs["exportDilationDistance"] # noqa + } + } + ] + } + + # Create the list of Texture Sets to export. + config["exportList"] = [] + for texture_set in substance_painter.textureset.all_texture_sets(): + config["exportList"].append({"rootPath": texture_set.name()}) + + # Consider None values from the creator attributes optionals + for override in config["exportParameters"]: + parameters = override.get("parameters") + for key, value in dict(parameters).items(): + if value is None: + parameters.pop(key) + + return config diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 8ebad3193f..e99b93cac9 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -1,55 +1,28 @@ from openpype.pipeline import KnownPublishError, publish - import substance_painter.export class ExtractTextures(publish.Extractor): - """Extract Textures using an output template config""" + """Extract Textures using an output template config. - label = "Extract Texture Sets" + Note: + This Extractor assumes that `collect_textureset_images` has prepared + the relevant export config and has also collected the individual image + instances for publishing including its representation. That is why this + particular Extractor doesn't specify representations to integrate. + + """ + # TODO: More explicitly detect UDIM tiles + # TODO: Get color spaces + # TODO: Detect what source data channels end up in each file + + label = "Extract Texture Set" hosts = ['substancepainter'] - families = ["textures"] + families = ["textureSet"] def process(self, instance): - staging_dir = self.staging_dir(instance) - - creator_attrs = instance.data["creator_attributes"] - preset_url = creator_attrs["exportPresetUrl"] - self.log.debug(f"Exporting using preset: {preset_url}") - - # See: https://substance3d.adobe.com/documentation/ptpy/api/substance_painter/export # noqa - config = { - "exportShaderParams": True, - "exportPath": staging_dir, - "defaultExportPreset": preset_url, - - # Custom overrides to the exporter - "exportParameters": [ - { - "parameters": { - "fileFormat": creator_attrs["exportFileFormat"], - "sizeLog2": creator_attrs["exportSize"], - "paddingAlgorithm": creator_attrs["exportPadding"], - "dilationDistance": creator_attrs["exportDilationDistance"] # noqa - } - } - ] - } - - # Create the list of Texture Sets to export. - config["exportList"] = [] - for texture_set in substance_painter.textureset.all_texture_sets(): - # stack = texture_set.get_stack() - config["exportList"].append({"rootPath": texture_set.name()}) - - # Consider None values optionals - for override in config["exportParameters"]: - parameters = override.get("parameters") - for key, value in dict(parameters).items(): - if value is None: - parameters.pop(key) - + config = instance.data["exportConfig"] result = substance_painter.export.export_project_textures(config) if result.status != substance_painter.export.ExportStatus.Success: @@ -57,18 +30,24 @@ class ExtractTextures(publish.Extractor): "Failed to export texture set: {}".format(result.message) ) - files = [] - for _stack, maps in result.textures.items(): + for (texture_set_name, stack_name), maps in result.textures.items(): + # Log our texture outputs + self.log.info(f"Processing stack: {stack_name}") for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") - files.append(texture_map) - # TODO: add the representations so they integrate the way we'd want - """ - instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': file, - "stagingDir": folder, - }] - """ + # TODO: Confirm outputs match what we collected + # TODO: Confirm the files indeed exist + # TODO: make sure representations are registered + + # Add a fake representation which won't be integrated so the + # Integrator leaves us alone - otherwise it would error + # TODO: Add `instance.data["integrate"] = False` support in Integrator? + instance.data["representations"] = [ + { + "name": "_fake", + "ext": "_fake", + "delete": True, + "files": [] + } + ] From bd73709463440b520deafb6e9ac82995b6e6e430 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 12:33:43 +0100 Subject: [PATCH 048/918] Fix indentation --- openpype/hosts/substancepainter/api/colorspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/api/colorspace.py b/openpype/hosts/substancepainter/api/colorspace.py index f7b9f7694a..a9df3eb066 100644 --- a/openpype/hosts/substancepainter/api/colorspace.py +++ b/openpype/hosts/substancepainter/api/colorspace.py @@ -135,10 +135,10 @@ def get_project_channel_data(): config_map = config["exportPresets"][0]["maps"][0] config_map["channels"] = [ { - "destChannel": x, - "srcChannel": x, - "srcMapType": "documentMap", - "srcMapName": channel + "destChannel": x, + "srcChannel": x, + "srcMapType": "documentMap", + "srcMapName": channel } for x in "RGB" ] From fbcb88b457faa1e468b71104a158da03558a4c23 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 12:35:00 +0100 Subject: [PATCH 049/918] Include texture set name in the logging --- .../hosts/substancepainter/plugins/publish/extract_textures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index e99b93cac9..a32a81db48 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -32,7 +32,7 @@ class ExtractTextures(publish.Extractor): for (texture_set_name, stack_name), maps in result.textures.items(): # Log our texture outputs - self.log.info(f"Processing stack: {stack_name}") + self.log.info(f"Processing stack: {texture_set_name} {stack_name}") for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") From 78c4875dcb26488cae3e8ccb27b6bc7f6f8c4350 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 18:03:34 +0100 Subject: [PATCH 050/918] Add support for thumbnail generation of extracted textures from Substance Painter --- .../plugins/publish/collect_textureset_images.py | 6 ++++++ .../substancepainter/plugins/publish/extract_textures.py | 3 +++ openpype/plugins/publish/extract_thumbnail.py | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 96f2daa525..5a179f7526 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -148,6 +148,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): 'files': map_fnames, }] + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_file) + image_instance.data["representations"][0]["tags"] = ["review"] + image_instance.data["representations"][0]["stagingDir"] = staging_dir # noqa + instance.append(image_instance) def get_export_config(self, instance): diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index a32a81db48..22acf07284 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -20,6 +20,9 @@ class ExtractTextures(publish.Extractor): hosts = ['substancepainter'] families = ["textureSet"] + # Run before thumbnail extractors + order = publish.Extractor.order - 0.1 + def process(self, instance): config = instance.data["exportConfig"] diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 14b43beae8..dcdb8341ba 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -19,9 +19,9 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): order = pyblish.api.ExtractorOrder families = [ "imagesequence", "render", "render2d", "prerender", - "source", "clip", "take" + "source", "clip", "take", "image" ] - hosts = ["shell", "fusion", "resolve", "traypublisher"] + hosts = ["shell", "fusion", "resolve", "traypublisher", "substancepainter"] enabled = False # presetable attribute From 5c0a7e30ed59b63bd177ff64c07c5f55417556f3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 18:14:18 +0100 Subject: [PATCH 051/918] Group textures together to look like a package/textureSet --- .../plugins/publish/collect_textureset_images.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 5a179f7526..3832f724d4 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -148,6 +148,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): 'files': map_fnames, }] + # Group the textures together in the loader + image_instance.data["subsetGroup"] = instance.data["subset"] + # Set up the representation for thumbnail generation # TODO: Simplify this once thumbnail extraction is refactored staging_dir = os.path.dirname(first_file) From cba71b9e0d22da265429fe2fcbcba1d77dd63a3e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 13 Jan 2023 18:29:59 +0100 Subject: [PATCH 052/918] Fix full path in representation for single images (non-UDIM) --- .../plugins/publish/collect_textureset_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 3832f724d4..851a22c1ee 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -120,7 +120,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): map_fnames = [os.path.basename(path) for path in map_output] else: first_file = map_output - map_fnames = map_output + map_fnames = os.path.basename(map_output) ext = os.path.splitext(first_file)[1] assert ext.lstrip('.'), f"No extension: {ext}" From b17ca1efeac834d9038555f522c8602bc4701035 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 15:38:22 +0100 Subject: [PATCH 053/918] More explicit parsing of extracted textures, prepare for color space data --- openpype/hosts/substancepainter/api/lib.py | 328 +++++++++++++++++- .../publish/collect_textureset_images.py | 177 +++------- .../plugins/publish/extract_textures.py | 3 - 3 files changed, 379 insertions(+), 129 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index b929f881a8..2406680a68 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -1,7 +1,9 @@ import os import re import json +from collections import defaultdict +import substance_painter.project import substance_painter.resource import substance_painter.js @@ -115,7 +117,7 @@ def get_channel_format(stack_path, channel): representation, false otherwise "bitDepth" (int): Bit per color channel (could be 8, 16 or 32 bpc) - Args: + Arguments: stack_path (list or str): Path to the stack, could be "Texture set name" or ["Texture set name", "Stack name"] channel (str): Identifier of the channel to export @@ -142,6 +144,330 @@ def get_document_structure(): return substance_painter.js.evaluate("alg.mapexport.documentStructure()") +def get_export_templates(config, format="png", strip_folder=True): + """Return export config outputs. + + This use the Javascript API `alg.mapexport.getPathsExportDocumentMaps` + which returns a different output than using the Python equivalent + `substance_painter.export.list_project_textures(config)`. + + The nice thing about the Javascript API version is that it returns the + output textures grouped by filename template. + + A downside is that it doesn't return all the UDIM tiles but per template + always returns a single file. + + Note: + The file format needs to be explicitly passed to the Javascript API + but upon exporting through the Python API the file format can be based + on the output preset. So it's likely the file extension will mismatch + + Warning: + Even though the function appears to solely get the expected outputs + the Javascript API will actually create the config's texture output + folder if it does not exist yet. As such, a valid path must be set. + + Example output: + { + "DefaultMaterial": { + "$textureSet_BaseColor(_$colorSpace)(.$udim)": "DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", # noqa + "$textureSet_Emissive(_$colorSpace)(.$udim)": "DefaultMaterial_Emissive_ACES - ACEScg.1002.png", # noqa + "$textureSet_Height(_$colorSpace)(.$udim)": "DefaultMaterial_Height_Utility - Raw.1002.png", # noqa + "$textureSet_Metallic(_$colorSpace)(.$udim)": "DefaultMaterial_Metallic_Utility - Raw.1002.png", # noqa + "$textureSet_Normal(_$colorSpace)(.$udim)": "DefaultMaterial_Normal_Utility - Raw.1002.png", # noqa + "$textureSet_Roughness(_$colorSpace)(.$udim)": "DefaultMaterial_Roughness_Utility - Raw.1002.png" # noqa + } + } + + Arguments: + config (dict) Export config + format (str, Optional): Output format to write to, defaults to 'png' + strip_folder (bool, Optional): Whether to strip the output folder + from the output filenames. + + Returns: + dict: The expected output maps. + + """ + folder = config["exportPath"] + preset = config["defaultExportPreset"] + cmd = f'alg.mapexport.getPathsExportDocumentMaps("{preset}", "{folder}", "{format}")' # noqa + result = substance_painter.js.evaluate(cmd) + + if strip_folder: + for stack, maps in result.items(): + for map_template, map_filepath in maps.items(): + map_filename = map_filepath[len(folder):].lstrip("/") + maps[map_template] = map_filename + + return result + + +def _templates_to_regex(templates, + texture_set, + colorspaces, + project, + mesh): + """Return regex based on a Substance Painter expot filename template. + + This converts Substance Painter export filename templates like + `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` into a regex + which can be used to query an output filename to help retrieve: + + - Which template filename the file belongs to. + - Which color space the file is written with. + - Which udim tile it is exactly. + + This is used by `get_parsed_export_maps` which tries to as explicitly + as possible match the filename pattern against the known possible outputs. + That's why Texture Set name, Color spaces, Project path and mesh path must + be provided. By doing so we get the best shot at correctly matching the + right template because otherwise $texture_set could basically be any string + and thus match even that of a color space or mesh. + + Arguments: + templates (list): List of templates to convert to regex. + texture_set (str): The texture set to match against. + colorspaces (list): The colorspaces defined in the current project. + project (str): Filepath of current substance project. + mesh (str): Path to mesh file used in current project. + + Returns: + dict: Template: Template regex pattern + + """ + def _filename_no_ext(path): + return os.path.splitext(os.path.basename(path))[0] + + if colorspaces and any(colorspaces): + colorspace_match = ( + "(" + "|".join(re.escape(c) for c in colorspaces) + ")" + ) + else: + # No colorspace support enabled + colorspace_match = "" + + # Key to regex valid search values + key_matches = { + "$project": re.escape(_filename_no_ext(project)), + "$mesh": re.escape(_filename_no_ext(mesh)), + "$textureSet": re.escape(texture_set), + "$colorSpace": colorspace_match, + "$udim": "([0-9]{4})" + } + + # Turn the templates into regexes + regexes = {} + for template in templates: + + # We need to tweak a temp + search_regex = re.escape(template) + + # Let's assume that any ( and ) character in the file template was + # intended as an optional template key and do a simple `str.replace` + # Note: we are matching against re.escape(template) so will need to + # search for the escaped brackets. + search_regex = search_regex.replace(re.escape("("), "(") + search_regex = search_regex.replace(re.escape(")"), ")?") + + # Substitute each key into a named group + for key, key_expected_regex in key_matches.items(): + + # We want to use the template as a regex basis in the end so will + # escape the whole thing first. Note that thus we'll need to + # search for the escaped versions of the keys too. + escaped_key = re.escape(key) + key_label = key[1:] # key without $ prefix + + key_expected_grp_regex = f"(?P<{key_label}>{key_expected_regex})" + search_regex = search_regex.replace(escaped_key, + key_expected_grp_regex) + + # The filename templates don't include the extension so we add it + # to be able to match the out filename beginning to end + ext_regex = "(?P\.[A-Za-z][A-Za-z0-9-]*)" + search_regex = rf"^{search_regex}{ext_regex}$" + + regexes[template] = search_regex + + return regexes + + +def strip_template(template, strip="._ "): + """Return static characters in a substance painter filename template. + + >>> strip_template("$textureSet_HELLO(.$udim)") + # HELLO + >>> strip_template("$mesh_$textureSet_HELLO_WORLD_$colorSpace(.$udim)") + # HELLO_WORLD + >>> strip_template("$textureSet_HELLO(.$udim)", strip=None) + # _HELLO + >>> strip_template("$mesh_$textureSet_$colorSpace(.$udim)", strip=None) + # _HELLO_ + >>> strip_template("$textureSet_HELLO(.$udim)") + # _HELLO + + Arguments: + template (str): Filename template to strip. + strip (str, optional): Characters to strip from beginning and end + of the static string in template. Defaults to: `._ `. + + Returns: + str: The static string in filename template. + + """ + # Return only characters that were part of the template that were static. + # Remove all keys + keys = ["$project", "$mesh", "$textureSet", "$udim", "$colorSpace"] + stripped_template = template + for key in keys: + stripped_template = stripped_template.replace(key, "") + + # Everything inside an optional bracket space is excluded since it's not + # static. We keep a counter to track whether we are currently iterating + # over parts of the template that are inside an 'optional' group or not. + counter = 0 + result = "" + for char in stripped_template: + if char == "(": + counter += 1 + elif char == ")": + counter -= 1 + if counter < 0: + counter = 0 + else: + if counter == 0: + result += char + + if strip: + # Strip of any trailing start/end characters. Technically these are + # static but usually start and end separators like space or underscore + # aren't wanted. + result = result.strip(strip) + + return result + + +def get_parsed_export_maps(config): + """ + + This tries to parse the texture outputs using a Python API export config. + + Parses template keys: $project, $mesh, $textureSet, $colorSpace, $udim + + Example: + {("DefaultMaterial", ""): { + "$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)": [ + { + // OUTPUT DATA FOR FILE #1 OF THE TEMPLATE + }, + { + // OUTPUT DATA FOR FILE #2 OF THE TEMPLATE + }, + ] + }, + }} + + File output data (all outputs are `str`). + 1) Parsed tokens: These are parsed tokens from the template, they will + only exist if found in the filename template and output filename. + + project: Workfile filename without extension + mesh: Filename of the loaded mesh without extension + textureSet: The texture set, e.g. "DefaultMaterial", + colorSpace: The color space, e.g. "ACES - ACEScg", + udim: The udim tile, e.g. "1001" + + 2) Template and file outputs + + filepath: Full path to the resulting texture map, e.g. + "/path/to/mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", + output: "mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png" + Note: if template had slashes (folders) then `output` will too. + So `output` might include a folder. + + channel: The stripped static characters of the filename template which + usually look like an identifier for that map, e.g. "BaseColor". + See `_stripped_template` + + Returns: + dict: [texture_set, stack]: {template: [file1_data, file2_data]} + + """ + import substance_painter.export + from .colorspace import get_project_channel_data + + outputs = substance_painter.export.list_project_textures(config) + templates = get_export_templates(config) + + # Get all color spaces set for the current project + project_colorspaces = set( + data["colorSpace"] for data in get_project_channel_data().values() + ) + + # Get current project mesh path and project path to explicitly match + # the $mesh and $project tokens + project_mesh_path = substance_painter.project.last_imported_mesh_path() + project_path = substance_painter.project.file_path() + + # Get the current export path to strip this of the beginning of filepath + # results, since filename templates don't have these we'll match without + # that part of the filename. + export_path = config["exportPath"] + export_path = export_path.replace("\\", "/") + if not export_path.endswith("/"): + export_path += "/" + + # Parse the outputs + result = {} + for key, filepaths in outputs.items(): + texture_set, stack = key + + if stack: + stack_path = f"{texture_set}/{stack}" + else: + stack_path = texture_set + + stack_templates = list(templates[stack_path].keys()) + + template_regex = _templates_to_regex(stack_templates, + texture_set=texture_set, + colorspaces=project_colorspaces, + mesh=project_mesh_path, + project=project_path) + + # Let's precompile the regexes + for template, regex in template_regex.items(): + template_regex[template] = re.compile(regex) + + stack_results = defaultdict(list) + for filepath in sorted(filepaths): + # We strip explicitly using the full parent export path instead of + # using `os.path.basename` because export template is allowed to + # have subfolders in its template which we want to match against + assert filepath.startswith(export_path) + filename = filepath[len(export_path):] + + for template, regex in template_regex.items(): + match = regex.match(filename) + if match: + parsed = match.groupdict(default={}) + + # Include some special outputs for convenience + parsed["filepath"] = filepath + parsed["output"] = filename + + stack_results[template].append(parsed) + break + else: + raise ValueError(f"Unable to match {filename} against any " + f"template in: {list(template_regex.keys())}") + + result[key] = dict(stack_results) + + return result + + def load_shelf(path, name=None): """Add shelf to substance painter (for current application session) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 851a22c1ee..6928bdb36c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -1,63 +1,19 @@ import os import copy -import clique import pyblish.api from openpype.pipeline import publish -import substance_painter.export -from openpype.hosts.substancepainter.api.colorspace import ( - get_project_channel_data, +import substance_painter.textureset +from openpype.hosts.substancepainter.api.lib import ( + get_parsed_export_maps, + strip_template ) -def get_project_color_spaces(): - """Return unique color space names used for exports. - - This is based on the Color Management preferences of the project. - - See also: - func:`get_project_channel_data` - - """ - return set( - data["colorSpace"] for data in get_project_channel_data().values() - ) - - -def _get_channel_name(path, - texture_set_name, - project_colorspaces): - """Return expected 'name' for the output image. - - This will be used as a suffix to the separate image publish subsets. - - """ - # TODO: This will require improvement before being production ready. - # TODO(Question): Should we preserve the texture set name in the suffix - # TODO so that exports with multiple texture sets can work within a single - # TODO parent textureSet, like `texture{Variant}.{TextureSet}{Channel}` - name = os.path.basename(path) # filename - name = os.path.splitext(name)[0] # no extension - # Usually the channel identifier comes after $textureSet in - # the export preset. Unfortunately getting the export maps - # and channels explicitly is not trivial so for now we just - # assume this will generate a nice identifier for the end user - name = name.split(f"{texture_set_name}_", 1)[-1] - - # TODO: We need more explicit ways to detect the color space part - for colorspace in project_colorspaces: - if name.endswith(f"_{colorspace}"): - name = name[:-len(f"_{colorspace}")] - break - - return name - - class CollectTextureSet(pyblish.api.InstancePlugin): """Extract Textures using an output template config""" - # TODO: More explicitly detect UDIM tiles - # TODO: Get color spaces + # TODO: Production-test usage of color spaces # TODO: Detect what source data channels end up in each file label = "Collect Texture Set images" @@ -68,96 +24,67 @@ class CollectTextureSet(pyblish.api.InstancePlugin): def process(self, instance): config = self.get_export_config(instance) - textures = substance_painter.export.list_project_textures(config) instance.data["exportConfig"] = config - - colorspaces = get_project_color_spaces() - - outputs = {} - for (texture_set_name, stack_name), maps in textures.items(): - - # Log our texture outputs - self.log.debug(f"Processing stack: {stack_name}") - for texture_map in maps: - self.log.debug(f"Expecting texture: {texture_map}") - - # For now assume the UDIM textures end with .. and - # when no trailing number is present before the extension then it's - # considered to *not* be a UDIM export. - collections, remainder = clique.assemble( - maps, - patterns=[clique.PATTERNS["frames"]], - minimum_items=True - ) - - outputs = {} - if collections: - # UDIM tile sequence - for collection in collections: - name = _get_channel_name(collection.head, - texture_set_name=texture_set_name, - project_colorspaces=colorspaces) - outputs[name] = collection - self.log.info(f"UDIM Collection: {collection}") - else: - # Single file per channel without UDIM number - for path in remainder: - name = _get_channel_name(path, - texture_set_name=texture_set_name, - project_colorspaces=colorspaces) - outputs[name] = path - self.log.info(f"Single file: {path}") + maps = get_parsed_export_maps(config) # Let's break the instance into multiple instances to integrate # a subset per generated texture or texture UDIM sequence + for (texture_set_name, stack_name), template_maps in maps.items(): + self.log.info(f"Processing {texture_set_name}/{stack_name}") + for template, outputs in template_maps.items(): + self.log.info(f"Processing {template}") + self.create_image_instance(instance, template, outputs) + + def create_image_instance(self, instance, template, outputs): + context = instance.context - for map_name, map_output in outputs.items(): + first_filepath = outputs[0]["filepath"] + fnames = [os.path.basename(output["filepath"]) for output in outputs] + ext = os.path.splitext(first_filepath)[1] + assert ext.lstrip('.'), f"No extension: {ext}" - is_udim = isinstance(map_output, clique.Collection) - if is_udim: - first_file = list(map_output)[0] - map_fnames = [os.path.basename(path) for path in map_output] - else: - first_file = map_output - map_fnames = os.path.basename(map_output) + map_identifier = strip_template(template) - ext = os.path.splitext(first_file)[1] - assert ext.lstrip('.'), f"No extension: {ext}" + # Define the suffix we want to give this particular texture + # set and set up a remapped subset naming for it. + suffix = f".{map_identifier}" + image_subset = instance.data["subset"][len("textureSet"):] + image_subset = "texture" + image_subset + suffix + # Prepare representation + representation = { + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': fnames, + } - # Define the suffix we want to give this particular texture - # set and set up a remapped subset naming for it. - suffix = f".{map_name}" - image_subset = instance.data["subset"][len("textureSet"):] - image_subset = "texture" + image_subset + suffix + # Mark as UDIM explicitly if it has UDIM tiles. + if bool(outputs[0].get("udim")): + representation["udim"] = True - # TODO: Retrieve and store color space with the representation + # TODO: Store color space with the representation - # Clone the instance - image_instance = context.create_instance(instance.name) - image_instance[:] = instance[:] - image_instance.data.update(copy.deepcopy(instance.data)) - image_instance.data["name"] = image_subset - image_instance.data["label"] = image_subset - image_instance.data["subset"] = image_subset - image_instance.data["family"] = "image" - image_instance.data["families"] = ["image", "textures"] - image_instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': map_fnames, - }] + # Clone the instance + image_instance = context.create_instance(instance.name) + image_instance[:] = instance[:] + image_instance.data.update(copy.deepcopy(instance.data)) + image_instance.data["name"] = image_subset + image_instance.data["label"] = image_subset + image_instance.data["subset"] = image_subset + image_instance.data["family"] = "image" + image_instance.data["families"] = ["image", "textures"] + image_instance.data['representations'] = [representation] - # Group the textures together in the loader - image_instance.data["subsetGroup"] = instance.data["subset"] + # Group the textures together in the loader + image_instance.data["subsetGroup"] = instance.data["subset"] - # Set up the representation for thumbnail generation - # TODO: Simplify this once thumbnail extraction is refactored - staging_dir = os.path.dirname(first_file) - image_instance.data["representations"][0]["tags"] = ["review"] - image_instance.data["representations"][0]["stagingDir"] = staging_dir # noqa + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_filepath) + image_instance.data["representations"][0]["tags"] = ["review"] + image_instance.data["representations"][0]["stagingDir"] = staging_dir - instance.append(image_instance) + instance.append(image_instance) def get_export_config(self, instance): """Return an export configuration dict for texture exports. diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 22acf07284..a5bb274b78 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -12,9 +12,6 @@ class ExtractTextures(publish.Extractor): particular Extractor doesn't specify representations to integrate. """ - # TODO: More explicitly detect UDIM tiles - # TODO: Get color spaces - # TODO: Detect what source data channels end up in each file label = "Extract Texture Set" hosts = ['substancepainter'] From 04b32350202e17877ddce8832767668e34e95715 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:32:05 +0100 Subject: [PATCH 054/918] Cosmetics --- .../plugins/publish/collect_textureset_images.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 6928bdb36c..f85861d0eb 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -37,13 +37,17 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.create_image_instance(instance, template, outputs) def create_image_instance(self, instance, template, outputs): + f"""Create a new instance per image or UDIM sequence. + + The new instances will be of family `image`. + + """ context = instance.context first_filepath = outputs[0]["filepath"] fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] assert ext.lstrip('.'), f"No extension: {ext}" - map_identifier = strip_template(template) # Define the suffix we want to give this particular texture @@ -51,6 +55,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): suffix = f".{map_identifier}" image_subset = instance.data["subset"][len("textureSet"):] image_subset = "texture" + image_subset + suffix + # Prepare representation representation = { 'name': ext.lstrip("."), @@ -84,6 +89,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): image_instance.data["representations"][0]["tags"] = ["review"] image_instance.data["representations"][0]["stagingDir"] = staging_dir + # Store the instance in the original instance as a member instance.append(image_instance) def get_export_config(self, instance): From d80e20482b96b388ab91edece375f067f2b9e6b4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:33:19 +0100 Subject: [PATCH 055/918] Cosmetics + add assertion --- openpype/hosts/substancepainter/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 2406680a68..bf4415af8a 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -195,8 +195,9 @@ def get_export_templates(config, format="png", strip_folder=True): result = substance_painter.js.evaluate(cmd) if strip_folder: - for stack, maps in result.items(): + for _stack, maps in result.items(): for map_template, map_filepath in maps.items(): + assert map_filepath.startswith(folder) map_filename = map_filepath[len(folder):].lstrip("/") maps[map_template] = map_filename From 196b91896bf9f55414ef766eb2e72631ef066e51 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:35:43 +0100 Subject: [PATCH 056/918] Shush hound --- openpype/hosts/substancepainter/api/lib.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index bf4415af8a..5b32e3a9aa 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -241,9 +241,8 @@ def _templates_to_regex(templates, return os.path.splitext(os.path.basename(path))[0] if colorspaces and any(colorspaces): - colorspace_match = ( - "(" + "|".join(re.escape(c) for c in colorspaces) + ")" - ) + colorspace_match = "|".join(re.escape(c) for c in set(colorspaces)) + colorspace_match = f"({colorspace_match})" else: # No colorspace support enabled colorspace_match = "" From 5bfb010fbfc0211c7266993fb1b9ddbc2d21162d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:36:23 +0100 Subject: [PATCH 057/918] Shush hound - fix invalid escape sequence --- openpype/hosts/substancepainter/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 5b32e3a9aa..278a23ce01 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -285,7 +285,7 @@ def _templates_to_regex(templates, # The filename templates don't include the extension so we add it # to be able to match the out filename beginning to end - ext_regex = "(?P\.[A-Za-z][A-Za-z0-9-]*)" + ext_regex = r"(?P\.[A-Za-z][A-Za-z0-9-]*)" search_regex = rf"^{search_regex}{ext_regex}$" regexes[template] = search_regex From 2335facfff9d800b32bd3b09f71cbb4daf57035e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:37:35 +0100 Subject: [PATCH 058/918] Fix docstring --- openpype/hosts/substancepainter/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 278a23ce01..7a10ae1eb6 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -349,7 +349,7 @@ def strip_template(template, strip="._ "): def get_parsed_export_maps(config): - """ + """Return Export Config's expected output textures with parsed data. This tries to parse the texture outputs using a Python API export config. From aa0c62b4d7e73d10e63f7384a9d534a12c8fd16e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:38:56 +0100 Subject: [PATCH 059/918] Cleanup --- .../plugins/publish/collect_textureset_images.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index f85861d0eb..53319ba96d 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -37,10 +37,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.create_image_instance(instance, template, outputs) def create_image_instance(self, instance, template, outputs): - f"""Create a new instance per image or UDIM sequence. - + """Create a new instance per image or UDIM sequence. + The new instances will be of family `image`. - + """ context = instance.context From cb04f6bb8b07b776544ed0666fe8440ff52a2ce1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 14 Jan 2023 20:56:29 +0100 Subject: [PATCH 060/918] Fix/Cleanup docstring --- openpype/hosts/substancepainter/api/lib.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 7a10ae1eb6..22dc3059fc 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -378,7 +378,7 @@ def get_parsed_export_maps(config): colorSpace: The color space, e.g. "ACES - ACEScg", udim: The udim tile, e.g. "1001" - 2) Template and file outputs + 2) Template output and filepath filepath: Full path to the resulting texture map, e.g. "/path/to/mesh_DefaultMaterial_BaseColor_ACES - ACEScg.1002.png", @@ -386,10 +386,6 @@ def get_parsed_export_maps(config): Note: if template had slashes (folders) then `output` will too. So `output` might include a folder. - channel: The stripped static characters of the filename template which - usually look like an identifier for that map, e.g. "BaseColor". - See `_stripped_template` - Returns: dict: [texture_set, stack]: {template: [file1_data, file2_data]} From 33aafc3ff6f7e1b4f213345e7baa80f50d4e1f51 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:30:43 +0100 Subject: [PATCH 061/918] Implement OCIO support for Substance Painter + publish color space with textures --- openpype/hooks/pre_host_set_ocio.py | 37 +++++++++++++++++++ .../publish/collect_textureset_images.py | 9 ++++- .../plugins/publish/extract_textures.py | 19 +++++++++- .../project_settings/substancepainter.json | 10 +++++ .../schema_project_substancepainter.json | 17 +++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 openpype/hooks/pre_host_set_ocio.py diff --git a/openpype/hooks/pre_host_set_ocio.py b/openpype/hooks/pre_host_set_ocio.py new file mode 100644 index 0000000000..b9e2b79bf4 --- /dev/null +++ b/openpype/hooks/pre_host_set_ocio.py @@ -0,0 +1,37 @@ +from openpype.lib import PreLaunchHook + +from openpype.pipeline.colorspace import get_imageio_config +from openpype.pipeline.template_data import get_template_data_with_names + + +class PreLaunchHostSetOCIO(PreLaunchHook): + """Set OCIO environment for the host""" + + order = 0 + app_groups = ["substancepainter"] + + def execute(self): + """Hook entry method.""" + + anatomy_data = get_template_data_with_names( + project_name=self.data["project_doc"]["name"], + asset_name=self.data["asset_doc"]["name"], + task_name=self.data["task_name"], + host_name=self.host_name, + system_settings=self.data["system_settings"] + ) + + ocio_config = get_imageio_config( + project_name=self.data["project_doc"]["name"], + host_name=self.host_name, + project_settings=self.data["project_settings"], + anatomy_data=anatomy_data, + anatomy=self.data["anatomy"] + ) + + if ocio_config: + ocio_path = ocio_config["path"] + self.log.info(f"Setting OCIO config path: {ocio_path}") + self.launch_context.env["OCIO"] = ocio_path + else: + self.log.debug("OCIO not set or enabled") diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 53319ba96d..0e445c9c1c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -67,8 +67,6 @@ class CollectTextureSet(pyblish.api.InstancePlugin): if bool(outputs[0].get("udim")): representation["udim"] = True - # TODO: Store color space with the representation - # Clone the instance image_instance = context.create_instance(instance.name) image_instance[:] = instance[:] @@ -83,6 +81,13 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Group the textures together in the loader image_instance.data["subsetGroup"] = instance.data["subset"] + # Store color space with the instance + # Note: The extractor will assign it to the representation + colorspace = outputs[0].get("colorSpace") + if colorspace: + self.log.debug(f"{image_subset} colorspace: {colorspace}") + image_instance.data["colorspace"] = colorspace + # Set up the representation for thumbnail generation # TODO: Simplify this once thumbnail extraction is refactored staging_dir = os.path.dirname(first_filepath) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index a5bb274b78..e66ce6dbf6 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -2,7 +2,7 @@ from openpype.pipeline import KnownPublishError, publish import substance_painter.export -class ExtractTextures(publish.Extractor): +class ExtractTextures(publish.ExtractorColormanaged): """Extract Textures using an output template config. Note: @@ -40,6 +40,23 @@ class ExtractTextures(publish.Extractor): # TODO: Confirm the files indeed exist # TODO: make sure representations are registered + # We'll insert the color space data for each image instance that we + # added into this texture set. The collector couldn't do so because + # some anatomy and other instance data needs to be collected prior + context = instance.context + for image_instance in instance: + + colorspace = image_instance.data.get("colorspace") + if not colorspace: + self.log.debug("No color space data present for instance: " + f"{image_instance}") + continue + + for representation in image_instance.data["representations"]: + self.set_representation_colorspace(representation, + context=context, + colorspace=colorspace) + # Add a fake representation which won't be integrated so the # Integrator leaves us alone - otherwise it would error # TODO: Add `instance.data["integrate"] = False` support in Integrator? diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json index a424a923da..0f9f1af71e 100644 --- a/openpype/settings/defaults/project_settings/substancepainter.json +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -1,3 +1,13 @@ { + "imageio": { + "ocio_config": { + "enabled": true, + "filepath": [] + }, + "file_rules": { + "enabled": true, + "rules": {} + } + }, "shelves": {} } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json index 4a02a9d8ca..79a39b8e6e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json @@ -5,6 +5,23 @@ "label": "Substance Painter", "is_file": true, "children": [ + { + "key": "imageio", + "type": "dict", + "label": "Color Management (ImageIO)", + "is_group": true, + "children": [ + { + "type": "schema", + "name": "schema_imageio_config" + }, + { + "type": "schema", + "name": "schema_imageio_file_rules" + } + + ] + }, { "type": "dict-modifiable", "key": "shelves", From eecf109cab26ab34940ece267e7b26ecd6dc6177 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:32:42 +0100 Subject: [PATCH 062/918] Support single image (otherwise integrator will fail) --- .../plugins/publish/collect_textureset_images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 53319ba96d..18d1e59c4c 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -60,7 +60,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): representation = { 'name': ext.lstrip("."), 'ext': ext.lstrip("."), - 'files': fnames, + 'files': fnames if len(fnames) > 1 else fnames[0], } # Mark as UDIM explicitly if it has UDIM tiles. From 30ae52770d551bca7d35c0b1cdd9893140cf6db7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:33:21 +0100 Subject: [PATCH 063/918] Rename application group to substancepainter for consistency and clarity --- openpype/hooks/pre_add_last_workfile_arg.py | 2 +- openpype/settings/defaults/system_settings/applications.json | 2 +- .../system_schema/host_settings/schema_substancepainter.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index d5a9a41e5a..49fb54d263 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -23,7 +23,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "blender", "photoshop", "tvpaint", - "substance", + "substancepainter", "aftereffects" ] diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 30c692d0e6..d78b54fa05 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1315,7 +1315,7 @@ } } }, - "substance": { + "substancepainter": { "enabled": true, "label": "Substance Painter", "icon": "app_icons/substancepainter.png", diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json index 513f98c610..fb3b21e63f 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_substancepainter.json @@ -1,6 +1,6 @@ { "type": "dict", - "key": "substance", + "key": "substancepainter", "label": "Substance Painter", "collapsible": true, "checkbox_key": "enabled", From 313cb0d550174bacb0a9377829a62283f3520523 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:34:00 +0100 Subject: [PATCH 064/918] Ensure safeguarding against forward/backslashes differences --- openpype/hosts/substancepainter/api/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 22dc3059fc..9bd408f0f2 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -189,7 +189,7 @@ def get_export_templates(config, format="png", strip_folder=True): dict: The expected output maps. """ - folder = config["exportPath"] + folder = config["exportPath"].replace("\\", "/") preset = config["defaultExportPreset"] cmd = f'alg.mapexport.getPathsExportDocumentMaps("{preset}", "{folder}", "{format}")' # noqa result = substance_painter.js.evaluate(cmd) @@ -197,6 +197,7 @@ def get_export_templates(config, format="png", strip_folder=True): if strip_folder: for _stack, maps in result.items(): for map_template, map_filepath in maps.items(): + map_filepath = map_filepath.replace("\\", "/") assert map_filepath.startswith(folder) map_filename = map_filepath[len(folder):].lstrip("/") maps[map_template] = map_filename @@ -441,7 +442,10 @@ def get_parsed_export_maps(config): # We strip explicitly using the full parent export path instead of # using `os.path.basename` because export template is allowed to # have subfolders in its template which we want to match against - assert filepath.startswith(export_path) + filepath = filepath.replace("\\", "/") + assert filepath.startswith(export_path), ( + f"Filepath {filepath} must start with folder {export_path}" + ) filename = filepath[len(export_path):] for template, regex in template_regex.items(): From ece0e7ded2d721dfe92849a8d246bfb4ef0464cd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 01:36:04 +0100 Subject: [PATCH 065/918] No need to strip folder for the templates, we're not using the filename values of the result. --- openpype/hosts/substancepainter/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 9bd408f0f2..754f8a2bd6 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -395,7 +395,7 @@ def get_parsed_export_maps(config): from .colorspace import get_project_channel_data outputs = substance_painter.export.list_project_textures(config) - templates = get_export_templates(config) + templates = get_export_templates(config, strip_folder=False) # Get all color spaces set for the current project project_colorspaces = set( From 31e37e5a33298718c541bb1969e464ff7ae930e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Jan 2023 02:07:00 +0100 Subject: [PATCH 066/918] Use project doc and asset doc directly for `get_template_data` --- openpype/hooks/pre_host_set_ocio.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hooks/pre_host_set_ocio.py b/openpype/hooks/pre_host_set_ocio.py index b9e2b79bf4..3620d88db6 100644 --- a/openpype/hooks/pre_host_set_ocio.py +++ b/openpype/hooks/pre_host_set_ocio.py @@ -1,7 +1,7 @@ from openpype.lib import PreLaunchHook from openpype.pipeline.colorspace import get_imageio_config -from openpype.pipeline.template_data import get_template_data_with_names +from openpype.pipeline.template_data import get_template_data class PreLaunchHostSetOCIO(PreLaunchHook): @@ -13,9 +13,9 @@ class PreLaunchHostSetOCIO(PreLaunchHook): def execute(self): """Hook entry method.""" - anatomy_data = get_template_data_with_names( - project_name=self.data["project_doc"]["name"], - asset_name=self.data["asset_doc"]["name"], + anatomy_data = get_template_data( + project_doc=self.data["project_doc"], + asset_doc=self.data["asset_doc"], task_name=self.data["task_name"], host_name=self.host_name, system_settings=self.data["system_settings"] From 9329ff28d57f75d54dec1ba5aa25f390e02f7f3d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jan 2023 15:39:59 +0100 Subject: [PATCH 067/918] Show new project prompt with mesh preloaded --- openpype/hosts/substancepainter/api/lib.py | 126 ++++++++++++++++++ .../plugins/load/load_mesh.py | 17 +-- 2 files changed, 131 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 754f8a2bd6..e552caee6d 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -520,3 +520,129 @@ def load_shelf(path, name=None): substance_painter.resource.Shelves.add(name, path) return name + + +def _get_new_project_action(): + """Return QAction which triggers Substance Painter's new project dialog""" + from PySide2 import QtGui + + main_window = substance_painter.ui.get_main_window() + + # Find the file menu's New file action + menubar = main_window.menuBar() + new_action = None + for action in menubar.actions(): + menu = action.menu() + if not menu: + continue + + if menu.objectName() != "file": + continue + + # Find the action with the CTRL+N key sequence + new_action = next(action for action in menu.actions() + if action.shortcut() == QtGui.QKeySequence.New) + break + + return new_action + + +def prompt_new_file_with_mesh(mesh_filepath): + """Prompts the user for a new file using Substance Painter's own dialog. + + This will set the mesh path to load to the given mesh and disables the + dialog box to disallow the user to change the path. This way we can allow + user configuration of a project but set the mesh path ourselves. + + Warning: + This is very hacky and experimental. + + Note: + If a project is currently open using the same mesh filepath it can't + accurately detect whether the user had actually accepted the new project + dialog or whether the project afterwards is still the original project, + for example when the user might have cancelled the operation. + + """ + from PySide2 import QtWidgets, QtCore + + app = QtWidgets.QApplication.instance() + assert os.path.isfile(mesh_filepath), \ + f"Mesh filepath does not exist: {mesh_filepath}" + + def _setup_file_dialog(): + """Set filepath in QFileDialog and trigger accept result""" + file_dialog = app.activeModalWidget() + assert isinstance(file_dialog, QtWidgets.QFileDialog) + + # Quickly hide the dialog + file_dialog.hide() + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 1000) + + file_dialog.setDirectory(os.path.dirname(mesh_filepath)) + url = QtCore.QUrl.fromLocalFile(os.path.basename(mesh_filepath)) + file_dialog.selectUrl(url) + + # Give the explorer window time to refresh to the folder and select + # the file + while not file_dialog.selectedFiles(): + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 1000) + print(f"Selected: {file_dialog.selectedFiles()}") + + # Set it again now we know the path is refreshed - without this + # accepting the dialog will often not trigger the correct filepath + file_dialog.setDirectory(os.path.dirname(mesh_filepath)) + url = QtCore.QUrl.fromLocalFile(os.path.basename(mesh_filepath)) + file_dialog.selectUrl(url) + + file_dialog.done(file_dialog.Accepted) + app.processEvents(QtCore.QEventLoop.AllEvents) + + def _setup_prompt(): + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) + dialog = app.activeModalWidget() + assert dialog.objectName() == "NewProjectDialog" + + # Set the window title + mesh = os.path.basename(mesh_filepath) + dialog.setWindowTitle(f"New Project with mesh: {mesh}") + + # Get the select mesh file button + mesh_select = dialog.findChild(QtWidgets.QPushButton, "meshSelect") + + # Hide the select mesh button to the user to block changing of mesh + mesh_select.setVisible(False) + + # Ensure UI is visually up-to-date + app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) + + # Trigger the 'select file' dialog to set the path and have the + # new file dialog to use the path. + QtCore.QTimer.singleShot(10, _setup_file_dialog) + mesh_select.click() + + app.processEvents(QtCore.QEventLoop.AllEvents, 5000) + + mesh_filename = dialog.findChild(QtWidgets.QFrame, "meshFileName") + mesh_filename_label = mesh_filename.findChild(QtWidgets.QLabel) + if not mesh_filename_label.text(): + dialog.close() + raise RuntimeError(f"Failed to set mesh path: {mesh_filepath}") + + new_action = _get_new_project_action() + if not new_action: + raise RuntimeError("Unable to detect new file action..") + + QtCore.QTimer.singleShot(0, _setup_prompt) + new_action.trigger() + app.processEvents(QtCore.QEventLoop.AllEvents, 5000) + + if not substance_painter.project.is_open(): + return + + # Confirm mesh was set as expected + project_mesh = substance_painter.project.last_imported_mesh_path() + if os.path.normpath(project_mesh) != os.path.normpath(mesh_filepath): + return + + return project_mesh diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 00f808199f..4e800bd623 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -7,6 +7,7 @@ from openpype.hosts.substancepainter.api.pipeline import ( set_container_metadata, remove_container_metadata ) +from openpype.hosts.substancepainter.api.lib import prompt_new_file_with_mesh import substance_painter.project import qargparse @@ -45,18 +46,10 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if not substance_painter.project.is_open(): # Allow to 'initialize' a new project - # TODO: preferably these settings would come from the actual - # new project prompt of Substance (or something that is - # visually similar to still allow artist decisions) - settings = substance_painter.project.Settings( - default_texture_resolution=4096, - import_cameras=import_cameras, - ) - - substance_painter.project.create( - mesh_file_path=self.fname, - settings=settings - ) + result = prompt_new_file_with_mesh(mesh_filepath=self.fname) + if not result: + self.log.info("User cancelled new project prompt.") + return else: # Reload the mesh From 033d37ca283e6fed6d9a9337e4001e5978b12271 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jan 2023 17:01:59 +0100 Subject: [PATCH 068/918] Early draft for Substance Painter documentation --- website/docs/artist_hosts_substancepainter.md | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 website/docs/artist_hosts_substancepainter.md diff --git a/website/docs/artist_hosts_substancepainter.md b/website/docs/artist_hosts_substancepainter.md new file mode 100644 index 0000000000..9ed83421af --- /dev/null +++ b/website/docs/artist_hosts_substancepainter.md @@ -0,0 +1,80 @@ +--- +id: artist_hosts_substancepainter +title: Substance Painter +sidebar_label: Substance Painter +--- + +## OpenPype global tools + +- [Work Files](artist_tools.md#workfiles) +- [Load](artist_tools.md#loader) +- [Manage (Inventory)](artist_tools.md#inventory) +- [Publish](artist_tools.md#publisher) +- [Library Loader](artist_tools.md#library-loader) + +## Working with OpenPype in Substance Painter + +The Substance Painter OpenPype integration allows you to: + +- Set the project mesh and easily keep it in sync with updates of the model +- Easily export your textures as versioned publishes for others to load and update. + +## Setting the project mesh + +Substance Painter requires a project file to have a mesh path configured. +As such, you can't start a workfile without choosing a mesh path. + +To start a new project using a published model you can _without an open project_ +use OpenPype > Load.. > Load Mesh on a supported publish. This will prompt you +with a New Project prompt preset to that particular mesh file. + +If you already have a project open, you can also replace (reload) your mesh +using the same Load Mesh functionality. + +After having the project mesh loaded or reloaded through the loader +tool the mesh will be _managed_ by OpenPype. For example, you'll be notified +on workfile open whether the mesh in your workfile is outdated. You can also +set it to specific version using OpenPype > Manage.. where you can right click +on the project mesh to perform _Set Version_ + +:::info +A Substance Painter project will always have only one mesh set. Whenever you +trigger _Load Mesh_ from the loader this will **replace** your currently loaded +mesh for your open project. +::: + +## Publishing textures + +To publish your textures we must first create a `textureSet` +publish instance. + +To create a **TextureSet instance** we will use OpenPype's publisher tool. Go +to **OpenPype → Publish... → TextureSet** + +The texture set instance will define what Substance Painter export template `.spexp` to +use and thus defines what texture maps will be exported from your workfile. + +:::info +The TextureSet instance gets saved with your Substance Painter project. As such, +you will only need to configure this once for your workfile. Next time you can +just click **OpenPype → Publish...** and start publishing directly with the +same settings. +::: + + +### Known issues + +#### Can't see the OpenPype menu? + +If you're unable to see the OpenPype top level menu in Substance Painter make +sure you have launched Substance Painter through OpenPype and that the OpenPype +Integration plug-in is loaded inside Substance Painter: **Python > openpype_plugin** + +#### Substance Painter + Steam + +Running the steam version of Substance Painter within OpenPype will require you +to close the Steam executable before launching Substance Painter through OpenPype. +Otherwise the Substance Painter process is launched using Steam's existing +environment and thus will not be able to pick up the pipeline integration. + +This appears to be a limitation of how Steam works. \ No newline at end of file From 1c77d2b002527a450c8be21d93040bccd588413e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Jan 2023 10:18:01 +0100 Subject: [PATCH 069/918] Fix UDIM integration --- .../plugins/publish/collect_textureset_images.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 18d1e59c4c..5f06880663 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -65,7 +65,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Mark as UDIM explicitly if it has UDIM tiles. if bool(outputs[0].get("udim")): - representation["udim"] = True + # The representation for a UDIM sequence should have a `udim` key + # that is a list of all udim tiles (str) like: ["1001", "1002"] + # strings. See CollectTextures plug-in and Integrators. + representation["udim"] = [output["udim"] for output in outputs] # TODO: Store color space with the representation From f9f95b84e68da86ce53f9881ee59b98acb6d9aef Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 25 Jan 2023 11:09:19 +0000 Subject: [PATCH 070/918] Basic implementation of the new Creator --- openpype/hosts/unreal/api/__init__.py | 6 +- openpype/hosts/unreal/api/pipeline.py | 53 ++++++- openpype/hosts/unreal/api/plugin.py | 209 +++++++++++++++++++++++++- 3 files changed, 262 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index ca9db259e6..2618a7677c 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- """Unreal Editor OpenPype host API.""" -from .plugin import Loader +from .plugin import ( + UnrealActorCreator, + UnrealAssetCreator, + Loader +) from .pipeline import ( install, diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 2081c8fd13..7a21effcbc 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import os +import json import logging from typing import List from contextlib import contextmanager @@ -16,13 +17,14 @@ from openpype.pipeline import ( ) from openpype.tools.utils import host_tools import openpype.hosts.unreal -from openpype.host import HostBase, ILoadHost +from openpype.host import HostBase, ILoadHost, IPublishHost import unreal # noqa - logger = logging.getLogger("openpype.hosts.unreal") + OPENPYPE_CONTAINERS = "OpenPypeContainers" +CONTEXT_CONTAINER = "OpenPype/context.json" UNREAL_VERSION = semver.VersionInfo( *os.getenv("OPENPYPE_UNREAL_VERSION").split(".") ) @@ -35,7 +37,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") -class UnrealHost(HostBase, ILoadHost): +class UnrealHost(HostBase, ILoadHost, IPublishHost): """Unreal host implementation. For some time this class will re-use functions from module based @@ -60,6 +62,26 @@ class UnrealHost(HostBase, ILoadHost): show_tools_dialog() + def update_context_data(self, data, changes): + unreal.log_warning("update_context_data") + unreal.log_warning(data) + content_path = unreal.Paths.project_content_dir() + op_ctx = content_path + CONTEXT_CONTAINER + with open(op_ctx, "w+") as f: + json.dump(data, f) + with open(op_ctx, "r") as fp: + test = eval(json.load(fp)) + unreal.log_warning(test) + + def get_context_data(self): + content_path = unreal.Paths.project_content_dir() + op_ctx = content_path + CONTEXT_CONTAINER + if not os.path.isfile(op_ctx): + return {} + with open(op_ctx, "r") as fp: + data = eval(json.load(fp)) + return data + def install(): """Install Unreal configuration for OpenPype.""" @@ -133,6 +155,31 @@ def ls(): yield data +def lsinst(): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + # UE 5.1 changed how class name is specified + class_name = [ + "/Script/OpenPype", + "OpenPypePublishInstance" + ] if ( + UNREAL_VERSION.major == 5 + and UNREAL_VERSION.minor > 0 + ) else "OpenPypePublishInstance" # noqa + instances = ar.get_assets_by_class(class_name, True) + + # get_asset_by_class returns AssetData. To get all metadata we need to + # load asset. get_tag_values() work only on metadata registered in + # Asset Registry Project settings (and there is no way to set it with + # python short of editing ini configuration file). + for asset_data in instances: + asset = asset_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = asset_data.asset_name + data = cast_map_to_str_dict(data) + + yield data + + def parse_container(container): """To get data from container, AssetContainer must be loaded. diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 6fc00cb71c..f89ff153b1 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,7 +1,212 @@ # -*- coding: utf-8 -*- -from abc import ABC +import sys +import six +from abc import ( + ABC, + ABCMeta, + abstractmethod +) -from openpype.pipeline import LoaderPlugin +import unreal + +from .pipeline import ( + create_publish_instance, + imprint, + lsinst +) +from openpype.lib import BoolDef +from openpype.pipeline import ( + Creator, + LoaderPlugin, + CreatorError, + CreatedInstance +) + + +class OpenPypeCreatorError(CreatorError): + pass + + +@six.add_metaclass(ABCMeta) +class UnrealBaseCreator(Creator): + """Base class for Unreal creator plugins.""" + root = "/Game/OpenPype/PublishInstances" + suffix = "_INS" + + @staticmethod + def cache_subsets(shared_data): + """Cache instances for Creators to shared data. + + Create `unreal_cached_subsets` key when needed in shared data and + fill it with all collected instances from the scene under its + respective creator identifiers. + + If legacy instances are detected in the scene, create + `unreal_cached_legacy_subsets` there and fill it with + all legacy subsets under family as a key. + + Args: + Dict[str, Any]: Shared data. + + Return: + Dict[str, Any]: Shared data dictionary. + + """ + if shared_data.get("unreal_cached_subsets") is None: + shared_data["unreal_cached_subsets"] = {} + if shared_data.get("unreal_cached_legacy_subsets") is None: + shared_data["unreal_cached_legacy_subsets"] = {} + cached_instances = lsinst() + for i in cached_instances: + if not i.get("creator_identifier"): + # we have legacy instance + family = i.get("family") + if (family not in + shared_data["unreal_cached_legacy_subsets"]): + shared_data[ + "unreal_cached_legacy_subsets"][family] = [i] + else: + shared_data[ + "unreal_cached_legacy_subsets"][family].append(i) + continue + + creator_id = i.get("creator_identifier") + if creator_id not in shared_data["unreal_cached_subsets"]: + shared_data["unreal_cached_subsets"][creator_id] = [i] + else: + shared_data["unreal_cached_subsets"][creator_id].append(i) + return shared_data + + @abstractmethod + def create(self, subset_name, instance_data, pre_create_data): + pass + + def collect_instances(self): + # cache instances if missing + self.cache_subsets(self.collection_shared_data) + for instance in self.collection_shared_data[ + "unreal_cached_subsets"].get(self.identifier, []): + created_instance = CreatedInstance.from_existing(instance, self) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + unreal.log_warning(f"Update instances: {update_list}") + for created_inst, _changes in update_list: + instance_node = created_inst.get("instance_path", "") + + if not instance_node: + unreal.log_warning( + f"Instance node not found for {created_inst}") + + new_values = { + key: new_value + for key, (_old_value, new_value) in _changes.items() + } + imprint( + instance_node, + new_values + ) + + def remove_instances(self, instances): + for instance in instances: + instance_node = instance.data.get("instance_path", "") + if instance_node: + unreal.EditorAssetLibrary.delete_asset(instance_node) + + self._remove_instance_from_context(instance) + + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", label="Use selection") + ] + + +@six.add_metaclass(ABCMeta) +class UnrealAssetCreator(UnrealBaseCreator): + """Base class for Unreal creator plugins based on assets.""" + + def create(self, subset_name, instance_data, pre_create_data): + """Create instance of the asset. + + Args: + subset_name (str): Name of the subset. + instance_data (dict): Data for the instance. + pre_create_data (dict): Data for the instance. + + Returns: + CreatedInstance: Created instance. + """ + try: + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + instance_data["members"] = selection + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) + + +@six.add_metaclass(ABCMeta) +class UnrealActorCreator(UnrealBaseCreator): + """Base class for Unreal creator plugins based on actors.""" + + def create(self, subset_name, instance_data, pre_create_data): + """Create instance of the asset. + + Args: + subset_name (str): Name of the subset. + instance_data (dict): Data for the instance. + pre_create_data (dict): Data for the instance. + + Returns: + CreatedInstance: Created instance. + """ + try: + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + selection = [a.get_path_name() for a in sel_objects] + + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + instance_data["members"] = selection + instance_data[ + "level"] = unreal.EditorLevelLibrary.get_editor_world() + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) class Loader(LoaderPlugin, ABC): From 4ffef19e28d1d8f22d36eb6416228545721eff4e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 13:59:19 +0100 Subject: [PATCH 071/918] Refactor to use new style creator --- .../plugins/create/create_arnold_rop.py | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index f9b5f0c53c..c01586ef11 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,7 +1,7 @@ from openpype.hosts.houdini.api import plugin -class CreateArnoldRop(plugin.Creator): +class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" label = "Arnold ROP" @@ -9,41 +9,47 @@ class CreateArnoldRop(plugin.Creator): icon = "magic" defaults = ["master"] - def __init__(self, *args, **kwargs): - super(CreateArnoldRop, self).__init__(*args, **kwargs) + # Default extension + ext = ".exr" - # Clear the family prefix from the subset - subset = self.data["subset"] - subset_no_prefix = subset[len(self.family):] - subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:] - self.data["subset"] = subset_no_prefix - - # Add chunk size attribute - self.data["chunkSize"] = 1 + def create(self, subset_name, instance_data, pre_create_data): + import hou # Remove the active, we are checking the bypass flag of the nodes - self.data.pop("active", None) + instance_data.pop("active", None) + instance_data.update({"node_type": "arnold"}) - self.data.update({"node_type": "arnold"}) + # Add chunk size attribute + instance_data["chunkSize"] = 1 - def _process(self, instance): + instance = super(CreateArnoldRop, self).create( + subset_name, + instance_data, + pre_create_data) # type: plugin.CreatedInstance - basename = instance.name() - instance.setName(basename + "_ROP", unique_name=True) + instance_node = hou.node(instance.get("instance_node")) - prefix = '${HIP}/render/${HIPNAME}/`chs("subset")`.$F4.exr' + # Hide Properties Tab on Arnold ROP since that's used + # for rendering instead of .ass Archive Export + parm_template_group = instance_node.parmTemplateGroup() + parm_template_group.hideFolder("Properties", True) + instance_node.setParmTemplateGroup(parm_template_group) + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4{}".format(subset_name, self.ext) + ) parms = { # Render frame range "trange": 1, # Arnold ROP settings - "ar_picture": prefix, + "ar_picture": filepath, "ar_exr_half_precision": 1 # half precision } - instance.setParms(parms) - # Lock some Avalon attributes + instance_node.setParms(parms) + + # Lock any parameters in this list to_lock = ["family", "id"] - for name in to_lock: - parm = instance.parm(name) - parm.lock(True) + self.lock_parameters(instance_node, to_lock) From 56885f0eb3755fa4469854b94f7d8cf8be9a1d3f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 14:26:13 +0100 Subject: [PATCH 072/918] Add identifier --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index c01586ef11..41c4fcfaef 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -4,6 +4,7 @@ from openpype.hosts.houdini.api import plugin class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" + identifier = "io.openpype.creators.houdini.arnold_rop" label = "Arnold ROP" family = "arnold_rop" icon = "magic" From 2b0f19a8a61ed2035781933686580ece30769119 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 14:28:23 +0100 Subject: [PATCH 073/918] Fix arnold rop collector for new style creator --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index d90530d3a0..4d82b74aa2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -69,7 +69,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): def process(self, instance): - rop = instance[0] + rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize chunk_size_parm = rop.parm("chunkSize") From 6fdbf2b0e16f5c368b31427e01ccd25154982594 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 14:30:57 +0100 Subject: [PATCH 074/918] The 'publish' key in instance data is optional and may not exist --- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 512ff800ee..51f2df19be 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -659,7 +659,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): # test if there is instance of workfile waiting # to be published. - assert i.data["publish"] is True, ( + assert i.data.get("publish", True) is True, ( "Workfile (scene) must be published along") return i From 4c50564d029dde1bfcce9a73a866f0d54ef0cfbe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:09:24 +0100 Subject: [PATCH 075/918] Fix node access --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 6b6eebba7e..a25bd0f0bb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -111,7 +111,7 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): context = instance.context # Output driver to render - driver = instance[0] + driver = hou.node(instance.data["instance_node"]) hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] plugin_info = DeadlinePluginInfo( From bbc20c16df3550bf0dcb340d88108c3bdb07a088 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:09:38 +0100 Subject: [PATCH 076/918] Fix job info usage --- .../publish/submit_houdini_render_deadline.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index a25bd0f0bb..d5c28994e0 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -88,20 +88,18 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) for key in keys: - val = environment.get(key) - if val: - job_info.EnvironmentKeyValue = "{key}={value}".format( - key=key, - value=val - ) + value = environment.get(key) + if value: + job_info.EnvironmentKeyValue[key] = value + # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1" + job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) fname = os.path.basename(filepath) - job_info.OutputDirectory = dirname.replace("\\", "/") - job_info.OutputFilename = fname + job_info.OutputDirectory += dirname.replace("\\", "/") + job_info.OutputFilename += fname return job_info From e2850d74e5e80bdeaae261ebbf2b2f0bfba8e47a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:12:50 +0100 Subject: [PATCH 077/918] Add dedicated collector for instance frame data --- .../publish/collect_instance_frame_data.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py new file mode 100644 index 0000000000..584343cd64 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py @@ -0,0 +1,56 @@ +import hou + +import pyblish.api + + +class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): + """Collect time range frame data for the instance node.""" + + order = pyblish.api.CollectorOrder + 0.001 + label = "Instance Node Frame Range" + hosts = ["houdini"] + + def process(self, instance): + + node_path = instance.data.get("instance_node") + node = hou.node(node_path) if node_path else None + if not node_path or not node: + self.log.debug("No instance node found for instance: " + "{}".format(instance)) + return + + frame_data = self.get_frame_data(node) + if not frame_data: + return + + self.log.info("Collected time data: {}".format(frame_data)) + instance.data.update(frame_data) + + def get_frame_data(self, node): + """Get the frame data: start frame, end frame and steps + Args: + node(hou.Node) + + Returns: + dict + + """ + + data = {} + + if node.parm("trange") is None: + self.log.debug("Node has no 'trange' parameter: " + "{}".format(node.path())) + return data + + if node.evalParm("trange") == 0: + # Ignore 'render current frame' + self.log.debug("Node '{}' has 'Render current frame' set. " + "Time range data ignored.".format(node.path())) + return data + + data["frameStart"] = node.evalParm("f1") + data["frameEnd"] = node.evalParm("f2") + data["byFrameStep"] = node.evalParm("f3") + + return data From b3e4d8efd2a949986335d05c9f7a34abbc2f911c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:16:07 +0100 Subject: [PATCH 078/918] Fix increment current file plug-in --- .../plugins/publish/increment_current_file.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 8947530874..3b6496c38b 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -5,6 +5,7 @@ from openpype.pipeline import registered_host from openpype.action import get_errored_plugins_from_data from openpype.hosts.houdini.api import HoudiniHost + class IncrementCurrentFile(pyblish.api.ContextPlugin): """Increment the current file. @@ -18,18 +19,8 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): families = ["workfile", "redshift_rop", "arnold_rop", "usdrender"] optional = True - def process(self, instance): + def process(self, context): - # This should be a ContextPlugin, but this is a workaround - # for a bug in pyblish to run once for a family: issue #250 - context = instance.context - key = "__hasRun{}".format(self.__class__.__name__) - if context.data.get(key, False): - return - else: - context.data[key] = True - - context = instance.context errored_plugins = get_errored_plugins_from_data(context) if any( plugin.__name__ == "HoudiniSubmitPublishDeadline" From bd3183bcbc25b670255416f302bd1b11d261abc8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:17:07 +0100 Subject: [PATCH 079/918] Tweak error messages --- .../hosts/houdini/plugins/publish/increment_current_file.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 3b6496c38b..a307ee452c 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -4,6 +4,7 @@ from openpype.lib import version_up from openpype.pipeline import registered_host from openpype.action import get_errored_plugins_from_data from openpype.hosts.houdini.api import HoudiniHost +from openpype.pipeline.publish import KnownPublishError class IncrementCurrentFile(pyblish.api.ContextPlugin): @@ -26,7 +27,7 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): plugin.__name__ == "HoudiniSubmitPublishDeadline" for plugin in errored_plugins ): - raise RuntimeError( + raise KnownPublishError( "Skipping incrementing current file because " "submission to deadline failed." ) @@ -36,7 +37,7 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): current_file = host.current_file() assert ( context.data["currentFile"] == current_file - ), "Collected filename from current scene name." + ), "Collected filename mismatches from current scene name." new_filepath = version_up(current_file) host.save_workfile(new_filepath) From fc09f0b532cf3a1ee496a9f74ae22d55753e7841 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 Jan 2023 17:35:11 +0000 Subject: [PATCH 080/918] Improved basic creator --- openpype/hosts/unreal/api/plugin.py | 95 ++++++++++++++++++----------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index f89ff153b1..6a561420fa 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -4,7 +4,6 @@ import six from abc import ( ABC, ABCMeta, - abstractmethod ) import unreal @@ -12,7 +11,8 @@ import unreal from .pipeline import ( create_publish_instance, imprint, - lsinst + lsinst, + UNREAL_VERSION ) from openpype.lib import BoolDef from openpype.pipeline import ( @@ -77,9 +77,28 @@ class UnrealBaseCreator(Creator): shared_data["unreal_cached_subsets"][creator_id].append(i) return shared_data - @abstractmethod def create(self, subset_name, instance_data, pre_create_data): - pass + try: + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) def collect_instances(self): # cache instances if missing @@ -117,7 +136,7 @@ class UnrealBaseCreator(Creator): def get_pre_create_attr_defs(self): return [ - BoolDef("use_selection", label="Use selection") + BoolDef("use_selection", label="Use selection", default=True) ] @@ -137,25 +156,21 @@ class UnrealAssetCreator(UnrealBaseCreator): CreatedInstance: Created instance. """ try: - selection = [] + # Check if instance data has members, filled by the plugin. + # If not, use selection. + if not instance_data.get("members"): + selection = [] - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] - instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) - instance_data["members"] = selection - instance_data["subset"] = subset_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - instance = CreatedInstance( - self.family, + instance_data["members"] = selection + + super(UnrealAssetCreator, self).create( subset_name, instance_data, - self) - self._add_instance_to_context(instance) - - imprint(f"{self.root}/{instance_name}", instance_data) + pre_create_data) except Exception as er: six.reraise( @@ -180,27 +195,33 @@ class UnrealActorCreator(UnrealBaseCreator): CreatedInstance: Created instance. """ try: - selection = [] + if UNREAL_VERSION.major == 5: + world = unreal.UnrealEditorSubsystem().get_editor_world() + else: + world = unreal.EditorLevelLibrary.get_editor_world() - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() - selection = [a.get_path_name() for a in sel_objects] + # Check if the level is saved + if world.get_path_name().startswith("/Temp/"): + raise OpenPypeCreatorError( + "Level must be saved before creating instances.") - instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) - instance_data["members"] = selection - instance_data[ - "level"] = unreal.EditorLevelLibrary.get_editor_world() - instance_data["subset"] = subset_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - instance = CreatedInstance( - self.family, + # Check if instance data has members, filled by the plugin. + # If not, use selection. + if not instance_data.get("members"): + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + selection = [a.get_path_name() for a in sel_objects] + + instance_data["members"] = selection + + instance_data["level"] = world.get_path_name() + + super(UnrealActorCreator, self).create( subset_name, instance_data, - self) - self._add_instance_to_context(instance) - - imprint(f"{self.root}/{instance_name}", instance_data) + pre_create_data) except Exception as er: six.reraise( From f57a6775cc0c0a88ec85002432fbbcaa394cf8ca Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 Jan 2023 17:35:42 +0000 Subject: [PATCH 081/918] Updated creators to be compatible with new publisher --- .../unreal/plugins/create/create_camera.py | 44 +++---------- .../unreal/plugins/create/create_layout.py | 39 ++--------- .../unreal/plugins/create/create_look.py | 64 +++++++++---------- .../plugins/create/create_staticmeshfbx.py | 34 ++-------- .../unreal/plugins/create/create_uasset.py | 44 ++++--------- 5 files changed, 65 insertions(+), 160 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index bf1489d688..239dc87db5 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -1,41 +1,13 @@ -import unreal -from unreal import EditorAssetLibrary as eal -from unreal import EditorLevelLibrary as ell - -from openpype.hosts.unreal.api.pipeline import instantiate -from openpype.pipeline import LegacyCreator +# -*- coding: utf-8 -*- +from openpype.hosts.unreal.api.plugin import ( + UnrealActorCreator, +) -class CreateCamera(LegacyCreator): - """Layout output for character rigs""" +class CreateCamera(UnrealActorCreator): + """Create Camera.""" - name = "layoutMain" + identifier = "io.openpype.creators.unreal.camera" label = "Camera" family = "camera" - icon = "cubes" - - root = "/Game/OpenPype/Instances" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateCamera, self).__init__(*args, **kwargs) - - def process(self): - data = self.data - - name = data["subset"] - - data["level"] = ell.get_editor_world().get_path_name() - - if not eal.does_directory_exist(self.root): - eal.make_directory(self.root) - - factory = unreal.LevelSequenceFactoryNew() - tools = unreal.AssetToolsHelpers().get_asset_tools() - tools.create_asset(name, f"{self.root}/{name}", None, factory) - - asset_name = f"{self.root}/{name}/{name}.{name}" - - data["members"] = [asset_name] - - instantiate(f"{self.root}", name, data, None, self.suffix) + icon = "camera" diff --git a/openpype/hosts/unreal/plugins/create/create_layout.py b/openpype/hosts/unreal/plugins/create/create_layout.py index c1067b00d9..1d2e800a13 100644 --- a/openpype/hosts/unreal/plugins/create/create_layout.py +++ b/openpype/hosts/unreal/plugins/create/create_layout.py @@ -1,42 +1,13 @@ # -*- coding: utf-8 -*- -from unreal import EditorLevelLibrary - -from openpype.pipeline import LegacyCreator -from openpype.hosts.unreal.api.pipeline import instantiate +from openpype.hosts.unreal.api.plugin import ( + UnrealActorCreator, +) -class CreateLayout(LegacyCreator): +class CreateLayout(UnrealActorCreator): """Layout output for character rigs.""" - name = "layoutMain" + identifier = "io.openpype.creators.unreal.layout" label = "Layout" family = "layout" icon = "cubes" - - root = "/Game" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateLayout, self).__init__(*args, **kwargs) - - def process(self): - data = self.data - - name = data["subset"] - - selection = [] - # if (self.options or {}).get("useSelection"): - # sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - # selection = [a.get_path_name() for a in sel_objects] - - data["level"] = EditorLevelLibrary.get_editor_world().get_path_name() - - data["members"] = [] - - if (self.options or {}).get("useSelection"): - # Set as members the selected actors - for actor in EditorLevelLibrary.get_selected_level_actors(): - data["members"].append("{}.{}".format( - actor.get_outer().get_name(), actor.get_name())) - - instantiate(self.root, name, data, selection, self.suffix) diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 4abf3f6095..08d61ab9f8 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -1,56 +1,53 @@ # -*- coding: utf-8 -*- -"""Create look in Unreal.""" -import unreal # noqa -from openpype.hosts.unreal.api import pipeline, plugin -from openpype.pipeline import LegacyCreator +import unreal + +from openpype.hosts.unreal.api.pipeline import ( + create_folder +) +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator +) -class CreateLook(LegacyCreator): +class CreateLook(UnrealAssetCreator): """Shader connections defining shape look.""" - name = "unrealLook" - label = "Unreal - Look" + identifier = "io.openpype.creators.unreal.look" + label = "Look" family = "look" icon = "paint-brush" - root = "/Game/Avalon/Assets" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateLook, self).__init__(*args, **kwargs) - - def process(self): - name = self.data["subset"] - + def create(self, subset_name, instance_data, pre_create_data): selection = [] - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] + if len(selection) != 1: + raise RuntimeError("Please select only one asset.") + + selected_asset = selection[0] + + look_directory = "/Game/OpenPype/Looks" + # Create the folder - path = f"{self.root}/{self.data['asset']}" - new_name = pipeline.create_folder(path, name) - full_path = f"{path}/{new_name}" + folder_name = create_folder(look_directory, subset_name) + path = f"{look_directory}/{folder_name}" # Create a new cube static mesh ar = unreal.AssetRegistryHelpers.get_asset_registry() cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") - # Create the avalon publish instance object - container_name = f"{name}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=full_path) - # Get the mesh of the selected object - original_mesh = ar.get_asset_by_object_path(selection[0]).get_asset() - materials = original_mesh.get_editor_property('materials') + original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() + materials = original_mesh.get_editor_property('static_materials') - self.data["members"] = [] + instance_data["members"] = [] # Add the materials to the cube for material in materials: - name = material.get_editor_property('material_slot_name') - object_path = f"{full_path}/{name}.{name}" + mat_name = material.get_editor_property('material_slot_name') + object_path = f"{path}/{mat_name}.{mat_name}" unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset( cube.get_asset(), object_path ) @@ -61,8 +58,11 @@ class CreateLook(LegacyCreator): unreal_object.add_material( material.get_editor_property('material_interface')) - self.data["members"].append(object_path) + instance_data["members"].append(object_path) unreal.EditorAssetLibrary.save_asset(object_path) - pipeline.imprint(f"{full_path}/{container_name}", self.data) + super(CreateLook, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py index 45d517d27d..1acf7084d1 100644 --- a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py @@ -1,35 +1,13 @@ # -*- coding: utf-8 -*- -"""Create Static Meshes as FBX geometry.""" -import unreal # noqa -from openpype.hosts.unreal.api.pipeline import ( - instantiate, +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, ) -from openpype.pipeline import LegacyCreator -class CreateStaticMeshFBX(LegacyCreator): - """Static FBX geometry.""" +class CreateStaticMeshFBX(UnrealAssetCreator): + """Create Static Meshes as FBX geometry.""" - name = "unrealStaticMeshMain" - label = "Unreal - Static Mesh" + identifier = "io.openpype.creators.unreal.staticmeshfbx" + label = "Static Mesh (FBX)" family = "unrealStaticMesh" icon = "cube" - asset_types = ["StaticMesh"] - - root = "/Game" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateStaticMeshFBX, self).__init__(*args, **kwargs) - - def process(self): - - name = self.data["subset"] - - selection = [] - if (self.options or {}).get("useSelection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - unreal.log("selection: {}".format(selection)) - instantiate(self.root, name, self.data, selection, self.suffix) diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index ee584ac00c..2d6fcc1d59 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -1,36 +1,25 @@ -"""Create UAsset.""" +# -*- coding: utf-8 -*- from pathlib import Path import unreal -from openpype.hosts.unreal.api import pipeline -from openpype.pipeline import LegacyCreator +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) -class CreateUAsset(LegacyCreator): - """UAsset.""" +class CreateUAsset(UnrealAssetCreator): + """Create UAsset.""" - name = "UAsset" + identifier = "io.openpype.creators.unreal.uasset" label = "UAsset" family = "uasset" icon = "cube" - root = "/Game/OpenPype" - suffix = "_INS" + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + ar = unreal.AssetRegistryHelpers.get_asset_registry() - def __init__(self, *args, **kwargs): - super(CreateUAsset, self).__init__(*args, **kwargs) - - def process(self): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - subset = self.data["subset"] - path = f"{self.root}/PublishInstances/" - - unreal.EditorAssetLibrary.make_directory(path) - - selection = [] - if (self.options or {}).get("useSelection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] @@ -50,12 +39,7 @@ class CreateUAsset(LegacyCreator): if Path(sys_path).suffix != ".uasset": raise RuntimeError(f"{Path(sys_path).name} is not a UAsset.") - unreal.log("selection: {}".format(selection)) - container_name = f"{subset}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=path) - - data = self.data.copy() - data["members"] = selection - - pipeline.imprint(f"{path}/{container_name}", data) + super(CreateUAsset, self).create( + subset_name, + instance_data, + pre_create_data) From e411e197379e487a5dd5342e867bba2501ad8442 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 27 Jan 2023 16:53:39 +0000 Subject: [PATCH 082/918] Updated render creator --- .../unreal/plugins/create/create_render.py | 174 ++++++++++-------- 1 file changed, 94 insertions(+), 80 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a85d17421b..de3efdad74 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,117 +1,131 @@ +# -*- coding: utf-8 -*- import unreal -from openpype.hosts.unreal.api import pipeline -from openpype.pipeline import LegacyCreator +from openpype.hosts.unreal.api.pipeline import ( + get_subsequences +) +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) -class CreateRender(LegacyCreator): +class CreateRender(UnrealAssetCreator): """Create instance for sequence for rendering""" - name = "unrealRender" - label = "Unreal - Render" + identifier = "io.openpype.creators.unreal.render" + label = "Render" family = "render" - icon = "cube" - asset_types = ["LevelSequence"] - - root = "/Game/OpenPype/PublishInstances" - suffix = "_INS" - - def process(self): - subset = self.data["subset"] + icon = "eye" + def create(self, subset_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() - # The asset name is the the third element of the path which contains - # the map. - # The index of the split path is 3 because the first element is an - # empty string, as the path begins with "/Content". - a = unreal.EditorUtilityLibrary.get_selected_assets()[0] - asset_name = a.get_path_name().split("/")[3] - - # Get the master sequence and the master level. - # There should be only one sequence and one level in the directory. - filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - sequences = ar.get_assets(filter) - ms = sequences[0].get_editor_property('object_path') - filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - levels = ar.get_assets(filter) - ml = levels[0].get_editor_property('object_path') - - selection = [] - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [ a.get_path_name() for a in sel_objects - if a.get_class().get_name() in self.asset_types] + if a.get_class().get_name() == "LevelSequence"] else: - selection.append(self.data['sequence']) + selection = [instance_data['sequence']] - unreal.log(f"selection: {selection}") + seq_data = None - path = f"{self.root}" - unreal.EditorAssetLibrary.make_directory(path) + for sel in selection: + selected_asset = ar.get_asset_by_object_path(sel).get_asset() + selected_asset_path = selected_asset.get_path_name() - ar = unreal.AssetRegistryHelpers.get_asset_registry() + # Check if the selected asset is a level sequence asset. + if selected_asset.get_class().get_name() != "LevelSequence": + unreal.log_warning( + f"Skipping {selected_asset.get_name()}. It isn't a Level " + "Sequence.") - for a in selection: - ms_obj = ar.get_asset_by_object_path(ms).get_asset() + # The asset name is the the third element of the path which + # contains the map. + # To take the asset name, we remove from the path the prefix + # "/Game/OpenPype/" and then we split the path by "/". + sel_path = selected_asset_path + asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] - seq_data = None + # Get the master sequence and the master level. + # There should be only one sequence and one level in the directory. + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"/Game/OpenPype/{asset_name}"], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + master_seq = sequences[0].get_asset().get_path_name() + master_seq_obj = sequences[0].get_asset() + ar_filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"/Game/OpenPype/{asset_name}"], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() - if a == ms: - seq_data = { - "sequence": ms_obj, - "output": f"{ms_obj.get_name()}", - "frame_range": ( - ms_obj.get_playback_start(), ms_obj.get_playback_end()) - } + # If the selected asset is the master sequence, we get its data + # and then we create the instance for the master sequence. + # Otherwise, we cycle from the master sequence to find the selected + # sequence and we get its data. This data will be used to create + # the instance for the selected sequence. In particular, + # we get the frame range of the selected sequence and its final + # output path. + master_seq_data = { + "sequence": master_seq_obj, + "output": f"{master_seq_obj.get_name()}", + "frame_range": ( + master_seq_obj.get_playback_start(), + master_seq_obj.get_playback_end())} + + if selected_asset_path == master_seq: + seq_data = master_seq_data else: - seq_data_list = [{ - "sequence": ms_obj, - "output": f"{ms_obj.get_name()}", - "frame_range": ( - ms_obj.get_playback_start(), ms_obj.get_playback_end()) - }] + seq_data_list = [master_seq_data] - for s in seq_data_list: - subscenes = pipeline.get_subsequences(s.get('sequence')) + for seq in seq_data_list: + subscenes = get_subsequences(seq.get('sequence')) - for ss in subscenes: + for sub_seq in subscenes: + sub_seq_obj = sub_seq.get_sequence() curr_data = { - "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), + "sequence": sub_seq_obj, + "output": (f"{seq.get('output')}/" + f"{sub_seq_obj.get_name()}"), "frame_range": ( - ss.get_start_frame(), ss.get_end_frame() - 1) - } + sub_seq.get_start_frame(), + sub_seq.get_end_frame() - 1)} - if ss.get_sequence().get_path_name() == a: + # If the selected asset is the current sub-sequence, + # we get its data and we break the loop. + # Otherwise, we add the current sub-sequence data to + # the list of sequences to check. + if sub_seq_obj.get_path_name() == selected_asset_path: seq_data = curr_data break + seq_data_list.append(curr_data) + # If we found the selected asset, we break the loop. if seq_data is not None: break + # If we didn't find the selected asset, we don't create the + # instance. if not seq_data: + unreal.log_warning( + f"Skipping {selected_asset.get_name()}. It isn't a " + "sub-sequence of the master sequence.") continue - d = self.data.copy() - d["members"] = [a] - d["sequence"] = a - d["master_sequence"] = ms - d["master_level"] = ml - d["output"] = seq_data.get('output') - d["frameStart"] = seq_data.get('frame_range')[0] - d["frameEnd"] = seq_data.get('frame_range')[1] + instance_data["members"] = [selected_asset_path] + instance_data["sequence"] = selected_asset_path + instance_data["master_sequence"] = master_seq + instance_data["master_level"] = master_lvl + instance_data["output"] = seq_data.get('output') + instance_data["frameStart"] = seq_data.get('frame_range')[0] + instance_data["frameEnd"] = seq_data.get('frame_range')[1] - container_name = f"{subset}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=path) - pipeline.imprint(f"{path}/{container_name}", d) + super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) From 575eb50c03e02227e2c9dedf8fc7c2a32f558c85 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 30 Jan 2023 11:17:21 +0000 Subject: [PATCH 083/918] Hound fixes --- openpype/hosts/unreal/api/plugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 6a561420fa..71ce0c18a7 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -104,7 +104,7 @@ class UnrealBaseCreator(Creator): # cache instances if missing self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ - "unreal_cached_subsets"].get(self.identifier, []): + "unreal_cached_subsets"].get(self.identifier, []): created_instance = CreatedInstance.from_existing(instance, self) self._add_instance_to_context(created_instance) @@ -162,7 +162,8 @@ class UnrealAssetCreator(UnrealBaseCreator): selection = [] if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + utility_lib = unreal.EditorUtilityLibrary + sel_objects = utility_lib.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] instance_data["members"] = selection @@ -211,7 +212,8 @@ class UnrealActorCreator(UnrealBaseCreator): selection = [] if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + utility_lib = unreal.EditorUtilityLibrary + sel_objects = utility_lib.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] instance_data["members"] = selection From af2737a99f608ef6598d54ae8d098a3509a6223b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Jan 2023 16:05:01 +0000 Subject: [PATCH 084/918] Collect instances is no longer needed with the new publisher --- .../plugins/publish/collect_instances.py | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/publish/collect_instances.py diff --git a/openpype/hosts/unreal/plugins/publish/collect_instances.py b/openpype/hosts/unreal/plugins/publish/collect_instances.py deleted file mode 100644 index 27b711cad6..0000000000 --- a/openpype/hosts/unreal/plugins/publish/collect_instances.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect publishable instances in Unreal.""" -import ast -import unreal # noqa -import pyblish.api -from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION -from openpype.pipeline.publish import KnownPublishError - - -class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by OpenPypePublishInstance class - - This collector finds all paths containing `OpenPypePublishInstance` class - asset - - Identifier: - id (str): "pyblish.avalon.instance" - - """ - - label = "Collect Instances" - order = pyblish.api.CollectorOrder - 0.1 - hosts = ["unreal"] - - def process(self, context): - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - class_name = [ - "/Script/OpenPype", - "OpenPypePublishInstance" - ] if ( - UNREAL_VERSION.major == 5 - and UNREAL_VERSION.minor > 0 - ) else "OpenPypePublishInstance" # noqa - instance_containers = ar.get_assets_by_class(class_name, True) - - for container_data in instance_containers: - asset = container_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = container_data.asset_name - # convert to strings - data = {str(key): str(value) for (key, value) in data.items()} - if not data.get("family"): - raise KnownPublishError("instance has no family") - - # content of container - members = ast.literal_eval(data.get("members")) - self.log.debug(members) - self.log.debug(asset.get_path_name()) - # remove instance container - self.log.info("Creating instance for {}".format(asset.get_name())) - - instance = context.create_instance(asset.get_name()) - instance[:] = members - - # Store the exact members of the object set - instance.data["setMembers"] = members - instance.data["families"] = [data.get("family")] - instance.data["level"] = data.get("level") - instance.data["parent"] = data.get("parent") - - label = "{0} ({1})".format(asset.get_name()[:-4], - data["asset"]) - - instance.data["label"] = label - - instance.data.update(data) From c93fc9aad0743d4252d6bb58c33ac21365b7eac7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 11:22:36 +0000 Subject: [PATCH 085/918] Use External Data in the Unreal Publish Instance to store members Not possible with all the families. Some families require to store actors in a scenes, and we cannot store them in the External Data. --- openpype/hosts/unreal/api/plugin.py | 24 ++++++--- .../unreal/plugins/create/create_look.py | 6 ++- .../publish/collect_instance_members.py | 49 +++++++++++++++++++ .../unreal/plugins/publish/extract_look.py | 4 +- .../unreal/plugins/publish/extract_uasset.py | 8 ++- 5 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 openpype/hosts/unreal/plugins/publish/collect_instance_members.py diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 71ce0c18a7..da571af9be 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -80,7 +80,7 @@ class UnrealBaseCreator(Creator): def create(self, subset_name, instance_data, pre_create_data): try: instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) + pub_instance = create_publish_instance(instance_name, self.root) instance_data["subset"] = subset_name instance_data["instance_path"] = f"{self.root}/{instance_name}" @@ -92,6 +92,15 @@ class UnrealBaseCreator(Creator): self) self._add_instance_to_context(instance) + pub_instance.set_editor_property('add_external_assets', True) + assets = pub_instance.get_editor_property('asset_data_external') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for member in pre_create_data.get("members", []): + obj = ar.get_asset_by_object_path(member).get_asset() + assets.add(obj) + imprint(f"{self.root}/{instance_name}", instance_data) except Exception as er: @@ -158,15 +167,14 @@ class UnrealAssetCreator(UnrealBaseCreator): try: # Check if instance data has members, filled by the plugin. # If not, use selection. - if not instance_data.get("members"): - selection = [] + if not pre_create_data.get("members"): + pre_create_data["members"] = [] if pre_create_data.get("use_selection"): - utility_lib = unreal.EditorUtilityLibrary - sel_objects = utility_lib.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - instance_data["members"] = selection + utilib = unreal.EditorUtilityLibrary + sel_objects = utilib.get_selected_assets() + pre_create_data["members"] = [ + a.get_path_name() for a in sel_objects] super(UnrealAssetCreator, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 08d61ab9f8..047764ef2a 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -34,6 +34,8 @@ class CreateLook(UnrealAssetCreator): folder_name = create_folder(look_directory, subset_name) path = f"{look_directory}/{folder_name}" + instance_data["look"] = path + # Create a new cube static mesh ar = unreal.AssetRegistryHelpers.get_asset_registry() cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") @@ -42,7 +44,7 @@ class CreateLook(UnrealAssetCreator): original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() materials = original_mesh.get_editor_property('static_materials') - instance_data["members"] = [] + pre_create_data["members"] = [] # Add the materials to the cube for material in materials: @@ -58,7 +60,7 @@ class CreateLook(UnrealAssetCreator): unreal_object.add_material( material.get_editor_property('material_interface')) - instance_data["members"].append(object_path) + pre_create_data["members"].append(object_path) unreal.EditorAssetLibrary.save_asset(object_path) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py new file mode 100644 index 0000000000..74969f5033 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -0,0 +1,49 @@ +import unreal + +import pyblish.api + + +class CollectInstanceMembers(pyblish.api.InstancePlugin): + """ + Collect members of instance. + + This collector will collect the assets for the families that support to + have them included as External Data, and will add them to the instance + as members. + """ + + order = pyblish.api.CollectorOrder + 0.1 + hosts = ["unreal"] + families = ["look", "unrealStaticMesh", "uasset"] + label = "Collect Instance Members" + + def process(self, instance): + """Collect members of instance.""" + self.log.info("Collecting instance members") + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + inst_path = instance.data.get('instance_path') + inst_name = instance.data.get('objectName') + + pub_instance = ar.get_asset_by_object_path( + f"{inst_path}.{inst_name}").get_asset() + + if not pub_instance: + self.log.error(f"{inst_path}.{inst_name}") + raise RuntimeError(f"Instance {instance} not found.") + + if not pub_instance.get_editor_property("add_external_assets"): + # No external assets in the instance + return + + assets = pub_instance.get_editor_property('asset_data_external') + + members = [] + + for asset in assets: + members.append(asset.get_path_name()) + + self.log.debug(f"Members: {members}") + + instance.data["members"] = members diff --git a/openpype/hosts/unreal/plugins/publish/extract_look.py b/openpype/hosts/unreal/plugins/publish/extract_look.py index f999ad8651..4b32b4eb95 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_look.py +++ b/openpype/hosts/unreal/plugins/publish/extract_look.py @@ -29,13 +29,13 @@ class ExtractLook(publish.Extractor): for member in instance: asset = ar.get_asset_by_object_path(member) - object = asset.get_asset() + obj = asset.get_asset() name = asset.get_editor_property('asset_name') json_element = {'material': str(name)} - material_obj = object.get_editor_property('static_materials')[0] + material_obj = obj.get_editor_property('static_materials')[0] material = material_obj.material_interface base_color = mat_lib.get_material_property_input_node( diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index 89d779d368..f719df2a82 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -22,7 +22,13 @@ class ExtractUAsset(publish.Extractor): staging_dir = self.staging_dir(instance) filename = "{}.uasset".format(instance.name) - obj = instance[0] + members = instance.data.get("members", []) + + if not members: + raise RuntimeError("No members found in instance.") + + # UAsset publishing supports only one member + obj = members[0] asset = ar.get_asset_by_object_path(obj).get_asset() sys_path = unreal.SystemLibrary.get_system_path(asset) From 20227c686d5339968b3f1e3c4fc8119b0dd8a8df Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 12:18:03 +0000 Subject: [PATCH 086/918] Improved attributes for the creators --- openpype/hosts/unreal/api/plugin.py | 20 +++++++++++++------ .../unreal/plugins/create/create_render.py | 6 ++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index da571af9be..7121aea20b 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -14,7 +14,10 @@ from .pipeline import ( lsinst, UNREAL_VERSION ) -from openpype.lib import BoolDef +from openpype.lib import ( + BoolDef, + UILabelDef +) from openpype.pipeline import ( Creator, LoaderPlugin, @@ -143,11 +146,6 @@ class UnrealBaseCreator(Creator): self._remove_instance_from_context(instance) - def get_pre_create_attr_defs(self): - return [ - BoolDef("use_selection", label="Use selection", default=True) - ] - @six.add_metaclass(ABCMeta) class UnrealAssetCreator(UnrealBaseCreator): @@ -187,6 +185,11 @@ class UnrealAssetCreator(UnrealBaseCreator): OpenPypeCreatorError(f"Creator error: {er}"), sys.exc_info()[2]) + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", label="Use selection", default=True) + ] + @six.add_metaclass(ABCMeta) class UnrealActorCreator(UnrealBaseCreator): @@ -239,6 +242,11 @@ class UnrealActorCreator(UnrealBaseCreator): OpenPypeCreatorError(f"Creator error: {er}"), sys.exc_info()[2]) + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select actors to create instance from them.") + ] + class Loader(LoaderPlugin, ABC): """This serves as skeleton for future OpenPype specific functionality""" diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index de3efdad74..8100a5016c 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -7,6 +7,7 @@ from openpype.hosts.unreal.api.pipeline import ( from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) +from openpype.lib import UILabelDef class CreateRender(UnrealAssetCreator): @@ -129,3 +130,8 @@ class CreateRender(UnrealAssetCreator): subset_name, instance_data, pre_create_data) + + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select the sequence to render.") + ] From 65e08973fe423c5f456a5a9654fc59d711e06adb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 16:15:03 +0000 Subject: [PATCH 087/918] Fix render creator problem with selection --- .../hosts/unreal/plugins/create/create_render.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 8100a5016c..a1e3e43a78 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -6,6 +6,7 @@ from openpype.hosts.unreal.api.pipeline import ( ) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, + OpenPypeCreatorError ) from openpype.lib import UILabelDef @@ -21,13 +22,13 @@ class CreateRender(UnrealAssetCreator): def create(self, subset_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [ - a.get_path_name() for a in sel_objects - if a.get_class().get_name() == "LevelSequence"] - else: - selection = [instance_data['sequence']] + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [ + a.get_path_name() for a in sel_objects + if a.get_class().get_name() == "LevelSequence"] + + if len(selection) == 0: + raise RuntimeError("Please select at least one Level Sequence.") seq_data = None From 106f9ca2bb750ebed02016264e3f46b199aa494f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 16:17:23 +0000 Subject: [PATCH 088/918] Hound fixes --- openpype/hosts/unreal/plugins/create/create_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a1e3e43a78..c957e50e29 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -5,8 +5,7 @@ from openpype.hosts.unreal.api.pipeline import ( get_subsequences ) from openpype.hosts.unreal.api.plugin import ( - UnrealAssetCreator, - OpenPypeCreatorError + UnrealAssetCreator ) from openpype.lib import UILabelDef From 8e30e565fdefb3a567567cd9651182eb0da2f68d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 11:41:57 +0000 Subject: [PATCH 089/918] Implemented suggestions from review --- openpype/hosts/unreal/api/pipeline.py | 3 - openpype/hosts/unreal/api/plugin.py | 69 +++++++------------ .../unreal/plugins/create/create_camera.py | 2 +- .../unreal/plugins/create/create_look.py | 14 ++-- 4 files changed, 37 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 7a21effcbc..0fe8c02ec5 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -69,9 +69,6 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): op_ctx = content_path + CONTEXT_CONTAINER with open(op_ctx, "w+") as f: json.dump(data, f) - with open(op_ctx, "r") as fp: - test = eval(json.load(fp)) - unreal.log_warning(test) def get_context_data(self): content_path = unreal.Paths.project_content_dir() diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 7121aea20b..fc724105b6 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- +import collections import sys import six -from abc import ( - ABC, - ABCMeta, -) +from abc import ABC import unreal @@ -26,11 +24,6 @@ from openpype.pipeline import ( ) -class OpenPypeCreatorError(CreatorError): - pass - - -@six.add_metaclass(ABCMeta) class UnrealBaseCreator(Creator): """Base class for Unreal creator plugins.""" root = "/Game/OpenPype/PublishInstances" @@ -56,28 +49,20 @@ class UnrealBaseCreator(Creator): """ if shared_data.get("unreal_cached_subsets") is None: - shared_data["unreal_cached_subsets"] = {} - if shared_data.get("unreal_cached_legacy_subsets") is None: - shared_data["unreal_cached_legacy_subsets"] = {} - cached_instances = lsinst() - for i in cached_instances: - if not i.get("creator_identifier"): - # we have legacy instance - family = i.get("family") - if (family not in - shared_data["unreal_cached_legacy_subsets"]): - shared_data[ - "unreal_cached_legacy_subsets"][family] = [i] - else: - shared_data[ - "unreal_cached_legacy_subsets"][family].append(i) - continue - - creator_id = i.get("creator_identifier") - if creator_id not in shared_data["unreal_cached_subsets"]: - shared_data["unreal_cached_subsets"][creator_id] = [i] + unreal_cached_subsets = collections.defaultdict(list) + unreal_cached_legacy_subsets = collections.defaultdict(list) + for instance in lsinst(): + creator_id = instance.get("creator_identifier") + if creator_id: + unreal_cached_subsets[creator_id].append(instance) else: - shared_data["unreal_cached_subsets"][creator_id].append(i) + family = instance.get("family") + unreal_cached_legacy_subsets[family].append(instance) + + shared_data["unreal_cached_subsets"] = unreal_cached_subsets + shared_data["unreal_cached_legacy_subsets"] = ( + unreal_cached_legacy_subsets + ) return shared_data def create(self, subset_name, instance_data, pre_create_data): @@ -108,8 +93,8 @@ class UnrealBaseCreator(Creator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def collect_instances(self): @@ -121,17 +106,17 @@ class UnrealBaseCreator(Creator): self._add_instance_to_context(created_instance) def update_instances(self, update_list): - unreal.log_warning(f"Update instances: {update_list}") - for created_inst, _changes in update_list: + for created_inst, changes in update_list: instance_node = created_inst.get("instance_path", "") if not instance_node: unreal.log_warning( f"Instance node not found for {created_inst}") + continue new_values = { - key: new_value - for key, (_old_value, new_value) in _changes.items() + key: changes[key].new_value + for key in changes.changed_keys } imprint( instance_node, @@ -147,7 +132,6 @@ class UnrealBaseCreator(Creator): self._remove_instance_from_context(instance) -@six.add_metaclass(ABCMeta) class UnrealAssetCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on assets.""" @@ -181,8 +165,8 @@ class UnrealAssetCreator(UnrealBaseCreator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def get_pre_create_attr_defs(self): @@ -191,7 +175,6 @@ class UnrealAssetCreator(UnrealBaseCreator): ] -@six.add_metaclass(ABCMeta) class UnrealActorCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on actors.""" @@ -214,7 +197,7 @@ class UnrealActorCreator(UnrealBaseCreator): # Check if the level is saved if world.get_path_name().startswith("/Temp/"): - raise OpenPypeCreatorError( + raise CreatorError( "Level must be saved before creating instances.") # Check if instance data has members, filled by the plugin. @@ -238,8 +221,8 @@ class UnrealActorCreator(UnrealBaseCreator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def get_pre_create_attr_defs(self): diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 239dc87db5..00815e1ed4 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -10,4 +10,4 @@ class CreateCamera(UnrealActorCreator): identifier = "io.openpype.creators.unreal.camera" label = "Camera" family = "camera" - icon = "camera" + icon = "fa.camera" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 047764ef2a..cecb88bca3 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -7,6 +7,7 @@ from openpype.hosts.unreal.api.pipeline import ( from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) +from openpype.lib import UILabelDef class CreateLook(UnrealAssetCreator): @@ -18,10 +19,10 @@ class CreateLook(UnrealAssetCreator): icon = "paint-brush" def create(self, subset_name, instance_data, pre_create_data): - selection = [] - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] + # We need to set this to True for the parent class to work + pre_create_data["use_selection"] = True + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] if len(selection) != 1: raise RuntimeError("Please select only one asset.") @@ -68,3 +69,8 @@ class CreateLook(UnrealAssetCreator): subset_name, instance_data, pre_create_data) + + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select the asset from which to create the look.") + ] From fa3a7419409598ba0b3b2c9cb42d1c42be20822b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 11:45:30 +0000 Subject: [PATCH 090/918] Fixed problem with the instance metadata --- openpype/hosts/unreal/api/pipeline.py | 2 +- openpype/hosts/unreal/api/plugin.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0fe8c02ec5..0810ec7c07 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -76,7 +76,7 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): if not os.path.isfile(op_ctx): return {} with open(op_ctx, "r") as fp: - data = eval(json.load(fp)) + data = json.load(fp) return data diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index fc724105b6..a852ed9bb1 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import ast import collections import sys import six @@ -89,7 +90,9 @@ class UnrealBaseCreator(Creator): obj = ar.get_asset_by_object_path(member).get_asset() assets.add(obj) - imprint(f"{self.root}/{instance_name}", instance_data) + imprint(f"{self.root}/{instance_name}", instance.data_to_store()) + + return instance except Exception as er: six.reraise( @@ -102,6 +105,11 @@ class UnrealBaseCreator(Creator): self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ "unreal_cached_subsets"].get(self.identifier, []): + # Unreal saves metadata as string, so we need to convert it back + instance['creator_attributes'] = ast.literal_eval( + instance.get('creator_attributes', '{}')) + instance['publish_attributes'] = ast.literal_eval( + instance.get('publish_attributes', '{}')) created_instance = CreatedInstance.from_existing(instance, self) self._add_instance_to_context(created_instance) From 614bcb320c3a6bde5e717000065b5c17088ccdc6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 16:56:21 +0000 Subject: [PATCH 091/918] Creator allows to create a new level sequence with render instance --- .../unreal/plugins/create/create_render.py | 126 +++++++++++++++--- 1 file changed, 111 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index c957e50e29..bc39b43802 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -2,12 +2,17 @@ import unreal from openpype.hosts.unreal.api.pipeline import ( - get_subsequences + UNREAL_VERSION, + create_folder, + get_subsequences, ) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) -from openpype.lib import UILabelDef +from openpype.lib import ( + BoolDef, + UILabelDef +) class CreateRender(UnrealAssetCreator): @@ -18,7 +23,88 @@ class CreateRender(UnrealAssetCreator): family = "render" icon = "eye" - def create(self, subset_name, instance_data, pre_create_data): + def create_instance( + self, instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data + ): + instance_data["members"] = [selected_asset_path] + instance_data["sequence"] = selected_asset_path + instance_data["master_sequence"] = master_seq + instance_data["master_level"] = master_lvl + instance_data["output"] = seq_data.get('output') + instance_data["frameStart"] = seq_data.get('frame_range')[0] + instance_data["frameEnd"] = seq_data.get('frame_range')[1] + + super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) + + def create_with_new_sequence( + self, subset_name, instance_data, pre_create_data + ): + # If the option to create a new level sequence is selected, + # create a new level sequence and a master level. + + root = f"/Game/OpenPype/Sequences" + + # Create a new folder for the sequence in root + sequence_dir_name = create_folder(root, subset_name) + sequence_dir = f"{root}/{sequence_dir_name}" + + unreal.log_warning(f"sequence_dir: {sequence_dir}") + + # Create the level sequence + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + seq = asset_tools.create_asset( + asset_name=subset_name, + package_path=sequence_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew()) + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) + + # Create the master level + prev_level = None + if UNREAL_VERSION.major >= 5: + curr_level = unreal.LevelEditorSubsystem().get_current_level() + else: + world = unreal.EditorLevelLibrary.get_editor_world() + levels = unreal.EditorLevelUtils.get_levels(world) + curr_level = levels[0] if len(levels) else None + if not curr_level: + raise RuntimeError("No level loaded.") + curr_level_path = curr_level.get_outer().get_path_name() + + # If the level path does not start with "/Game/", the current + # level is a temporary, unsaved level. + if curr_level_path.startswith("/Game/"): + prev_level = curr_level_path + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().save_current_level() + else: + unreal.EditorLevelLibrary.save_current_level() + + ml_path = f"{sequence_dir}/{subset_name}_MasterLevel" + + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().new_level(ml_path) + else: + unreal.EditorLevelLibrary.new_level(ml_path) + + seq_data = { + "sequence": seq, + "output": f"{seq.get_name()}", + "frame_range": ( + seq.get_playback_start(), + seq.get_playback_end())} + + self.create_instance( + instance_data, subset_name, pre_create_data, + seq.get_path_name(), seq.get_path_name(), ml_path, seq_data) + + def create_from_existing_sequence( + self, subset_name, instance_data, pre_create_data + ): ar = unreal.AssetRegistryHelpers.get_asset_registry() sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() @@ -118,20 +204,30 @@ class CreateRender(UnrealAssetCreator): "sub-sequence of the master sequence.") continue - instance_data["members"] = [selected_asset_path] - instance_data["sequence"] = selected_asset_path - instance_data["master_sequence"] = master_seq - instance_data["master_level"] = master_lvl - instance_data["output"] = seq_data.get('output') - instance_data["frameStart"] = seq_data.get('frame_range')[0] - instance_data["frameEnd"] = seq_data.get('frame_range')[1] + self.create_instance( + instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data) - super(CreateRender, self).create( - subset_name, - instance_data, - pre_create_data) + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("create_seq"): + self.create_with_new_sequence( + subset_name, instance_data, pre_create_data) + else: + self.create_from_existing_sequence( + subset_name, instance_data, pre_create_data) def get_pre_create_attr_defs(self): return [ - UILabelDef("Select the sequence to render.") + UILabelDef( + "Select a Level Sequence to render or create a new one." + ), + BoolDef( + "create_seq", + label="Create a new Level Sequence", + default=False + ), + UILabelDef( + "WARNING: If you create a new Level Sequence, the current " + "level will be saved and a new Master Level will be created." + ) ] From f94cae429e0cb7b056153211adec7fa7813b28f8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Feb 2023 11:46:10 +0000 Subject: [PATCH 092/918] Allow the user to set frame range of new sequence --- .../unreal/plugins/create/create_render.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index bc39b43802..b999f9ae20 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -11,7 +11,7 @@ from openpype.hosts.unreal.api.plugin import ( ) from openpype.lib import ( BoolDef, - UILabelDef + NumberDef ) @@ -61,6 +61,10 @@ class CreateRender(UnrealAssetCreator): package_path=sequence_dir, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew()) + + seq.set_playback_start(pre_create_data.get("start_frame")) + seq.set_playback_end(pre_create_data.get("end_frame")) + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) # Create the master level @@ -229,5 +233,19 @@ class CreateRender(UnrealAssetCreator): UILabelDef( "WARNING: If you create a new Level Sequence, the current " "level will be saved and a new Master Level will be created." - ) + ), + NumberDef( + "start_frame", + label="Start Frame", + default=0, + minimum=-999999, + maximum=999999 + ), + NumberDef( + "end_frame", + label="Start Frame", + default=150, + minimum=-999999, + maximum=999999 + ), ] From e2ea7fad1a7f4d0aaec178dd18e76ffa18e3f3af Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Feb 2023 11:47:26 +0000 Subject: [PATCH 093/918] Added option to not include hierarchy when creating a render instance --- .../unreal/plugins/create/create_render.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index b999f9ae20..6f2049693f 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from pathlib import Path + import unreal from openpype.hosts.unreal.api.pipeline import ( @@ -10,6 +12,8 @@ from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) from openpype.lib import ( + UILabelDef, + UISeparatorDef, BoolDef, NumberDef ) @@ -68,7 +72,6 @@ class CreateRender(UnrealAssetCreator): unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) # Create the master level - prev_level = None if UNREAL_VERSION.major >= 5: curr_level = unreal.LevelEditorSubsystem().get_current_level() else: @@ -82,7 +85,6 @@ class CreateRender(UnrealAssetCreator): # If the level path does not start with "/Game/", the current # level is a temporary, unsaved level. if curr_level_path.startswith("/Game/"): - prev_level = curr_level_path if UNREAL_VERSION.major >= 5: unreal.LevelEditorSubsystem().save_current_level() else: @@ -131,25 +133,31 @@ class CreateRender(UnrealAssetCreator): f"Skipping {selected_asset.get_name()}. It isn't a Level " "Sequence.") - # The asset name is the the third element of the path which - # contains the map. - # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". - sel_path = selected_asset_path - asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] + if pre_create_data.get("use_hierarchy"): + # The asset name is the the third element of the path which + # contains the map. + # To take the asset name, we remove from the path the prefix + # "/Game/OpenPype/" and then we split the path by "/". + sel_path = selected_asset_path + asset_name = sel_path.replace( + "/Game/OpenPype/", "").split("/")[0] + + search_path = f"/Game/OpenPype/{asset_name}" + else: + search_path = Path(selected_asset_path).parent.as_posix() # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. ar_filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[search_path], recursive_paths=False) sequences = ar.get_assets(ar_filter) master_seq = sequences[0].get_asset().get_path_name() master_seq_obj = sequences[0].get_asset() ar_filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[search_path], recursive_paths=False) levels = ar.get_assets(ar_filter) master_lvl = levels[0].get_asset().get_path_name() @@ -168,7 +176,8 @@ class CreateRender(UnrealAssetCreator): master_seq_obj.get_playback_start(), master_seq_obj.get_playback_end())} - if selected_asset_path == master_seq: + if (selected_asset_path == master_seq or + pre_create_data.get("use_hierarchy")): seq_data = master_seq_data else: seq_data_list = [master_seq_data] @@ -231,7 +240,7 @@ class CreateRender(UnrealAssetCreator): default=False ), UILabelDef( - "WARNING: If you create a new Level Sequence, the current " + "WARNING: If you create a new Level Sequence, the current\n" "level will be saved and a new Master Level will be created." ), NumberDef( @@ -248,4 +257,14 @@ class CreateRender(UnrealAssetCreator): minimum=-999999, maximum=999999 ), + UISeparatorDef(), + UILabelDef( + "The following settings are valid only if you are not\n" + "creating a new sequence." + ), + BoolDef( + "use_hierarchy", + label="Use Hierarchy", + default=False + ), ] From d2403bcbdace79d8e645b6dbd68e439dbb144e03 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 16 Feb 2023 12:00:48 +0000 Subject: [PATCH 094/918] Hanldes IndexError when looking for hierarchy for selected sequence --- .../unreal/plugins/create/create_render.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 6f2049693f..b2a246d3a8 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -148,19 +148,23 @@ class CreateRender(UnrealAssetCreator): # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. - ar_filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[search_path], - recursive_paths=False) - sequences = ar.get_assets(ar_filter) - master_seq = sequences[0].get_asset().get_path_name() - master_seq_obj = sequences[0].get_asset() - ar_filter = unreal.ARFilter( - class_names=["World"], - package_paths=[search_path], - recursive_paths=False) - levels = ar.get_assets(ar_filter) - master_lvl = levels[0].get_asset().get_path_name() + try: + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[search_path], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + master_seq = sequences[0].get_asset().get_path_name() + master_seq_obj = sequences[0].get_asset() + ar_filter = unreal.ARFilter( + class_names=["World"], + package_paths=[search_path], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() + except IndexError: + raise RuntimeError( + f"Could not find the hierarchy for the selected sequence.") # If the selected asset is the master sequence, we get its data # and then we create the instance for the master sequence. From a31b6035fe81ff0fe71b335fbd96e6c6f8e5ab9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Mar 2023 17:18:05 +0800 Subject: [PATCH 095/918] add model creator, extractors and loaders --- .../hosts/max/plugins/create/create_model.py | 26 +++++ .../hosts/max/plugins/load/load_max_scene.py | 3 +- openpype/hosts/max/plugins/load/load_model.py | 98 +++++++++++++++++++ .../hosts/max/plugins/load/load_pointcache.py | 3 +- .../plugins/publish/extract_max_scene_raw.py | 3 +- .../max/plugins/publish/extract_model.py | 74 ++++++++++++++ 6 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/max/plugins/create/create_model.py create mode 100644 openpype/hosts/max/plugins/load/load_model.py create mode 100644 openpype/hosts/max/plugins/publish/extract_model.py diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py new file mode 100644 index 0000000000..a78a30e0c7 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for model.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateModel(plugin.MaxCreator): + identifier = "io.openpype.creators.max.model" + label = "Model" + family = "model" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + sel_obj = list(rt.selection) + instance = super(CreateModel, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + container = rt.getNodeByName(instance.data.get("instance_node")) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + for obj in sel_obj: + obj.parent = container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index b863b9363f..d37d3439fb 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -9,7 +9,8 @@ from openpype.hosts.max.api import lib class MaxSceneLoader(load.LoaderPlugin): """Max Scene Loader""" - families = ["camera"] + families = ["camera", + "model"] representations = ["max"] order = -8 icon = "code-fork" diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py new file mode 100644 index 0000000000..e6262b4f86 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -0,0 +1,98 @@ + +import os +from openpype.pipeline import ( + load, get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ModelAbcLoader(load.LoaderPlugin): + """Loading model with the Alembic loader.""" + + families = ["model"] + label = "Load Model(Alembic)" + representations = ["abc"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + file_path = os.path.normpath(self.fname) + + abc_before = { + c for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + abc_import_cmd = (f""" +AlembicImport.ImportToRoot = false +AlembicImport.CustomAttributes = true +AlembicImport.UVs = true +AlembicImport.VertexColors = true + +importFile @"{file_path}" #noPrompt + """) + + self.log.debug(f"Executing command: {abc_import_cmd}") + rt.execute(abc_import_cmd) + + abc_after = { + c for c in rt.rootNode.Children + if rt.classOf(c) == rt.AlembicContainer + } + + # This should yield new AlembicContainer node + abc_containers = abc_after.difference(abc_before) + + if len(abc_containers) != 1: + self.log.error("Something failed when loading.") + + abc_container = abc_containers.pop() + + return containerise( + name, [abc_container], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + alembic_objects = self.get_container_children(node, "AlembicObject") + for alembic_object in alembic_objects: + alembic_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) + + @staticmethod + def get_container_children(parent, type_name): + from pymxs import runtime as rt + + def list_children(node): + children = [] + for c in node.Children: + children.append(c) + children += list_children(c) + return children + + filtered = [] + for child in list_children(parent): + class_type = str(rt.classOf(child.baseObject)) + if class_type == type_name: + filtered.append(child) + + return filtered diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index f7a72ece25..b3e12adc7b 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -15,8 +15,7 @@ from openpype.hosts.max.api import lib class AbcLoader(load.LoaderPlugin): """Alembic loader.""" - families = ["model", - "camera", + families = ["camera", "animation", "pointcache"] label = "Load Alembic" diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index cacc84c591..aa01ad1a3a 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -20,7 +20,8 @@ class ExtractMaxSceneRaw(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] - families = ["camera"] + families = ["camera", + "model"] optional = True def process(self, instance): diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py new file mode 100644 index 0000000000..710ad5f97d --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -0,0 +1,74 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModel(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in Alembic Format + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract Geometry (Alembic)" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.abc".format(**instance.data) + filepath = os.path.join(stagingdir, filename) + + # We run the render + self.log.info("Writing alembic '%s' to '%s'" % (filename, + stagingdir)) + + export_cmd = ( + f""" +AlembicExport.ArchiveType = #ogawa +AlembicExport.CoordinateSystem = #maya +AlembicExport.CustomAttributes = true +AlembicExport.UVs = true +AlembicExport.VertexColors = true +AlembicExport.PreserveInstances = true + +exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport + + """) + + self.log.debug(f"Executing command: {export_cmd}") + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(export_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + filepath)) From f18455717c95b67846558e59a785143961d5fc58 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Mar 2023 17:25:45 +0800 Subject: [PATCH 096/918] OP-4245 - Data Exchange: geometry --- .../max/plugins/publish/extract_model_usd.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/extract_model_usd.py diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py new file mode 100644 index 0000000000..1c8bf073da --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -0,0 +1,112 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModelUSD(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in USDA Format + """ + + order = pyblish.api.ExtractorOrder - 0.05 + label = "Extract Geometry (USD)" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + asset_filename = "{name}.usda".format(**instance.data) + asset_filepath = os.path.join(stagingdir, + asset_filename) + self.log.info("Writing USD '%s' to '%s'" % (asset_filepath, + stagingdir)) + + log_filename ="{name}.txt".format(**instance.data) + log_filepath = os.path.join(stagingdir, + log_filename) + self.log.info("Writing log '%s' to '%s'" % (log_filepath, + stagingdir)) + + # get the nodes which need to be exported + export_options = self.get_export_options(log_filepath) + with maintained_selection(): + # select and export + node_list = self.get_node_list(container) + rt.USDExporter.ExportFile(asset_filepath, + exportOptions=export_options, + nodeList=node_list) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'usda', + 'ext': 'usda', + 'files': asset_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + log_representation = { + 'name': 'txt', + 'ext': 'txt', + 'files': log_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(log_representation) + + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + asset_filepath)) + + def get_node_list(self, container): + """ + Get the target nodes which are + the children of the container + """ + node_list = [] + + container_node = rt.getNodeByName(container) + target_node = container_node.Children + rt.select(target_node) + for sel in rt.selection: + node_list.append(sel) + + return node_list + + def get_export_options(self, log_path): + """Set Export Options for USD Exporter""" + + export_options = rt.USDExporter.createOptions() + + export_options.Meshes = True + export_options.Lights = False + export_options.Cameras = False + export_options.Materials = False + export_options.FileFormat = rt.name('ascii') + export_options.UpAxis = rt.name('y') + export_options.LogLevel = rt.name('info') + export_options.LogPath = log_path + export_options.PreserveEdgeOrientation = True + export_options.TimeMode = rt.name('current') + + rt.USDexporter.UIOptions = export_options + + return export_options From a7c11f0aece3b0484d94b64e92955103fc5b93e2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 2 Mar 2023 17:30:25 +0800 Subject: [PATCH 097/918] hound fix --- openpype/hosts/max/plugins/publish/extract_model_usd.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 1c8bf073da..0f8d283907 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -38,7 +37,7 @@ class ExtractModelUSD(publish.Extractor, self.log.info("Writing USD '%s' to '%s'" % (asset_filepath, stagingdir)) - log_filename ="{name}.txt".format(**instance.data) + log_filename = "{name}.txt".format(**instance.data) log_filepath = os.path.join(stagingdir, log_filename) self.log.info("Writing log '%s' to '%s'" % (log_filepath, From b5d748f466858557d09680923a30f1851cc8e6a2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 13:02:24 +0800 Subject: [PATCH 098/918] add export options to the usd extractors and add usd loader --- .../hosts/max/plugins/load/load_model_usd.py | 59 +++++++++++++++++++ .../max/plugins/publish/extract_model_usd.py | 2 + 2 files changed, 61 insertions(+) create mode 100644 openpype/hosts/max/plugins/load/load_model_usd.py diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py new file mode 100644 index 0000000000..c6c414b91c --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -0,0 +1,59 @@ +import os +from openpype.pipeline import ( + load, get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ModelUSDLoader(load.LoaderPlugin): + """Loading model with the USD loader.""" + + families = ["model"] + label = "Load Model(USD)" + representations = ["usda"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + # asset_filepath + filepath = os.path.normpath(self.fname) + import_options = rt.USDImporter.CreateOptions() + base_filename = os.path.basename(filepath) + filename, ext = os.path.splitext(base_filename) + log_filepath = filepath.replace(ext, "txt") + + rt.LogPath = log_filepath + rt.LogLevel = rt.name('info') + rt.USDImporter.importFile(filepath, + importOptions=import_options) + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + usd_objects = self.get_container_children(node) + for usd_object in usd_objects: + usd_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 0f8d283907..2f89e4de16 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -96,9 +96,11 @@ class ExtractModelUSD(publish.Extractor, export_options = rt.USDExporter.createOptions() export_options.Meshes = True + export_options.Shapes = True export_options.Lights = False export_options.Cameras = False export_options.Materials = False + export_options.MeshFormat = rt.name('fromScene') export_options.FileFormat = rt.name('ascii') export_options.UpAxis = rt.name('y') export_options.LogLevel = rt.name('info') From 519cef018529e17fb94c7c8bb197885c762ede93 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 13:34:35 +0800 Subject: [PATCH 099/918] add validator for model family --- .../max/plugins/publish/extract_model_usd.py | 2 +- .../publish/validate_model_contents.py | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_model_contents.py diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 2f89e4de16..b20fd45eae 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -96,7 +96,7 @@ class ExtractModelUSD(publish.Extractor, export_options = rt.USDExporter.createOptions() export_options.Meshes = True - export_options.Shapes = True + export_options.Shapes = False export_options.Lights = False export_options.Cameras = False export_options.Materials = False diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py new file mode 100644 index 0000000000..01ae869c30 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt + + +class ValidateModelContent(pyblish.api.InstancePlugin): + """Validates Model instance contents. + + A model instance may only hold either geometry + or editable meshes. + """ + + order = pyblish.api.ValidatorOrder + families = ["model"] + hosts = ["max"] + label = "Model Contents" + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError("Model instance must only include" + "Geometry and Editable Mesh") + + def get_invalid(self, instance): + """ + Get invalid nodes if the instance is not camera + """ + invalid = list() + container = instance.data["instance_node"] + self.log.info("Validating look content for " + "{}".format(container)) + + con = rt.getNodeByName(container) + selection_list = list(con.Children) + for sel in selection_list: + if rt.classOf(sel) in rt.Camera.classes: + invalid.append(sel) + if rt.classOf(sel) in rt.Light.classes: + invalid.append(sel) + if rt.classOf(sel) in rt.Shape.classes: + invalid.append(sel) + + return invalid From 12211d70371354fafad96f980d05743542be6c5e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 13:35:52 +0800 Subject: [PATCH 100/918] add info in docstring for the validator --- openpype/hosts/max/plugins/publish/validate_model_contents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index 01ae869c30..dd9c8de2cf 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -7,8 +7,8 @@ from pymxs import runtime as rt class ValidateModelContent(pyblish.api.InstancePlugin): """Validates Model instance contents. - A model instance may only hold either geometry - or editable meshes. + A model instance may only hold either geometry-related + object(excluding Shapes) or editable meshes. """ order = pyblish.api.ValidatorOrder From c98160691b9e1273de8294ad1080792e8080c8a5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 16:01:22 +0800 Subject: [PATCH 101/918] add usdmodel as families --- .../max/plugins/create/create_model_usd.py | 22 +++++++++++++++++++ .../hosts/max/plugins/load/load_model_usd.py | 2 +- .../max/plugins/publish/extract_model_usd.py | 2 +- .../publish/validate_model_contents.py | 2 +- openpype/plugins/publish/integrate.py | 1 + openpype/plugins/publish/integrate_legacy.py | 1 + 6 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/plugins/create/create_model_usd.py diff --git a/openpype/hosts/max/plugins/create/create_model_usd.py b/openpype/hosts/max/plugins/create/create_model_usd.py new file mode 100644 index 0000000000..237ae8f4ae --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_model_usd.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for model exported in USD format.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateUSDModel(plugin.MaxCreator): + identifier = "io.openpype.creators.max.usdmodel" + label = "USD Model" + family = "usdmodel" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + _ = super(CreateUSDModel, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index c6c414b91c..ac318fbb57 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -9,7 +9,7 @@ from openpype.hosts.max.api import lib class ModelUSDLoader(load.LoaderPlugin): """Loading model with the USD loader.""" - families = ["model"] + families = ["usdmodel"] label = "Load Model(USD)" representations = ["usda"] order = -10 diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index b20fd45eae..e0ad3bb23e 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -19,7 +19,7 @@ class ExtractModelUSD(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.05 label = "Extract Geometry (USD)" hosts = ["max"] - families = ["model"] + families = ["usdmodel"] optional = True def process(self, instance): diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index dd9c8de2cf..34578e6920 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -12,7 +12,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["model"] + families = ["model", "usdmodel"] hosts = ["max"] label = "Model Contents" diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index b117006871..fc098b416a 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -124,6 +124,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "xgen", "hda", "usd", + "usdmodel", "staticMesh", "skeletalMesh", "mvLook", diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index b93abab1d8..ba32c376d8 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -120,6 +120,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "xgen", "hda", "usd", + "usdmodel", "staticMesh", "skeletalMesh", "mvLook", From fd6aa8302eee6cfcb44cb4d80f30466cd994485d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 16:02:24 +0800 Subject: [PATCH 102/918] add usdmodel as families --- openpype/hosts/max/plugins/create/create_model_usd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_model_usd.py b/openpype/hosts/max/plugins/create/create_model_usd.py index 237ae8f4ae..21407ae1f3 100644 --- a/openpype/hosts/max/plugins/create/create_model_usd.py +++ b/openpype/hosts/max/plugins/create/create_model_usd.py @@ -11,7 +11,6 @@ class CreateUSDModel(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt _ = super(CreateUSDModel, self).create( subset_name, instance_data, From 5b4eff51acd3fdb3b6700fa154986fc34cb022a6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 3 Mar 2023 20:30:05 +0800 Subject: [PATCH 103/918] include only model family --- openpype/hosts/max/plugins/publish/validate_model_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index 34578e6920..dd9c8de2cf 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -12,7 +12,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): """ order = pyblish.api.ValidatorOrder - families = ["model", "usdmodel"] + families = ["model"] hosts = ["max"] label = "Model Contents" From 1511ddbccf7f89f5ce90d934536d8a5d1b0eeb71 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 6 Mar 2023 16:22:17 +0800 Subject: [PATCH 104/918] usdmodel extractor with selected node and remove usdmodel family --- .../max/plugins/create/create_model_usd.py | 21 ------------------- .../hosts/max/plugins/load/load_model_usd.py | 2 +- .../max/plugins/publish/extract_model_usd.py | 3 ++- openpype/plugins/publish/integrate.py | 1 - openpype/plugins/publish/integrate_legacy.py | 1 - 5 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 openpype/hosts/max/plugins/create/create_model_usd.py diff --git a/openpype/hosts/max/plugins/create/create_model_usd.py b/openpype/hosts/max/plugins/create/create_model_usd.py deleted file mode 100644 index 21407ae1f3..0000000000 --- a/openpype/hosts/max/plugins/create/create_model_usd.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -"""Creator plugin for model exported in USD format.""" -from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance - - -class CreateUSDModel(plugin.MaxCreator): - identifier = "io.openpype.creators.max.usdmodel" - label = "USD Model" - family = "usdmodel" - icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - _ = super(CreateUSDModel, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index ac318fbb57..c6c414b91c 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -9,7 +9,7 @@ from openpype.hosts.max.api import lib class ModelUSDLoader(load.LoaderPlugin): """Loading model with the USD loader.""" - families = ["usdmodel"] + families = ["model"] label = "Load Model(USD)" representations = ["usda"] order = -10 diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index e0ad3bb23e..0bed2d855e 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -19,7 +19,7 @@ class ExtractModelUSD(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.05 label = "Extract Geometry (USD)" hosts = ["max"] - families = ["usdmodel"] + families = ["model"] optional = True def process(self, instance): @@ -50,6 +50,7 @@ class ExtractModelUSD(publish.Extractor, node_list = self.get_node_list(container) rt.USDExporter.ExportFile(asset_filepath, exportOptions=export_options, + contentSource=rt.name("selected"), nodeList=node_list) self.log.info("Performing Extraction ...") diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index fc098b416a..b117006871 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -124,7 +124,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "xgen", "hda", "usd", - "usdmodel", "staticMesh", "skeletalMesh", "mvLook", diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index ba32c376d8..b93abab1d8 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -120,7 +120,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "xgen", "hda", "usd", - "usdmodel", "staticMesh", "skeletalMesh", "mvLook", From 6064fa2d45ca59269cf101b6f19edcf557996f24 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 6 Mar 2023 11:09:49 +0000 Subject: [PATCH 105/918] Added settings for rendering --- .../settings/defaults/project_settings/unreal.json | 2 ++ .../schemas/projects_schema/schema_project_unreal.json | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 75cee11bd9..ff290ef254 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -11,6 +11,8 @@ }, "level_sequences_for_layouts": false, "delete_unmatched_assets": false, + "render_config_path": "", + "preroll_frames": 0, "project_setup": { "dev_mode": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 8988dd2ff0..40bbb40ccc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -32,6 +32,16 @@ "key": "delete_unmatched_assets", "label": "Delete assets that are not matched" }, + { + "type": "text", + "key": "render_config_path", + "label": "Render Config Path" + }, + { + "type": "number", + "key": "preroll_frames", + "label": "Pre-roll frames" + }, { "type": "dict", "collapsible": true, From 095c792ad229d23e0b0d2b5f4fa44eb0ae229862 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 6 Mar 2023 11:10:42 +0000 Subject: [PATCH 106/918] Uses settings for rendering --- openpype/hosts/unreal/api/rendering.py | 54 +++++++++++++++++--------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 29e4747f6e..5ef4792000 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -2,6 +2,7 @@ import os import unreal +from openpype.settings import get_project_settings from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline @@ -66,6 +67,13 @@ def start_rendering(): ar = unreal.AssetRegistryHelpers.get_asset_registry() + data = get_project_settings(project) + config = None + config_path = str(data.get("unreal").get("render_config_path")) + if config_path and unreal.EditorAssetLibrary.does_asset_exist(config_path): + unreal.log("Found saved render configuration") + config = ar.get_asset_by_object_path(config_path).get_asset() + for i in inst_data: sequence = ar.get_asset_by_object_path(i["sequence"]).get_asset() @@ -81,47 +89,50 @@ def start_rendering(): # Get all the sequences to render. If there are subsequences, # add them and their frame ranges to the render list. We also # use the names for the output paths. - for s in sequences: - subscenes = pipeline.get_subsequences(s.get('sequence')) + for seq in sequences: + subscenes = pipeline.get_subsequences(seq.get('sequence')) if subscenes: - for ss in subscenes: + for sub_seq in subscenes: sequences.append({ - "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), + "sequence": sub_seq.get_sequence(), + "output": (f"{seq.get('output')}/" + f"{sub_seq.get_sequence().get_name()}"), "frame_range": ( - ss.get_start_frame(), ss.get_end_frame()) + sub_seq.get_start_frame(), sub_seq.get_end_frame()) }) else: # Avoid rendering camera sequences - if "_camera" not in s.get('sequence').get_name(): - render_list.append(s) + if "_camera" not in seq.get('sequence').get_name(): + render_list.append(seq) # Create the rendering jobs and add them to the queue. - for r in render_list: + for render_setting in render_list: job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) job.sequence = unreal.SoftObjectPath(i["master_sequence"]) job.map = unreal.SoftObjectPath(i["master_level"]) job.author = "OpenPype" + # If we have a saved configuration, copy it to the job. + if config: + job.get_configuration().copy_from(config) + # User data could be used to pass data to the job, that can be # read in the job's OnJobFinished callback. We could, # for instance, pass the AvalonPublishInstance's path to the job. # job.user_data = "" + output_dir = render_setting.get('output') + shot_name = render_setting.get('sequence').get_name() + settings = job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineOutputSetting) settings.output_resolution = unreal.IntPoint(1920, 1080) - settings.custom_start_frame = r.get("frame_range")[0] - settings.custom_end_frame = r.get("frame_range")[1] + settings.custom_start_frame = render_setting.get("frame_range")[0] + settings.custom_end_frame = render_setting.get("frame_range")[1] settings.use_custom_playback_range = True - settings.file_name_format = "{sequence_name}.{frame_number}" - settings.output_directory.path = f"{render_dir}/{r.get('output')}" - - renderPass = job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineDeferredPassBase) - renderPass.disable_multisample_effects = True + settings.file_name_format = f"{shot_name}" + ".{frame_number}" + settings.output_directory.path = f"{render_dir}/{output_dir}" job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineImageSequenceOutput_PNG) @@ -130,6 +141,13 @@ def start_rendering(): if queue.get_jobs(): global executor executor = unreal.MoviePipelinePIEExecutor() + + preroll_frames = data.get("unreal").get("preroll_frames", 0) + + settings = unreal.MoviePipelinePIEExecutorSettings() + settings.set_editor_property( + "initial_delay_frame_count", preroll_frames) + executor.on_executor_finished_delegate.add_callable_unique( _queue_finish_callback) executor.on_individual_job_finished_delegate.add_callable_unique( From 839d5834ca611c20f042c3036bcf422ce5ee32ce Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 8 Mar 2023 11:12:24 +0100 Subject: [PATCH 107/918] Fix merge problem --- openpype/hosts/unreal/plugins/create/create_camera.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 33a0662d7d..642924e2d6 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -7,8 +7,6 @@ from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) -class CreateCamera(UnrealActorCreator): - """Create Camera.""" class CreateCamera(UnrealAssetCreator): """Create Camera.""" From d8efd09797467cf1464d06c36b654f8ec3e02b17 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Thu, 9 Mar 2023 07:03:53 +0100 Subject: [PATCH 108/918] update the mesh format to poly mesh --- openpype/hosts/max/plugins/publish/extract_model_usd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 0bed2d855e..f70a14ba0b 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -101,7 +101,7 @@ class ExtractModelUSD(publish.Extractor, export_options.Lights = False export_options.Cameras = False export_options.Materials = False - export_options.MeshFormat = rt.name('fromScene') + export_options.MeshFormat = rt.name('polyMesh') export_options.FileFormat = rt.name('ascii') export_options.UpAxis = rt.name('y') export_options.LogLevel = rt.name('info') From 5218b6550678cb3e3a5127a449a631652d62c249 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 11 Mar 2023 18:59:43 +0100 Subject: [PATCH 109/918] Shush hound --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 3c7250e65b..d781496cde 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -12,6 +12,7 @@ from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo from openpype.lib import is_running_from_build + @attr.s class DeadlinePluginInfo(): SceneFile = attr.ib(default=None) From 861d60ca0cd3144e75e8ddb135ce071d0b1b65ae Mon Sep 17 00:00:00 2001 From: moonyuet Date: Mon, 13 Mar 2023 11:31:42 +0100 Subject: [PATCH 110/918] fbx obj extractors and oaders --- .../hosts/max/plugins/load/load_camera_fbx.py | 2 - .../hosts/max/plugins/load/load_model_fbx.py | 62 ++++++++++++++++ .../hosts/max/plugins/load/load_model_obj.py | 56 ++++++++++++++ .../max/plugins/publish/extract_model_fbx.py | 74 +++++++++++++++++++ .../max/plugins/publish/extract_model_obj.py | 59 +++++++++++++++ 5 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/max/plugins/load/load_model_fbx.py create mode 100644 openpype/hosts/max/plugins/load/load_model_obj.py create mode 100644 openpype/hosts/max/plugins/publish/extract_model_fbx.py create mode 100644 openpype/hosts/max/plugins/publish/extract_model_obj.py diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 3a6947798e..205e815dc8 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -36,8 +36,6 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) - container_name = f"{name}_CON" - asset = rt.getNodeByName(f"{name}") return containerise( diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py new file mode 100644 index 0000000000..38b8555d28 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -0,0 +1,62 @@ +import os +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class FbxModelLoader(load.LoaderPlugin): + """Fbx Model Loader""" + + families = ["model"] + representations = ["fbx"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = os.path.normpath(self.fname) + + fbx_import_cmd = ( + f""" + +FBXImporterSetParam "Animation" false +FBXImporterSetParam "Cameras" false +FBXImporterSetParam "AxisConversionMethod" true +FbxExporterSetParam "UpAxis" "Y" +FbxExporterSetParam "Preserveinstances" true + +importFile @"{filepath}" #noPrompt using:FBXIMP + """) + + self.log.debug(f"Executing command: {fbx_import_cmd}") + rt.execute(fbx_import_cmd) + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + fbx_objects = self.get_container_children(node) + for fbx_object in fbx_objects: + fbx_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py new file mode 100644 index 0000000000..06b411cb5c --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -0,0 +1,56 @@ +import os +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class ObjLoader(load.LoaderPlugin): + """Obj Loader""" + + families = ["model"] + representations = ["obj"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = os.path.normpath(self.fname) + self.log.debug(f"Executing command to import..") + + rt.execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') + # get current selection + for selection in rt.getCurrentSelection(): + # create "missing" container for obj import + container = rt.container() + container.name = f"{name}" + selection.Parent = container + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + objects = self.get_container_children(node) + for obj in objects: + obj.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py new file mode 100644 index 0000000000..ce58e8cc17 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -0,0 +1,74 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModelFbx(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in FBX Format + """ + + order = pyblish.api.ExtractorOrder - 0.05 + label = "Extract FBX" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.fbx".format(**instance.data) + filepath = os.path.join(stagingdir, + filename) + self.log.info("Writing FBX '%s' to '%s'" % (filepath, + stagingdir)) + + export_fbx_cmd = ( + f""" +FBXExporterSetParam "Animation" false +FBXExporterSetParam "Cameras" false +FBXExporterSetParam "Lights" false +FBXExporterSetParam "PointCache" false +FBXExporterSetParam "AxisConversionMethod" "Animation" +FbxExporterSetParam "UpAxis" "Y" +FbxExporterSetParam "Preserveinstances" true + +exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP + + """) + + self.log.debug(f"Executing command: {export_fbx_cmd}") + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(export_fbx_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + filepath)) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py new file mode 100644 index 0000000000..298e19151d --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -0,0 +1,59 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractModelObj(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Geometry in OBJ Format + """ + + order = pyblish.api.ExtractorOrder - 0.05 + label = "Extract OBJ" + hosts = ["max"] + families = ["model"] + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + container = instance.data["instance_node"] + + self.log.info("Extracting Geometry ...") + + stagingdir = self.staging_dir(instance) + filename = "{name}.obj".format(**instance.data) + filepath = os.path.join(stagingdir, + filename) + self.log.info("Writing OBJ '%s' to '%s'" % (filepath, + stagingdir)) + + with maintained_selection(): + # select and export + rt.select(get_all_children(rt.getNodeByName(container))) + rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'obj', + 'ext': 'obj', + 'files': filename, + "stagingDir": stagingdir, + } + + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + filepath)) From 64e8ff68b54d420c142c9276674e6cac74646ce0 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Mon, 13 Mar 2023 11:32:47 +0100 Subject: [PATCH 111/918] cosmetic issue fixed --- openpype/hosts/max/plugins/publish/extract_model_obj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 298e19151d..7bda237880 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -41,7 +41,7 @@ class ExtractModelObj(publish.Extractor, with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') + rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa self.log.info("Performing Extraction ...") if "representations" not in instance.data: From 55a10a87932130828eeca112f7098e4a4cf5a24f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Mar 2023 22:55:00 +0100 Subject: [PATCH 112/918] Use new style `ColormanagedPyblishPluginMixin` --- .../hosts/substancepainter/plugins/publish/extract_textures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index e66ce6dbf6..469f8501f7 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -2,7 +2,8 @@ from openpype.pipeline import KnownPublishError, publish import substance_painter.export -class ExtractTextures(publish.ExtractorColormanaged): +class ExtractTextures(publish.Extractor, + publish.ColormanagedPyblishPluginMixin): """Extract Textures using an output template config. Note: From c104805830c349260b30756b19560836bd9866a6 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:17:17 +0100 Subject: [PATCH 113/918] adding creator, loader for redshift proxy in 3dsmax --- .../plugins/create/create_redshift_proxy.py | 26 +++++++ .../max/plugins/load/load_redshift_proxy.py | 59 ++++++++++++++ .../plugins/publish/extract_redshift_proxy.py | 78 +++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 openpype/hosts/max/plugins/create/create_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/load/load_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/publish/extract_redshift_proxy.py diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py new file mode 100644 index 0000000000..83ddc3a193 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating camera.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateRedshiftProxy(plugin.MaxCreator): + identifier = "io.openpype.creators.max.redshiftproxy" + label = "Redshift Proxy" + family = "redshiftproxy" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + sel_obj = list(rt.selection) + instance = super(CreateRedshiftProxy, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + container = rt.getNodeByName(instance.data.get("instance_node")) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + for obj in sel_obj: + obj.parent = container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py new file mode 100644 index 0000000000..7a5e94158f --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -0,0 +1,59 @@ +import os +import clique + +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class RedshiftProxyLoader(load.LoaderPlugin): + """Redshift Proxy Loader""" + + families = ["redshiftproxy"] + representations = ["rs"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = os.path.normpath(self.fname) + rs_proxy = rt.RedshiftProxy() + rs_proxy.file = filepath + files_in_folder = os.listdir(os.path.dirname(filepath)) + collections, remainder = clique.assemble(files_in_folder) + if collections: + rs_proxy.is_sequence = True + + container = rt.container() + container.name = f"{name}" + rs_proxy.Parent = container + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + proxy_objects = self.get_container_children(node) + for proxy in proxy_objects: + proxy.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py new file mode 100644 index 0000000000..f9dd726ef4 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -0,0 +1,78 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractCameraAlembic(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Camera with AlembicExport + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract RedShift Proxy" + hosts = ["max"] + families = ["redshiftproxy"] + + def process(self, instance): + container = instance.data["instance_node"] + start = int(instance.context.data.get("frameStart")) + end = int(instance.context.data.get("frameEnd")) + + self.log.info("Extracting Redshift Proxy...") + stagingdir = self.staging_dir(instance) + rs_filename = "{name}.rs".format(**instance.data) + + rs_filepath = os.path.join(stagingdir, rs_filename) + + # MaxScript command for export + export_cmd = ( + f""" +fn ProxyExport fp selected:true compress:false connectivity:false startFrame: endFrame: camera:undefined warnExisting:true transformPivotToOrigin:false = ( + if startFrame == unsupplied then ( + startFrame = (currentTime.frame as integer) + ) + + if endFrame == unsupplied then ( + endFrame = (currentTime.frame as integer) + ) + + ret = rsProxy fp selected compress connectivity startFrame endFrame camera warnExisting transformPivotToOrigin + + ret +) +execute = ProxyExport fp selected:true compress:false connectivity:false startFrame:{start} endFrame:{end} warnExisting:false transformPivotToOrigin:bTransformPivotToOrigin + + """) # noqa + + with maintained_selection(): + # select and export + rt.select(container.Children) + rt.execute(export_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'rs', + 'ext': 'rs', + # need to count the files + 'files': rs_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + rs_filepath)) + + # TODO: set sequence + def get_rsfiles(self, container, startFrame, endFrame): + pass From 5af9867dedda3eb154fedd4aba0c93d8438b80df Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:25:00 +0100 Subject: [PATCH 114/918] update fix --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index f9dd726ef4..85d249b020 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) From 9cbac449fded786bc033931a07c9e44dd18907e2 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:26:06 +0100 Subject: [PATCH 115/918] change the name --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 85d249b020..938a7e8c2c 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -10,7 +10,7 @@ from openpype.hosts.max.api import ( ) -class ExtractCameraAlembic(publish.Extractor, +class ExtractRedshiftProxy(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport From 119bb1a586548e6997f3e2724660c19e0d346a56 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:37:25 +0100 Subject: [PATCH 116/918] update the loader and creator --- openpype/hosts/max/plugins/create/create_redshift_proxy.py | 5 +---- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 83ddc3a193..ca0891fc5b 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -18,9 +18,6 @@ class CreateRedshiftProxy(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance container = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container + for obj in sel_obj: obj.parent = container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 7a5e94158f..13003d764a 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -10,8 +10,8 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): - """Redshift Proxy Loader""" + label = "Load Redshift Proxy" families = ["redshiftproxy"] representations = ["rs"] order = -9 @@ -21,7 +21,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - filepath = os.path.normpath(self.fname) + filepath = self.filepath_from_context(context) rs_proxy = rt.RedshiftProxy() rs_proxy.file = filepath files_in_folder = os.listdir(os.path.dirname(filepath)) @@ -30,7 +30,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): rs_proxy.is_sequence = True container = rt.container() - container.name = f"{name}" + container.name = name rs_proxy.Parent = container asset = rt.getNodeByName(f"{name}") From 7714ebc990730053afa3424bb297c890548a457a Mon Sep 17 00:00:00 2001 From: Joseff Date: Thu, 16 Mar 2023 20:25:00 +0100 Subject: [PATCH 117/918] Renaming the plugin to Ayon. --- .../OpenPype/Config/DefaultAyonSettings.ini | 2 + .../UE_4.7/OpenPype/OpenPype.uplugin | 5 + .../UE_4.7/OpenPype/Resources/ayon128.png | Bin 0 -> 2358 bytes .../UE_4.7/OpenPype/Resources/ayon40.png | Bin 0 -> 721 bytes .../UE_4.7/OpenPype/Resources/ayon512.png | Bin 0 -> 16705 bytes .../UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs | 61 ++++++ .../OpenPype/Source/Ayon/Private/Ayon.cpp | 156 ++++++++++++++ .../Ayon/Private/AyonAssetContainer.cpp | 115 ++++++++++ .../Private/AyonAssetContainerFactory.cpp | 20 ++ .../OpenPype/Source/Ayon/Private/AyonLib.cpp | 53 +++++ .../Ayon/Private/AyonPublishInstance.cpp | 201 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 21 ++ .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 ++ .../Source/Ayon/Private/AyonSettings.cpp | 20 ++ .../Source/Ayon/Private/AyonStyle.cpp | 70 ++++++ .../Private/Commandlets/AyonActionResult.cpp | 41 ++++ .../AyonGenerateProjectCommandlet.cpp | 141 ++++++++++++ .../UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h | 22 ++ .../Source/Ayon/Public/AyonAssetContainer.h | 39 ++++ .../Ayon/Public/AyonAssetContainerFactory.h | 21 ++ .../Source/Ayon/Public/AyonConstants.h | 15 ++ .../OpenPype/Source/Ayon/Public/AyonLib.h | 20 ++ .../Source/Ayon/Public/AyonPublishInstance.h | 102 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h | 20 ++ .../Source/Ayon/Public/AyonPythonBridge.h | 21 ++ .../Source/Ayon/Public/AyonSettings.h | 31 +++ .../OpenPype/Source/Ayon/Public/AyonStyle.h | 23 ++ .../Public/Commandlets/AyonActionResult.h | 83 ++++++++ .../AyonGenerateProjectCommandlet.h | 60 ++++++ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 + .../OpenPype/Private/AssetContainer.cpp | 115 ++++++++++ .../Private/AssetContainerFactory.cpp | 20 ++ .../Source/OpenPype/Public/AssetContainer.h | 39 ++++ .../OpenPype/Public/AssetContainerFactory.h | 21 ++ .../OpenPype/Config/DefaultAyonSettings.ini | 2 + .../UE_5.0/OpenPype/OpenPype.uplugin | 5 + .../UE_5.0/OpenPype/Resources/ayon128.png | Bin 0 -> 2358 bytes .../UE_5.0/OpenPype/Resources/ayon40.png | Bin 0 -> 721 bytes .../UE_5.0/OpenPype/Resources/ayon512.png | Bin 0 -> 16705 bytes .../UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs | 65 ++++++ .../OpenPype/Source/Ayon/Private/Ayon.cpp | 139 ++++++++++++ .../Ayon/Private/AyonAssetContainer.cpp | 113 ++++++++++ .../Private/AyonAssetContainerFactory.cpp | 20 ++ .../Source/Ayon/Private/AyonCommands.cpp | 13 ++ .../OpenPype/Source/Ayon/Private/AyonLib.cpp | 51 +++++ .../Ayon/Private/AyonPublishInstance.cpp | 201 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 21 ++ .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 ++ .../Source/Ayon/Private/AyonSettings.cpp | 21 ++ .../Source/Ayon/Private/AyonStyle.cpp | 62 ++++++ .../Private/Commandlets/AyonActionResult.cpp | 40 ++++ .../AyonGenerateProjectCommandlet.cpp | 140 ++++++++++++ .../UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h | 24 +++ .../Source/Ayon/Public/AyonAssetContainer.h | 34 +++ .../Ayon/Public/AyonAssetContainerFactory.h | 18 ++ .../Source/Ayon/Public/AyonCommands.h | 24 +++ .../Source/Ayon/Public/AyonConstants.h | 13 ++ .../OpenPype/Source/Ayon/Public/AyonLib.h | 19 ++ .../Source/Ayon/Public/AyonPublishInstance.h | 102 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h | 20 ++ .../Source/Ayon/Public/AyonPythonBridge.h | 20 ++ .../Source/Ayon/Public/AyonSettings.h | 32 +++ .../OpenPype/Source/Ayon/Public/AyonStyle.h | 19 ++ .../Public/Commandlets/AyonActionResult.h | 83 ++++++++ .../AyonGenerateProjectCommandlet.h | 61 ++++++ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 + 66 files changed, 2956 insertions(+) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon40.png create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Ayon.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon40.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Ayon.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini new file mode 100644 index 0000000000..9ad7f55201 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini @@ -0,0 +1,2 @@ +[/Script/Ayon.AyonSettings] +FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin index b2cbe3cff3..37bb170eb4 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin @@ -18,6 +18,11 @@ "Name": "OpenPype", "Type": "Editor", "LoadingPhase": "Default" + }, + { + "Name": "Ayon", + "Type": "Editor", + "LoadingPhase": "Default" } ] } \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png new file mode 100644 index 0000000000000000000000000000000000000000..799d849aa3163ecb16be39c641a6ac30324906b9 GIT binary patch literal 2358 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 D;gw7~ literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png new file mode 100644 index 0000000000000000000000000000000000000000..990d5917e232a0644820428fb2790943de5ffaa4 GIT binary patch literal 16705 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"LevelEditor"); + + TSharedPtr MenuExtender = MakeShareable(new FExtender()); + TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); + + MenuExtender->AddMenuExtension( + "LevelEditor", + EExtensionHook::After, + NULL, + FMenuExtensionDelegate::CreateRaw(this, &FAyonModule::AddMenuEntry) + ); + ToolbarExtender->AddToolBarExtension( + "Settings", + EExtensionHook::After, + NULL, + FToolBarExtensionDelegate::CreateRaw(this, &FAyonModule::AddToobarEntry)); + + + LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); + LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); + + RegisterSettings(); + } +} + +void FAyonModule::ShutdownModule() +{ + FAyonStyle::Shutdown(); +} + + +void FAyonModule::AddMenuEntry(FMenuBuilder& MenuBuilder) +{ + // Create Section + MenuBuilder.BeginSection("Ayon", TAttribute(FText::FromString("Ayon"))); + { + // Create a Submenu inside of the Section + MenuBuilder.AddMenuEntry( + FText::FromString("Tools..."), + FText::FromString("Pipeline tools"), + FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup)) + ); + + MenuBuilder.AddMenuEntry( + FText::FromString("Tools dialog..."), + FText::FromString("Pipeline tools dialog"), + FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), + FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog)) + ); + } + MenuBuilder.EndSection(); +} + +void FAyonModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) +{ + ToolbarBuilder.BeginSection(TEXT("Ayon")); + { + ToolbarBuilder.AddToolBarButton( + FUIAction( + FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), + NULL, + FIsActionChecked() + + ), + NAME_None, + LOCTEXT("Ayon_label", "Ayon"), + LOCTEXT("Ayon_tooltip", "Ayon Tools"), + FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo") + ); + } + ToolbarBuilder.EndSection(); +} + +void FAyonModule::RegisterSettings() +{ + ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); + + // Create the new category + // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! + ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); + + UAyonSettings* Settings = GetMutableDefault(); + + // Register the settings + ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", + LOCTEXT("RuntimeGeneralSettingsName", + "General"), + LOCTEXT("RuntimeGeneralSettingsDescription", + "Base configuration for Open Pype Module"), + Settings + ); + + // Register the save handler to your settings, you might want to use it to + // validate those or just act to settings changes. + if (SettingsSection.IsValid()) + { + SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); + } +} + +bool FAyonModule::HandleSettingsSaved() +{ + UAyonSettings* Settings = GetMutableDefault(); + bool ResaveSettings = false; + + // You can put any validation code in here and resave the settings in case an invalid + // value has been entered + + if (ResaveSettings) + { + Settings->SaveConfig(); + } + + return true; +} + + +void FAyonModule::MenuPopup() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Popup(); +} + +void FAyonModule::MenuDialog() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Dialog(); +} + +IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp new file mode 100644 index 0000000000..316c4015af --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp @@ -0,0 +1,115 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AyonAssetContainer.h" +#include "AssetRegistryModule.h" +#include "Misc/PackageName.h" +#include "Engine.h" +#include "Containers/UnrealString.h" + +UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) +: UAssetUserData(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAyonAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); +} + +void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AyonAssetContainer") + { + assets.Add(assetPath); + assetsData.Add(AssetData); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + FString path = UAyonAssetContainer::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AyonAssetContainer") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + assetsData.Remove(AssetData); + } + } +} + +void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AyonAssetContainer") + { + + assets.Remove(str); + assets.Add(assetPath); + assetsData.Remove(AssetData); + // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); + } + } +} + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp new file mode 100644 index 0000000000..086fc1036e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AyonAssetContainerFactory.h" +#include "AyonAssetContainer.h" + +UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp new file mode 100644 index 0000000000..bff99caee3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp @@ -0,0 +1,53 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonLib.h" + +#include "AssetViewUtils.h" +#include "Misc/Paths.h" +#include "Misc/ConfigCacheIni.h" +#include "UObject/UnrealType.h" + +/** + * Sets color on folder icon on given path + * @param InPath - path to folder + * @param InFolderColor - color of the folder + * @warning This color will appear only after Editor restart. Is there a better way? + */ + +bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) +{ + if (AssetViewUtils::DoesFolderExist(FolderPath)) + { + const TSharedPtr LinearColor = MakeShared(FolderColor); + + AssetViewUtils::SaveColor(FolderPath, LinearColor, true); + UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), + *FolderPath) + return true; + } + + UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), + *FolderColor.ToString(), *FolderPath) + return false; +} + +/** + * Returns all poperties on given object + * @param cls - class + * @return TArray of properties + */ +TArray UAyonLib::GetAllProperties(UClass* cls) +{ + TArray Ret; + if (cls != nullptr) + { + for (TFieldIterator It(cls); It; ++It) + { + FProperty* Property = *It; + if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) + { + Ret.Add(Property->GetName()); + } + } + } + return Ret; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..424addd7bf --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,201 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistryModule.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Widgets/Notifications/SNotificationList.h" + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif + +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..c54e789dca --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp new file mode 100644 index 0000000000..0ed4b2f704 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp @@ -0,0 +1,14 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPythonBridge.h" + +UAyonPythonBridge* UAyonPythonBridge::Get() +{ + TArray AyonPythonBridgeClasses; + GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); + int32 NumClasses = AyonPythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp new file mode 100644 index 0000000000..d91dc94db1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonSettings.h" + +#include "Interfaces/IPluginManager.h" + +/** + * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config + */ +UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) +{ + + const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; + + // This has to be probably in the future set using the UE Reflection system + FColor Color; + GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); + + FolderColor = Color; +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp new file mode 100644 index 0000000000..dc8f0f1f40 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp @@ -0,0 +1,70 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyle.h" +#include "Styling/SlateStyleRegistry.h" + + +TUniquePtr< FSlateStyleSet > FAyonStyle::AyonStyleInstance = nullptr; + +void FAyonStyle::Initialize() +{ + if (!AyonStyleInstance.IsValid()) + { + AyonStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); + } +} + +void FAyonStyle::Shutdown() +{ + if (AyonStyleInstance.IsValid()) + { + FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); + AyonStyleInstance.Reset(); + } +} + +FName FAyonStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("AyonStyle")); + return StyleSetName; +} + +FName FAyonStyle::GetContextName() +{ + static FName ContextName(TEXT("Ayon")); + return ContextName; +} + +#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) + +const FVector2D Icon40x40(40.0f, 40.0f); + +TUniquePtr< FSlateStyleSet > FAyonStyle::Create() +{ + TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); + Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/OpenPype/Resources")); + + return Style; +} + +void FAyonStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) +{ + FSlateStyleSet* Style = AyonStyleInstance.Get(); + + FString Name(GetContextName().ToString()); + Name = Name + "." + StyleName; + Style->Set(*Name, new FSlateImageBrush(Style->RootToContentDir(ResourcePath, TEXT(".png")), Icon40x40)); + + + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); +} + +#undef IMAGE_BRUSH + +const ISlateStyle& FAyonStyle::Get() +{ + check(AyonStyleInstance); + return *AyonStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp new file mode 100644 index 0000000000..49376e8648 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp @@ -0,0 +1,41 @@ +// Copyright 2023, Ayon, All rights reserved. + + +#include "Commandlets/AyonActionResult.h" +#include "Logging/Ayon_Log.h" + +EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() +{ + return Status; +} + +FText& FAyon_ActionResult::GetReason() +{ + return Reason; +} + +FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) +{ + +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FAyon_ActionResult::IsProblem() const +{ + return Status != EAyon_ActionResult::Ok; +} + +void FAyon_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..0328d3b7e6 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp @@ -0,0 +1,141 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" + +#include "Editor.h" +#include "GameProjectUtils.h" +#include "AyonConstants.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" + +int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_AYON_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_AYON_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_AYON_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for Ayon + return 0; +} + + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") +{ +} + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FAyonGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); + return FAyon_ActionResult(); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); +} + +void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor AyonPluginDescriptor; + AyonPluginDescriptor.bEnabled = true; + AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; + ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); +} + +FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FAyonGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h new file mode 100644 index 0000000000..9535ff4b13 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h @@ -0,0 +1,22 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "Engine.h" + + +class FAyonModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + void RegisterSettings(); + bool HandleSettingsSaved(); + + void AddMenuEntry(FMenuBuilder& MenuBuilder); + void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); + void MenuPopup(); + void MenuDialog(); +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h new file mode 100644 index 0000000000..cc17b3960a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h @@ -0,0 +1,39 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Engine/AssetUserData.h" +#include "AssetData.h" +#include "AyonAssetContainer.generated.h" + +/** + * + */ +UCLASS(Blueprintable) +class AYON_API UAyonAssetContainer : public UAssetUserData +{ + GENERATED_BODY() + +public: + + UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAyonAssetContainer(); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") + TArray assets; + + // There seems to be no reflection option to expose array of FAssetData + /* + UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) + TArray assetsData; + */ +private: + TArray assetsData; + void OnAssetAdded(const FAssetData& AssetData); + void OnAssetRemoved(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& str); +}; + + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h new file mode 100644 index 0000000000..7c35897911 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h @@ -0,0 +1,21 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AyonAssetContainerFactory.generated.h" + +/** + * + */ +UCLASS() +class AYON_API UAyonAssetContainerFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h new file mode 100644 index 0000000000..6a02b5682f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h @@ -0,0 +1,15 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "CoreMinimal.h" + +namespace AyonConstants +{ + const FString Ayon_PluginName = "Ayon"; + const FString PythonScript_PluginName = "PythonScriptPlugin"; + const FString SequencerScripting_PluginName = "SequencerScripting"; + const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; + const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; +} + + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h new file mode 100644 index 0000000000..ed657a735c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "Engine.h" +#include "AyonLib.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonLib : public UBlueprintFunctionLibrary +{ + + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd); + + UFUNCTION(BlueprintCallable, Category = Python) + static TArray GetAllProperties(UClass* cls); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..4eace68827 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,102 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "Engine.h" +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h new file mode 100644 index 0000000000..443d618c9a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AyonPublishInstanceFactory.generated.h" + +/** + * + */ +UCLASS() +class AYON_API UAyonPublishInstanceFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h new file mode 100644 index 0000000000..831ac022a5 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "Engine.h" +#include "AyonPythonBridge.generated.h" + +UCLASS(Blueprintable) +class UAyonPythonBridge : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static UAyonPythonBridge* Get(); + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Popup() const; + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Dialog() const; + +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h new file mode 100644 index 0000000000..f600cfbf9a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h @@ -0,0 +1,31 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AyonSettings.generated.h" + +#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") + +UCLASS(Config=AyonSettings, DefaultConfig) +class AYON_API UAyonSettings : public UObject +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FColor GetFolderFColor() const + { + return FolderColor; + } + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FLinearColor GetFolderFLinearColor() const + { + return FLinearColor(FolderColor); + } + +protected: + + UPROPERTY(config, EditAnywhere, Category = Folders) + FColor FolderColor = FColor(25,45,223); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h new file mode 100644 index 0000000000..188e4a510c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "CoreMinimal.h" + +class FSlateStyleSet; +class ISlateStyle; + + +class FAyonStyle +{ +public: + static void Initialize(); + static void Shutdown(); + static const ISlateStyle& Get(); + static FName GetStyleSetName(); + static FName GetContextName(); + + static void SetIcon(const FString& StyleName, const FString& ResourcePath); + +private: + static TUniquePtr< FSlateStyleSet > Create(); + static TUniquePtr< FSlateStyleSet > AyonStyleInstance; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h new file mode 100644 index 0000000000..4694055164 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h @@ -0,0 +1,83 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AyonActionResult.generated.h" + +/** + * @brief This macro returns error code when is problem or does nothing when there is no problem. + * @param ActionResult FAyon_ActionResult structure + */ +#define EVALUATE_AYON_ACTION_RESULT(ActionResult) \ + if(ActionResult.IsProblem()) \ + return ActionResult.GetStatus(); + +/** +* @brief This enum values are humanly readable mapping of error codes. +* Here should be all error codes to be possible find what went wrong. +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +*/ +UENUM() +namespace EAyon_ActionResult +{ + enum Type + { + Ok, + ProjectNotCreated, + ProjectNotLoaded, + ProjectNotSaved, + //....Here insert another values + + //Do not remove! + //Usable for looping through enum values + __Last UMETA(Hidden) + }; +} + + +/** + * @brief This struct holds action result enum and optionally reason of fail + */ +USTRUCT() +struct FAyon_ActionResult +{ + GENERATED_BODY() + +public: + /** @brief Default constructor usable when there is no problem */ + FAyon_ActionResult(); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + * @param InReason Reason of potential fail + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); + +private: + /** @brief Action status */ + EAyon_ActionResult::Type Status; + + /** @brief Optional reason of fail */ + FText Reason; + +public: + /** + * @brief Checks if there is problematic state + * @return true when status is not equal to EAyon_ActionResult::Ok + */ + bool IsProblem() const; + EAyon_ActionResult::Type& GetStatus(); + FText& GetReason(); + +private: + void TryLog() const; +}; + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h new file mode 100644 index 0000000000..cabd524b8c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h @@ -0,0 +1,60 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "GameProjectUtils.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" +#include "Commandlets/Commandlet.h" +#include "AyonGenerateProjectCommandlet.generated.h" + +struct FProjectDescriptor; +struct FProjectInformation; + +/** +* @brief Structure which parses command line parameters and generates FProjectInformation +*/ +USTRUCT() +struct FAyonGenerateProjectParams +{ + GENERATED_BODY() + +private: + FString CommandLineParams; + TArray Tokens; + TArray Switches; + +public: + FAyonGenerateProjectParams(); + FAyonGenerateProjectParams(const FString& CommandLineParams); + + FProjectInformation GenerateUEProjectInformation() const; + +private: + FString TryGetToken(const int32 Index) const; + FString GetProjectFileName() const; + + bool IsSwitchPresent(const FString& Switch) const; +}; + +UCLASS() +class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet +{ + GENERATED_BODY() + +private: + FProjectInformation ProjectInformation; + FProjectDescriptor ProjectDescriptor; + +public: + UAyonGenerateProjectCommandlet(); + + virtual int32 Main(const FString& CommandLineParams) override; + +private: + FAyonGenerateProjectParams ParseParameters(const FString& Params) const; + FAyon_ActionResult TryCreateProject() const; + FAyon_ActionResult TryLoadProjectDescriptor(); + void AttachPluginsToProjectDescriptor(); + FAyon_ActionResult TrySave(); +}; + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h new file mode 100644 index 0000000000..21571afd02 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h @@ -0,0 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp new file mode 100644 index 0000000000..c766f87a8e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp @@ -0,0 +1,115 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AssetContainer.h" +#include "AssetRegistryModule.h" +#include "Misc/PackageName.h" +#include "Engine.h" +#include "Containers/UnrealString.h" + +UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) +: UAssetUserData(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed); +} + +void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + assets.Add(assetPath); + assetsData.Add(AssetData); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + FString path = UAssetContainer::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + assetsData.Remove(AssetData); + } + } +} + +void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.AssetClass.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + + assets.Remove(str); + assets.Add(assetPath); + assetsData.Remove(AssetData); + // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); + } + } +} + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp new file mode 100644 index 0000000000..b943150bdd --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AssetContainerFactory.h" +#include "AssetContainer.h" + +UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h new file mode 100644 index 0000000000..3b0230391c --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h @@ -0,0 +1,39 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Engine/AssetUserData.h" +#include "AssetData.h" +#include "AssetContainer.generated.h" + +/** + * + */ +UCLASS(Blueprintable) +class OPENPYPE_API UAssetContainer : public UAssetUserData +{ + GENERATED_BODY() + +public: + + UAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAssetContainer(); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") + TArray assets; + + // There seems to be no reflection option to expose array of FAssetData + /* + UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) + TArray assetsData; + */ +private: + TArray assetsData; + void OnAssetAdded(const FAssetData& AssetData); + void OnAssetRemoved(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& str); +}; + + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h new file mode 100644 index 0000000000..331ce6bb50 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h @@ -0,0 +1,21 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AssetContainerFactory.generated.h" + +/** + * + */ +UCLASS() +class OPENPYPE_API UAssetContainerFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini new file mode 100644 index 0000000000..9ad7f55201 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini @@ -0,0 +1,2 @@ +[/Script/Ayon.AyonSettings] +FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin index ff08edc13e..0fe7b249a8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin @@ -19,6 +19,11 @@ "Name": "OpenPype", "Type": "Editor", "LoadingPhase": "Default" + }, + { + "Name": "Ayon", + "Type": "Editor", + "LoadingPhase": "Default" } ] } \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png new file mode 100644 index 0000000000000000000000000000000000000000..799d849aa3163ecb16be39c641a6ac30324906b9 GIT binary patch literal 2358 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 D;gw7~ literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png new file mode 100644 index 0000000000000000000000000000000000000000..990d5917e232a0644820428fb2790943de5ffaa4 GIT binary patch literal 16705 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 + FAyonCommands::Get().AyonTools, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), + FCanExecuteAction()); + PluginCommands->MapAction( + FAyonCommands::Get().AyonToolsDialog, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), + FCanExecuteAction()); + + UToolMenus::RegisterStartupCallback( + FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); + + RegisterSettings(); +} + +void FAyonModule::ShutdownModule() +{ + UToolMenus::UnRegisterStartupCallback(this); + + UToolMenus::UnregisterOwner(this); + + FAyonStyle::Shutdown(); + + FAyonCommands::Unregister(); +} + + +void FAyonModule::RegisterSettings() +{ + ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); + + // Create the new category + // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! + ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); + + UAyonSettings* Settings = GetMutableDefault(); + + // Register the settings + ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", + LOCTEXT("RuntimeGeneralSettingsName", + "General"), + LOCTEXT("RuntimeGeneralSettingsDescription", + "Base configuration for Open Pype Module"), + Settings + ); + + // Register the save handler to your settings, you might want to use it to + // validate those or just act to settings changes. + if (SettingsSection.IsValid()) + { + SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); + } +} + +bool FAyonModule::HandleSettingsSaved() +{ + UAyonSettings* Settings = GetMutableDefault(); + bool ResaveSettings = false; + + // You can put any validation code in here and resave the settings in case an invalid + // value has been entered + + if (ResaveSettings) + { + Settings->SaveConfig(); + } + + return true; +} + +void FAyonModule::RegisterMenus() +{ + // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner + FToolMenuOwnerScoped OwnerScoped(this); + + { + UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); + { + // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); + FToolMenuSection& Section = Menu->AddSection( + "Ayon", + TAttribute(FText::FromString("Ayon")), + FToolMenuInsert("Programming", EToolMenuInsertType::Before) + ); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); + } + UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); + { + FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); + { + FToolMenuEntry& Entry = Section.AddEntry( + FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); + Entry.SetCommandList(PluginCommands); + } + } + } +} + + +void FAyonModule::MenuPopup() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Popup(); +} + +void FAyonModule::MenuDialog() +{ + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); + bridge->RunInPython_Dialog(); +} + +IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp new file mode 100644 index 0000000000..869aa45256 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp @@ -0,0 +1,113 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "AyonAssetContainer.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Misc/PackageName.h" +#include "Containers/UnrealString.h" + +UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) +: UAssetUserData(ObjectInitializer) +{ + FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); + FString path = UAyonAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); + FARFilter Filter; + Filter.PackagePaths.Add(FName(*path)); + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); +} + +void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.ObjectPath.ToString(); + UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + assets.Add(assetPath); + assetsData.Add(AssetData); + UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); + } + } +} + +void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.ObjectPath.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + + // take interest only in paths starting with path of current container + FString path = UAyonAssetContainer::GetPathName(); + FString lpp = FPackageName::GetLongPackagePath(*path); + + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); + assets.Remove(assetPath); + assetsData.Remove(AssetData); + } + } +} + +void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +{ + TArray split; + + // get directory of current container + FString selfFullPath = UAyonAssetContainer::GetPathName(); + FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); + + // get asset path and class + FString assetPath = AssetData.GetFullName(); + FString assetFName = AssetData.ObjectPath.ToString(); + + // split path + assetPath.ParseIntoArray(split, TEXT(" "), true); + + FString assetDir = FPackageName::GetLongPackagePath(*split[1]); + if (assetDir.StartsWith(*selfDir)) + { + // exclude self + if (assetFName != "AssetContainer") + { + + assets.Remove(str); + assets.Add(assetPath); + assetsData.Remove(AssetData); + // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); + } + } +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp new file mode 100644 index 0000000000..086fc1036e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AyonAssetContainerFactory.h" +#include "AyonAssetContainer.h" + +UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp new file mode 100644 index 0000000000..566ee1dcd1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp @@ -0,0 +1,13 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonCommands.h" + +#define LOCTEXT_NAMESPACE "FAyonModule" + +void FAyonCommands::RegisterCommands() +{ + UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp new file mode 100644 index 0000000000..7cfa0c9c30 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp @@ -0,0 +1,51 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonLib.h" + +#include "AssetViewUtils.h" +#include "UObject/UnrealType.h" + +/** + * Sets color on folder icon on given path + * @param InPath - path to folder + * @param InFolderColor - color of the folder + * @warning This color will appear only after Editor restart. Is there a better way? + */ + +bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) +{ + if (AssetViewUtils::DoesFolderExist(FolderPath)) + { + const TSharedPtr LinearColor = MakeShared(FolderColor); + + AssetViewUtils::SaveColor(FolderPath, LinearColor, true); + UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), + *FolderPath) + return true; + } + + UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), + *FolderColor.ToString(), *FolderPath) + return false; +} + +/** + * Returns all poperties on given object + * @param cls - class + * @return TArray of properties + */ +TArray UAyonLib::GetAllProperties(UClass* cls) +{ + TArray Ret; + if (cls != nullptr) + { + for (TFieldIterator It(cls); It; ++It) + { + FProperty* Property = *It; + if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) + { + Ret.Add(Property->GetName()); + } + } + } + return Ret; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..f8d95ac048 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,201 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "Framework/Notifications/NotificationManager.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Widgets/Notifications/SNotificationList.h" + + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..c54e789dca --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp new file mode 100644 index 0000000000..0ed4b2f704 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp @@ -0,0 +1,14 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPythonBridge.h" + +UAyonPythonBridge* UAyonPythonBridge::Get() +{ + TArray AyonPythonBridgeClasses; + GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); + int32 NumClasses = AyonPythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp new file mode 100644 index 0000000000..da388fbc8f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonSettings.h" + +#include "Interfaces/IPluginManager.h" +#include "UObject/UObjectGlobals.h" + +/** + * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config + */ +UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) +{ + + const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; + + // This has to be probably in the future set using the UE Reflection system + FColor Color; + GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); + + FolderColor = Color; +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp new file mode 100644 index 0000000000..91a0c6996b --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp @@ -0,0 +1,62 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyleRegistry.h" +#include "Slate/SlateGameResources.h" +#include "Interfaces/IPluginManager.h" +#include "Styling/SlateStyleMacros.h" + +#define RootToContentDir Style->RootToContentDir + +TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; + +void FAyonStyle::Initialize() +{ + if (!AyonStyleInstance.IsValid()) + { + AyonStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); + } +} + +void FAyonStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); + ensure(AyonStyleInstance.IsUnique()); + AyonStyleInstance.Reset(); +} + +FName FAyonStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("AyonStyle")); + return StyleSetName; +} + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); +const FVector2D Icon40x40(40.0f, 40.0f); + +TSharedRef< FSlateStyleSet > FAyonStyle::Create() +{ + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); + + Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + + return Style; +} + +void FAyonStyle::ReloadTextures() +{ + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } +} + +const ISlateStyle& FAyonStyle::Get() +{ + return *AyonStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp new file mode 100644 index 0000000000..2a137e3ed7 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp @@ -0,0 +1,40 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "Commandlets/AyonActionResult.h" +#include "Logging/Ayon_Log.h" + +EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() +{ + return Status; +} + +FText& FAyon_ActionResult::GetReason() +{ + return Reason; +} + +FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) +{ + +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FAyon_ActionResult::IsProblem() const +{ + return Status != EAyon_ActionResult::Ok; +} + +void FAyon_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..ed876c8128 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp @@ -0,0 +1,140 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" + +#include "GameProjectUtils.h" +#include "AyonConstants.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" + +int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_Ayon_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for Ayon + return 0; +} + + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") +{ +} + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FAyonGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); + return FAyon_ActionResult(); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); +} + +void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor AyonPluginDescriptor; + AyonPluginDescriptor.bEnabled = true; + AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; + ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); +} + +FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FAyonGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h new file mode 100644 index 0000000000..bb25430411 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h @@ -0,0 +1,24 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" + + +class FAyonModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + void RegisterMenus(); + void RegisterSettings(); + bool HandleSettingsSaved(); + + void MenuPopup(); + void MenuDialog(); + +private: + TSharedPtr PluginCommands; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h new file mode 100644 index 0000000000..d40642b149 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h @@ -0,0 +1,34 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "Engine/AssetUserData.h" +#include "AssetRegistry/AssetData.h" +#include "AyonAssetContainer.generated.h" + +UCLASS(Blueprintable) +class AYON_API UAyonAssetContainer : public UAssetUserData +{ + GENERATED_BODY() + +public: + + UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAyonAssetContainer(); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") + TArray assets; + + // There seems to be no reflection option to expose array of FAssetData + /* + UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) + TArray assetsData; + */ +private: + TArray assetsData; + void OnAssetAdded(const FAssetData& AssetData); + void OnAssetRemoved(const FAssetData& AssetData); + void OnAssetRenamed(const FAssetData& AssetData, const FString& str); +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h new file mode 100644 index 0000000000..da424cde2e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h @@ -0,0 +1,18 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AyonAssetContainerFactory.generated.h" + +UCLASS() +class AYON_API UAyonAssetContainerFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h new file mode 100644 index 0000000000..9c40dc8241 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h @@ -0,0 +1,24 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" +#include "AyonStyle.h" + +class FAyonCommands : public TCommands +{ +public: + + FAyonCommands() + : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) + { + } + + // TCommands<> interface + virtual void RegisterCommands() override; + +public: + TSharedPtr< FUICommandInfo > AyonTools; + TSharedPtr< FUICommandInfo > AyonToolsDialog; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h new file mode 100644 index 0000000000..5fe7c14360 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h @@ -0,0 +1,13 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +namespace AyonConstants +{ + const FString Ayon_PluginName = "Ayon"; + const FString PythonScript_PluginName = "PythonScriptPlugin"; + const FString SequencerScripting_PluginName = "SequencerScripting"; + const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; + const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; +} + + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h new file mode 100644 index 0000000000..da83b448fb --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h @@ -0,0 +1,19 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonLib.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonLib : public UBlueprintFunctionLibrary +{ + + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd); + + UFUNCTION(BlueprintCallable, Category = Python) + static TArray GetAllProperties(UClass* cls); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..1c51f98b4a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,102 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h new file mode 100644 index 0000000000..443d618c9a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +#include "CoreMinimal.h" +#include "Factories/Factory.h" +#include "AyonPublishInstanceFactory.generated.h" + +/** + * + */ +UCLASS() +class AYON_API UAyonPublishInstanceFactory : public UFactory +{ + GENERATED_BODY() + +public: + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; + virtual bool ShouldShowInNewMenu() const override; +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h new file mode 100644 index 0000000000..3c429fd7d3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h @@ -0,0 +1,20 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "AyonPythonBridge.generated.h" + +UCLASS(Blueprintable) +class UAyonPythonBridge : public UObject +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = Python) + static UAyonPythonBridge* Get(); + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Popup() const; + + UFUNCTION(BlueprintImplementableEvent, Category = Python) + void RunInPython_Dialog() const; + +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h new file mode 100644 index 0000000000..42a724b95a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h @@ -0,0 +1,32 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "AyonSettings.generated.h" + +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") + +UCLASS(Config=AyonSettings, DefaultConfig) +class AYON_API UAyonSettings : public UObject +{ + GENERATED_UCLASS_BODY() + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FColor GetFolderFColor() const + { + return FolderColor; + } + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) + FLinearColor GetFolderFLinearColor() const + { + return FLinearColor(FolderColor); + } + +protected: + + UPROPERTY(config, EditAnywhere, Category = Folders) + FColor FolderColor = FColor(25,45,223); +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h new file mode 100644 index 0000000000..58f6af656e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h @@ -0,0 +1,19 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once +#include "CoreMinimal.h" +#include "Styling/SlateStyle.h" + +class FAyonStyle +{ +public: + static void Initialize(); + static void Shutdown(); + static void ReloadTextures(); + static const ISlateStyle& Get(); + static FName GetStyleSetName(); + + +private: + static TSharedRef< class FSlateStyleSet > Create(); + static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h new file mode 100644 index 0000000000..bb995ec452 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h @@ -0,0 +1,83 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "AyonActionResult.generated.h" + +/** + * @brief This macro returns error code when is problem or does nothing when there is no problem. + * @param ActionResult FAyon_ActionResult structure + */ +#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ + if(ActionResult.IsProblem()) \ + return ActionResult.GetStatus(); + +/** +* @brief This enum values are humanly readable mapping of error codes. +* Here should be all error codes to be possible find what went wrong. +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +*/ +UENUM() +namespace EAyon_ActionResult +{ + enum Type + { + Ok, + ProjectNotCreated, + ProjectNotLoaded, + ProjectNotSaved, + //....Here insert another values + + //Do not remove! + //Usable for looping through enum values + __Last UMETA(Hidden) + }; +} + + +/** + * @brief This struct holds action result enum and optionally reason of fail + */ +USTRUCT() +struct FAyon_ActionResult +{ + GENERATED_BODY() + +public: + /** @brief Default constructor usable when there is no problem */ + FAyon_ActionResult(); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + * @param InReason Reason of potential fail + */ + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); + +private: + /** @brief Action status */ + EAyon_ActionResult::Type Status; + + /** @brief Optional reason of fail */ + FText Reason; + +public: + /** + * @brief Checks if there is problematic state + * @return true when status is not equal to EAyon_ActionResult::Ok + */ + bool IsProblem() const; + EAyon_ActionResult::Type& GetStatus(); + FText& GetReason(); + +private: + void TryLog() const; +}; + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h new file mode 100644 index 0000000000..da8e9af661 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h @@ -0,0 +1,61 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + + +#include "GameProjectUtils.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" +#include "Commandlets/Commandlet.h" +#include "AyonGenerateProjectCommandlet.generated.h" + +struct FProjectDescriptor; +struct FProjectInformation; + +/** +* @brief Structure which parses command line parameters and generates FProjectInformation +*/ +USTRUCT() +struct FAyonGenerateProjectParams +{ + GENERATED_BODY() + +private: + FString CommandLineParams; + TArray Tokens; + TArray Switches; + +public: + FAyonGenerateProjectParams(); + FAyonGenerateProjectParams(const FString& CommandLineParams); + + FProjectInformation GenerateUEProjectInformation() const; + +private: + FString TryGetToken(const int32 Index) const; + FString GetProjectFileName() const; + + bool IsSwitchPresent(const FString& Switch) const; +}; + +UCLASS() +class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet +{ + GENERATED_BODY() + +private: + FProjectInformation ProjectInformation; + FProjectDescriptor ProjectDescriptor; + +public: + UAyonGenerateProjectCommandlet(); + + virtual int32 Main(const FString& CommandLineParams) override; + +private: + FAyonGenerateProjectParams ParseParameters(const FString& Params) const; + FAyon_ActionResult TryCreateProject() const; + FAyon_ActionResult TryLoadProjectDescriptor(); + void AttachPluginsToProjectDescriptor(); + FAyon_ActionResult TrySave(); +}; + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h new file mode 100644 index 0000000000..25b33a63e8 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h @@ -0,0 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file From d780974b1b1e0f2e49fdeffddc8ed8d44e673f0e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Mar 2023 17:23:44 +0800 Subject: [PATCH 118/918] usd mesh format to trimesh and adjustment on update function in loaders. --- openpype/hosts/max/plugins/load/load_model_fbx.py | 2 +- openpype/hosts/max/plugins/load/load_model_obj.py | 2 +- openpype/hosts/max/plugins/load/load_model_usd.py | 2 +- openpype/hosts/max/plugins/publish/extract_model_usd.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 38b8555d28..1729874a6b 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -47,7 +47,7 @@ importFile @"{filepath}" #noPrompt using:FBXIMP path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - fbx_objects = self.get_container_children(node) + fbx_objects = node.Children for fbx_object in fbx_objects: fbx_object.source = path diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 06b411cb5c..281a986934 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -41,7 +41,7 @@ class ObjLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - objects = self.get_container_children(node) + objects = node.Children for obj in objects: obj.source = path diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index c6c414b91c..b6a41f4e68 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -41,7 +41,7 @@ class ModelUSDLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - usd_objects = self.get_container_children(node) + usd_objects = node.Children for usd_object in usd_objects: usd_object.source = path diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index f70a14ba0b..60dddc8670 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -101,7 +101,7 @@ class ExtractModelUSD(publish.Extractor, export_options.Lights = False export_options.Cameras = False export_options.Materials = False - export_options.MeshFormat = rt.name('polyMesh') + export_options.MeshFormat = rt.name('triMesh') export_options.FileFormat = rt.name('ascii') export_options.UpAxis = rt.name('y') export_options.LogLevel = rt.name('info') From fd2d210522fbeddd27f707b0683e2f7411affd8e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 11:26:48 +0100 Subject: [PATCH 119/918] Use create context environment Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../substancepainter/plugins/create/create_workfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 729cc8f718..29191a1714 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -29,9 +29,9 @@ class CreateWorkfile(AutoCreator): variant = self.default_variant project_name = self.project_name - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - host_name = legacy_io.Session["AVALON_APP"] + asset_name = self.create_context.get_current_asset_name() + task_name = self.create_context.get_current_task_name() + host_name = self.create_context.host_name # Workfile instance should always exist and must only exist once. # As such we'll first check if it already exists and is collected. From eeaa807588317b10e641e83566a07e278f3be6a7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 11:41:54 +0100 Subject: [PATCH 120/918] Remove unused import --- .../hosts/substancepainter/plugins/create/create_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 29191a1714..4e316f3b64 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -2,7 +2,6 @@ """Creator plugin for creating workfiles.""" from openpype.pipeline import CreatedInstance, AutoCreator -from openpype.pipeline import legacy_io from openpype.client import get_asset_by_name from openpype.hosts.substancepainter.api.pipeline import ( From 9020bf23d325b706485ed7374d22f6073aa71e79 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 11:44:56 +0100 Subject: [PATCH 121/918] Implement `get_context_data` and `update_context_data` --- .../hosts/substancepainter/api/pipeline.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index f4d4c5b00c..b377db1641 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -38,6 +38,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") OPENPYPE_METADATA_KEY = "OpenPype" OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key +OPENPYPE_METADATA_CONTEXT_KEY = "context" # child key class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): @@ -140,15 +141,21 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): container["objectName"] = key yield container - @staticmethod - def create_context_node(): - pass - def update_context_data(self, data, changes): - pass + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + metadata.set(OPENPYPE_METADATA_CONTEXT_KEY, data) def get_context_data(self): - pass + + if not substance_painter.project.is_open(): + return + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(OPENPYPE_METADATA_CONTEXT_KEY) or {} def _install_menu(self): from PySide2 import QtWidgets From eeb2388475d664aa95dff4b09fdef9fc6ed17549 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 14:13:21 +0100 Subject: [PATCH 122/918] Use `openpype.pipeline.create.get_subset_name` to define the subset name --- .../publish/collect_textureset_images.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 04187d4079..b368c86749 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -9,6 +9,8 @@ from openpype.hosts.substancepainter.api.lib import ( get_parsed_export_maps, strip_template ) +from openpype.pipeline.create import get_subset_name +from openpype.client import get_asset_by_name class CollectTextureSet(pyblish.api.InstancePlugin): @@ -24,6 +26,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): def process(self, instance): config = self.get_export_config(instance) + asset_doc = get_asset_by_name( + project_name=instance.context.data["projectName"], + asset_name=instance.data["asset"] + ) instance.data["exportConfig"] = config maps = get_parsed_export_maps(config) @@ -34,9 +40,11 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.log.info(f"Processing {texture_set_name}/{stack_name}") for template, outputs in template_maps.items(): self.log.info(f"Processing {template}") - self.create_image_instance(instance, template, outputs) + self.create_image_instance(instance, template, outputs, + asset_doc=asset_doc) - def create_image_instance(self, instance, template, outputs): + def create_image_instance(self, instance, template, outputs, + asset_doc): """Create a new instance per image or UDIM sequence. The new instances will be of family `image`. @@ -53,8 +61,17 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Define the suffix we want to give this particular texture # set and set up a remapped subset naming for it. suffix = f".{map_identifier}" - image_subset = instance.data["subset"][len("textureSet"):] - image_subset = "texture" + image_subset + suffix + image_subset = get_subset_name( + # TODO: The family actually isn't 'texture' currently but for now + # this is only done so the subset name starts with 'texture' + family="texture", + variant=instance.data["variant"] + suffix, + task_name=instance.data.get("task"), + asset_doc=asset_doc, + project_name=context.data["projectName"], + host_name=context.data["hostName"], + project_settings=context.data["project_settings"] + ) # Prepare representation representation = { From f8a3e24c606048883fa0942c01df1f7aff893436 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 20:04:36 +0100 Subject: [PATCH 123/918] Explain how Texture Sets are split into separate publishes per output map in documentation --- website/docs/artist_hosts_substancepainter.md | 33 ++++++++++++++++-- ...ter_pbrmetallicroughness_export_preset.png | Bin 0 -> 45842 bytes ...painter_pbrmetallicroughness_published.png | Bin 0 -> 7497 bytes 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 website/docs/assets/substancepainter_pbrmetallicroughness_export_preset.png create mode 100644 website/docs/assets/substancepainter_pbrmetallicroughness_published.png diff --git a/website/docs/artist_hosts_substancepainter.md b/website/docs/artist_hosts_substancepainter.md index 9ed83421af..86bcbba82e 100644 --- a/website/docs/artist_hosts_substancepainter.md +++ b/website/docs/artist_hosts_substancepainter.md @@ -51,8 +51,9 @@ publish instance. To create a **TextureSet instance** we will use OpenPype's publisher tool. Go to **OpenPype → Publish... → TextureSet** -The texture set instance will define what Substance Painter export template `.spexp` to -use and thus defines what texture maps will be exported from your workfile. +The texture set instance will define what Substance Painter export template (`.spexp`) to +use and thus defines what texture maps will be exported from your workfile. This +can be set with the **Output Template** attribute on the instance. :::info The TextureSet instance gets saved with your Substance Painter project. As such, @@ -61,8 +62,34 @@ just click **OpenPype → Publish...** and start publishing directly with the same settings. ::: +#### Publish per output map of the Substance Painter preset -### Known issues +The Texture Set instance generates a publish per output map that is defined in +the Substance Painter's export preset. For example a publish from a default +PBR Metallic Roughness texture set results in six separate published subsets +(if all the channels exist in your file). + +![Substance Painter PBR Metallic Roughness Export Preset](assets/substancepainter_pbrmetallicroughness_export_preset.png) + +When publishing for example a texture set with variant **Main** six instances will +be published with the variants: +- Main.**BaseColor** +- Main.**Emissive** +- Main.**Height** +- Main.**Metallic** +- Main.**Normal** +- Main.**Roughness** + +The bold output map name for the publish is based on the string that is pulled +from the what is considered to be the static part of the filename templates in +the export preset. The tokens like `$mesh` and `(_$colorSpace)` are ignored. +So `$mesh_$textureSet_BaseColor(_$colorSpace)(.$udim)` becomes `BaseColor`. + +An example output for PBR Metallic Roughness would be: + +![Substance Painter PBR Metallic Roughness Publish Example in Loader](assets/substancepainter_pbrmetallicroughness_published.png) + +## Known issues #### Can't see the OpenPype menu? diff --git a/website/docs/assets/substancepainter_pbrmetallicroughness_export_preset.png b/website/docs/assets/substancepainter_pbrmetallicroughness_export_preset.png new file mode 100644 index 0000000000000000000000000000000000000000..35a4545f83563332e983ada1698433955498a969 GIT binary patch literal 45842 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/substancepainter_pbrmetallicroughness_published.png b/website/docs/assets/substancepainter_pbrmetallicroughness_published.png new file mode 100644 index 0000000000000000000000000000000000000000..15b0e5b87687a8675c481a8265e5f0c5639de8fe GIT binary patch literal 7497 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 Date: Mon, 20 Mar 2023 20:07:23 +0100 Subject: [PATCH 124/918] Simplify setting review tag and stagingDir for thumbnail on representation --- .../plugins/publish/collect_textureset_images.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index b368c86749..f7187b638f 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -87,6 +87,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # strings. See CollectTextures plug-in and Integrators. representation["udim"] = [output["udim"] for output in outputs] + # Set up the representation for thumbnail generation + # TODO: Simplify this once thumbnail extraction is refactored + staging_dir = os.path.dirname(first_filepath) + representation["tags"] = ["review"] + representation["stagingDir"] = staging_dir + # Clone the instance image_instance = context.create_instance(instance.name) image_instance[:] = instance[:] @@ -108,12 +114,6 @@ class CollectTextureSet(pyblish.api.InstancePlugin): self.log.debug(f"{image_subset} colorspace: {colorspace}") image_instance.data["colorspace"] = colorspace - # Set up the representation for thumbnail generation - # TODO: Simplify this once thumbnail extraction is refactored - staging_dir = os.path.dirname(first_filepath) - image_instance.data["representations"][0]["tags"] = ["review"] - image_instance.data["representations"][0]["stagingDir"] = staging_dir - # Store the instance in the original instance as a member instance.append(image_instance) From 0b3cb6942dc03e231743fd1713f3e919fdc785f7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Mar 2023 20:27:34 +0100 Subject: [PATCH 125/918] Add todo about a potentially critical issue to still be solved. --- .../plugins/publish/collect_textureset_images.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index f7187b638f..14168138b6 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -60,6 +60,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Define the suffix we want to give this particular texture # set and set up a remapped subset naming for it. + # TODO (Critical) Support needs to be added to have multiple materials + # with each their own maps. So we might need to include the + # material or alike in the variant suffix too? suffix = f".{map_identifier}" image_subset = get_subset_name( # TODO: The family actually isn't 'texture' currently but for now From 700927c1645fc9183a739abfd4529f4a94e027d2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Mar 2023 15:21:35 +0000 Subject: [PATCH 126/918] Restored lost changes --- .../unreal/plugins/create/create_render.py | 212 ++++++++++++++---- 1 file changed, 174 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 5834d2e7a7..b2a246d3a8 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,14 +1,22 @@ # -*- coding: utf-8 -*- +from pathlib import Path + import unreal -from openpype.pipeline import CreatorError from openpype.hosts.unreal.api.pipeline import ( - get_subsequences + UNREAL_VERSION, + create_folder, + get_subsequences, ) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) -from openpype.lib import UILabelDef +from openpype.lib import ( + UILabelDef, + UISeparatorDef, + BoolDef, + NumberDef +) class CreateRender(UnrealAssetCreator): @@ -19,7 +27,90 @@ class CreateRender(UnrealAssetCreator): family = "render" icon = "eye" - def create(self, subset_name, instance_data, pre_create_data): + def create_instance( + self, instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data + ): + instance_data["members"] = [selected_asset_path] + instance_data["sequence"] = selected_asset_path + instance_data["master_sequence"] = master_seq + instance_data["master_level"] = master_lvl + instance_data["output"] = seq_data.get('output') + instance_data["frameStart"] = seq_data.get('frame_range')[0] + instance_data["frameEnd"] = seq_data.get('frame_range')[1] + + super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) + + def create_with_new_sequence( + self, subset_name, instance_data, pre_create_data + ): + # If the option to create a new level sequence is selected, + # create a new level sequence and a master level. + + root = f"/Game/OpenPype/Sequences" + + # Create a new folder for the sequence in root + sequence_dir_name = create_folder(root, subset_name) + sequence_dir = f"{root}/{sequence_dir_name}" + + unreal.log_warning(f"sequence_dir: {sequence_dir}") + + # Create the level sequence + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + seq = asset_tools.create_asset( + asset_name=subset_name, + package_path=sequence_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew()) + + seq.set_playback_start(pre_create_data.get("start_frame")) + seq.set_playback_end(pre_create_data.get("end_frame")) + + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) + + # Create the master level + if UNREAL_VERSION.major >= 5: + curr_level = unreal.LevelEditorSubsystem().get_current_level() + else: + world = unreal.EditorLevelLibrary.get_editor_world() + levels = unreal.EditorLevelUtils.get_levels(world) + curr_level = levels[0] if len(levels) else None + if not curr_level: + raise RuntimeError("No level loaded.") + curr_level_path = curr_level.get_outer().get_path_name() + + # If the level path does not start with "/Game/", the current + # level is a temporary, unsaved level. + if curr_level_path.startswith("/Game/"): + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().save_current_level() + else: + unreal.EditorLevelLibrary.save_current_level() + + ml_path = f"{sequence_dir}/{subset_name}_MasterLevel" + + if UNREAL_VERSION.major >= 5: + unreal.LevelEditorSubsystem().new_level(ml_path) + else: + unreal.EditorLevelLibrary.new_level(ml_path) + + seq_data = { + "sequence": seq, + "output": f"{seq.get_name()}", + "frame_range": ( + seq.get_playback_start(), + seq.get_playback_end())} + + self.create_instance( + instance_data, subset_name, pre_create_data, + seq.get_path_name(), seq.get_path_name(), ml_path, seq_data) + + def create_from_existing_sequence( + self, subset_name, instance_data, pre_create_data + ): ar = unreal.AssetRegistryHelpers.get_asset_registry() sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() @@ -27,8 +118,8 @@ class CreateRender(UnrealAssetCreator): a.get_path_name() for a in sel_objects if a.get_class().get_name() == "LevelSequence"] - if not selection: - raise CreatorError("Please select at least one Level Sequence.") + if len(selection) == 0: + raise RuntimeError("Please select at least one Level Sequence.") seq_data = None @@ -42,28 +133,38 @@ class CreateRender(UnrealAssetCreator): f"Skipping {selected_asset.get_name()}. It isn't a Level " "Sequence.") - # The asset name is the third element of the path which - # contains the map. - # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". - sel_path = selected_asset_path - asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] + if pre_create_data.get("use_hierarchy"): + # The asset name is the the third element of the path which + # contains the map. + # To take the asset name, we remove from the path the prefix + # "/Game/OpenPype/" and then we split the path by "/". + sel_path = selected_asset_path + asset_name = sel_path.replace( + "/Game/OpenPype/", "").split("/")[0] + + search_path = f"/Game/OpenPype/{asset_name}" + else: + search_path = Path(selected_asset_path).parent.as_posix() # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. - ar_filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - sequences = ar.get_assets(ar_filter) - master_seq = sequences[0].get_asset().get_path_name() - master_seq_obj = sequences[0].get_asset() - ar_filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - levels = ar.get_assets(ar_filter) - master_lvl = levels[0].get_asset().get_path_name() + try: + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[search_path], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + master_seq = sequences[0].get_asset().get_path_name() + master_seq_obj = sequences[0].get_asset() + ar_filter = unreal.ARFilter( + class_names=["World"], + package_paths=[search_path], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() + except IndexError: + raise RuntimeError( + f"Could not find the hierarchy for the selected sequence.") # If the selected asset is the master sequence, we get its data # and then we create the instance for the master sequence. @@ -79,7 +180,8 @@ class CreateRender(UnrealAssetCreator): master_seq_obj.get_playback_start(), master_seq_obj.get_playback_end())} - if selected_asset_path == master_seq: + if (selected_asset_path == master_seq or + pre_create_data.get("use_hierarchy")): seq_data = master_seq_data else: seq_data_list = [master_seq_data] @@ -119,20 +221,54 @@ class CreateRender(UnrealAssetCreator): "sub-sequence of the master sequence.") continue - instance_data["members"] = [selected_asset_path] - instance_data["sequence"] = selected_asset_path - instance_data["master_sequence"] = master_seq - instance_data["master_level"] = master_lvl - instance_data["output"] = seq_data.get('output') - instance_data["frameStart"] = seq_data.get('frame_range')[0] - instance_data["frameEnd"] = seq_data.get('frame_range')[1] + self.create_instance( + instance_data, subset_name, pre_create_data, + selected_asset_path, master_seq, master_lvl, seq_data) - super(CreateRender, self).create( - subset_name, - instance_data, - pre_create_data) + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("create_seq"): + self.create_with_new_sequence( + subset_name, instance_data, pre_create_data) + else: + self.create_from_existing_sequence( + subset_name, instance_data, pre_create_data) def get_pre_create_attr_defs(self): return [ - UILabelDef("Select the sequence to render.") + UILabelDef( + "Select a Level Sequence to render or create a new one." + ), + BoolDef( + "create_seq", + label="Create a new Level Sequence", + default=False + ), + UILabelDef( + "WARNING: If you create a new Level Sequence, the current\n" + "level will be saved and a new Master Level will be created." + ), + NumberDef( + "start_frame", + label="Start Frame", + default=0, + minimum=-999999, + maximum=999999 + ), + NumberDef( + "end_frame", + label="Start Frame", + default=150, + minimum=-999999, + maximum=999999 + ), + UISeparatorDef(), + UILabelDef( + "The following settings are valid only if you are not\n" + "creating a new sequence." + ), + BoolDef( + "use_hierarchy", + label="Use Hierarchy", + default=False + ), ] From 423cbf9e5465ee146523460376efde0595e44374 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Mar 2023 17:06:36 +0000 Subject: [PATCH 127/918] Fix level sequence not being added to instance --- openpype/hosts/unreal/plugins/create/create_render.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index b2a246d3a8..b9c443c456 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -69,6 +69,8 @@ class CreateRender(UnrealAssetCreator): seq.set_playback_start(pre_create_data.get("start_frame")) seq.set_playback_end(pre_create_data.get("end_frame")) + pre_create_data["members"] = [seq.get_path_name()] + unreal.EditorAssetLibrary.save_asset(seq.get_path_name()) # Create the master level From 7d1e376761f8c4532af04f649355f9aead58e61f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 22 Mar 2023 11:35:20 +0000 Subject: [PATCH 128/918] Added warning if no assets selected when starting rendering --- openpype/hosts/unreal/api/rendering.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 29e4747f6e..25faa2ac2c 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -4,6 +4,7 @@ import unreal from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline +from openpype.widgets.message_window import Window queue = None @@ -37,6 +38,15 @@ def start_rendering(): # Get selected sequences assets = unreal.EditorUtilityLibrary.get_selected_assets() + if not assets: + Window( + parent=None, + title="No assets selected", + message="No assets selected. Select a render instance.", + level="warning") + raise RuntimeError( + "No assets selected. You need to select a render instance.") + # instances = pipeline.ls_inst() instances = [ a for a in assets From 217b9dd70822ecccfaf6e2d45b4caac0d479835b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:54:18 +0100 Subject: [PATCH 129/918] Move and refactor PySide2 imports to `qtpy` and top of file --- openpype/hosts/substancepainter/api/lib.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index e552caee6d..e299ab03de 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -7,6 +7,8 @@ import substance_painter.project import substance_painter.resource import substance_painter.js +from qtpy import QtGui, QtWidgets, QtCore + def get_export_presets(): """Return Export Preset resource URLs for all available Export Presets. @@ -391,8 +393,6 @@ def get_parsed_export_maps(config): dict: [texture_set, stack]: {template: [file1_data, file2_data]} """ - import substance_painter.export - from .colorspace import get_project_channel_data outputs = substance_painter.export.list_project_textures(config) templates = get_export_templates(config, strip_folder=False) @@ -524,7 +524,6 @@ def load_shelf(path, name=None): def _get_new_project_action(): """Return QAction which triggers Substance Painter's new project dialog""" - from PySide2 import QtGui main_window = substance_painter.ui.get_main_window() @@ -564,7 +563,6 @@ def prompt_new_file_with_mesh(mesh_filepath): for example when the user might have cancelled the operation. """ - from PySide2 import QtWidgets, QtCore app = QtWidgets.QApplication.instance() assert os.path.isfile(mesh_filepath), \ From 1cc2db14bbd0be5a380fadc7108f0ed646f95abc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:56:14 +0100 Subject: [PATCH 130/918] Add back in imports that accidentally got removed --- openpype/hosts/substancepainter/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index e299ab03de..2cd08f862e 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -6,6 +6,7 @@ from collections import defaultdict import substance_painter.project import substance_painter.resource import substance_painter.js +import substance_painter.export from qtpy import QtGui, QtWidgets, QtCore @@ -393,6 +394,8 @@ def get_parsed_export_maps(config): dict: [texture_set, stack]: {template: [file1_data, file2_data]} """ + # Import is here to avoid recursive lib <-> colorspace imports + from .colorspace import get_project_channel_data outputs = substance_painter.export.list_project_textures(config) templates = get_export_templates(config, strip_folder=False) From 8b3ce3044a9368663d91ba45279c7a63fcb3876e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:56:58 +0100 Subject: [PATCH 131/918] Raise KnownPublishError instead of assert Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/substancepainter/plugins/publish/save_workfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 5e86785e0d..2bd342cda1 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -13,7 +13,8 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): def process(self, context): host = registered_host() - assert context.data['currentFile'] == host.get_current_workfile() + if context.data['currentFile'] != host.get_current_workfile(): + raise KnownPublishError("Workfile has changed during publishing!") if host.has_unsaved_changes(): self.log.info("Saving current file..") From 17fc4ed9251551c37f5405101f12af8e1bc8e890 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 10:58:04 +0100 Subject: [PATCH 132/918] Fix import --- .../hosts/substancepainter/plugins/publish/save_workfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 2bd342cda1..f19deccb0e 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -1,6 +1,9 @@ import pyblish.api -from openpype.pipeline import registered_host +from openpype.pipeline import ( + registered_host, + KnownPublishError +) class SaveCurrentWorkfile(pyblish.api.ContextPlugin): From 4fdb31611dc9810346a45a10c50ea9a209d7a99f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:03:54 +0100 Subject: [PATCH 133/918] Allow to mark an instance to skip integration explicitly Use `instance.data["integrate"] = False` --- .../plugins/publish/extract_textures.py | 15 ++++----------- openpype/plugins/publish/integrate.py | 5 +++++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index 469f8501f7..bd933610f4 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -58,14 +58,7 @@ class ExtractTextures(publish.Extractor, context=context, colorspace=colorspace) - # Add a fake representation which won't be integrated so the - # Integrator leaves us alone - otherwise it would error - # TODO: Add `instance.data["integrate"] = False` support in Integrator? - instance.data["representations"] = [ - { - "name": "_fake", - "ext": "_fake", - "delete": True, - "files": [] - } - ] + # The TextureSet instance should not be integrated. It generates no + # output data. Instead the separated texture instances are generated + # from it which themselves integrate into the database. + instance.data["integrate"] = False diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6a0327ec84..c24758ba0f 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -160,6 +160,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "Instance is marked to be processed on farm. Skipping") return + # Instance is marked to not get integrated + if instance.data.get("integrate", True): + self.log.info("Instance is marked to skip integrating. Skipping") + return + filtered_repres = self.filter_representations(instance) # Skip instance if there are not representations to integrate # all representations should not be integrated From 5b3af11f0f6bbd53dcc590de49f51660dbdeb556 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:04:25 +0100 Subject: [PATCH 134/918] Fix the if statement --- openpype/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index c24758ba0f..fa29d2a58b 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -161,7 +161,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): return # Instance is marked to not get integrated - if instance.data.get("integrate", True): + if not instance.data.get("integrate", True): self.log.info("Instance is marked to skip integrating. Skipping") return From ddc0117aeda6fd1542d96ee54fb374a1339d8aae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:14:39 +0100 Subject: [PATCH 135/918] Update openpype/settings/defaults/project_settings/substancepainter.json Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../settings/defaults/project_settings/substancepainter.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json index 0f9f1af71e..60929e85fd 100644 --- a/openpype/settings/defaults/project_settings/substancepainter.json +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -10,4 +10,4 @@ } }, "shelves": {} -} \ No newline at end of file +} From 57b84f18bc343b4892382d642927847496f3e43e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 11:18:37 +0100 Subject: [PATCH 136/918] Fix docstring --- openpype/hosts/substancepainter/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index b377db1641..652ec9ec7d 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Pipeline tools for OpenPype Gaffer integration.""" +"""Pipeline tools for OpenPype Substance Painter integration.""" import os import logging from functools import partial From f4d423dc4f7b1a42310540c74230ba3a1dcd20ab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 14:39:48 +0100 Subject: [PATCH 137/918] Add Create... menu entry to match other hosts --- openpype/hosts/substancepainter/api/pipeline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index 652ec9ec7d..df41d9bb70 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -165,6 +165,12 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): menu = QtWidgets.QMenu("OpenPype") + action = menu.addAction("Create...") + action.triggered.connect( + lambda: host_tools.show_publisher(parent=parent, + tab="create") + ) + action = menu.addAction("Load...") action.triggered.connect( lambda: host_tools.show_loader(parent=parent, use_context=True) @@ -172,7 +178,8 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): action = menu.addAction("Publish...") action.triggered.connect( - lambda: host_tools.show_publisher(parent=parent) + lambda: host_tools.show_publisher(parent=parent, + tab="publish") ) action = menu.addAction("Manage...") From d4a0c6634cd0d9c31ea8f1cf12b92fee5e7ba797 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 15:45:13 +0100 Subject: [PATCH 138/918] Optimize logic --- openpype/hosts/substancepainter/api/colorspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/api/colorspace.py b/openpype/hosts/substancepainter/api/colorspace.py index a9df3eb066..375b61b39b 100644 --- a/openpype/hosts/substancepainter/api/colorspace.py +++ b/openpype/hosts/substancepainter/api/colorspace.py @@ -25,11 +25,11 @@ def _iter_document_stack_channels(): material_name = material["name"] for stack in material["stacks"]: stack_name = stack["name"] + if stack_name: + stack_path = [material_name, stack_name] + else: + stack_path = material_name for channel in stack["channels"]: - if stack_name: - stack_path = [material_name, stack_name] - else: - stack_path = material_name yield stack_path, channel From 22d628d054809a9e8f1d816994a7426197d864f8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Mar 2023 18:09:13 +0100 Subject: [PATCH 139/918] Store instances in single project metadata key by id + fix adding/removing instances --- .../hosts/substancepainter/api/pipeline.py | 67 ++++++++++++++----- .../plugins/create/create_textures.py | 39 ++++++----- .../plugins/create/create_workfile.py | 27 +++++--- 3 files changed, 93 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index df41d9bb70..b995c9030d 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -39,6 +39,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") OPENPYPE_METADATA_KEY = "OpenPype" OPENPYPE_METADATA_CONTAINERS_KEY = "containers" # child key OPENPYPE_METADATA_CONTEXT_KEY = "context" # child key +OPENPYPE_METADATA_INSTANCES_KEY = "instances" # child key class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): @@ -312,21 +313,6 @@ def imprint_container(container, container[key] = value -def set_project_metadata(key, data): - """Set a key in project's OpenPype metadata.""" - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - metadata.set(key, data) - - -def get_project_metadata(key): - """Get a key from project's OpenPype metadata.""" - if not substance_painter.project.is_open(): - return - - metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) - return metadata.get(key) - - def set_container_metadata(object_name, container_data, update=False): """Helper method to directly set the data for a specific container @@ -359,3 +345,54 @@ def remove_container_metadata(object_name): if containers: containers.pop(object_name, None) metadata.set("containers", containers) + + +def set_instance(instance_id, instance_data, update=False): + """Helper method to directly set the data for a specific container + + Args: + instance_id (str): Unique identifier for the instance + instance_data (dict): The instance data to store in the metaadata. + """ + set_instances({instance_id: instance_data}, update=update) + + +def set_instances(instance_data_by_id, update=False): + """Store data for multiple instances at the same time. + + This is more optimal than querying and setting them in the metadata one + by one. + """ + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + + for instance_id, instance_data in instance_data_by_id.items(): + if update: + existing_data = instances.get(instance_id, {}) + existing_data.update(instance_data) + else: + instances[instance_id] = instance_data + + metadata.set("instances", instances) + + +def remove_instance(instance_id): + """Helper method to remove the data for a specific container""" + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + instances = metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + instances.pop(instance_id, None) + metadata.set("instances", instances) + + +def get_instances_by_id(): + """Return all instances stored in the project instances metadata""" + if not substance_painter.project.is_open(): + return {} + + metadata = substance_painter.project.Metadata(OPENPYPE_METADATA_KEY) + return metadata.get(OPENPYPE_METADATA_INSTANCES_KEY) or {} + + +def get_instances(): + """Return all instances stored in the project instances as a list""" + return list(get_instances_by_id().values()) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 9d641215dc..19133768a5 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating textures.""" -from openpype.pipeline import CreatedInstance, Creator +from openpype.pipeline import CreatedInstance, Creator, CreatorError from openpype.lib import ( EnumDef, UILabelDef, @@ -9,8 +9,10 @@ from openpype.lib import ( ) from openpype.hosts.substancepainter.api.pipeline import ( - set_project_metadata, - get_project_metadata + get_instances, + set_instance, + set_instances, + remove_instance ) from openpype.hosts.substancepainter.api.lib import get_export_presets @@ -29,27 +31,34 @@ class CreateTextures(Creator): def create(self, subset_name, instance_data, pre_create_data): if not substance_painter.project.is_open(): - return + raise CreatorError("Can't create a Texture Set instance without " + "an open project.") - instance = self.create_instance_in_context(subset_name, instance_data) - set_project_metadata("textureSet", instance.data_to_store()) + instance = self.create_instance_in_context(subset_name, + instance_data) + set_instance( + instance_id=instance["instance_id"], + instance_data=instance.data_to_store() + ) def collect_instances(self): - workfile = get_project_metadata("textureSet") - if workfile: - self.create_instance_in_context_from_existing(workfile) + for instance in get_instances(): + if (instance.get("creator_identifier") == self.identifier or + instance.get("family") == self.family): + self.create_instance_in_context_from_existing(instance) def update_instances(self, update_list): + instance_data_by_id = {} for instance, _changes in update_list: - # Update project's metadata - data = get_project_metadata("textureSet") or {} - data.update(instance.data_to_store()) - set_project_metadata("textureSet", data) + # Persist the data + instance_id = instance.get("instance_id") + instance_data = instance.data_to_store() + instance_data_by_id[instance_id] = instance_data + set_instances(instance_data_by_id, update=True) def remove_instances(self, instances): for instance in instances: - # TODO: Implement removal - # api.remove_instance(instance) + remove_instance(instance["instance_id"]) self._remove_instance_from_context(instance) # Helper methods (this might get moved into Creator class) diff --git a/openpype/hosts/substancepainter/plugins/create/create_workfile.py b/openpype/hosts/substancepainter/plugins/create/create_workfile.py index 4e316f3b64..d7f31f9dcf 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_workfile.py +++ b/openpype/hosts/substancepainter/plugins/create/create_workfile.py @@ -5,8 +5,9 @@ from openpype.pipeline import CreatedInstance, AutoCreator from openpype.client import get_asset_by_name from openpype.hosts.substancepainter.api.pipeline import ( - set_project_metadata, - get_project_metadata + set_instances, + set_instance, + get_instances ) import substance_painter.project @@ -66,19 +67,25 @@ class CreateWorkfile(AutoCreator): current_instance["task"] = task_name current_instance["subset"] = subset_name - set_project_metadata("workfile", current_instance.data_to_store()) + set_instance( + instance_id=current_instance.get("instance_id"), + instance_data=current_instance.data_to_store() + ) def collect_instances(self): - workfile = get_project_metadata("workfile") - if workfile: - self.create_instance_in_context_from_existing(workfile) + for instance in get_instances(): + if (instance.get("creator_identifier") == self.identifier or + instance.get("family") == self.family): + self.create_instance_in_context_from_existing(instance) def update_instances(self, update_list): + instance_data_by_id = {} for instance, _changes in update_list: - # Update project's workfile metadata - data = get_project_metadata("workfile") or {} - data.update(instance.data_to_store()) - set_project_metadata("workfile", data) + # Persist the data + instance_id = instance.get("instance_id") + instance_data = instance.data_to_store() + instance_data_by_id[instance_id] = instance_data + set_instances(instance_data_by_id, update=True) # Helper methods (this might get moved into Creator class) def create_instance_in_context(self, subset_name, data): From b430d9f71db9699add0a07140af31c267a822ad9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Mar 2023 21:05:56 +0100 Subject: [PATCH 140/918] cross hosts: enhancing imageio settings with Enable toggle also adding imageio settings to Max --- .../project_settings/aftereffects.json | 1 + .../defaults/project_settings/blender.json | 1 + .../defaults/project_settings/celaction.json | 1 + .../defaults/project_settings/flame.json | 1 + .../defaults/project_settings/fusion.json | 1 + .../defaults/project_settings/harmony.json | 1 + .../defaults/project_settings/hiero.json | 1 + .../defaults/project_settings/houdini.json | 1 + .../defaults/project_settings/max.json | 11 +++++++++ .../defaults/project_settings/maya.json | 1 + .../defaults/project_settings/nuke.json | 1 + .../defaults/project_settings/photoshop.json | 1 + .../defaults/project_settings/resolve.json | 1 + .../project_settings/traypublisher.json | 1 + .../defaults/project_settings/tvpaint.json | 1 + .../defaults/project_settings/unreal.json | 1 + .../project_settings/webpublisher.json | 1 + .../schema_project_aftereffects.json | 6 +++++ .../schema_project_blender.json | 6 +++++ .../schema_project_celaction.json | 6 +++++ .../projects_schema/schema_project_flame.json | 6 +++++ .../schema_project_fusion.json | 6 +++++ .../schema_project_harmony.json | 6 +++++ .../projects_schema/schema_project_hiero.json | 6 +++++ .../schema_project_houdini.json | 8 ++++++- .../projects_schema/schema_project_max.json | 23 +++++++++++++++++++ .../projects_schema/schema_project_maya.json | 6 +++++ .../schema_project_photoshop.json | 6 +++++ .../schema_project_resolve.json | 6 +++++ .../schema_project_traypublisher.json | 6 +++++ .../schema_project_tvpaint.json | 6 +++++ .../schema_project_unreal.json | 6 +++++ .../schema_project_webpublisher.json | 6 +++++ .../schemas/schema_nuke_imageio.json | 8 ++++++- 34 files changed, 148 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 669e1db0b8..d1b2309d26 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 20eec0c09d..d9aabea9ad 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/celaction.json b/openpype/settings/defaults/project_settings/celaction.json index 822604fd2f..10dfd70ac6 100644 --- a/openpype/settings/defaults/project_settings/celaction.json +++ b/openpype/settings/defaults/project_settings/celaction.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index 5a13d81384..f323af7496 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index f974eebaca..9130c9322c 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index 3f51a9c28b..97c9cdf761 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index 100c1f5b47..65d6da59f9 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 1b7faf8526..8d7f9865c5 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index d59cdf8c4a..8df4d0ca57 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,4 +1,15 @@ { + "imageio": { + "enabled": false, + "ocio_config": { + "enabled": false, + "filepath": [] + }, + "file_rules": { + "enabled": false, + "rules": {} + } + }, "RenderSettings": { "default_render_image_folder": "renders/3dsmax", "aov_separator": "underscore", diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2aa95fd1be..aa05ae145b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1,6 +1,7 @@ { "open_workfile_post_initialization": false, "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c249955dc8..72f599c98b 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -9,6 +9,7 @@ } }, "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index bcf21f55dd..9fd4fe54f1 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 264f3bd902..3720dc54f4 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index fdea4aeaba..311c5b0cfc 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 9173a8c3d5..5da63110be 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 75cee11bd9..d83e090fae 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/webpublisher.json b/openpype/settings/defaults/project_settings/webpublisher.json index e830ba6a40..a9dc9012eb 100644 --- a/openpype/settings/defaults/project_settings/webpublisher.json +++ b/openpype/settings/defaults/project_settings/webpublisher.json @@ -1,5 +1,6 @@ { "imageio": { + "enabled": false, "ocio_config": { "enabled": false, "filepath": [] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 8dc83f5506..2d48e06ccb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 725d9bfb08..2e4dcb4e31 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json index c5ca3eb9f5..efecb2a89c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index aab8f21d15..7c839037ad 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 464cf2c06d..87856380ac 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "collapsible": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index e6bf835c9f..da80648a14 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index f44f92438c..3e9ac41b1c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -11,7 +11,13 @@ "label": "Color Management (ImageIO)", "is_group": true, "collapsible": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 24b06f77db..74ffbbe9f4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" @@ -35,4 +41,4 @@ "name": "schema_houdini_publish" } ] -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 4fba9aff0a..de7b4aca0b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -5,6 +5,29 @@ "label": "Max", "is_file": true, "children": [ + { + "key": "imageio", + "type": "dict", + "label": "Color Management (ImageIO)", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "schema", + "name": "schema_imageio_config" + }, + { + "type": "schema", + "name": "schema_imageio_file_rules" + } + + ] + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 47dfb37024..8373a57429 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -16,7 +16,13 @@ "label": "Color Management (ImageIO)", "collapsible": true, "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 0071e632af..95f402ca7c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index b326f22394..da252dd9b1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 2ef1d2a414..4bce299747 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 708b688ba5..3af5e7f5ca 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 8988dd2ff0..b330fd600f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index 66ccca644d..d0c2145298 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -10,7 +10,13 @@ "type": "dict", "label": "Color Management (ImageIO)", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index 1cd6f0e67f..743b0d66a6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -4,7 +4,13 @@ "label": "Color Management (ImageIO)", "collapsible": true, "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "label", "label": "'Custom OCIO config path' has deprecated.
If you need to set custom config, just enable and add path into 'OCIO config'.
Anatomy keys are supported.." @@ -257,4 +263,4 @@ ] } ] -} \ No newline at end of file +} From 52f4b75b62b3776f7b1a3c4d65690b3e5b53cace Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Mar 2023 22:30:23 +0100 Subject: [PATCH 141/918] Flame: skip colorspace management if imageio is not enabled also set enabled to True by default since some productions are already using it --- openpype/hosts/flame/hooks/pre_flame_setup.py | 21 +++++++++++++++---- .../defaults/project_settings/flame.json | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 713daf1031..6367e132de 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -47,6 +47,13 @@ class FlamePrelaunch(PreLaunchHook): imageio_flame = project_settings["flame"]["imageio"] + colormanaged = True + # check if host settings are having enabled key and if it is False + if imageio_flame.get("enabled") and imageio_flame["enabled"] is False: + # if host settings are disabled return False because + # it is expected that no colorspace management is needed + colormanaged = False + # get user name and host name user_name = get_openpype_username() user_name = user_name.replace(".", "_") @@ -68,9 +75,7 @@ class FlamePrelaunch(PreLaunchHook): "FrameWidth": int(width), "FrameHeight": int(height), "AspectRatio": float((width / height) * _db_p_data["pixelAspect"]), - "FrameRate": self._get_flame_fps(fps), - "FrameDepth": str(imageio_flame["project"]["frameDepth"]), - "FieldDominance": str(imageio_flame["project"]["fieldDominance"]) + "FrameRate": self._get_flame_fps(fps) } data_to_script = { @@ -78,7 +83,6 @@ class FlamePrelaunch(PreLaunchHook): "host_name": _env.get("FLAME_WIRETAP_HOSTNAME") or hostname, "volume_name": volume_name, "group_name": _env.get("FLAME_WIRETAP_GROUP"), - "color_policy": str(imageio_flame["project"]["colourPolicy"]), # from project "project_name": project_name, @@ -86,6 +90,15 @@ class FlamePrelaunch(PreLaunchHook): "project_data": project_data } + # add color management data + if colormanaged: + project_data.update({ + "FrameDepth": str(imageio_flame["project"]["frameDepth"]), + "FieldDominance": str(imageio_flame["project"]["fieldDominance"]) + }) + data_to_script["color_policy"] = str( + imageio_flame["project"]["colourPolicy"]) + self.log.info(pformat(dict(_env))) self.log.info(pformat(data_to_script)) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index f323af7496..5eb6ec2d2a 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -1,6 +1,6 @@ { "imageio": { - "enabled": false, + "enabled": true, "ocio_config": { "enabled": false, "filepath": [] From e09fea633417f5ca7d193e022673466a279bedc7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Mar 2023 22:31:16 +0100 Subject: [PATCH 142/918] global: adding imageio enable switch to colorspace module --- openpype/pipeline/colorspace.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 2085e2d37f..e42838d9dd 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -336,6 +336,7 @@ def get_imageio_config( anatomy_data = get_template_data_from_session() formatting_data = deepcopy(anatomy_data) + # add project roots to anatomy data formatting_data["root"] = anatomy.roots formatting_data["platform"] = platform.system().lower() @@ -344,6 +345,12 @@ def get_imageio_config( imageio_global, imageio_host = _get_imageio_settings( project_settings, host_name) + # check if host settings are having enabled key and if it is False + if imageio_host.get("enabled") and imageio_host["enabled"] is False: + # if host settings are disabled return False because + # it is expected that no colorspace management is needed + return False + config_host = imageio_host.get("ocio_config", {}) if config_host.get("enabled"): From 75aee631c18d9dc2eb1cd9e31d6a4f1785a00cd2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Mar 2023 22:31:50 +0100 Subject: [PATCH 143/918] Nuke: implementing imageio enable switch --- openpype/hosts/nuke/api/lib.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 2a14096f0e..1296bca9b0 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2001,18 +2001,18 @@ class WorkfileSettings(object): "Attention! Viewer nodes {} were erased." "It had wrong color profile".format(erased_viewers)) - def set_root_colorspace(self, nuke_colorspace): + def set_root_colorspace(self, imageio_host): ''' Adds correct colorspace to root Arguments: - nuke_colorspace (dict): adjustmensts from presets + imageio_host (dict): adjustments from presets ''' - workfile_settings = nuke_colorspace["workfile"] + workfile_settings = imageio_host["workfile"] - # resolve config data if they are enabled in host + # get config data if imageio is enabled config_data = None - if nuke_colorspace.get("ocio_config", {}).get("enabled"): + if imageio_host.get("enabled") and imageio_host["enabled"] is True: # switch ocio config to custom config workfile_settings["OCIO_config"] = "custom" workfile_settings["colorManagement"] = "OCIO" From 4f430c56bb9aeef4baeabdbe47bc862b10c6ae37 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Mar 2023 22:32:01 +0100 Subject: [PATCH 144/918] Maya: implementing imageio enable switch --- openpype/hosts/maya/plugins/load/load_image.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/load/load_image.py b/openpype/hosts/maya/plugins/load/load_image.py index b464c268fc..552bcc33af 100644 --- a/openpype/hosts/maya/plugins/load/load_image.py +++ b/openpype/hosts/maya/plugins/load/load_image.py @@ -273,6 +273,11 @@ class FileNodeLoader(load.LoaderPlugin): project_name, host_name, project_settings=project_settings ) + + # ignore if host imageio is not enabled + if not config_data: + return + file_rules = get_imageio_file_rules( project_name, host_name, project_settings=project_settings From f388fa3e1670342f2501bb2e5d0d5a670e4cc46a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Mar 2023 22:32:13 +0100 Subject: [PATCH 145/918] Fusion: implementing imageio enable switch --- openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py index 6bf0f55081..4dfb4ef223 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py @@ -26,7 +26,9 @@ class FusionPreLaunchOCIO(PreLaunchHook): anatomy_data=template_data, anatomy=self.data["anatomy"] ) - ocio_path = config_data["path"] - self.log.info(f"Setting OCIO config path: {ocio_path}") - self.launch_context.env["OCIO"] = ocio_path + if config_data: + ocio_path = config_data["path"] + + self.log.info(f"Setting OCIO config path: {ocio_path}") + self.launch_context.env["OCIO"] = ocio_path From 56a775710fd2fc4f1dec8051ed824e54062efab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 24 Mar 2023 14:22:18 +0100 Subject: [PATCH 146/918] Update openpype/hosts/flame/hooks/pre_flame_setup.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/flame/hooks/pre_flame_setup.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 6367e132de..019bf1adda 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -47,12 +47,9 @@ class FlamePrelaunch(PreLaunchHook): imageio_flame = project_settings["flame"]["imageio"] - colormanaged = True - # check if host settings are having enabled key and if it is False - if imageio_flame.get("enabled") and imageio_flame["enabled"] is False: - # if host settings are disabled return False because - # it is expected that no colorspace management is needed - colormanaged = False + colormanaged = imageio_flame.get("enabled") + if colormanaged is None: + colormanaged = True # get user name and host name user_name = get_openpype_username() From 5feee4cdff183b6dc119ffd25bbea9c15245b7ad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 24 Mar 2023 14:30:49 +0100 Subject: [PATCH 147/918] hound --- openpype/hosts/flame/hooks/pre_flame_setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 6367e132de..a5cab84d0d 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -94,7 +94,8 @@ class FlamePrelaunch(PreLaunchHook): if colormanaged: project_data.update({ "FrameDepth": str(imageio_flame["project"]["frameDepth"]), - "FieldDominance": str(imageio_flame["project"]["fieldDominance"]) + "FieldDominance": str( + imageio_flame["project"]["fieldDominance"]) }) data_to_script["color_policy"] = str( imageio_flame["project"]["colourPolicy"]) From 4ee7604dd7518188f1a9c4662402463b2a558520 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 24 Mar 2023 14:46:30 +0100 Subject: [PATCH 148/918] hound --- openpype/hosts/flame/hooks/pre_flame_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index c6db0d6462..3249fbfe9a 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -92,7 +92,7 @@ class FlamePrelaunch(PreLaunchHook): project_data.update({ "FrameDepth": str(imageio_flame["project"]["frameDepth"]), "FieldDominance": str( - imageio_flame["project"]["fieldDominance"]) + imageio_flame["project"]["fieldDominance"]) }) data_to_script["color_policy"] = str( imageio_flame["project"]["colourPolicy"]) From bc50139ec8ef7d93636f7ccfc861bd98dbdc2ba0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Mar 2023 15:20:01 +0000 Subject: [PATCH 149/918] Only parent to world on extraction if nested. --- openpype/hosts/maya/plugins/publish/extract_xgen.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_xgen.py b/openpype/hosts/maya/plugins/publish/extract_xgen.py index 0cc842b4ec..fb097ca84a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_xgen.py +++ b/openpype/hosts/maya/plugins/publish/extract_xgen.py @@ -65,9 +65,10 @@ class ExtractXgen(publish.Extractor): ) cmds.delete(set(children) - set(shapes)) - duplicate_transform = cmds.parent( - duplicate_transform, world=True - )[0] + if cmds.listRelatives(duplicate_transform, parent=True): + duplicate_transform = cmds.parent( + duplicate_transform, world=True + )[0] duplicate_nodes.append(duplicate_transform) From 399541602898ce342f3f8639a1969a144c9824c7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Mar 2023 15:27:33 +0000 Subject: [PATCH 150/918] Validation for required namespace. --- .../hosts/maya/plugins/publish/validate_xgen.py | 13 +++++++++++++ website/docs/artist_hosts_maya_xgen.md | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_xgen.py b/openpype/hosts/maya/plugins/publish/validate_xgen.py index 2870909974..47b24e218c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_xgen.py +++ b/openpype/hosts/maya/plugins/publish/validate_xgen.py @@ -57,3 +57,16 @@ class ValidateXgen(pyblish.api.InstancePlugin): json.dumps(inactive_modifiers, indent=4, sort_keys=True) ) ) + + # We need a namespace else there will be a naming conflict when + # extracting because of stripping namespaces and parenting to world. + node_names = [instance.data["xgmPalette"]] + for _, connections in instance.data["xgenConnections"].items(): + node_names.append(connections["transform"].split(".")[0]) + + non_namespaced_nodes = [n for n in node_names if ":" not in n] + if non_namespaced_nodes: + raise PublishValidationError( + "Could not find namespace on {}. Namespace is required for" + " xgen publishing.".format(non_namespaced_nodes) + ) diff --git a/website/docs/artist_hosts_maya_xgen.md b/website/docs/artist_hosts_maya_xgen.md index ec5f2ed921..db7bbd0557 100644 --- a/website/docs/artist_hosts_maya_xgen.md +++ b/website/docs/artist_hosts_maya_xgen.md @@ -43,6 +43,10 @@ Create an Xgen instance to publish. This needs to contain only **one Xgen collec You can create multiple Xgen instances if you have multiple collections to publish. +:::note +The Xgen publishing requires a namespace on the Xgen collection (palette) and the geometry used. +::: + ### Publish The publishing process will grab geometry used for Xgen along with any external files used in the collection's descriptions. This creates an isolated Maya file with just the Xgen collection's dependencies, so you can use any nested geometry when creating the Xgen description. An Xgen version will consist of: From 7dc59ece7b245e3bf47daf5bb7cbbd76cf49cf33 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:46:35 +0100 Subject: [PATCH 151/918] Define settings --- .../projects_schema/schema_project_maya.json | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 47dfb37024..80e2d43411 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -10,6 +10,41 @@ "key": "open_workfile_post_initialization", "label": "Open Workfile Post Initialization" }, + { + "type": "dict", + "key": "explicit_plugins_loading", + "label": "Explicit Plugins Loading", + "collapsible": true, + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "plugins_to_load", + "label": "Plugins To Load", + "object_type": { + "type": "dict", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "name", + "label": "Name" + } + ] + } + } + ] + }, { "key": "imageio", "type": "dict", From 5349579f748bc1522d0ade1ef24da3c07337ad7c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:46:46 +0100 Subject: [PATCH 152/918] Define setting defaults --- .../defaults/project_settings/maya.json | 409 ++++++++++++++++++ 1 file changed, 409 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2aa95fd1be..cc3a76c599 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1,5 +1,414 @@ { "open_workfile_post_initialization": false, + "explicit_plugins_loading": { + "enabled": false, + "plugins_to_load": [ + { + "enabled": false, + "name": "AbcBullet" + }, + { + "enabled": true, + "name": "AbcExport" + }, + { + "enabled": true, + "name": "AbcImport" + }, + { + "enabled": false, + "name": "animImportExport" + }, + { + "enabled": false, + "name": "ArubaTessellator" + }, + { + "enabled": false, + "name": "ATFPlugin" + }, + { + "enabled": false, + "name": "atomImportExport" + }, + { + "enabled": false, + "name": "AutodeskPacketFile" + }, + { + "enabled": false, + "name": "autoLoader" + }, + { + "enabled": false, + "name": "bifmeshio" + }, + { + "enabled": false, + "name": "bifrostGraph" + }, + { + "enabled": false, + "name": "bifrostshellnode" + }, + { + "enabled": false, + "name": "bifrostvisplugin" + }, + { + "enabled": false, + "name": "blast2Cmd" + }, + { + "enabled": false, + "name": "bluePencil" + }, + { + "enabled": false, + "name": "Boss" + }, + { + "enabled": false, + "name": "bullet" + }, + { + "enabled": true, + "name": "cacheEvaluator" + }, + { + "enabled": false, + "name": "cgfxShader" + }, + { + "enabled": false, + "name": "cleanPerFaceAssignment" + }, + { + "enabled": false, + "name": "clearcoat" + }, + { + "enabled": false, + "name": "convertToComponentTags" + }, + { + "enabled": false, + "name": "curveWarp" + }, + { + "enabled": false, + "name": "ddsFloatReader" + }, + { + "enabled": true, + "name": "deformerEvaluator" + }, + { + "enabled": false, + "name": "dgProfiler" + }, + { + "enabled": false, + "name": "drawUfe" + }, + { + "enabled": false, + "name": "dx11Shader" + }, + { + "enabled": false, + "name": "fbxmaya" + }, + { + "enabled": false, + "name": "fltTranslator" + }, + { + "enabled": false, + "name": "freeze" + }, + { + "enabled": false, + "name": "Fur" + }, + { + "enabled": false, + "name": "gameFbxExporter" + }, + { + "enabled": false, + "name": "gameInputDevice" + }, + { + "enabled": false, + "name": "GamePipeline" + }, + { + "enabled": false, + "name": "gameVertexCount" + }, + { + "enabled": false, + "name": "geometryReport" + }, + { + "enabled": false, + "name": "geometryTools" + }, + { + "enabled": false, + "name": "glslShader" + }, + { + "enabled": true, + "name": "GPUBuiltInDeformer" + }, + { + "enabled": false, + "name": "gpuCache" + }, + { + "enabled": false, + "name": "hairPhysicalShader" + }, + { + "enabled": false, + "name": "ik2Bsolver" + }, + { + "enabled": false, + "name": "ikSpringSolver" + }, + { + "enabled": false, + "name": "invertShape" + }, + { + "enabled": false, + "name": "lges" + }, + { + "enabled": false, + "name": "lookdevKit" + }, + { + "enabled": false, + "name": "MASH" + }, + { + "enabled": false, + "name": "matrixNodes" + }, + { + "enabled": false, + "name": "mayaCharacterization" + }, + { + "enabled": false, + "name": "mayaHIK" + }, + { + "enabled": false, + "name": "MayaMuscle" + }, + { + "enabled": false, + "name": "mayaUsdPlugin" + }, + { + "enabled": false, + "name": "mayaVnnPlugin" + }, + { + "enabled": false, + "name": "melProfiler" + }, + { + "enabled": false, + "name": "meshReorder" + }, + { + "enabled": false, + "name": "modelingToolkit" + }, + { + "enabled": false, + "name": "mtoa" + }, + { + "enabled": false, + "name": "mtoh" + }, + { + "enabled": false, + "name": "nearestPointOnMesh" + }, + { + "enabled": true, + "name": "objExport" + }, + { + "enabled": false, + "name": "OneClick" + }, + { + "enabled": false, + "name": "OpenEXRLoader" + }, + { + "enabled": false, + "name": "pgYetiMaya" + }, + { + "enabled": false, + "name": "pgyetiVrayMaya" + }, + { + "enabled": false, + "name": "polyBoolean" + }, + { + "enabled": false, + "name": "poseInterpolator" + }, + { + "enabled": false, + "name": "quatNodes" + }, + { + "enabled": false, + "name": "randomizerDevice" + }, + { + "enabled": false, + "name": "redshift4maya" + }, + { + "enabled": true, + "name": "renderSetup" + }, + { + "enabled": false, + "name": "retargeterNodes" + }, + { + "enabled": false, + "name": "RokokoMotionLibrary" + }, + { + "enabled": false, + "name": "rotateHelper" + }, + { + "enabled": false, + "name": "sceneAssembly" + }, + { + "enabled": false, + "name": "shaderFXPlugin" + }, + { + "enabled": false, + "name": "shotCamera" + }, + { + "enabled": false, + "name": "snapTransform" + }, + { + "enabled": false, + "name": "stage" + }, + { + "enabled": true, + "name": "stereoCamera" + }, + { + "enabled": false, + "name": "stlTranslator" + }, + { + "enabled": false, + "name": "studioImport" + }, + { + "enabled": false, + "name": "Substance" + }, + { + "enabled": false, + "name": "substancelink" + }, + { + "enabled": false, + "name": "substancemaya" + }, + { + "enabled": false, + "name": "substanceworkflow" + }, + { + "enabled": false, + "name": "svgFileTranslator" + }, + { + "enabled": false, + "name": "sweep" + }, + { + "enabled": false, + "name": "testify" + }, + { + "enabled": false, + "name": "tiffFloatReader" + }, + { + "enabled": false, + "name": "timeSliderBookmark" + }, + { + "enabled": false, + "name": "Turtle" + }, + { + "enabled": false, + "name": "Type" + }, + { + "enabled": false, + "name": "udpDevice" + }, + { + "enabled": false, + "name": "ufeSupport" + }, + { + "enabled": false, + "name": "Unfold3D" + }, + { + "enabled": false, + "name": "VectorRender" + }, + { + "enabled": false, + "name": "vrayformaya" + }, + { + "enabled": false, + "name": "vrayvolumegrid" + }, + { + "enabled": false, + "name": "xgenToolkit" + }, + { + "enabled": false, + "name": "xgenVray" + } + ] + }, "imageio": { "ocio_config": { "enabled": false, From f99c968df3dca4949e2e12a24ee908d5bf1ca997 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:48:13 +0100 Subject: [PATCH 153/918] Add launch arguments and env --- openpype/hooks/pre_add_last_workfile_arg.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 2558daef30..3d5f59cc67 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -44,10 +44,20 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): # Determine whether to open workfile post initialization. if self.host_name == "maya": - key = "open_workfile_post_initialization" - if self.data["project_settings"]["maya"][key]: + maya_settings = self.data["project_settings"]["maya"] + + if maya_settings["explicit_plugins_loading"]["enabled"]: + self.log.debug("Explicit plugins loading.") + self.launch_context.launch_args.append("-noAutoloadPlugins") + + keys = [ + "open_workfile_post_initialization", "explicit_plugins_loading" + ] + values = [maya_settings[k] for k in keys] + if any(values): self.log.debug("Opening workfile post initialization.") - self.data["env"]["OPENPYPE_" + key.upper()] = "1" + key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" + self.data["env"][key] = "1" return # Add path to workfile to arguments From ae4468bd209144fa406ba17b15a5c4d54c147516 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:48:25 +0100 Subject: [PATCH 154/918] Load plugins explicitly --- openpype/hosts/maya/startup/userSetup.py | 34 +++++++++++++++++------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index c77ecb829e..4932bf14c0 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,5 +1,4 @@ import os -from functools import partial from openpype.settings import get_project_settings from openpype.pipeline import install_host @@ -13,23 +12,40 @@ install_host(host) print("Starting OpenPype usersetup...") +settings = get_project_settings(os.environ['AVALON_PROJECT']) + +# Loading plugins explicitly. +if settings["maya"]["explicit_plugins_loading"]["enabled"]: + def _explicit_load_plugins(): + project_settings = get_project_settings(os.environ["AVALON_PROJECT"]) + maya_settings = project_settings["maya"] + explicit_plugins_loading = maya_settings["explicit_plugins_loading"] + if explicit_plugins_loading["enabled"]: + for plugin in explicit_plugins_loading["plugins_to_load"]: + if plugin["enabled"]: + print("Loading " + plugin["name"]) + try: + cmds.loadPlugin(plugin["name"], quiet=True) + except RuntimeError as e: + print(e) + + cmds.evalDeferred( + _explicit_load_plugins, + lowestPriority=True + ) # Open Workfile Post Initialization. key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" if bool(int(os.environ.get(key, "0"))): + def _log_and_open(): + print("Opening \"{}\"".format(os.environ["AVALON_LAST_WORKFILE"])) + cmds.file(os.environ["AVALON_LAST_WORKFILE"], open=True, force=True) cmds.evalDeferred( - partial( - cmds.file, - os.environ["AVALON_LAST_WORKFILE"], - open=True, - force=True - ), + _log_and_open, lowestPriority=True ) - # Build a shelf. -settings = get_project_settings(os.environ['AVALON_PROJECT']) shelf_preset = settings['maya'].get('project_shelf') if shelf_preset: From 53361fe9dfae15bd4a1f318561dd4ea8393ce353 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 09:53:41 +0100 Subject: [PATCH 155/918] Modeling Toolkit is default loaded. --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index cc3a76c599..9b71b97d75 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -228,7 +228,7 @@ "name": "meshReorder" }, { - "enabled": false, + "enabled": true, "name": "modelingToolkit" }, { From 4bfb4aa75779cdd75d380cb0a976b5cb1757cbbd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 10:03:53 +0100 Subject: [PATCH 156/918] Docs --- website/docs/admin_hosts_maya.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 23cacb4193..edbfa8da36 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -172,3 +172,12 @@ Fill in the necessary fields (the optional fields are regex filters) - Build your workfile ![maya build template](assets/maya-build_workfile_from_template.png) + +## Explicit Plugins Loading +You can define which plugins to load on launch of Maya here; `project_settings/maya/explicit_plugins_loading`. This can help improve Maya's launch speed, if you know which plugins are needed. + +By default only the required plugins are enabled. You can also add any plugin to the list to enable on launch. + +:::note technical +When enabling this feature, the workfile will be launched post initialization no matter the setting on `project_settings/maya/open_workfile_post_initialization`. This is to avoid any issues with references needing plugins. +::: From 4dd58e15d89383a870890562e9f084ee3fb189bf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 27 Mar 2023 11:13:04 +0100 Subject: [PATCH 157/918] Fixed error on rendering --- openpype/hosts/unreal/api/rendering.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 5ef4792000..e197f9075d 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -134,6 +134,9 @@ def start_rendering(): settings.file_name_format = f"{shot_name}" + ".{frame_number}" settings.output_directory.path = f"{render_dir}/{output_dir}" + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineDeferredPassBase) + job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineImageSequenceOutput_PNG) From 45ea981efb5af84deaae232a8737e0aae6abab21 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 27 Mar 2023 11:15:20 +0100 Subject: [PATCH 158/918] Added setting for rendering format --- openpype/hosts/unreal/api/rendering.py | 18 +++++++++++++++--- .../defaults/project_settings/unreal.json | 1 + .../projects_schema/schema_project_unreal.json | 12 ++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index e197f9075d..a2be041c18 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -33,7 +33,7 @@ def start_rendering(): """ Start the rendering process. """ - print("Starting rendering...") + unreal.log("Starting rendering...") # Get selected sequences assets = unreal.EditorUtilityLibrary.get_selected_assets() @@ -137,8 +137,20 @@ def start_rendering(): job.get_configuration().find_or_add_setting_by_class( unreal.MoviePipelineDeferredPassBase) - job.get_configuration().find_or_add_setting_by_class( - unreal.MoviePipelineImageSequenceOutput_PNG) + render_format = data.get("unreal").get("render_format", "png") + + if render_format == "png": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_PNG) + elif render_format == "exr": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_EXR) + elif render_format == "jpg": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_JPG) + elif render_format == "bmp": + job.get_configuration().find_or_add_setting_by_class( + unreal.MoviePipelineImageSequenceOutput_BMP) # If there are jobs in the queue, start the rendering process. if queue.get_jobs(): diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index ff290ef254..737a17d289 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -13,6 +13,7 @@ "delete_unmatched_assets": false, "render_config_path": "", "preroll_frames": 0, + "render_format": "png", "project_setup": { "dev_mode": true } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 40bbb40ccc..35eb0b24f1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -42,6 +42,18 @@ "key": "preroll_frames", "label": "Pre-roll frames" }, + { + "key": "render_format", + "label": "Render format", + "type": "enum", + "multiselection": false, + "enum_items": [ + {"png": "PNG"}, + {"exr": "EXR"}, + {"jpg": "JPG"}, + {"bmp": "BMP"} + ] + }, { "type": "dict", "collapsible": true, From a579dfc860b7e22d344c617afefef37899dae994 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 27 Mar 2023 12:31:02 +0100 Subject: [PATCH 159/918] Get the correct frame range data --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index 87f1338ee8..e6584e130f 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -20,6 +20,7 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): def process(self, instance): representations = instance.data.get("representations") for repr in representations: + data = instance.data.get("assetEntity", {}).get("data", {}) patterns = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble( repr["files"], minimum_items=1, patterns=patterns) @@ -30,8 +31,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): frames = list(collection.indexes) current_range = (frames[0], frames[-1]) - required_range = (instance.data["frameStart"], - instance.data["frameEnd"]) + required_range = (data["frameStart"], + data["frameEnd"]) if current_range != required_range: raise ValueError(f"Invalid frame range: {current_range} - " From 6d2a45e9516ef55b84a181d0b30a8abe5405afbd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 16:42:35 +0100 Subject: [PATCH 160/918] Move -noAutoLoadPlugins flag to separate hook. --- openpype/hooks/pre_add_last_workfile_arg.py | 7 +------ .../hosts/maya/hooks/pre_auto_load_plugins.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 openpype/hosts/maya/hooks/pre_auto_load_plugins.py diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index 3d5f59cc67..df4aa5cc5d 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -44,15 +44,10 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): # Determine whether to open workfile post initialization. if self.host_name == "maya": - maya_settings = self.data["project_settings"]["maya"] - - if maya_settings["explicit_plugins_loading"]["enabled"]: - self.log.debug("Explicit plugins loading.") - self.launch_context.launch_args.append("-noAutoloadPlugins") - keys = [ "open_workfile_post_initialization", "explicit_plugins_loading" ] + maya_settings = self.data["project_settings"]["maya"] values = [maya_settings[k] for k in keys] if any(values): self.log.debug("Opening workfile post initialization.") diff --git a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py new file mode 100644 index 0000000000..3c3ddbe4dc --- /dev/null +++ b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py @@ -0,0 +1,15 @@ +from openpype.lib import PreLaunchHook + + +class PreAutoLoadPlugins(PreLaunchHook): + """Define -noAutoloadPlugins command flag.""" + + # Execute before workfile argument. + order = 0 + app_groups = ["maya"] + + def execute(self): + maya_settings = self.data["project_settings"]["maya"] + if maya_settings["explicit_plugins_loading"]["enabled"]: + self.log.debug("Explicit plugins loading.") + self.launch_context.launch_args.append("-noAutoloadPlugins") From 72af67fc657fd7e52eca302d86f887c9af041212 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 27 Mar 2023 16:42:45 +0100 Subject: [PATCH 161/918] Warn about render farm support. --- website/docs/admin_hosts_maya.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index edbfa8da36..5211760632 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -180,4 +180,6 @@ By default only the required plugins are enabled. You can also add any plugin to :::note technical When enabling this feature, the workfile will be launched post initialization no matter the setting on `project_settings/maya/open_workfile_post_initialization`. This is to avoid any issues with references needing plugins. + +Renderfarm integration is not supported for this feature. ::: From c20f45e88136371dd2a8a35eca66cf28f7ac3ee8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 27 Mar 2023 23:48:27 +0800 Subject: [PATCH 162/918] skip unrelated script --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 205e815dc8..3a6947798e 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -36,6 +36,8 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) + container_name = f"{name}_CON" + asset = rt.getNodeByName(f"{name}") return containerise( From 32bb42e37922dd2de79f01c6e133b17ee8e7c6fa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 17:26:04 +0800 Subject: [PATCH 163/918] update the obj loader and add maintained_selection for loaders --- openpype/hosts/max/plugins/load/load_model.py | 12 ++++++++---- openpype/hosts/max/plugins/load/load_model_fbx.py | 4 ++++ openpype/hosts/max/plugins/load/load_model_obj.py | 12 ++++++++---- openpype/hosts/max/plugins/load/load_model_usd.py | 4 ++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index c248d75718..95ee014e07 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -5,6 +5,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class ModelAbcLoader(load.LoaderPlugin): @@ -57,12 +58,8 @@ importFile @"{file_path}" #noPrompt def update(self, container, representation): from pymxs import runtime as rt - path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) rt.select(node.Children) for alembic in rt.selection: @@ -76,6 +73,13 @@ importFile @"{file_path}" #noPrompt alembic_obj = rt.getNodeByName(abc_obj.name) alembic_obj.source = path + with maintained_selection(): + rt.select(node) + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index d8f4011277..88b8f1ed89 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -5,6 +5,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class FbxModelLoader(load.LoaderPlugin): @@ -59,6 +60,9 @@ importFile @"{path}" #noPrompt using:FBXIMP """) rt.execute(fbx_reimport_cmd) + with maintained_selection(): + rt.select(node) + lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index 63ae058ae0..c55e462111 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -5,6 +5,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class ObjLoader(load.LoaderPlugin): @@ -42,16 +43,19 @@ class ObjLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] node = rt.getNodeByName(node_name) + instance_name, _ = node_name.split("_") + container = rt.getNodeByName(instance_name) + for n in container.Children: + rt.delete(n) rt.execute(f'importFile @"{path}" #noPrompt using:ObjImp') - # create "missing" container for obj import - container = rt.container() - container.name = f"{instance_name}" # get current selection for selection in rt.getCurrentSelection(): selection.Parent = container - container.Parent = node + + with maintained_selection(): + rt.select(node) lib.imprint(node_name, { "representation": str(representation["_id"]) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 2237426187..143f91f40b 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -4,6 +4,7 @@ from openpype.pipeline import ( ) from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib +from openpype.hosts.max.api.lib import maintained_selection class ModelUSDLoader(load.LoaderPlugin): @@ -60,6 +61,9 @@ class ModelUSDLoader(load.LoaderPlugin): asset = rt.getNodeByName(f"{instance_name}") asset.Parent = node + with maintained_selection(): + rt.select(node) + lib.imprint(node_name, { "representation": str(representation["_id"]) }) From f3bd329d5a40793a6e083198326b22b43a58c621 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 18:48:58 +0800 Subject: [PATCH 164/918] add validator for checking if the current renderer is redshift before the extraction --- .../validate_renderer_redshift_proxy.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py new file mode 100644 index 0000000000..3a921c386e --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt +from openpype.pipeline.publish import RepairAction +from openpype.hosts.max.api.lib import get_current_renderer + + +class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): + """ + Validates Redshift as the current renderer for creating + Redshift Proxy + """ + + order = pyblish.api.ValidatorOrder + families = ["redshiftproxy"] + hosts = ["max"] + label = "Redshift Renderer" + actions = [RepairAction] + + def process(self, instance): + invalid = self.get_all_renderer(instance) + if invalid: + raise PublishValidationError("Please install Redshift for 3dsMax" + " before using this!") + invalid = self.get_current_renderer(instance) + if invalid: + raise PublishValidationError("Current Renderer is not Redshift") + + def get_all_renderer(self, instance): + invalid = list() + max_renderers_list = str(rt.RendererClass.classes) + if "Redshift_Renderer" not in max_renderers_list: + invalid.append(max_renderers_list) + + return invalid + + def get_current_renderer(self, instance): + invalid = list() + renderer_class = get_current_renderer() + current_renderer = str(renderer_class).split(":")[0] + if current_renderer != "Redshift_Renderer": + invalid.append(current_renderer) + + return invalid + + @classmethod + def repair(cls, instance): + if "Redshift_Renderer" in str(rt.RendererClass.classes[2]()): + rt.renderers.production = rt.RendererClass.classes[2]() From 91abe54b01ce08856004b7e395735c9508ab8300 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:35:02 +0800 Subject: [PATCH 165/918] add the extractor for redshift proxy --- .../plugins/publish/extract_redshift_proxy.py | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 938a7e8c2c..1616ead0ac 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -6,7 +6,8 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection + maintained_selection, + get_all_children ) @@ -29,35 +30,22 @@ class ExtractRedshiftProxy(publish.Extractor, self.log.info("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) rs_filename = "{name}.rs".format(**instance.data) - rs_filepath = os.path.join(stagingdir, rs_filename) + rs_filepath = rs_filepath.replace("\\", "/") - # MaxScript command for export - export_cmd = ( - f""" -fn ProxyExport fp selected:true compress:false connectivity:false startFrame: endFrame: camera:undefined warnExisting:true transformPivotToOrigin:false = ( - if startFrame == unsupplied then ( - startFrame = (currentTime.frame as integer) - ) - - if endFrame == unsupplied then ( - endFrame = (currentTime.frame as integer) - ) - - ret = rsProxy fp selected compress connectivity startFrame endFrame camera warnExisting transformPivotToOrigin - - ret -) -execute = ProxyExport fp selected:true compress:false connectivity:false startFrame:{start} endFrame:{end} warnExisting:false transformPivotToOrigin:bTransformPivotToOrigin - - """) # noqa + rs_filenames = self.get_rsfiles(instance, start, end) with maintained_selection(): # select and export - rt.select(container.Children) - rt.execute(export_cmd) + # con = rt.getNodeByName(container) + rt.select(get_all_children(rt.getNodeByName(container))) + # Redshift rsProxy command + # rsProxy fp selected compress connectivity startFrame endFrame + # camera warnExisting transformPivotToOrigin + rt.rsProxy(rs_filepath, 1, 0, 0, start, end, 0, 1, 1) self.log.info("Performing Extraction ...") + if "representations" not in instance.data: instance.data["representations"] = [] @@ -65,13 +53,19 @@ execute = ProxyExport fp selected:true compress:false connectivity:false startFr 'name': 'rs', 'ext': 'rs', # need to count the files - 'files': rs_filename, + 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" % (instance.name, - rs_filepath)) + stagingdir)) # TODO: set sequence - def get_rsfiles(self, container, startFrame, endFrame): - pass + def get_rsfiles(self, instance, startFrame, endFrame): + rs_filenames = [] + rs_name = instance.data["name"] + for frame in range(startFrame, endFrame + 1): + rs_filename = "%s.%04d.rs" % (rs_name, frame) + rs_filenames.append(rs_filename) + + return rs_filenames From be6813293c605d5b477af72fedcca047b6e7f0c0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:36:22 +0800 Subject: [PATCH 166/918] shut hound --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 1616ead0ac..8924242a93 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -53,7 +53,7 @@ class ExtractRedshiftProxy(publish.Extractor, 'name': 'rs', 'ext': 'rs', # need to count the files - 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], + 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa "stagingDir": stagingdir, } instance.data["representations"].append(representation) From f25b5d309ad212b273a2ba6ccdfa6b1d950e5a25 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:42:28 +0800 Subject: [PATCH 167/918] cleanup --- .../max/plugins/publish/extract_redshift_proxy.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 8924242a93..bf16c8d4a9 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -1,9 +1,6 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish from pymxs import runtime as rt from openpype.hosts.max.api import ( maintained_selection, @@ -11,10 +8,9 @@ from openpype.hosts.max.api import ( ) -class ExtractRedshiftProxy(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractRedshiftProxy(publish.Extractor): """ - Extract Camera with AlembicExport + Extract Redshift Proxy """ order = pyblish.api.ExtractorOrder - 0.1 From 6d51333a20e13b07d8f32ea69c299163b1c90fc4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:45:52 +0800 Subject: [PATCH 168/918] add docstrings --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index bf16c8d4a9..c91391429d 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -10,7 +10,7 @@ from openpype.hosts.max.api import ( class ExtractRedshiftProxy(publish.Extractor): """ - Extract Redshift Proxy + Extract Redshift Proxy with rsProxy """ order = pyblish.api.ExtractorOrder - 0.1 From b9ec96fdd64b4d8e7f770dfc722e459f28cd596e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:48:21 +0800 Subject: [PATCH 169/918] add docstring for the rs loader --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 13003d764a..30879bca78 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -11,6 +11,8 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): + """Load rs files with Redshift Proxy""" + label = "Load Redshift Proxy" families = ["redshiftproxy"] representations = ["rs"] From 3ba7b9b1ffc8ba9877024f8175af8adcbd984e08 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 23:05:25 +0800 Subject: [PATCH 170/918] fix selection of children --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index c91391429d..5aba257443 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -33,8 +33,8 @@ class ExtractRedshiftProxy(publish.Extractor): with maintained_selection(): # select and export - # con = rt.getNodeByName(container) - rt.select(get_all_children(rt.getNodeByName(container))) + con = rt.getNodeByName(container) + rt.select(con.Children) # Redshift rsProxy command # rsProxy fp selected compress connectivity startFrame endFrame # camera warnExisting transformPivotToOrigin From 35448073aba93b3ead99513b0d831accb00c76cb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 23:07:05 +0800 Subject: [PATCH 171/918] hound fix --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 5aba257443..0a3579d687 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -2,10 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection class ExtractRedshiftProxy(publish.Extractor): From a1e57e54cdf11be787406fd61b91ee823d99a0e1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Mar 2023 22:19:22 +0200 Subject: [PATCH 172/918] more explicit condition for backward compatibility --- openpype/pipeline/colorspace.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index e42838d9dd..5c7442e1c9 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -345,8 +345,13 @@ def get_imageio_config( imageio_global, imageio_host = _get_imageio_settings( project_settings, host_name) - # check if host settings are having enabled key and if it is False - if imageio_host.get("enabled") and imageio_host["enabled"] is False: + # check if host settings group is having enabled key + imageio_enabled = imageio_host.get("enabled") + if imageio_enabled is None: + # it it does not have enabled key, use global settings + imageio_enabled = True + + if not imageio_enabled : # if host settings are disabled return False because # it is expected that no colorspace management is needed return False From f6ac8138b3649b4a6ad5239c094b8636c4dbcb88 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Mar 2023 22:30:48 +0200 Subject: [PATCH 173/918] simplification of condition --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 1296bca9b0..411c0afb8a 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2012,7 +2012,7 @@ class WorkfileSettings(object): # get config data if imageio is enabled config_data = None - if imageio_host.get("enabled") and imageio_host["enabled"] is True: + if imageio_host.get("enabled"): # switch ocio config to custom config workfile_settings["OCIO_config"] = "custom" workfile_settings["colorManagement"] = "OCIO" From 6f1f4046d3a9e957cb89087cc5caf84c806f6cdd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Mar 2023 22:48:32 +0200 Subject: [PATCH 174/918] creating global prelaunch hook for OCIO env var --- .../pre_ocio_hook.py} | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) rename openpype/{hosts/fusion/hooks/pre_fusion_ocio_hook.py => hooks/pre_ocio_hook.py} (80%) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py similarity index 80% rename from openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py rename to openpype/hooks/pre_ocio_hook.py index 4dfb4ef223..ff16a8d174 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -4,9 +4,17 @@ from openpype.pipeline.colorspace import get_imageio_config from openpype.pipeline.template_data import get_template_data_with_names -class FusionPreLaunchOCIO(PreLaunchHook): - """Set OCIO environment variable for Fusion""" - app_groups = ["fusion"] +class OCIOEnvHook(PreLaunchHook): + """Set OCIO environment variable for hosts that use OpenColorIO.""" + + order = 0 + app_groups = [ + "fusion", + "blender", + "aftereffects", + "3dsmax", + "houdini" + ] def execute(self): """Hook entry method.""" From f58bcc8abbaf5d2de14ae31d67fe872c2fd74e0a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Mar 2023 23:25:33 +0200 Subject: [PATCH 175/918] flame in line comments --- openpype/hosts/flame/hooks/pre_flame_setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 3249fbfe9a..b5939b168c 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -47,7 +47,10 @@ class FlamePrelaunch(PreLaunchHook): imageio_flame = project_settings["flame"]["imageio"] + # get host imageio settings enabled key if exists colormanaged = imageio_flame.get("enabled") + # if key was not found, set to True + # ensuring backward compatibility if colormanaged is None: colormanaged = True From 62eb6d269b82d19510d30d80a82878fa1042939d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Mar 2023 23:29:45 +0200 Subject: [PATCH 176/918] making docstring more readable --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 411c0afb8a..d183f2fea9 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2005,7 +2005,7 @@ class WorkfileSettings(object): ''' Adds correct colorspace to root Arguments: - imageio_host (dict): adjustments from presets + imageio_host (dict): host colorspace configurations ''' workfile_settings = imageio_host["workfile"] From a644949a29b017f2d38c6697fd73c67e0d11f1c5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 29 Mar 2023 15:21:03 +0800 Subject: [PATCH 177/918] make sure the render dialog is close for the update of resolution and other render settings --- openpype/hosts/max/api/lib.py | 8 +++++++- openpype/hosts/max/plugins/create/create_render.py | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ac7d75db08..519eeffd7f 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -173,10 +173,16 @@ def set_scene_resolution(width: int, height: int): None """ + # make sure the render dialog is closed + # for the update of resolution + # Changing the Render Setup dialog settingsshould be done + # with the actual Render Setup dialog in a closed state. + if rt.renderSceneDialog.isOpen(): + rt.renderSceneDialog.close() + rt.renderWidth = width rt.renderHeight = height - def reset_scene_resolution(): """Apply the scene resolution from the project definition diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 269fff2e32..a8720f464d 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -27,6 +27,13 @@ class CreateRender(plugin.MaxCreator): # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) + # make sure the render dialog is closed + # for the update of resolution + # Changing the Render Setup dialog settings should be done + # with the actual Render Setup dialog in a closed state. + if rt.renderSceneDialog.isOpen(): + rt.renderSceneDialog.close() + # set viewport camera for rendering(mandatory for deadline) RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) From 32743b7a855e1a9df980f923f35da0620adbb237 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 30 Mar 2023 12:48:00 +0100 Subject: [PATCH 178/918] Setup settings. --- .../defaults/project_settings/maya.json | 245 ++-- .../schemas/schema_maya_capture.json | 1258 +++++++++-------- 2 files changed, 770 insertions(+), 733 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e914eb29f9..4044bdf5df 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -788,126 +788,133 @@ "validate_shapes": true }, "ExtractPlayblast": { - "capture_preset": { - "Codec": { - "compression": "png", - "format": "image", - "quality": 95 - }, - "Display Options": { - "background": [ - 125, - 125, - 125, - 255 - ], - "backgroundBottom": [ - 125, - 125, - 125, - 255 - ], - "backgroundTop": [ - 125, - 125, - 125, - 255 - ], - "override_display": true - }, - "Generic": { - "isolate_view": true, - "off_screen": true, - "pan_zoom": false - }, - "Renderer": { - "rendererName": "vp2Renderer" - }, - "Resolution": { - "width": 1920, - "height": 1080 - }, - "Viewport Options": { - "override_viewport_options": true, - "displayLights": "default", - "displayTextures": true, - "textureMaxResolution": 1024, - "renderDepthOfField": true, - "shadows": true, - "twoSidedLighting": true, - "lineAAEnable": true, - "multiSample": 8, - "useDefaultMaterial": false, - "wireframeOnShaded": false, - "xray": false, - "jointXray": false, - "backfaceCulling": false, - "ssaoEnable": false, - "ssaoAmount": 1, - "ssaoRadius": 16, - "ssaoFilterRadius": 16, - "ssaoSamples": 16, - "fogging": false, - "hwFogFalloff": "0", - "hwFogDensity": 0.0, - "hwFogStart": 0, - "hwFogEnd": 100, - "hwFogAlpha": 0, - "hwFogColorR": 1.0, - "hwFogColorG": 1.0, - "hwFogColorB": 1.0, - "motionBlurEnable": false, - "motionBlurSampleCount": 8, - "motionBlurShutterOpenFraction": 0.2, - "cameras": false, - "clipGhosts": false, - "deformers": false, - "dimensions": false, - "dynamicConstraints": false, - "dynamics": false, - "fluids": false, - "follicles": false, - "gpuCacheDisplayFilter": false, - "greasePencils": false, - "grid": false, - "hairSystems": true, - "handles": false, - "headsUpDisplay": false, - "ikHandles": false, - "imagePlane": true, - "joints": false, - "lights": false, - "locators": false, - "manipulators": false, - "motionTrails": false, - "nCloths": false, - "nParticles": false, - "nRigids": false, - "controlVertices": false, - "nurbsCurves": false, - "hulls": false, - "nurbsSurfaces": false, - "particleInstancers": false, - "pivots": false, - "planes": false, - "pluginShapes": false, - "polymeshes": true, - "strokes": false, - "subdivSurfaces": false, - "textures": false - }, - "Camera Options": { - "displayGateMask": false, - "displayResolution": false, - "displayFilmGate": false, - "displayFieldChart": false, - "displaySafeAction": false, - "displaySafeTitle": false, - "displayFilmPivot": false, - "displayFilmOrigin": false, - "overscan": 1.0 + "profiles": [ + { + "task_types": [], + "task_names": [], + "subsets": [], + "capture_preset": { + "Codec": { + "compression": "png", + "format": "image", + "quality": 95 + }, + "Display Options": { + "background": [ + 125, + 125, + 125, + 255 + ], + "backgroundBottom": [ + 125, + 125, + 125, + 255 + ], + "backgroundTop": [ + 125, + 125, + 125, + 255 + ], + "override_display": true + }, + "Generic": { + "isolate_view": true, + "off_screen": true, + "pan_zoom": false + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "width": 1920, + "height": 1080 + }, + "Viewport Options": { + "override_viewport_options": true, + "displayLights": "default", + "displayTextures": true, + "textureMaxResolution": 1024, + "renderDepthOfField": true, + "shadows": true, + "twoSidedLighting": true, + "lineAAEnable": true, + "multiSample": 8, + "useDefaultMaterial": false, + "wireframeOnShaded": false, + "xray": false, + "jointXray": false, + "backfaceCulling": false, + "ssaoEnable": false, + "ssaoAmount": 1, + "ssaoRadius": 16, + "ssaoFilterRadius": 16, + "ssaoSamples": 16, + "fogging": false, + "hwFogFalloff": "0", + "hwFogDensity": 0.0, + "hwFogStart": 0, + "hwFogEnd": 100, + "hwFogAlpha": 0, + "hwFogColorR": 1.0, + "hwFogColorG": 1.0, + "hwFogColorB": 1.0, + "motionBlurEnable": false, + "motionBlurSampleCount": 0, + "motionBlurShutterOpenFraction": 0.2, + "cameras": false, + "clipGhosts": false, + "deformers": false, + "dimensions": false, + "dynamicConstraints": false, + "dynamics": false, + "fluids": false, + "follicles": false, + "gpuCacheDisplayFilter": false, + "greasePencils": false, + "grid": false, + "hairSystems": true, + "handles": false, + "headsUpDisplay": false, + "ikHandles": false, + "imagePlane": true, + "joints": false, + "lights": false, + "locators": false, + "manipulators": false, + "motionTrails": false, + "nCloths": false, + "nParticles": false, + "nRigids": false, + "controlVertices": false, + "nurbsCurves": false, + "hulls": false, + "nurbsSurfaces": false, + "particleInstancers": false, + "pivots": false, + "planes": false, + "pluginShapes": false, + "polymeshes": false, + "strokes": false, + "subdivSurfaces": false, + "textures": false + }, + "Camera Options": { + "displayGateMask": false, + "displayResolution": false, + "displayFilmGate": false, + "displayFieldChart": false, + "displaySafeAction": false, + "displaySafeTitle": false, + "displayFilmPivot": false, + "displayFilmOrigin": false, + "overscan": 1.0 + } + } } - } + ] }, "ExtractMayaSceneRaw": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 416e530db2..1d0f94e5b8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -5,622 +5,652 @@ "label": "Extract Playblast settings", "children": [ { - "type": "dict", - "key": "capture_preset", - "children": [ - { - "type": "dict", - "key": "Codec", - "children": [ - { - "type": "label", - "label": "Codec" - }, - { - "type": "text", - "key": "compression", - "label": "Encoding" - }, - { - "type": "text", - "key": "format", - "label": "Format" - }, - { - "type": "number", - "key": "quality", - "label": "Quality", - "decimal": 0, - "minimum": 0, - "maximum": 100 - }, + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "capture_preset", + "children": [ + { + "type": "dict", + "key": "Codec", + "children": [ + { + "type": "label", + "label": "Codec" + }, + { + "type": "text", + "key": "compression", + "label": "Encoding" + }, + { + "type": "text", + "key": "format", + "label": "Format" + }, + { + "type": "number", + "key": "quality", + "label": "Quality", + "decimal": 0, + "minimum": 0, + "maximum": 100 + }, - { - "type": "splitter" - } - ] - }, - { - "type": "dict", - "key": "Display Options", - "children": [ - { - "type": "label", - "label": "Display Options" - }, + { + "type": "splitter" + } + ] + }, + { + "type": "dict", + "key": "Display Options", + "children": [ + { + "type": "label", + "label": "Display Options" + }, - { - "type": "color", - "key": "background", - "label": "Background Color: " - }, - { - "type": "color", - "key": "backgroundBottom", - "label": "Background Bottom: " - }, - { - "type": "color", - "key": "backgroundTop", - "label": "Background Top: " - }, - { - "type": "boolean", - "key": "override_display", - "label": "Override display options" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Generic", - "children": [ - { - "type": "label", - "label": "Generic" - }, - { - "type": "boolean", - "key": "isolate_view", - "label": " Isolate view" - }, - { - "type": "boolean", - "key": "off_screen", - "label": " Off Screen" - }, - { - "type": "boolean", - "key": "pan_zoom", - "label": " 2D Pan/Zoom" - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "key": "Renderer", - "children": [ - { - "type": "label", - "label": "Renderer" - }, - { - "type": "enum", - "key": "rendererName", - "label": "Renderer name", - "enum_items": [ - { "vp2Renderer": "Viewport 2.0" } - ] - } - ] - }, - { - "type": "dict", - "key": "Resolution", - "children": [ - { - "type": "splitter" - }, - { - "type": "label", - "label": "Resolution" - }, - { - "type": "number", - "key": "width", - "label": " Width", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - }, - { - "type": "number", - "key": "height", - "label": "Height", - "decimal": 0, - "minimum": 0, - "maximum": 99999 - } - ] - }, - { - "type": "splitter" - }, - { - "type": "dict", - "collapsible": true, - "key": "Viewport Options", - "label": "Viewport Options", - "children": [ - { - "type": "boolean", - "key": "override_viewport_options", - "label": "Override Viewport Options" - }, - { - "type": "enum", - "key": "displayLights", - "label": "Display Lights", - "enum_items": [ - { "default": "Default Lighting"}, - { "all": "All Lights"}, - { "selected": "Selected Lights"}, - { "flat": "Flat Lighting"}, - { "nolights": "No Lights"} - ] - }, - { - "type": "boolean", - "key": "displayTextures", - "label": "Display Textures" - }, - { - "type": "number", - "key": "textureMaxResolution", - "label": "Texture Clamp Resolution", - "decimal": 0 - }, - { - "type": "splitter" - }, - { - "type": "label", - "label": "Display" - }, - { - "type":"boolean", - "key": "renderDepthOfField", - "label": "Depth of Field" - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "shadows", - "label": "Display Shadows" - }, - { - "type": "boolean", - "key": "twoSidedLighting", - "label": "Two Sided Lighting" - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "lineAAEnable", - "label": "Enable Anti-Aliasing" - }, - { - "type": "number", - "key": "multiSample", - "label": "Anti Aliasing Samples", - "decimal": 0, - "minimum": 0, - "maximum": 32 - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "useDefaultMaterial", - "label": "Use Default Material" - }, - { - "type": "boolean", - "key": "wireframeOnShaded", - "label": "Wireframe On Shaded" - }, - { - "type": "boolean", - "key": "xray", - "label": "X-Ray" - }, - { - "type": "boolean", - "key": "jointXray", - "label": "X-Ray Joints" - }, - { - "type": "boolean", - "key": "backfaceCulling", - "label": "Backface Culling" - }, - { - "type": "boolean", - "key": "ssaoEnable", - "label": "Screen Space Ambient Occlusion" - }, - { - "type": "number", - "key": "ssaoAmount", - "label": "SSAO Amount" - }, - { - "type": "number", - "key": "ssaoRadius", - "label": "SSAO Radius" - }, - { - "type": "number", - "key": "ssaoFilterRadius", - "label": "SSAO Filter Radius", - "decimal": 0, - "minimum": 1, - "maximum": 32 - }, - { - "type": "number", - "key": "ssaoSamples", - "label": "SSAO Samples", - "decimal": 0, - "minimum": 8, - "maximum": 32 - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "fogging", - "label": "Enable Hardware Fog" - }, - { - "type": "enum", - "key": "hwFogFalloff", - "label": "Hardware Falloff", - "enum_items": [ - { "0": "Linear"}, - { "1": "Exponential"}, - { "2": "Exponential Squared"} - ] - }, - { - "type": "number", - "key": "hwFogDensity", - "label": "Fog Density", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "number", - "key": "hwFogStart", - "label": "Fog Start" - }, - { - "type": "number", - "key": "hwFogEnd", - "label": "Fog End" - }, - { - "type": "number", - "key": "hwFogAlpha", - "label": "Fog Alpha" - }, - { - "type": "number", - "key": "hwFogColorR", - "label": "Fog Color R", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "number", - "key": "hwFogColorG", - "label": "Fog Color G", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "number", - "key": "hwFogColorB", - "label": "Fog Color B", - "decimal": 2, - "minimum": 0, - "maximum": 1 - }, - { - "type": "splitter" - }, - { - "type": "boolean", - "key": "motionBlurEnable", - "label": "Enable Motion Blur" - }, - { - "type": "number", - "key": "motionBlurSampleCount", - "label": "Motion Blur Sample Count", - "decimal": 0, - "minimum": 8, - "maximum": 32 - }, - { - "type": "number", - "key": "motionBlurShutterOpenFraction", - "label": "Shutter Open Fraction", - "decimal": 3, - "minimum": 0.01, - "maximum": 32 - }, - { - "type": "splitter" - }, - { - "type": "label", - "label": "Show" - }, - { - "type": "boolean", - "key": "cameras", - "label": "Cameras" - }, - { - "type": "boolean", - "key": "clipGhosts", - "label": "Clip Ghosts" - }, - { - "type": "boolean", - "key": "deformers", - "label": "Deformers" - }, - { - "type": "boolean", - "key": "dimensions", - "label": "Dimensions" - }, - { - "type": "boolean", - "key": "dynamicConstraints", - "label": "Dynamic Constraints" - }, - { - "type": "boolean", - "key": "dynamics", - "label": "Dynamics" - }, - { - "type": "boolean", - "key": "fluids", - "label": "Fluids" - }, - { - "type": "boolean", - "key": "follicles", - "label": "Follicles" - }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "GPU Cache" - }, - { - "type": "boolean", - "key": "greasePencils", - "label": "Grease Pencil" - }, - { - "type": "boolean", - "key": "grid", - "label": "Grid" - }, - { - "type": "boolean", - "key": "hairSystems", - "label": "Hair Systems" - }, - { - "type": "boolean", - "key": "handles", - "label": "Handles" - }, - { - "type": "boolean", - "key": "headsUpDisplay", - "label": "HUD" - }, - { - "type": "boolean", - "key": "ikHandles", - "label": "IK Handles" - }, - { - "type": "boolean", - "key": "imagePlane", - "label": "Image Planes" - }, - { - "type": "boolean", - "key": "joints", - "label": "Joints" - }, - { - "type": "boolean", - "key": "lights", - "label": "Lights" - }, - { - "type": "boolean", - "key": "locators", - "label": "Locators" - }, - { - "type": "boolean", - "key": "manipulators", - "label": "Manipulators" - }, - { - "type": "boolean", - "key": "motionTrails", - "label": "Motion Trails" - }, - { - "type": "boolean", - "key": "nCloths", - "label": "nCloths" - }, - { - "type": "boolean", - "key": "nParticles", - "label": "nParticles" - }, - { - "type": "boolean", - "key": "nRigids", - "label": "nRigids" - }, - { - "type": "boolean", - "key": "controlVertices", - "label": "NURBS CVs" - }, - { - "type": "boolean", - "key": "nurbsCurves", - "label": "NURBS Curves" - }, - { - "type": "boolean", - "key": "hulls", - "label": "NURBS Hulls" - }, - { - "type": "boolean", - "key": "nurbsSurfaces", - "label": "NURBS Surfaces" - }, - { - "type": "boolean", - "key": "particleInstancers", - "label": "Particle Instancers" - }, - { - "type": "boolean", - "key": "pivots", - "label": "Pivots" - }, - { - "type": "boolean", - "key": "planes", - "label": "Planes" - }, - { - "type": "boolean", - "key": "pluginShapes", - "label": "Plugin Shapes" - }, - { - "type": "boolean", - "key": "polymeshes", - "label": "Polygons" - }, - { - "type": "boolean", - "key": "strokes", - "label": "Strokes" - }, - { - "type": "boolean", - "key": "subdivSurfaces", - "label": "Subdiv Surfaces" - }, - { - "type": "boolean", - "key": "textures", - "label": "Texture Placements" - } - ] - }, - { - "type": "dict", - "collapsible": true, - "key": "Camera Options", - "label": "Camera Options", - "children": [ - { - "type": "boolean", - "key": "displayGateMask", - "label": "Display Gate Mask" - }, - { - "type": "boolean", - "key": "displayResolution", - "label": "Display Resolution" - }, - { - "type": "boolean", - "key": "displayFilmGate", - "label": "Display Film Gate" - }, - { - "type": "boolean", - "key": "displayFieldChart", - "label": "Display Field Chart" - }, - { - "type": "boolean", - "key": "displaySafeAction", - "label": "Display Safe Action" - }, - { - "type": "boolean", - "key": "displaySafeTitle", - "label": "Display Safe Title" - }, - { - "type": "boolean", - "key": "displayFilmPivot", - "label": "Display Film Pivot" - }, - { - "type": "boolean", - "key": "displayFilmOrigin", - "label": "Display Film Origin" - }, - { - "type": "number", - "key": "overscan", - "label": "Overscan", - "decimal": 1, - "minimum": 0, - "maximum": 10 - } - ] - } - ] + { + "type": "color", + "key": "background", + "label": "Background Color: " + }, + { + "type": "color", + "key": "backgroundBottom", + "label": "Background Bottom: " + }, + { + "type": "color", + "key": "backgroundTop", + "label": "Background Top: " + }, + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Generic", + "children": [ + { + "type": "label", + "label": "Generic" + }, + { + "type": "boolean", + "key": "isolate_view", + "label": " Isolate view" + }, + { + "type": "boolean", + "key": "off_screen", + "label": " Off Screen" + }, + { + "type": "boolean", + "key": "pan_zoom", + "label": " 2D Pan/Zoom" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Renderer", + "children": [ + { + "type": "label", + "label": "Renderer" + }, + { + "type": "enum", + "key": "rendererName", + "label": "Renderer name", + "enum_items": [ + { "vp2Renderer": "Viewport 2.0" } + ] + } + ] + }, + { + "type": "dict", + "key": "Resolution", + "children": [ + { + "type": "splitter" + }, + { + "type": "label", + "label": "Resolution" + }, + { + "type": "number", + "key": "width", + "label": " Width", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "height", + "label": "Height", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "collapsible": true, + "key": "Viewport Options", + "label": "Viewport Options", + "children": [ + { + "type": "boolean", + "key": "override_viewport_options", + "label": "Override Viewport Options" + }, + { + "type": "enum", + "key": "displayLights", + "label": "Display Lights", + "enum_items": [ + { "default": "Default Lighting"}, + { "all": "All Lights"}, + { "selected": "Selected Lights"}, + { "flat": "Flat Lighting"}, + { "nolights": "No Lights"} + ] + }, + { + "type": "boolean", + "key": "displayTextures", + "label": "Display Textures" + }, + { + "type": "number", + "key": "textureMaxResolution", + "label": "Texture Clamp Resolution", + "decimal": 0 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Display" + }, + { + "type":"boolean", + "key": "renderDepthOfField", + "label": "Depth of Field" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "shadows", + "label": "Display Shadows" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "Two Sided Lighting" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "lineAAEnable", + "label": "Enable Anti-Aliasing" + }, + { + "type": "number", + "key": "multiSample", + "label": "Anti Aliasing Samples", + "decimal": 0, + "minimum": 0, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "useDefaultMaterial", + "label": "Use Default Material" + }, + { + "type": "boolean", + "key": "wireframeOnShaded", + "label": "Wireframe On Shaded" + }, + { + "type": "boolean", + "key": "xray", + "label": "X-Ray" + }, + { + "type": "boolean", + "key": "jointXray", + "label": "X-Ray Joints" + }, + { + "type": "boolean", + "key": "backfaceCulling", + "label": "Backface Culling" + }, + { + "type": "boolean", + "key": "ssaoEnable", + "label": "Screen Space Ambient Occlusion" + }, + { + "type": "number", + "key": "ssaoAmount", + "label": "SSAO Amount" + }, + { + "type": "number", + "key": "ssaoRadius", + "label": "SSAO Radius" + }, + { + "type": "number", + "key": "ssaoFilterRadius", + "label": "SSAO Filter Radius", + "decimal": 0, + "minimum": 1, + "maximum": 32 + }, + { + "type": "number", + "key": "ssaoSamples", + "label": "SSAO Samples", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "fogging", + "label": "Enable Hardware Fog" + }, + { + "type": "enum", + "key": "hwFogFalloff", + "label": "Hardware Falloff", + "enum_items": [ + { "0": "Linear"}, + { "1": "Exponential"}, + { "2": "Exponential Squared"} + ] + }, + { + "type": "number", + "key": "hwFogDensity", + "label": "Fog Density", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogStart", + "label": "Fog Start" + }, + { + "type": "number", + "key": "hwFogEnd", + "label": "Fog End" + }, + { + "type": "number", + "key": "hwFogAlpha", + "label": "Fog Alpha" + }, + { + "type": "number", + "key": "hwFogColorR", + "label": "Fog Color R", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorG", + "label": "Fog Color G", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorB", + "label": "Fog Color B", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "motionBlurEnable", + "label": "Enable Motion Blur" + }, + { + "type": "number", + "key": "motionBlurSampleCount", + "label": "Motion Blur Sample Count", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "number", + "key": "motionBlurShutterOpenFraction", + "label": "Shutter Open Fraction", + "decimal": 3, + "minimum": 0.01, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Show" + }, + { + "type": "boolean", + "key": "cameras", + "label": "Cameras" + }, + { + "type": "boolean", + "key": "clipGhosts", + "label": "Clip Ghosts" + }, + { + "type": "boolean", + "key": "deformers", + "label": "Deformers" + }, + { + "type": "boolean", + "key": "dimensions", + "label": "Dimensions" + }, + { + "type": "boolean", + "key": "dynamicConstraints", + "label": "Dynamic Constraints" + }, + { + "type": "boolean", + "key": "dynamics", + "label": "Dynamics" + }, + { + "type": "boolean", + "key": "fluids", + "label": "Fluids" + }, + { + "type": "boolean", + "key": "follicles", + "label": "Follicles" + }, + { + "type": "boolean", + "key": "gpuCacheDisplayFilter", + "label": "GPU Cache" + }, + { + "type": "boolean", + "key": "greasePencils", + "label": "Grease Pencil" + }, + { + "type": "boolean", + "key": "grid", + "label": "Grid" + }, + { + "type": "boolean", + "key": "hairSystems", + "label": "Hair Systems" + }, + { + "type": "boolean", + "key": "handles", + "label": "Handles" + }, + { + "type": "boolean", + "key": "headsUpDisplay", + "label": "HUD" + }, + { + "type": "boolean", + "key": "ikHandles", + "label": "IK Handles" + }, + { + "type": "boolean", + "key": "imagePlane", + "label": "Image Planes" + }, + { + "type": "boolean", + "key": "joints", + "label": "Joints" + }, + { + "type": "boolean", + "key": "lights", + "label": "Lights" + }, + { + "type": "boolean", + "key": "locators", + "label": "Locators" + }, + { + "type": "boolean", + "key": "manipulators", + "label": "Manipulators" + }, + { + "type": "boolean", + "key": "motionTrails", + "label": "Motion Trails" + }, + { + "type": "boolean", + "key": "nCloths", + "label": "nCloths" + }, + { + "type": "boolean", + "key": "nParticles", + "label": "nParticles" + }, + { + "type": "boolean", + "key": "nRigids", + "label": "nRigids" + }, + { + "type": "boolean", + "key": "controlVertices", + "label": "NURBS CVs" + }, + { + "type": "boolean", + "key": "nurbsCurves", + "label": "NURBS Curves" + }, + { + "type": "boolean", + "key": "hulls", + "label": "NURBS Hulls" + }, + { + "type": "boolean", + "key": "nurbsSurfaces", + "label": "NURBS Surfaces" + }, + { + "type": "boolean", + "key": "particleInstancers", + "label": "Particle Instancers" + }, + { + "type": "boolean", + "key": "pivots", + "label": "Pivots" + }, + { + "type": "boolean", + "key": "planes", + "label": "Planes" + }, + { + "type": "boolean", + "key": "pluginShapes", + "label": "Plugin Shapes" + }, + { + "type": "boolean", + "key": "polymeshes", + "label": "Polygons" + }, + { + "type": "boolean", + "key": "strokes", + "label": "Strokes" + }, + { + "type": "boolean", + "key": "subdivSurfaces", + "label": "Subdiv Surfaces" + }, + { + "type": "boolean", + "key": "textures", + "label": "Texture Placements" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Camera Options", + "label": "Camera Options", + "children": [ + { + "type": "boolean", + "key": "displayGateMask", + "label": "Display Gate Mask" + }, + { + "type": "boolean", + "key": "displayResolution", + "label": "Display Resolution" + }, + { + "type": "boolean", + "key": "displayFilmGate", + "label": "Display Film Gate" + }, + { + "type": "boolean", + "key": "displayFieldChart", + "label": "Display Field Chart" + }, + { + "type": "boolean", + "key": "displaySafeAction", + "label": "Display Safe Action" + }, + { + "type": "boolean", + "key": "displaySafeTitle", + "label": "Display Safe Title" + }, + { + "type": "boolean", + "key": "displayFilmPivot", + "label": "Display Film Pivot" + }, + { + "type": "boolean", + "key": "displayFilmOrigin", + "label": "Display Film Origin" + }, + { + "type": "number", + "key": "overscan", + "label": "Overscan", + "decimal": 1, + "minimum": 0, + "maximum": 10 + } + ] + } + ] + } + ] + } } ] } From 0079322a6de30b70108dd5103bb04403a0730e56 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 31 Mar 2023 16:25:51 +0800 Subject: [PATCH 179/918] close the render setup dialog before the render settings --- openpype/hosts/max/api/lib_rendersettings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 4940265a23..e7f4ee1e6b 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -105,6 +105,9 @@ class RenderSettings(object): rt.rendSaveFile = True + if rt.renderSceneDialog.isOpen(): + rt.renderSceneDialog.close() + def arnold_setup(self): # get Arnold RenderView run in the background # for setting up renderable camera From ee8b5e770013b42553b3c3e32aec44d37479728f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 31 Mar 2023 16:28:41 +0800 Subject: [PATCH 180/918] cleanup --- openpype/hosts/max/plugins/create/create_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index a8720f464d..68ae5eac72 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -31,8 +31,6 @@ class CreateRender(plugin.MaxCreator): # for the update of resolution # Changing the Render Setup dialog settings should be done # with the actual Render Setup dialog in a closed state. - if rt.renderSceneDialog.isOpen(): - rt.renderSceneDialog.close() # set viewport camera for rendering(mandatory for deadline) RenderSettings().set_render_camera(sel_obj) From 2278478598dbda1b465f2d9108ce27106d807e21 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Sun, 2 Apr 2023 08:25:06 +0100 Subject: [PATCH 181/918] Update openpype/hosts/maya/plugins/publish/collect_review.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/collect_review.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index d15eb7a12b..a184865602 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -143,4 +143,8 @@ class CollectReview(pyblish.api.InstancePlugin): # Convert enum attribute index to string. index = instance.data.get("displayLights", 0) - instance.data["displayLights"] = lib.DISPLAY_LIGHTS[index] + display_lights = lib.DISPLAY_LIGHTS[index] + if display_lights == "project_settings": + # project_settings/maya/publish/ExtractPlayblast/capture_preset/Viewport Options/displayLights + display_lights = instance.context.data["project_settings"]["maya"]["publish"]["ExtractPlayblast"]["capture_preset"]["Viewport Options"]["displayLights"] # noqa + instance.data["displayLights"] = display_lights From b9e9750377d44836c94013f2222422bca736c644 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Sun, 2 Apr 2023 08:25:25 +0100 Subject: [PATCH 182/918] Update openpype/hosts/maya/plugins/publish/extract_playblast.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 801f05a770..2167f2c5b3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -95,10 +95,8 @@ class ExtractPlayblast(publish.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) - # Show lighting mode. - display_lights = instance.data["displayLights"] - if display_lights != "project_settings": - preset["viewport_options"]["displayLights"] = display_lights + # Use displayLights setting from instance + preset["viewport_options"]["displayLights"] = instance.data["displayLights"] # Override transparency if requested. transparency = instance.data.get("transparency", 0) From 3ebac0b326e4f06aaa5a396554e4fff16df5efeb Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Sun, 2 Apr 2023 08:25:34 +0100 Subject: [PATCH 183/918] Update openpype/hosts/maya/plugins/publish/extract_thumbnail.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 79c768228f..92d0141f01 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -105,11 +105,8 @@ class ExtractThumbnail(publish.Extractor): pm.currentTime(refreshFrameInt - 1, edit=True) pm.currentTime(refreshFrameInt, edit=True) - # Show lighting mode. - display_lights = instance.data["displayLights"] - if display_lights != "project_settings": - preset["viewport_options"]["displayLights"] = display_lights - + # Use displayLights setting from instance + preset["viewport_options"]["displayLights"] = instance.data["displayLights"] # Override transparency if requested. transparency = instance.data.get("transparency", 0) if transparency != 0: From 2e234a84dc791bf3452fb9e0610a23bda3cec233 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sun, 2 Apr 2023 08:34:26 +0100 Subject: [PATCH 184/918] Hound --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0381a8adc1..f790d08ae3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -117,7 +117,8 @@ class ExtractPlayblast(publish.Extractor): pm.currentTime(refreshFrameInt, edit=True) # Use displayLights setting from instance - preset["viewport_options"]["displayLights"] = instance.data["displayLights"] + key = "displayLights" + preset["viewport_options"][key] = instance.data[key] # Override transparency if requested. transparency = instance.data.get("transparency", 0) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 430322c911..d66f65ce88 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -106,7 +106,9 @@ class ExtractThumbnail(publish.Extractor): pm.currentTime(refreshFrameInt, edit=True) # Use displayLights setting from instance - preset["viewport_options"]["displayLights"] = instance.data["displayLights"] + key = "displayLights" + preset["viewport_options"][key] = instance.data[key] + # Override transparency if requested. transparency = instance.data.get("transparency", 0) if transparency != 0: From 14b8139a5cfe92680233343d8e4120ae2253865f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Apr 2023 16:47:15 +0800 Subject: [PATCH 185/918] Roy's comment & fix the loader update --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 30879bca78..fd79a2b97c 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -35,7 +35,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): container.name = name rs_proxy.Parent = container - asset = rt.getNodeByName(f"{name}") + asset = rt.getNodeByName(name) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -45,10 +45,10 @@ class RedshiftProxyLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - - proxy_objects = self.get_container_children(node) - for proxy in proxy_objects: - proxy.source = path + for children in node.Children: + children_node = rt.getNodeByName(children.name) + for proxy in children_node.Children: + proxy.file = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) From 6423479078a7305d9e73f55243eb42796acb7820 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Apr 2023 16:50:33 +0800 Subject: [PATCH 186/918] add switch version in the loader --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index fd79a2b97c..9451e5299b 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -54,6 +54,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): "representation": str(representation["_id"]) }) + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): from pymxs import runtime as rt From 76c0a0266f9ea976d992718dc0c3a4a3ca0c62c3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 11:59:23 +0200 Subject: [PATCH 187/918] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- .../hosts/substancepainter/plugins/load/load_mesh.py | 4 ++-- .../plugins/publish/collect_textureset_images.py | 12 ++++++------ .../publish/collect_workfile_representation.py | 10 +++++----- .../plugins/publish/extract_textures.py | 2 +- .../plugins/publish/save_workfile.py | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 4e800bd623..a93b830de0 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -62,7 +62,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa print("Reload succeeded") else: - raise RuntimeError("Reload of mesh failed") + raise LoadError("Reload of mesh failed") path = self.fname substance_painter.project.reload_mesh(path, @@ -105,7 +105,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: print("Reload succeeded") else: - raise RuntimeError("Reload of mesh failed") + raise LoaderError("Reload of mesh failed") substance_painter.project.reload_mesh(path, settings, on_mesh_reload) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 14168138b6..56694614eb 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -19,7 +19,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # TODO: Detect what source data channels end up in each file label = "Collect Texture Set images" - hosts = ['substancepainter'] + hosts = ["substancepainter"] families = ["textureSet"] order = pyblish.api.CollectorOrder @@ -55,7 +55,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): first_filepath = outputs[0]["filepath"] fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] - assert ext.lstrip('.'), f"No extension: {ext}" + assert ext.lstrip("."), f"No extension: {ext}" map_identifier = strip_template(template) # Define the suffix we want to give this particular texture @@ -78,9 +78,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Prepare representation representation = { - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': fnames if len(fnames) > 1 else fnames[0], + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": fnames if len(fnames) > 1 else fnames[0], } # Mark as UDIM explicitly if it has UDIM tiles. @@ -105,7 +105,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): image_instance.data["subset"] = image_subset image_instance.data["family"] = "image" image_instance.data["families"] = ["image", "textures"] - image_instance.data['representations'] = [representation] + image_instance.data["representations"] = [representation] # Group the textures together in the loader image_instance.data["subsetGroup"] = instance.data["subset"] diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py index 563c2d4c07..8d98d0b014 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_workfile_representation.py @@ -7,7 +7,7 @@ class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder label = "Workfile representation" - hosts = ['substancepainter'] + hosts = ["substancepainter"] families = ["workfile"] def process(self, instance): @@ -18,9 +18,9 @@ class CollectWorkfileRepresentation(pyblish.api.InstancePlugin): folder, file = os.path.split(current_file) filename, ext = os.path.splitext(file) - instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': file, + instance.data["representations"] = [{ + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": file, "stagingDir": folder, }] diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index bd933610f4..b9654947db 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -15,7 +15,7 @@ class ExtractTextures(publish.Extractor, """ label = "Extract Texture Set" - hosts = ['substancepainter'] + hosts = ["substancepainter"] families = ["textureSet"] # Run before thumbnail extractors diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index f19deccb0e..4874b5e5c7 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -16,7 +16,7 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): def process(self, context): host = registered_host() - if context.data['currentFile'] != host.get_current_workfile(): + if context.data["currentFile"] != host.get_current_workfile(): raise KnownPublishError("Workfile has changed during publishing!") if host.has_unsaved_changes(): From 35428df6b0942e779a0bbaa50578e0c0fbfa2921 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 12:00:51 +0200 Subject: [PATCH 188/918] Fix LoadError --- openpype/hosts/substancepainter/plugins/load/load_mesh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index a93b830de0..2450a9316e 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -2,6 +2,7 @@ from openpype.pipeline import ( load, get_representation_path, ) +from openpype.pipeline.load import LoadError from openpype.hosts.substancepainter.api.pipeline import ( imprint_container, set_container_metadata, @@ -105,7 +106,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: print("Reload succeeded") else: - raise LoaderError("Reload of mesh failed") + raise LoadError("Reload of mesh failed") substance_painter.project.reload_mesh(path, settings, on_mesh_reload) From 5c0dee53188e12b7ddb8eec364495596b36de29c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 12:01:24 +0200 Subject: [PATCH 189/918] Log instead of print --- openpype/hosts/substancepainter/plugins/load/load_mesh.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/load/load_mesh.py b/openpype/hosts/substancepainter/plugins/load/load_mesh.py index 2450a9316e..822095641d 100644 --- a/openpype/hosts/substancepainter/plugins/load/load_mesh.py +++ b/openpype/hosts/substancepainter/plugins/load/load_mesh.py @@ -61,7 +61,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa - print("Reload succeeded") + self.log.info("Reload succeeded") else: raise LoadError("Reload of mesh failed") @@ -104,7 +104,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): if status == substance_painter.project.ReloadMeshStatus.SUCCESS: - print("Reload succeeded") + self.log.info("Reload succeeded") else: raise LoadError("Reload of mesh failed") From 4300939199f9cfcd4626c0bcbdafdf5a05926649 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 12:17:48 +0200 Subject: [PATCH 190/918] Allow formatting shelf path using anatomy data --- .../hosts/substancepainter/api/pipeline.py | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/substancepainter/api/pipeline.py b/openpype/hosts/substancepainter/api/pipeline.py index b995c9030d..9406fb8edb 100644 --- a/openpype/hosts/substancepainter/api/pipeline.py +++ b/openpype/hosts/substancepainter/api/pipeline.py @@ -9,17 +9,23 @@ import substance_painter.ui import substance_painter.event import substance_painter.project -from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost -from openpype.settings import get_current_project_settings - import pyblish.api +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost +from openpype.settings import ( + get_current_project_settings, + get_system_settings +) + +from openpype.pipeline.template_data import get_template_data_with_names from openpype.pipeline import ( register_creator_plugin_path, register_loader_plugin_path, - AVALON_CONTAINER_ID + AVALON_CONTAINER_ID, + Anatomy ) from openpype.lib import ( + StringTemplate, register_event_callback, emit_event, ) @@ -234,9 +240,32 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def _install_shelves(self, project_settings): shelves = project_settings["substancepainter"].get("shelves", {}) + if not shelves: + return + + # Prepare formatting data if we detect any path which might have + # template tokens like {asset} in there. + formatting_data = {} + has_formatting_entries = any("{" in path for path in shelves.values()) + if has_formatting_entries: + project_name = self.get_current_project_name() + asset_name = self.get_current_asset_name() + task_name = self.get_current_asset_name() + system_settings = get_system_settings() + formatting_data = get_template_data_with_names(project_name, + asset_name, + task_name, + system_settings) + anatomy = Anatomy(project_name) + formatting_data["root"] = anatomy.roots + for name, path in shelves.items(): - # TODO: Allow formatting with anatomy for the paths shelf_name = None + + # Allow formatting with anatomy for the paths + if "{" in path: + path = StringTemplate.format_template(path, formatting_data) + try: shelf_name = lib.load_shelf(path, name=name) except ValueError as exc: From 87dc14fe9e91c202d4eefa82f85093a4a2814c76 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 3 Apr 2023 11:59:16 +0100 Subject: [PATCH 191/918] Default values for profiles. --- .../defaults/project_settings/maya.json | 6 +- openpype/settings/entities/color_entity.py | 6 +- openpype/settings/entities/input_entities.py | 4 +- .../schemas/schema_maya_capture.json | 267 ++++++++++++------ 4 files changed, 188 insertions(+), 95 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 4044bdf5df..f6162182e8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -829,8 +829,8 @@ "rendererName": "vp2Renderer" }, "Resolution": { - "width": 1920, - "height": 1080 + "width": 0, + "height": 0 }, "Viewport Options": { "override_viewport_options": true, @@ -896,7 +896,7 @@ "pivots": false, "planes": false, "pluginShapes": false, - "polymeshes": false, + "polymeshes": true, "strokes": false, "subdivSurfaces": false, "textures": false diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index bdaab6f583..a542f2fa38 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -11,7 +11,9 @@ class ColorEntity(InputEntity): def _item_initialization(self): self.valid_value_types = (list, ) - self.value_on_not_set = [0, 0, 0, 255] + self.value_on_not_set = self.convert_to_valid_type( + self.schema_data.get("default", [0, 0, 0, 255]) + ) self.use_alpha = self.schema_data.get("use_alpha", True) def set_override_state(self, *args, **kwargs): @@ -64,6 +66,6 @@ class ColorEntity(InputEntity): new_value.append(item) # Make sure - if not self.use_alpha: + if hasattr(self, "use_alpha") and not self.use_alpha: new_value[3] = 255 return new_value diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 89f12afd9b..842117ad48 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -442,7 +442,9 @@ class TextEntity(InputEntity): def _item_initialization(self): self.valid_value_types = (STRING_TYPE, ) - self.value_on_not_set = "" + self.value_on_not_set = self.convert_to_valid_type( + self.schema_data.get("default", "") + ) # GUI attributes self.multiline = self.schema_data.get("multiline", False) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1d0f94e5b8..beaa7c442d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -46,12 +46,14 @@ { "type": "text", "key": "compression", - "label": "Encoding" + "label": "Encoding", + "default": "png" }, { "type": "text", "key": "format", - "label": "Format" + "label": "Format", + "default": "image" }, { "type": "number", @@ -59,7 +61,8 @@ "label": "Quality", "decimal": 0, "minimum": 0, - "maximum": 100 + "maximum": 100, + "default": 95 }, { @@ -79,22 +82,26 @@ { "type": "color", "key": "background", - "label": "Background Color: " + "label": "Background Color: ", + "default": [125, 125, 125, 255] }, { "type": "color", "key": "backgroundBottom", - "label": "Background Bottom: " + "label": "Background Bottom: ", + "default": [125, 125, 125, 255] }, { "type": "color", "key": "backgroundTop", - "label": "Background Top: " + "label": "Background Top: ", + "default": [125, 125, 125, 255] }, { "type": "boolean", "key": "override_display", - "label": "Override display options" + "label": "Override display options", + "default": true } ] }, @@ -112,17 +119,20 @@ { "type": "boolean", "key": "isolate_view", - "label": " Isolate view" + "label": " Isolate view", + "default": true }, { "type": "boolean", "key": "off_screen", - "label": " Off Screen" + "label": " Off Screen", + "default": true }, { "type": "boolean", "key": "pan_zoom", - "label": " 2D Pan/Zoom" + "label": " 2D Pan/Zoom", + "default": false } ] }, @@ -143,7 +153,8 @@ "label": "Renderer name", "enum_items": [ { "vp2Renderer": "Viewport 2.0" } - ] + ], + "default": "vp2Renderer" } ] }, @@ -164,7 +175,8 @@ "label": " Width", "decimal": 0, "minimum": 0, - "maximum": 99999 + "maximum": 99999, + "default": 0 }, { "type": "number", @@ -172,7 +184,8 @@ "label": "Height", "decimal": 0, "minimum": 0, - "maximum": 99999 + "maximum": 99999, + "default": 0 } ] }, @@ -188,7 +201,8 @@ { "type": "boolean", "key": "override_viewport_options", - "label": "Override Viewport Options" + "label": "Override Viewport Options", + "default": true }, { "type": "enum", @@ -200,18 +214,21 @@ { "selected": "Selected Lights"}, { "flat": "Flat Lighting"}, { "nolights": "No Lights"} - ] + ], + "default": "default" }, { "type": "boolean", "key": "displayTextures", - "label": "Display Textures" + "label": "Display Textures", + "default": true }, { "type": "number", "key": "textureMaxResolution", "label": "Texture Clamp Resolution", - "decimal": 0 + "decimal": 0, + "default": 1024 }, { "type": "splitter" @@ -223,7 +240,8 @@ { "type":"boolean", "key": "renderDepthOfField", - "label": "Depth of Field" + "label": "Depth of Field", + "default": true }, { "type": "splitter" @@ -231,12 +249,14 @@ { "type": "boolean", "key": "shadows", - "label": "Display Shadows" + "label": "Display Shadows", + "default": true }, { "type": "boolean", "key": "twoSidedLighting", - "label": "Two Sided Lighting" + "label": "Two Sided Lighting", + "default": true }, { "type": "splitter" @@ -244,7 +264,8 @@ { "type": "boolean", "key": "lineAAEnable", - "label": "Enable Anti-Aliasing" + "label": "Enable Anti-Aliasing", + "default": true }, { "type": "number", @@ -252,7 +273,8 @@ "label": "Anti Aliasing Samples", "decimal": 0, "minimum": 0, - "maximum": 32 + "maximum": 32, + "default": 8 }, { "type": "splitter" @@ -260,42 +282,50 @@ { "type": "boolean", "key": "useDefaultMaterial", - "label": "Use Default Material" + "label": "Use Default Material", + "default": false }, { "type": "boolean", "key": "wireframeOnShaded", - "label": "Wireframe On Shaded" + "label": "Wireframe On Shaded", + "default": false }, { "type": "boolean", "key": "xray", - "label": "X-Ray" + "label": "X-Ray", + "default": false }, { "type": "boolean", "key": "jointXray", - "label": "X-Ray Joints" + "label": "X-Ray Joints", + "default": false }, { "type": "boolean", "key": "backfaceCulling", - "label": "Backface Culling" + "label": "Backface Culling", + "default": false }, { "type": "boolean", "key": "ssaoEnable", - "label": "Screen Space Ambient Occlusion" + "label": "Screen Space Ambient Occlusion", + "default": false }, { "type": "number", "key": "ssaoAmount", - "label": "SSAO Amount" + "label": "SSAO Amount", + "default": 1 }, { "type": "number", "key": "ssaoRadius", - "label": "SSAO Radius" + "label": "SSAO Radius", + "default": 16 }, { "type": "number", @@ -303,7 +333,8 @@ "label": "SSAO Filter Radius", "decimal": 0, "minimum": 1, - "maximum": 32 + "maximum": 32, + "default": 16 }, { "type": "number", @@ -311,7 +342,8 @@ "label": "SSAO Samples", "decimal": 0, "minimum": 8, - "maximum": 32 + "maximum": 32, + "default": 16 }, { "type": "splitter" @@ -319,7 +351,8 @@ { "type": "boolean", "key": "fogging", - "label": "Enable Hardware Fog" + "label": "Enable Hardware Fog", + "default": false }, { "type": "enum", @@ -329,7 +362,8 @@ { "0": "Linear"}, { "1": "Exponential"}, { "2": "Exponential Squared"} - ] + ], + "default": "0" }, { "type": "number", @@ -337,22 +371,26 @@ "label": "Fog Density", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 0 }, { "type": "number", "key": "hwFogStart", - "label": "Fog Start" + "label": "Fog Start", + "default": 0 }, { "type": "number", "key": "hwFogEnd", - "label": "Fog End" + "label": "Fog End", + "default": 100 }, { "type": "number", "key": "hwFogAlpha", - "label": "Fog Alpha" + "label": "Fog Alpha", + "default": 0 }, { "type": "number", @@ -360,7 +398,8 @@ "label": "Fog Color R", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 1 }, { "type": "number", @@ -368,7 +407,8 @@ "label": "Fog Color G", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 1 }, { "type": "number", @@ -376,7 +416,8 @@ "label": "Fog Color B", "decimal": 2, "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 1 }, { "type": "splitter" @@ -384,7 +425,8 @@ { "type": "boolean", "key": "motionBlurEnable", - "label": "Enable Motion Blur" + "label": "Enable Motion Blur", + "default": false }, { "type": "number", @@ -392,7 +434,8 @@ "label": "Motion Blur Sample Count", "decimal": 0, "minimum": 8, - "maximum": 32 + "maximum": 32, + "default": 8 }, { "type": "number", @@ -400,7 +443,8 @@ "label": "Shutter Open Fraction", "decimal": 3, "minimum": 0.01, - "maximum": 32 + "maximum": 32, + "default": 0.2 }, { "type": "splitter" @@ -412,182 +456,218 @@ { "type": "boolean", "key": "cameras", - "label": "Cameras" + "label": "Cameras", + "default": false }, { "type": "boolean", "key": "clipGhosts", - "label": "Clip Ghosts" + "label": "Clip Ghosts", + "default": false }, { "type": "boolean", "key": "deformers", - "label": "Deformers" + "label": "Deformers", + "default": false }, { "type": "boolean", "key": "dimensions", - "label": "Dimensions" + "label": "Dimensions", + "default": false }, { "type": "boolean", "key": "dynamicConstraints", - "label": "Dynamic Constraints" + "label": "Dynamic Constraints", + "default": false }, { "type": "boolean", "key": "dynamics", - "label": "Dynamics" + "label": "Dynamics", + "default": false }, { "type": "boolean", "key": "fluids", - "label": "Fluids" + "label": "Fluids", + "default": false }, { "type": "boolean", "key": "follicles", - "label": "Follicles" + "label": "Follicles", + "default": false }, { "type": "boolean", "key": "gpuCacheDisplayFilter", - "label": "GPU Cache" + "label": "GPU Cache", + "default": false }, { "type": "boolean", "key": "greasePencils", - "label": "Grease Pencil" + "label": "Grease Pencil", + "default": false }, { "type": "boolean", "key": "grid", - "label": "Grid" + "label": "Grid", + "default": false }, { "type": "boolean", "key": "hairSystems", - "label": "Hair Systems" + "label": "Hair Systems", + "default": true }, { "type": "boolean", "key": "handles", - "label": "Handles" + "label": "Handles", + "default": false }, { "type": "boolean", "key": "headsUpDisplay", - "label": "HUD" + "label": "HUD", + "default": false }, { "type": "boolean", "key": "ikHandles", - "label": "IK Handles" + "label": "IK Handles", + "default": false }, { "type": "boolean", "key": "imagePlane", - "label": "Image Planes" + "label": "Image Planes", + "default": true }, { "type": "boolean", "key": "joints", - "label": "Joints" + "label": "Joints", + "default": false }, { "type": "boolean", "key": "lights", - "label": "Lights" + "label": "Lights", + "default": false }, { "type": "boolean", "key": "locators", - "label": "Locators" + "label": "Locators", + "default": false }, { "type": "boolean", "key": "manipulators", - "label": "Manipulators" + "label": "Manipulators", + "default": false }, { "type": "boolean", "key": "motionTrails", - "label": "Motion Trails" + "label": "Motion Trails", + "default": false }, { "type": "boolean", "key": "nCloths", - "label": "nCloths" + "label": "nCloths", + "default": false }, { "type": "boolean", "key": "nParticles", - "label": "nParticles" + "label": "nParticles", + "default": false }, { "type": "boolean", "key": "nRigids", - "label": "nRigids" + "label": "nRigids", + "default": false }, { "type": "boolean", "key": "controlVertices", - "label": "NURBS CVs" + "label": "NURBS CVs", + "default": false }, { "type": "boolean", "key": "nurbsCurves", - "label": "NURBS Curves" + "label": "NURBS Curves", + "default": false }, { "type": "boolean", "key": "hulls", - "label": "NURBS Hulls" + "label": "NURBS Hulls", + "default": false }, { "type": "boolean", "key": "nurbsSurfaces", - "label": "NURBS Surfaces" + "label": "NURBS Surfaces", + "default": false }, { "type": "boolean", "key": "particleInstancers", - "label": "Particle Instancers" + "label": "Particle Instancers", + "default": false }, { "type": "boolean", "key": "pivots", - "label": "Pivots" + "label": "Pivots", + "default": false }, { "type": "boolean", "key": "planes", - "label": "Planes" + "label": "Planes", + "default": false }, { "type": "boolean", "key": "pluginShapes", - "label": "Plugin Shapes" + "label": "Plugin Shapes", + "default": false }, { "type": "boolean", "key": "polymeshes", - "label": "Polygons" + "label": "Polygons", + "default": true }, { "type": "boolean", "key": "strokes", - "label": "Strokes" + "label": "Strokes", + "default": false }, { "type": "boolean", "key": "subdivSurfaces", - "label": "Subdiv Surfaces" + "label": "Subdiv Surfaces", + "default": false }, { "type": "boolean", "key": "textures", - "label": "Texture Placements" + "label": "Texture Placements", + "default": false } ] }, @@ -600,42 +680,50 @@ { "type": "boolean", "key": "displayGateMask", - "label": "Display Gate Mask" + "label": "Display Gate Mask", + "default": false }, { "type": "boolean", "key": "displayResolution", - "label": "Display Resolution" + "label": "Display Resolution", + "default": false }, { "type": "boolean", "key": "displayFilmGate", - "label": "Display Film Gate" + "label": "Display Film Gate", + "default": false }, { "type": "boolean", "key": "displayFieldChart", - "label": "Display Field Chart" + "label": "Display Field Chart", + "default": false }, { "type": "boolean", "key": "displaySafeAction", - "label": "Display Safe Action" + "label": "Display Safe Action", + "default": false }, { "type": "boolean", "key": "displaySafeTitle", - "label": "Display Safe Title" + "label": "Display Safe Title", + "default": false }, { "type": "boolean", "key": "displayFilmPivot", - "label": "Display Film Pivot" + "label": "Display Film Pivot", + "default": false }, { "type": "boolean", "key": "displayFilmOrigin", - "label": "Display Film Origin" + "label": "Display Film Origin", + "default": false }, { "type": "number", @@ -643,7 +731,8 @@ "label": "Overscan", "decimal": 1, "minimum": 0, - "maximum": 10 + "maximum": 10, + "default": 1 } ] } From 655ae7e7f879cac7127fc754bd472426d09ce9b1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 3 Apr 2023 12:09:26 +0100 Subject: [PATCH 192/918] create review for profiles --- .../maya/plugins/create/create_review.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index e709239ae7..5a1afe9790 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -1,8 +1,14 @@ from collections import OrderedDict +import json + from openpype.hosts.maya.api import ( lib, plugin ) +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io +from openpype.lib.profiles_filtering import filter_profiles +from openpype.client import get_asset_by_name class CreateReview(plugin.Creator): @@ -32,6 +38,30 @@ class CreateReview(plugin.Creator): super(CreateReview, self).__init__(*args, **kwargs) data = OrderedDict(**self.data) + project_name = legacy_io.Session["AVALON_PROJECT"] + profiles = get_project_settings( + project_name + )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + + preset = None + if not profiles: + self.log.warning("No profiles present for extract playblast.") + else: + asset_doc = get_asset_by_name(project_name, data["asset"]) + task_name = legacy_io.Session["AVALON_TASK"] + task_type = asset_doc["data"]["tasks"][task_name]["type"] + + filtering_criteria = { + "hosts": "maya", + "families": "review", + "task_names": task_name, + "task_types": task_type, + "subset": data["subset"] + } + preset = filter_profiles( + profiles, filtering_criteria, logger=self.log + )["capture_preset"] + # Option for using Maya or asset frame range in settings. frame_range = lib.get_frame_range() if self.useMayaTimeline: @@ -40,6 +70,7 @@ class CreateReview(plugin.Creator): data[key] = value data["fps"] = lib.collect_animation_data(fps=True)["fps"] + data["review_width"] = self.Width data["review_height"] = self.Height data["isolate"] = self.isolate @@ -48,4 +79,16 @@ class CreateReview(plugin.Creator): data["transparency"] = self.transparency data["panZoom"] = self.panZoom + if preset: + self.log.info( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) + ) + data["review_width"] = preset["Resolution"]["width"] + data["review_height"] = preset["Resolution"]["height"] + data["isolate"] = preset["Generic"]["isolate_view"] + data["imagePlane"] = preset["Viewport Options"]["imagePlane"] + data["panZoom"] = preset["Generic"]["pan_zoom"] + self.data = data From 9d68db0e16bc91a87f0b4fd4f7935426c70a8ffb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Apr 2023 16:03:57 +0200 Subject: [PATCH 193/918] Validate the generated output maps for missing channels --- .../plugins/create/create_textures.py | 10 +- .../publish/collect_textureset_images.py | 2 +- .../plugins/publish/extract_textures.py | 18 ++- .../plugins/publish/validate_ouput_maps.py | 108 ++++++++++++++++++ 4 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 19133768a5..6070a06367 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -5,7 +5,8 @@ from openpype.pipeline import CreatedInstance, Creator, CreatorError from openpype.lib import ( EnumDef, UILabelDef, - NumberDef + NumberDef, + BoolDef ) from openpype.hosts.substancepainter.api.pipeline import ( @@ -80,6 +81,13 @@ class CreateTextures(Creator): EnumDef("exportPresetUrl", items=get_export_presets(), label="Output Template"), + BoolDef("allowSkippedMaps", + label="Allow Skipped Output Maps", + tooltip="When enabled this allows the publish to ignore " + "output maps in the used output template if one " + "or more maps are skipped due to the required " + "channels not being present in the current file.", + default=True), EnumDef("exportFileFormat", items={ None: "Based on output template", diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 56694614eb..50a96b94ae 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -97,7 +97,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): representation["stagingDir"] = staging_dir # Clone the instance - image_instance = context.create_instance(instance.name) + image_instance = context.create_instance(image_subset) image_instance[:] = instance[:] image_instance.data.update(copy.deepcopy(instance.data)) image_instance.data["name"] = image_subset diff --git a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py index b9654947db..bb6f15ead9 100644 --- a/openpype/hosts/substancepainter/plugins/publish/extract_textures.py +++ b/openpype/hosts/substancepainter/plugins/publish/extract_textures.py @@ -1,6 +1,7 @@ -from openpype.pipeline import KnownPublishError, publish import substance_painter.export +from openpype.pipeline import KnownPublishError, publish + class ExtractTextures(publish.Extractor, publish.ColormanagedPyblishPluginMixin): @@ -31,21 +32,19 @@ class ExtractTextures(publish.Extractor, "Failed to export texture set: {}".format(result.message) ) + # Log what files we generated for (texture_set_name, stack_name), maps in result.textures.items(): # Log our texture outputs - self.log.info(f"Processing stack: {texture_set_name} {stack_name}") + self.log.info(f"Exported stack: {texture_set_name} {stack_name}") for texture_map in maps: self.log.info(f"Exported texture: {texture_map}") - # TODO: Confirm outputs match what we collected - # TODO: Confirm the files indeed exist - # TODO: make sure representations are registered - # We'll insert the color space data for each image instance that we # added into this texture set. The collector couldn't do so because # some anatomy and other instance data needs to be collected prior context = instance.context for image_instance in instance: + representation = next(iter(image_instance.data["representations"])) colorspace = image_instance.data.get("colorspace") if not colorspace: @@ -53,10 +52,9 @@ class ExtractTextures(publish.Extractor, f"{image_instance}") continue - for representation in image_instance.data["representations"]: - self.set_representation_colorspace(representation, - context=context, - colorspace=colorspace) + self.set_representation_colorspace(representation, + context=context, + colorspace=colorspace) # The TextureSet instance should not be integrated. It generates no # output data. Instead the separated texture instances are generated diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py new file mode 100644 index 0000000000..203cf7c5fe --- /dev/null +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -0,0 +1,108 @@ +import copy +import os + +import pyblish.api + +import substance_painter.export + +from openpype.pipeline import PublishValidationError + + +class ValidateOutputMaps(pyblish.api.InstancePlugin): + """Validate all output maps for Output Template are generated. + + Output maps will be skipped by Substance Painter if it is an output + map in the Substance Output Template which uses channels that the current + substance painter project has not painted or generated. + + """ + + order = pyblish.api.ValidatorOrder + label = "Validate output maps" + hosts = ["substancepainter"] + families = ["textureSet"] + + def process(self, instance): + + config = instance.data["exportConfig"] + + # Substance Painter API does not allow to query the actual output maps + # it will generate without actually exporting the files. So we try to + # generate the smallest size / fastest export as possible + config = copy.deepcopy(config) + parameters = config["exportParameters"][0]["parameters"] + parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) + parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) + parameters["dithering"] = False # no dithering (faster) + config["exportParameters"][0]["parameters"]["sizeLog2"] = [1, 1] + + result = substance_painter.export.export_project_textures(config) + if result.status != substance_painter.export.ExportStatus.Success: + raise PublishValidationError( + "Failed to export texture set: {}".format(result.message) + ) + + generated_files = set() + for texture_maps in result.textures.values(): + for texture_map in texture_maps: + generated_files.add(os.path.normpath(texture_map)) + # Directly clean up our temporary export + os.remove(texture_map) + + creator_attributes = instance.data.get("creator_attributes", {}) + allow_skipped_maps = creator_attributes.get("allowSkippedMaps", True) + error_report_missing = [] + for image_instance in instance: + + # Confirm whether the instance has its expected files generated. + # We assume there's just one representation and that it is + # the actual texture representation from the collector. + representation = next(iter(image_instance.data["representations"])) + staging_dir = representation["stagingDir"] + filenames = representation["files"] + if not isinstance(filenames, (list, tuple)): + # Convert single file to list + filenames = [filenames] + + missing = [] + for filename in filenames: + filepath = os.path.join(staging_dir, filename) + filepath = os.path.normpath(filepath) + if filepath not in generated_files: + self.log.warning(f"Missing texture: {filepath}") + missing.append(filepath) + + if allow_skipped_maps: + # TODO: This is changing state on the instance's which + # usually should not be done during validation. + self.log.warning(f"Disabling texture instance: " + f"{image_instance}") + image_instance.data["active"] = False + image_instance.data["integrate"] = False + representation.setdefault("tags", []).append("delete") + continue + + if missing: + error_report_missing.append((image_instance, missing)) + + if error_report_missing: + + message = ( + "The Texture Set skipped exporting some output maps which are " + "defined in the Output Template. This happens if the Output " + "Templates exports maps from channels which you do not " + "have in your current Substance Painter project.\n\n" + "To allow this enable the *Allow Skipped Output Maps* setting " + "on the instance.\n\n" + f"Instance {instance} skipped exporting output maps:\n" + "" + ) + + for image_instance, missing in error_report_missing: + missing_str = ", ".join(missing) + message += f"- **{image_instance}** skipped: {missing_str}\n" + + raise PublishValidationError( + message=message, + title="Missing output maps" + ) From 23568e5b060caff2a56d65ba3229cc74f588b62c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Apr 2023 00:11:49 +0200 Subject: [PATCH 194/918] Fix allow skipped maps logic --- .../plugins/publish/validate_ouput_maps.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py index 203cf7c5fe..e3d4c733e1 100644 --- a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -72,17 +72,19 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin): self.log.warning(f"Missing texture: {filepath}") missing.append(filepath) + if not missing: + continue + if allow_skipped_maps: # TODO: This is changing state on the instance's which - # usually should not be done during validation. + # should not be done during validation. self.log.warning(f"Disabling texture instance: " f"{image_instance}") image_instance.data["active"] = False image_instance.data["integrate"] = False representation.setdefault("tags", []).append("delete") continue - - if missing: + else: error_report_missing.append((image_instance, missing)) if error_report_missing: From 5059cf74b5bddfa85b4b9157fd2ffe7f346cc203 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Apr 2023 00:13:50 +0200 Subject: [PATCH 195/918] Support multiple texture sets + stacks --- .../publish/collect_textureset_images.py | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 50a96b94ae..d11abd1019 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -41,10 +41,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): for template, outputs in template_maps.items(): self.log.info(f"Processing {template}") self.create_image_instance(instance, template, outputs, - asset_doc=asset_doc) + asset_doc=asset_doc, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, - asset_doc): + asset_doc, texture_set_name, stack_name): """Create a new instance per image or UDIM sequence. The new instances will be of family `image`. @@ -56,14 +58,27 @@ class CollectTextureSet(pyblish.api.InstancePlugin): fnames = [os.path.basename(output["filepath"]) for output in outputs] ext = os.path.splitext(first_filepath)[1] assert ext.lstrip("."), f"No extension: {ext}" - map_identifier = strip_template(template) + + always_include_texture_set_name = False # todo: make this configurable + all_texture_sets = substance_painter.textureset.all_texture_sets() + texture_set = substance_painter.textureset.TextureSet.from_name( + texture_set_name + ) # Define the suffix we want to give this particular texture # set and set up a remapped subset naming for it. - # TODO (Critical) Support needs to be added to have multiple materials - # with each their own maps. So we might need to include the - # material or alike in the variant suffix too? - suffix = f".{map_identifier}" + suffix = "" + if always_include_texture_set_name or len(all_texture_sets) > 1: + # More than one texture set, include texture set name + suffix += f".{texture_set_name}" + if texture_set.is_layered_material() and stack_name: + # More than one stack, include stack name + suffix += f".{stack_name}" + + # Always include the map identifier + map_identifier = strip_template(template) + suffix += f".{map_identifier}" + image_subset = get_subset_name( # TODO: The family actually isn't 'texture' currently but for now # this is only done so the subset name starts with 'texture' @@ -110,6 +125,10 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Group the textures together in the loader image_instance.data["subsetGroup"] = instance.data["subset"] + # Store the texture set name and stack name on the instance + image_instance.data["textureSetName"] = texture_set_name + image_instance.data["textureStackName"] = stack_name + # Store color space with the instance # Note: The extractor will assign it to the representation colorspace = outputs[0].get("colorSpace") From 0ff0b6b645e1f7293347a24a638bb2afb80556e9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 07:39:53 +0100 Subject: [PATCH 196/918] Move launch logic to host module. --- openpype/hooks/pre_add_last_workfile_arg.py | 13 ---------- .../hosts/maya/hooks/pre_auto_load_plugins.py | 22 +++++++++++++--- .../pre_open_workfile_post_initialization.py | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py index df4aa5cc5d..2a35db869a 100644 --- a/openpype/hooks/pre_add_last_workfile_arg.py +++ b/openpype/hooks/pre_add_last_workfile_arg.py @@ -42,18 +42,5 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): self.log.info("Current context does not have any workfile yet.") return - # Determine whether to open workfile post initialization. - if self.host_name == "maya": - keys = [ - "open_workfile_post_initialization", "explicit_plugins_loading" - ] - maya_settings = self.data["project_settings"]["maya"] - values = [maya_settings[k] for k in keys] - if any(values): - self.log.debug("Opening workfile post initialization.") - key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" - self.data["env"][key] = "1" - return - # Add path to workfile to arguments self.launch_context.launch_args.append(last_workfile) diff --git a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py index 3c3ddbe4dc..689d7adb4f 100644 --- a/openpype/hosts/maya/hooks/pre_auto_load_plugins.py +++ b/openpype/hosts/maya/hooks/pre_auto_load_plugins.py @@ -1,15 +1,29 @@ from openpype.lib import PreLaunchHook -class PreAutoLoadPlugins(PreLaunchHook): +class MayaPreAutoLoadPlugins(PreLaunchHook): """Define -noAutoloadPlugins command flag.""" - # Execute before workfile argument. - order = 0 + # Before AddLastWorkfileToLaunchArgs + order = 9 app_groups = ["maya"] def execute(self): + + # Ignore if there's no last workfile to start. + if not self.data.get("start_last_workfile"): + return + maya_settings = self.data["project_settings"]["maya"] - if maya_settings["explicit_plugins_loading"]["enabled"]: + enabled = maya_settings["explicit_plugins_loading"]["enabled"] + if enabled: + # Force disable the `AddLastWorkfileToLaunchArgs`. + self.data.pop("start_last_workfile") + + # Force post initialization so our dedicated plug-in load can run + # prior to Maya opening a scene file. + key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" + self.launch_context.env[key] = "1" + self.log.debug("Explicit plugins loading.") self.launch_context.launch_args.append("-noAutoloadPlugins") diff --git a/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py new file mode 100644 index 0000000000..7582ce0591 --- /dev/null +++ b/openpype/hosts/maya/hooks/pre_open_workfile_post_initialization.py @@ -0,0 +1,25 @@ +from openpype.lib import PreLaunchHook + + +class MayaPreOpenWorkfilePostInitialization(PreLaunchHook): + """Define whether open last workfile should run post initialize.""" + + # Before AddLastWorkfileToLaunchArgs. + order = 9 + app_groups = ["maya"] + + def execute(self): + + # Ignore if there's no last workfile to start. + if not self.data.get("start_last_workfile"): + return + + maya_settings = self.data["project_settings"]["maya"] + enabled = maya_settings["open_workfile_post_initialization"] + if enabled: + # Force disable the `AddLastWorkfileToLaunchArgs`. + self.data.pop("start_last_workfile") + + self.log.debug("Opening workfile post initialization.") + key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" + self.launch_context.env[key] = "1" From 7444e33a941498bf040bacd6b3710a34f9f59e92 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 07:53:50 +0100 Subject: [PATCH 197/918] Move review camera validation to validator. --- .../maya/plugins/publish/collect_review.py | 11 +++----- .../maya/plugins/publish/validate_review.py | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/validate_review.py diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 00565c5819..ab730db66e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -31,14 +31,11 @@ class CollectReview(pyblish.api.InstancePlugin): # get cameras members = instance.data['setMembers'] - cameras = cmds.ls(members, long=True, - dag=True, cameras=True) self.log.debug('members: {}'.format(members)) - - # validate required settings - assert len(cameras) == 1, "Not a single camera found in extraction" - camera = cameras[0] - self.log.debug('camera: {}'.format(camera)) + cameras = cmds.ls(members, long=True, dag=True, cameras=True) + camera = None + if cameras: + camera = cameras[0] objectset = instance.context.data['objectsets'] diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py new file mode 100644 index 0000000000..fd11b2147b --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -0,0 +1,25 @@ +from maya import cmds + +import pyblish.api + +from openpype.pipeline.publish import ( + ValidateContentsOrder, PublishValidationError +) + + +class ValidateReview(pyblish.api.InstancePlugin): + """Validate review.""" + + order = ValidateContentsOrder + label = "Validate Review" + families = ["review"] + + def process(self, instance): + cameras = cmds.ls( + instance.data["setMembers"], long=True, dag=True, cameras=True + ) + + if len(cameras) != 1: + raise PublishValidationError( + "Not a single camera found in instance." + ) From c4b887597a3ad9318367553f0151675063ab9560 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 07:54:31 +0100 Subject: [PATCH 198/918] Support review profiles in extraction --- .../maya/plugins/publish/extract_playblast.py | 51 +++++++++++--- .../maya/plugins/publish/extract_thumbnail.py | 67 ++++++++++++++----- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 72b1489522..0556fd9eea 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -7,6 +7,7 @@ import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib +from openpype.lib.profiles_filtering import filter_profiles from maya import cmds import pymel.core as pm @@ -34,7 +35,7 @@ class ExtractPlayblast(publish.Extractor): hosts = ["maya"] families = ["review"] optional = True - capture_preset = {} + profiles = None def _capture(self, preset): self.log.info( @@ -47,6 +48,10 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("playblast path {}".format(path)) def process(self, instance): + if not self.profiles: + self.log.warning("No profiles present for Extract Playblast") + return + self.log.info("Extracting capture..") # get scene fps @@ -66,12 +71,35 @@ class ExtractPlayblast(publish.Extractor): # get cameras camera = instance.data["review_camera"] - preset = lib.load_capture_preset(data=self.capture_preset) - # Grab capture presets from the project settings - capture_presets = self.capture_preset + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + capture_preset = filter_profiles( + self.profiles, filtering_criteria, logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset( + data=capture_preset + ) + + # "isolate_view" will already have been applied at creation, so we'll + # ignore it here. + preset.pop("isolate_view") + # Set resolution variables from capture presets - width_preset = capture_presets["Resolution"]["width"] - height_preset = capture_presets["Resolution"]["height"] + width_preset = capture_preset["Resolution"]["width"] + height_preset = capture_preset["Resolution"]["height"] + # Set resolution variables from asset values asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("resolutionWidth") @@ -122,8 +150,9 @@ class ExtractPlayblast(publish.Extractor): preset["viewport2_options"]["transparencyAlgorithm"] = transparency # Isolate view is requested by having objects in the set besides a - # camera. - if preset.pop("isolate_view", False) and instance.data.get("isolate"): + # camera. If there is only 1 member it'll be the camera because we + # validate to have 1 camera only. + if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: preset["isolate"] = instance.data["setMembers"] # Show/Hide image planes on request. @@ -158,7 +187,7 @@ class ExtractPlayblast(publish.Extractor): ) override_viewport_options = ( - capture_presets["Viewport Options"]["override_viewport_options"] + capture_preset["Viewport Options"]["override_viewport_options"] ) # Force viewer to False in call to capture because we have our own @@ -234,8 +263,8 @@ class ExtractPlayblast(publish.Extractor): collected_files = collected_files[0] representation = { - "name": self.capture_preset["Codec"]["compression"], - "ext": self.capture_preset["Codec"]["compression"], + "name": capture_preset["Codec"]["compression"], + "ext": capture_preset["Codec"]["compression"], "files": collected_files, "stagingDir": stagingdir, "frameStart": start, diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index f2d084b828..4672940254 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -1,11 +1,14 @@ import os import glob import tempfile +import json import capture -from openpype.pipeline import publish +from openpype.pipeline import publish, legacy_io from openpype.hosts.maya.api import lib +from openpype.lib.profiles_filtering import filter_profiles +from openpype.settings import get_project_settings from maya import cmds import pymel.core as pm @@ -24,26 +27,48 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): + project_name = legacy_io.Session["AVALON_PROJECT"] + profiles = get_project_settings( + project_name + )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + + if not profiles: + self.log.warning("No profiles present for Extract Playblast") + return + self.log.info("Extracting capture..") camera = instance.data["review_camera"] - maya_setting = instance.context.data["project_settings"]["maya"] - plugin_setting = maya_setting["publish"]["ExtractPlayblast"] - capture_preset = plugin_setting["capture_preset"] + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + capture_preset = filter_profiles( + profiles, filtering_criteria, logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset( + data=capture_preset + ) + + # "isolate_view" will already have been applied at creation, so we'll + # ignore it here. + preset.pop("isolate_view") + override_viewport_options = ( capture_preset["Viewport Options"]["override_viewport_options"] ) - try: - preset = lib.load_capture_preset(data=capture_preset) - except KeyError as ke: - self.log.error("Error loading capture presets: {}".format(str(ke))) - preset = {} - self.log.info("Using viewport preset: {}".format(preset)) - - # preset["off_screen"] = False - preset["camera"] = camera preset["start_frame"] = instance.data["frameStart"] preset["end_frame"] = instance.data["frameStart"] @@ -59,10 +84,9 @@ class ExtractThumbnail(publish.Extractor): "overscan": 1.0, "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), } - capture_presets = capture_preset # Set resolution variables from capture presets - width_preset = capture_presets["Resolution"]["width"] - height_preset = capture_presets["Resolution"]["height"] + width_preset = capture_preset["Resolution"]["width"] + height_preset = capture_preset["Resolution"]["height"] # Set resolution variables from asset values asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("resolutionWidth") @@ -111,8 +135,9 @@ class ExtractThumbnail(publish.Extractor): preset["viewport2_options"]["transparencyAlgorithm"] = transparency # Isolate view is requested by having objects in the set besides a - # camera. - if preset.pop("isolate_view", False) and instance.data.get("isolate"): + # camera. If there is only 1 member it'll be the camera because we + # validate to have 1 camera only. + if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: preset["isolate"] = instance.data["setMembers"] # Show or Hide Image Plane @@ -140,6 +165,12 @@ class ExtractThumbnail(publish.Extractor): preset.update(panel_preset) cmds.setFocus(panel) + self.log.info( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) + ) + path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From b960b653300bff616918f14cb1b6f3a65d519056 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 08:13:35 +0100 Subject: [PATCH 199/918] Order display options better. --- .../schemas/schema_maya_capture.json | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index a8961b48dd..3fc92a1b05 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -77,13 +77,24 @@ "type": "label", "label": "Display Options" }, - + { + "type": "boolean", + "key": "override_display", + "label": "Override display options", + "default": true + }, { "type": "color", "key": "background", "label": "Background Color: ", "default": [125, 125, 125, 255] }, + { + "type": "boolean", + "key": "displayGradient", + "label": "Display background gradient", + "default": true + }, { "type": "color", "key": "backgroundBottom", @@ -95,18 +106,6 @@ "key": "backgroundTop", "label": "Background Top: ", "default": [125, 125, 125, 255] - }, - { - "type": "boolean", - "key": "override_display", - "label": "Override display options", - "default": true - }, - { - "type": "boolean", - "key": "displayGradient", - "label": "Display background gradient", - "default": true } ] }, From bc004453edd0cc42648ce228e1249c9eb05a2700 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 08:33:18 +0100 Subject: [PATCH 200/918] Update openpype/hosts/maya/startup/userSetup.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/startup/userSetup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 4932bf14c0..b28d89e7bd 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -38,8 +38,9 @@ if settings["maya"]["explicit_plugins_loading"]["enabled"]: key = "OPENPYPE_OPEN_WORKFILE_POST_INITIALIZATION" if bool(int(os.environ.get(key, "0"))): def _log_and_open(): - print("Opening \"{}\"".format(os.environ["AVALON_LAST_WORKFILE"])) - cmds.file(os.environ["AVALON_LAST_WORKFILE"], open=True, force=True) + path = os.environ["AVALON_LAST_WORKFILE"] + print("Opening \"{}\"".format(path)) + cmds.file(path, open=True, force=True) cmds.evalDeferred( _log_and_open, lowestPriority=True From b5e80e565b5de71625531beb7818d34d9b7da1df Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 08:34:42 +0100 Subject: [PATCH 201/918] Update openpype/hosts/maya/startup/userSetup.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/startup/userSetup.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index b28d89e7bd..4a00c3dce7 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -20,14 +20,13 @@ if settings["maya"]["explicit_plugins_loading"]["enabled"]: project_settings = get_project_settings(os.environ["AVALON_PROJECT"]) maya_settings = project_settings["maya"] explicit_plugins_loading = maya_settings["explicit_plugins_loading"] - if explicit_plugins_loading["enabled"]: - for plugin in explicit_plugins_loading["plugins_to_load"]: - if plugin["enabled"]: - print("Loading " + plugin["name"]) - try: - cmds.loadPlugin(plugin["name"], quiet=True) - except RuntimeError as e: - print(e) + for plugin in explicit_plugins_loading["plugins_to_load"]: + if plugin["enabled"]: + print("Loading plug-in: " + plugin["name"]) + try: + cmds.loadPlugin(plugin["name"], quiet=True) + except RuntimeError as e: + print(e) cmds.evalDeferred( _explicit_load_plugins, From 0c626f54c5aa69730692f50a6de3123a555d3419 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 08:52:53 +0100 Subject: [PATCH 202/918] Refactor settings variables. --- openpype/hosts/maya/startup/userSetup.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 4a00c3dce7..b58ebb0f7f 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -12,14 +12,12 @@ install_host(host) print("Starting OpenPype usersetup...") -settings = get_project_settings(os.environ['AVALON_PROJECT']) +project_settings = get_project_settings(os.environ['AVALON_PROJECT']) # Loading plugins explicitly. -if settings["maya"]["explicit_plugins_loading"]["enabled"]: +explicit_plugins_loading = project_settings["maya"]["explicit_plugins_loading"] +if explicit_plugins_loading["enabled"]: def _explicit_load_plugins(): - project_settings = get_project_settings(os.environ["AVALON_PROJECT"]) - maya_settings = project_settings["maya"] - explicit_plugins_loading = maya_settings["explicit_plugins_loading"] for plugin in explicit_plugins_loading["plugins_to_load"]: if plugin["enabled"]: print("Loading plug-in: " + plugin["name"]) @@ -46,7 +44,7 @@ if bool(int(os.environ.get(key, "0"))): ) # Build a shelf. -shelf_preset = settings['maya'].get('project_shelf') +shelf_preset = project_settings['maya'].get('project_shelf') if shelf_preset: project = os.environ["AVALON_PROJECT"] From 4b3f96af5e41ccc29461fd5e0fec9306a062edd1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 08:53:08 +0100 Subject: [PATCH 203/918] Comment deferred evaluation --- openpype/hosts/maya/startup/userSetup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index b58ebb0f7f..ae6a999d98 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -26,6 +26,8 @@ if explicit_plugins_loading["enabled"]: except RuntimeError as e: print(e) + # We need to load plugins deferred as loading them directly does not work + # correctly due to Maya's initialization. cmds.evalDeferred( _explicit_load_plugins, lowestPriority=True From 2d6d1ba88200fbf0cd9813dbe8b56553a86c6c55 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 10:11:20 +0100 Subject: [PATCH 204/918] Fix OP-5542 # Traceback (most recent call last): # File "C:\Users\florianbehr\AppData\Local\pypeclub\openpype\3.15\openpype-v3.15.4-thescope230404\openpype\hosts\maya\tools\mayalookassigner\app.py", line 272, in on_process_selected # nodes = list(set(item["nodes"]).difference(arnold_standins)) # UnboundLocalError: local variable 'arnold_standins' referenced before assignment --- openpype/hosts/maya/tools/mayalookassigner/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index 2a8775fff6..4619c80913 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -263,14 +263,14 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) + + nodes = list(set(item["nodes"]).difference(arnold_standins)) else: self.echo( "Could not assign to aiStandIn because mtoa plugin is not " "loaded." ) - nodes = list(set(item["nodes"]).difference(arnold_standins)) - # Assign look if nodes: assign_look_by_version(nodes, version_id=version["_id"]) From 94cd27fbc27d1c11a1b2fd0edb193257fc2717e9 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 10:25:35 +0100 Subject: [PATCH 205/918] Update openpype/hosts/maya/tools/mayalookassigner/app.py --- openpype/hosts/maya/tools/mayalookassigner/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index 4619c80913..a8d0f243e9 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -263,7 +263,6 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) - nodes = list(set(item["nodes"]).difference(arnold_standins)) else: self.echo( From 122a4dc9db074f7bd14421e1d8b9244a05318da7 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Wed, 5 Apr 2023 12:14:06 +0200 Subject: [PATCH 206/918] add sync to specific projects or listen only --- openpype/modules/kitsu/kitsu_module.py | 16 ++++++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 25 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index b91373af20..f4e3dd5691 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,19 +124,31 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") @click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password): +def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password + projects (str): specific kitsu projects + + SYNC_PROJECTS: + *: all projects + ^: dont sync any project just listen + "project01 project02 ...": to choose custom projects + + """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password) + projects = projects.strip() + projects = projects.split(' ') + + sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4f4f0810bc..a397198a13 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -359,7 +359,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None + login: str, password: str, ignore_projects: list = None, specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -367,6 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names + specific_projects (list): List of synced project names Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -381,7 +382,27 @@ def sync_all_projects( dbcon = AvalonMongoDB() dbcon.install() all_projects = gazu.project.all_projects() - for project in all_projects: + + + project_to_sync = [] + if specific_projects == ['*']: + project_to_sync = all_projects + + elif specific_projects == ['^']: + return + + elif isinstance(specific_projects, list): + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in specific_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exists in kitsu.' + f' Please make sure you write the project correctly.') + else: + return + + for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: continue sync_project_from_kitsu(dbcon, project) From 912f757390a93df678bb964c6beb3665bc5fb08d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:31:27 +0100 Subject: [PATCH 207/918] Update openpype/hosts/maya/plugins/create/create_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/create/create_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 5a1afe9790..eeccc5b21e 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -38,7 +38,7 @@ class CreateReview(plugin.Creator): super(CreateReview, self).__init__(*args, **kwargs) data = OrderedDict(**self.data) - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = get_current_project_name() profiles = get_project_settings( project_name )["maya"]["publish"]["ExtractPlayblast"]["profiles"] From 462d3247e82725c9bfaf083b63c240441de5ef40 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:42:40 +0100 Subject: [PATCH 208/918] Update openpype/hosts/maya/plugins/create/create_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/create/create_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index eeccc5b21e..1eb8e421a1 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -48,7 +48,7 @@ class CreateReview(plugin.Creator): self.log.warning("No profiles present for extract playblast.") else: asset_doc = get_asset_by_name(project_name, data["asset"]) - task_name = legacy_io.Session["AVALON_TASK"] + task_name = get_current_task_name() task_type = asset_doc["data"]["tasks"][task_name]["type"] filtering_criteria = { From 54a135a1284e72330dcf57e7c70d45fef3de5ce8 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:43:01 +0100 Subject: [PATCH 209/918] Update openpype/hosts/maya/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 2daea7f3eb..ca08016fab 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -26,7 +26,7 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - project_name = legacy_io.Session["AVALON_PROJECT"] + project_name = instance.context.data["projectName"] profiles = get_project_settings( project_name )["maya"]["publish"]["ExtractPlayblast"]["profiles"] From c969120d15a9dab03548f538c2ba662ced596166 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:44:11 +0100 Subject: [PATCH 210/918] Update openpype/hosts/maya/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index ca08016fab..038a3c0c7f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -27,7 +27,8 @@ class ExtractThumbnail(publish.Extractor): def process(self, instance): project_name = instance.context.data["projectName"] - profiles = get_project_settings( + project_settings = instance.context.data["project_settings"] + profiles = project_settings["maya"]["publish"]["ExtractPlayblast"]["profiles"] project_name )["maya"]["publish"]["ExtractPlayblast"]["profiles"] From a3d358a661b9c32dea6cce3c4b6b0e4dcd010bbb Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:44:35 +0100 Subject: [PATCH 211/918] Update openpype/settings/entities/color_entity.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/entities/color_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index a542f2fa38..e9a2136754 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -11,10 +11,10 @@ class ColorEntity(InputEntity): def _item_initialization(self): self.valid_value_types = (list, ) + self.use_alpha = self.schema_data.get("use_alpha", True) self.value_on_not_set = self.convert_to_valid_type( self.schema_data.get("default", [0, 0, 0, 255]) ) - self.use_alpha = self.schema_data.get("use_alpha", True) def set_override_state(self, *args, **kwargs): super(ColorEntity, self).set_override_state(*args, **kwargs) From c290422fcde6077f744e7d14593076412c79f991 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:44:48 +0100 Subject: [PATCH 212/918] Update openpype/settings/entities/color_entity.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/settings/entities/color_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index e9a2136754..f838a6b0ad 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -66,6 +66,6 @@ class ColorEntity(InputEntity): new_value.append(item) # Make sure - if hasattr(self, "use_alpha") and not self.use_alpha: + if not self.use_alpha: new_value[3] = 255 return new_value From d8d2a317ac24c3618d0a99ee5235b3b71e1718d7 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 5 Apr 2023 11:45:20 +0100 Subject: [PATCH 213/918] Update openpype/settings/entities/color_entity.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> From 890d88908c20e97b384e02bb88d605c22804934f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:46:45 +0100 Subject: [PATCH 214/918] BigRoy feedback --- .../hosts/maya/plugins/publish/collect_review.py | 9 ++++----- .../hosts/maya/plugins/publish/validate_review.py | 7 ++----- .../settings/defaults/project_settings/maya.json | 6 ++++-- .../projects_schema/schemas/schema_maya_capture.json | 12 ++++++------ 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 858ee24026..0b3799ac13 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -31,9 +31,6 @@ class CollectReview(pyblish.api.InstancePlugin): members = instance.data['setMembers'] self.log.debug('members: {}'.format(members)) cameras = cmds.ls(members, long=True, dag=True, cameras=True) - camera = None - if cameras: - camera = cameras[0] context = instance.context objectset = context.data['objectsets'] @@ -64,7 +61,8 @@ class CollectReview(pyblish.api.InstancePlugin): else: data['families'] = ['review'] - data['review_camera'] = camera + data["cameras"] = cameras + data['review_camera'] = cameras[0] if cameras else None data['frameStartFtrack'] = instance.data["frameStartHandle"] data['frameEndFtrack'] = instance.data["frameEndHandle"] data['frameStartHandle'] = instance.data["frameStartHandle"] @@ -98,7 +96,8 @@ class CollectReview(pyblish.api.InstancePlugin): self.log.debug("Existing subsets found, keep legacy name.") instance.data['subset'] = legacy_subset_name - instance.data['review_camera'] = camera + instance.data["cameras"] = cameras + instance.data['review_camera'] = cameras[0] if cameras else None instance.data['frameStartFtrack'] = \ instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = \ diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py index 7e9b86c64f..68e8c4a74a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review.py +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -15,9 +15,7 @@ class ValidateReview(pyblish.api.InstancePlugin): families = ["review"] def process(self, instance): - cameras = cmds.ls( - instance.data["setMembers"], long=True, dag=True, cameras=True - ) + cameras = instance.data["cameras"] # validate required settings if len(cameras) == 0: @@ -31,5 +29,4 @@ class ValidateReview(pyblish.api.InstancePlugin): "Cameras found: {}".format(instance, ", ".join(cameras)) ) - camera = cameras[0] - self.log.debug('camera: {}'.format(camera)) + self.log.debug('camera: {}'.format(instance.data["review_camera"])) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a54c869939..24d55de1fd 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -874,7 +874,6 @@ "dynamics": false, "fluids": false, "follicles": false, - "gpuCacheDisplayFilter": false, "greasePencils": false, "grid": false, "hairSystems": true, @@ -901,7 +900,10 @@ "polymeshes": true, "strokes": false, "subdivSurfaces": false, - "textures": false + "textures": false, + "pluginObjects": { + "gpuCacheDisplayFilter": false + } }, "Camera Options": { "displayGateMask": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 3fc92a1b05..1909a20cf5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -505,12 +505,6 @@ "label": "Follicles", "default": false }, - { - "type": "boolean", - "key": "gpuCacheDisplayFilter", - "label": "GPU Cache", - "default": false - }, { "type": "boolean", "key": "greasePencils", @@ -672,6 +666,12 @@ "key": "textures", "label": "Texture Placements", "default": false + }, + { + "type": "dict-modifiable", + "key": "pluginObjects", + "label": "Plugin Objects", + "object_type": "boolean" } ] }, From a5918bc3f8116309c2a5bd2d790686eacb2e63bb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:50:05 +0100 Subject: [PATCH 215/918] Move preset debug log behind OPENPYPE_DEBUG --- openpype/hosts/maya/plugins/create/create_review.py | 10 ++++++---- .../hosts/maya/plugins/publish/extract_playblast.py | 9 +++++---- .../hosts/maya/plugins/publish/extract_thumbnail.py | 9 +++++---- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 5a1afe9790..786c795a1a 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -1,3 +1,4 @@ +import os from collections import OrderedDict import json @@ -80,11 +81,12 @@ class CreateReview(plugin.Creator): data["panZoom"] = self.panZoom if preset: - self.log.info( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - ) data["review_width"] = preset["Resolution"]["width"] data["review_height"] = preset["Resolution"]["height"] data["isolate"] = preset["Generic"]["isolate_view"] diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 7787c1df7f..81007520a8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -37,11 +37,12 @@ class ExtractPlayblast(publish.Extractor): profiles = None def _capture(self, preset): - self.log.info( - "Using preset:\n{}".format( - json.dumps(preset, sort_keys=True, indent=4) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - ) path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 2daea7f3eb..ee64c11ca4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -164,11 +164,12 @@ class ExtractThumbnail(publish.Extractor): preset.update(panel_preset) cmds.setFocus(panel) - self.log.info( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - ) path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From 70c9c534f017fe3281547f9248a9bb41c1bcb765 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:57:06 +0100 Subject: [PATCH 216/918] Hound --- openpype/hosts/maya/plugins/create/create_review.py | 2 +- .../hosts/maya/plugins/publish/extract_thumbnail.py | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 75a1a5bf08..594faa7978 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -7,7 +7,7 @@ from openpype.hosts.maya.api import ( plugin ) from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_project_name, get_current_task_name from openpype.lib.profiles_filtering import filter_profiles from openpype.client import get_asset_by_name diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 67d085e2f5..cf0f80fa15 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -5,10 +5,9 @@ import json import capture -from openpype.pipeline import publish, legacy_io +from openpype.pipeline import publish from openpype.hosts.maya.api import lib from openpype.lib.profiles_filtering import filter_profiles -from openpype.settings import get_project_settings from maya import cmds @@ -26,11 +25,8 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - project_name = instance.context.data["projectName"] - project_settings = instance.context.data["project_settings"] - profiles = project_settings["maya"]["publish"]["ExtractPlayblast"]["profiles"] - project_name - )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + maya_settings = instance.context.data["project_settings"]["maya"] + profiles = maya_settings["publish"]["ExtractPlayblast"]["profiles"] if not profiles: self.log.warning("No profiles present for Extract Playblast") From 56fc69a9c98639be06a18c2a6a6e74ee05386744 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 11:58:40 +0100 Subject: [PATCH 217/918] Hound --- openpype/hosts/maya/plugins/publish/validate_review.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py index 68e8c4a74a..346fb54ac4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review.py +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -1,5 +1,3 @@ -from maya import cmds - import pyblish.api from openpype.pipeline.publish import ( From 359b039388dc68cd88893efd80b0e5db5be0eaab Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 5 Apr 2023 13:37:57 +0200 Subject: [PATCH 218/918] Removed the previous Module OpenPype in plugin (OPPublishInstance still remains), Created new UE 5.1 ver. --- openpype/hosts/unreal/addon.py | 5 +- .../unreal/hooks/pre_workfile_preparation.py | 2 +- .../OpenPype => UE_4.27/Ayon}/.gitignore | 0 .../Ayon/Ayon.uplugin} | 5 - .../Ayon}/Config/DefaultAyonSettings.ini | 0 .../Ayon}/Config/FilterPlugin.ini | 0 .../Ayon}/Content/Python/init_unreal.py | 2 +- .../OpenPype => UE_4.27/Ayon}/README.md | 0 .../Ayon}/Resources/ayon128.png | Bin .../Ayon}/Resources/ayon40.png | Bin .../Ayon}/Resources/ayon512.png | Bin .../Ayon}/Source/Ayon/Ayon.Build.cs | 0 .../Ayon}/Source/Ayon/Private/Ayon.cpp | 0 .../Ayon/Private/AyonAssetContainer.cpp | 0 .../Private/AyonAssetContainerFactory.cpp | 0 .../Ayon}/Source/Ayon/Private/AyonLib.cpp | 0 .../Source/Ayon/Private/AyonPythonBridge.cpp | 0 .../Source/Ayon/Private/AyonSettings.cpp | 0 .../Ayon}/Source/Ayon/Private/AyonStyle.cpp | 2 +- .../Private/Commandlets/AyonActionResult.cpp | 0 .../AyonGenerateProjectCommandlet.cpp | 0 .../Ayon}/Private/OpenPypePublishInstance.cpp | 12 +- .../OpenPypePublishInstanceFactory.cpp | 0 .../Ayon}/Source/Ayon/Public/Ayon.h | 0 .../Source/Ayon/Public/AyonAssetContainer.h | 0 .../Ayon/Public/AyonAssetContainerFactory.h | 0 .../Ayon}/Source/Ayon/Public/AyonConstants.h | 0 .../Ayon}/Source/Ayon/Public/AyonLib.h | 0 .../Source/Ayon/Public/AyonPythonBridge.h | 0 .../Ayon}/Source/Ayon/Public/AyonSettings.h | 2 +- .../Ayon}/Source/Ayon/Public/AyonStyle.h | 0 .../Public/Commandlets/AyonActionResult.h | 0 .../AyonGenerateProjectCommandlet.h | 0 .../Source/Ayon/Public/Logging/Ayon_Log.h | 0 .../Ayon}/Public/OpenPypePublishInstance.h | 2 +- .../Public/OpenPypePublishInstanceFactory.h | 2 +- .../CommandletProject/.gitignore | 0 .../CommandletProject.uproject | 2 +- .../Config/DefaultOpenPypeSettings.ini | 2 - .../UE_4.7/OpenPype/Resources/openpype128.png | Bin 14594 -> 0 bytes .../UE_4.7/OpenPype/Resources/openpype40.png | Bin 4884 -> 0 bytes .../UE_4.7/OpenPype/Resources/openpype512.png | Bin 85856 -> 0 bytes .../Ayon/Private/AyonPublishInstance.cpp | 201 ------------------ .../Private/AyonPublishInstanceFactory.cpp | 21 -- .../Source/OpenPype/OpenPype.Build.cs | 59 ----- .../OpenPype/Private/AssetContainer.cpp | 115 ---------- .../Private/AssetContainerFactory.cpp | 20 -- .../OPGenerateProjectCommandlet.cpp | 141 ------------ .../Private/Commandlets/OPActionResult.cpp | 41 ---- .../OpenPype/Private/Logging/OP_Log.cpp | 1 - .../Source/OpenPype/Private/OpenPype.cpp | 155 -------------- .../OpenPype/Private/OpenPypePythonBridge.cpp | 14 -- .../OpenPype/Private/OpenPypeSettings.cpp | 20 -- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 70 ------ .../Source/OpenPype/Public/AssetContainer.h | 39 ---- .../OpenPype/Public/AssetContainerFactory.h | 21 -- .../OPGenerateProjectCommandlet.h | 60 ------ .../Source/OpenPype/Public/Logging/OP_Log.h | 4 - .../Source/OpenPype/Public/OpenPype.h | 22 -- .../Source/OpenPype/Public/OpenPypeLib.h | 20 -- .../Source/OpenPype/Public/OpenPypeSettings.h | 31 --- .../Source/OpenPype/Public/OpenPypeStyle.h | 23 -- .../UE_5.0/{OpenPype => Ayon}/.gitignore | 0 .../OpenPype.uplugin => Ayon/Ayon.uplugin} | 5 - .../Config/DefaultAyonSettings.ini | 0 .../Config/FilterPlugin.ini | 0 .../Ayon}/Content/Python/init_unreal.py | 2 +- .../UE_5.0/{OpenPype => Ayon}/README.md | 0 .../{OpenPype => Ayon}/Resources/ayon128.png | Bin .../{OpenPype => Ayon}/Resources/ayon40.png | Bin .../{OpenPype => Ayon}/Resources/ayon512.png | Bin .../Source/Ayon/Ayon.Build.cs | 0 .../Source/Ayon/Private/Ayon.cpp | 0 .../Ayon/Private/AyonAssetContainer.cpp | 0 .../Private/AyonAssetContainerFactory.cpp | 0 .../Source/Ayon/Private/AyonCommands.cpp | 0 .../Source/Ayon/Private/AyonLib.cpp | 0 .../Source/Ayon/Private/AyonPythonBridge.cpp | 0 .../Source/Ayon/Private/AyonSettings.cpp | 0 .../Source/Ayon/Private/AyonStyle.cpp | 2 +- .../Private/Commandlets/AyonActionResult.cpp | 0 .../AyonGenerateProjectCommandlet.cpp | 0 .../Ayon}/Private/OpenPypePublishInstance.cpp | 10 +- .../OpenPypePublishInstanceFactory.cpp | 0 .../Source/Ayon/Public/Ayon.h | 0 .../Source/Ayon/Public/AyonAssetContainer.h | 0 .../Ayon/Public/AyonAssetContainerFactory.h | 0 .../Source/Ayon/Public/AyonCommands.h | 0 .../Source/Ayon/Public/AyonConstants.h | 0 .../Source/Ayon/Public/AyonLib.h | 0 .../Source/Ayon/Public/AyonPythonBridge.h | 0 .../Source/Ayon/Public/AyonSettings.h | 2 +- .../Source/Ayon/Public/AyonStyle.h | 0 .../Public/Commandlets/AyonActionResult.h | 0 .../AyonGenerateProjectCommandlet.h | 0 .../Source/Ayon/Public/Logging/Ayon_Log.h | 0 .../Ayon}/Public/OpenPypePublishInstance.h | 2 +- .../Public/OpenPypePublishInstanceFactory.h | 2 +- .../CommandletProject.uproject | 2 +- .../Config/DefaultOpenPypeSettings.ini | 2 - .../UE_5.0/OpenPype/Resources/openpype128.png | Bin 14594 -> 0 bytes .../UE_5.0/OpenPype/Resources/openpype40.png | Bin 4884 -> 0 bytes .../UE_5.0/OpenPype/Resources/openpype512.png | Bin 85856 -> 0 bytes .../Private/AyonPublishInstanceFactory.cpp | 21 -- .../Source/Ayon/Public/AyonPublishInstance.h | 102 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 20 -- .../Private/AssetContainerFactory.cpp | 20 -- .../OPGenerateProjectCommandlet.cpp | 141 ------------ .../Private/Commandlets/OPActionResult.cpp | 40 ---- .../OpenPype/Private/Logging/OP_Log.cpp | 3 - .../OpenPype/Private/OpenPypeCommands.cpp | 13 -- .../Source/OpenPype/Private/OpenPypeLib.cpp | 53 ----- .../OpenPype/Private/OpenPypePythonBridge.cpp | 14 -- .../OpenPype/Private/OpenPypeSettings.cpp | 21 -- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 63 ------ .../Public/Commandlets/OPActionResult.h | 83 -------- .../Source/OpenPype/Public/Logging/OP_Log.h | 4 - .../Source/OpenPype/Public/OPConstants.h | 13 -- .../Source/OpenPype/Public/OpenPypeCommands.h | 24 --- .../OpenPype/Public/OpenPypePythonBridge.h | 21 -- .../unreal/integration/UE_5.1/Ayon/.gitignore | 35 +++ .../integration/UE_5.1/Ayon/Ayon.uplugin | 24 +++ .../Ayon/Config/DefaultAyonSettings.ini | 2 + .../UE_5.1/Ayon/Config/FilterPlugin.ini | 8 + .../UE_5.1/Ayon/Content/Python/init_unreal.py | 30 +++ .../unreal/integration/UE_5.1/Ayon/README.md | 11 + .../UE_5.1/Ayon/Resources/ayon128.png | Bin 0 -> 2358 bytes .../UE_5.1/Ayon/Resources/ayon40.png | Bin 0 -> 721 bytes .../UE_5.1/Ayon/Resources/ayon512.png | Bin 0 -> 16705 bytes .../Ayon/Source/Ayon/Ayon.Build.cs} | 4 +- .../Ayon/Source/Ayon/Private/Ayon.cpp} | 75 ++++--- .../Ayon/Private/AyonAssetContainer.cpp} | 35 ++- .../Private/AyonAssetContainerFactory.cpp | 20 ++ .../Ayon/Source/Ayon/Private/AyonCommands.cpp | 13 ++ .../Ayon/Source/Ayon/Private/AyonLib.cpp} | 10 +- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 ++ .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 21 ++ .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 62 ++++++ .../Private/Commandlets/AyonActionResult.cpp | 40 ++++ .../AyonGenerateProjectCommandlet.cpp | 140 ++++++++++++ .../Ayon/Private/OpenPypePublishInstance.cpp} | 47 ++-- .../OpenPypePublishInstanceFactory.cpp | 21 ++ .../Ayon/Source/Ayon/Public/Ayon.h} | 3 +- .../Source/Ayon/Public/AyonAssetContainer.h} | 11 +- .../Ayon/Public/AyonAssetContainerFactory.h} | 9 +- .../Ayon/Source/Ayon/Public/AyonCommands.h | 24 +++ .../Ayon/Source/Ayon/Public/AyonConstants.h} | 4 +- .../Ayon/Source/Ayon/Public/AyonLib.h} | 5 +- .../Source/Ayon/Public/AyonPythonBridge.h} | 7 +- .../Ayon/Source/Ayon/Public/AyonSettings.h} | 10 +- .../Ayon/Source/Ayon/Public/AyonStyle.h} | 4 +- .../Public/Commandlets/AyonActionResult.h} | 32 +-- .../AyonGenerateProjectCommandlet.h} | 22 +- .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 + .../Ayon/Public/OpenPypePublishInstance.h} | 9 +- .../Public/OpenPypePublishInstanceFactory.h} | 6 +- .../UE_5.1/CommandletProject/.gitignore | 41 ++++ .../CommandletProject.uproject | 20 ++ openpype/hosts/unreal/lib.py | 15 +- openpype/hosts/unreal/ue_workers.py | 8 +- 160 files changed, 708 insertions(+), 1939 deletions(-) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/.gitignore (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/OpenPype.uplugin => UE_4.27/Ayon/Ayon.uplugin} (86%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Config/DefaultAyonSettings.ini (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Config/FilterPlugin.ini (100%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype => UE_4.27/Ayon}/Content/Python/init_unreal.py (93%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/README.md (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Resources/ayon128.png (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Resources/ayon40.png (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Resources/ayon512.png (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Ayon.Build.cs (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/Ayon.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonAssetContainer.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonAssetContainerFactory.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonLib.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonPythonBridge.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonSettings.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/AyonStyle.cpp (98%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/Commandlets/AyonActionResult.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Private/OpenPypePublishInstance.cpp (94%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Ayon.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonAssetContainer.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonAssetContainerFactory.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonConstants.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonLib.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonPythonBridge.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonSettings.h (89%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/AyonStyle.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Commandlets/AyonActionResult.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_4.27/Ayon}/Source/Ayon/Public/Logging/Ayon_Log.h (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Public/OpenPypePublishInstance.h (97%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype => UE_4.27/Ayon/Source/Ayon}/Public/OpenPypePublishInstanceFactory.h (88%) rename openpype/hosts/unreal/integration/{UE_4.7 => UE_4.27}/CommandletProject/.gitignore (100%) rename openpype/hosts/unreal/integration/{UE_4.7 => UE_4.27}/CommandletProject/CommandletProject.uproject (85%) delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype40.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/.gitignore (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/OpenPype.uplugin => Ayon/Ayon.uplugin} (87%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Config/DefaultAyonSettings.ini (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Config/FilterPlugin.ini (100%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype => UE_5.0/Ayon}/Content/Python/init_unreal.py (93%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/README.md (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Resources/ayon128.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Resources/ayon40.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Resources/ayon512.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Ayon.Build.cs (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/Ayon.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonAssetContainer.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonAssetContainerFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonCommands.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonLib.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonPythonBridge.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonSettings.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/AyonStyle.cpp (93%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/Commandlets/AyonActionResult.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/Source/OpenPype => Ayon/Source/Ayon}/Private/OpenPypePublishInstance.cpp (95%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/Source/OpenPype => Ayon/Source/Ayon}/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Ayon.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonAssetContainer.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonAssetContainerFactory.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonCommands.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonConstants.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonLib.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonPythonBridge.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonSettings.h (90%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/AyonStyle.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Commandlets/AyonActionResult.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype => Ayon}/Source/Ayon/Public/Logging/Ayon_Log.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{OpenPype/Source/OpenPype => Ayon/Source/Ayon}/Public/OpenPypePublishInstance.h (97%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype => UE_5.0/Ayon/Source/Ayon}/Public/OpenPypePublishInstanceFactory.h (88%) delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype40.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon40.png create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs => UE_5.1/Ayon/Source/Ayon/Ayon.Build.cs} (93%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp => UE_5.1/Ayon/Source/Ayon/Private/Ayon.cpp} (57%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp => UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp} (71%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp => UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp} (79%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp => UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp} (72%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h => UE_5.1/Ayon/Source/Ayon/Public/Ayon.h} (81%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h => UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h} (80%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h => UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h} (68%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h => UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h} (83%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h => UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h} (75%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h => UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h} (70%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h => UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h} (59%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h => UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h} (79%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h => UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h} (63%) rename openpype/hosts/unreal/integration/{UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h => UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h} (63%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h => UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h} (95%) rename openpype/hosts/unreal/integration/{UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h => UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h} (65%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore create mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 24e2db975d..2fb55a9b11 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -15,10 +15,11 @@ class UnrealAddon(OpenPypeModule, IHostAddon): """Modify environments to contain all required for implementation.""" # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation - ue_plugin = "UE_5.0" if app.name[:1] == "5" else "UE_4.7" + ue_version = app.name.replace("-",".") unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", ue_plugin, "OpenPype" + UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) + if not env.get("OPENPYPE_UNREAL_PLUGIN") or \ env.get("OPENPYPE_UNREAL_PLUGIN") != unreal_plugin_path: env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index da12bc75de..80ed946ec1 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -210,7 +210,7 @@ class UnrealPrelaunchHook(PreLaunchHook): if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): self.log.info(( - f"{self.signature} using OpenPype plugin from " + f"{self.signature} using Ayon plugin from " f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}" )) env_key = "OPENPYPE_UNREAL_PLUGIN" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin similarity index 86% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin index 37bb170eb4..299a5edc6a 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin @@ -14,11 +14,6 @@ "CanContainContent": true, "Installed": true, "Modules": [ - { - "Name": "OpenPype", - "Type": "Editor", - "LoadingPhase": "Default" - }, { "Name": "Ayon", "Type": "Editor", diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultAyonSettings.ini rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py similarity index 93% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py index b85f970699..9ed5a2cb19 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py @@ -16,7 +16,7 @@ if openpype_detected: @unreal.uclass() -class OpenPypeIntegration(unreal.OpenPypePythonBridge): +class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): unreal.log_warning("OpenPype: showing tools popup") diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon128.png rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon40.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon40.png rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon40.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/ayon512.png rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Ayon.Build.cs rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Ayon.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Ayon.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonLib.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonSettings.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp similarity index 98% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp index dc8f0f1f40..b133225fd5 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp @@ -44,7 +44,7 @@ const FVector2D Icon40x40(40.0f, 40.0f); TUniquePtr< FSlateStyleSet > FAyonStyle::Create() { TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); - Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/OpenPype/Resources")); + Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/Ayon/Resources")); return Style; } diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp similarity index 94% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 05638fbd0b..548bc4c399 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -3,8 +3,8 @@ #include "OpenPypePublishInstance.h" #include "AssetRegistryModule.h" -#include "OpenPypeLib.h" -#include "OpenPypeSettings.h" +#include "AyonLib.h" +#include "AyonSettings.h" #include "Framework/Notifications/NotificationManager.h" #include "Widgets/Notifications/SNotificationList.h" @@ -124,12 +124,12 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); //Get the current settings - const UOpenPypeSettings* Settings = GetMutableDefault(); + const UAyonSettings* Settings = GetMutableDefault(); //Color the base folder - UOpenPypeLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( "AssetRegistry"); @@ -141,7 +141,7 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() { for (const FString& Path : PathList) { - UOpenPypeLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); } } } diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Ayon.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainer.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonConstants.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonLib.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPythonBridge.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h similarity index 89% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h index f600cfbf9a..0902019c72 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonSettings.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h @@ -5,7 +5,7 @@ #include "CoreMinimal.h" #include "AyonSettings.generated.h" -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") +#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") UCLASS(Config=AyonSettings, DefaultConfig) class AYON_API UAyonSettings : public UObject diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonStyle.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h similarity index 97% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 8cfcd067c0..8f2dca5d69 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -6,7 +6,7 @@ UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypePublishInstance : public UPrimaryDataAsset +class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset { GENERATED_UCLASS_BODY() diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h similarity index 88% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 3fdb984411..54dc3e8c1d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -9,7 +9,7 @@ * */ UCLASS() -class OPENPYPE_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UOpenPypePublishInstanceFactory : public UFactory { GENERATED_BODY() diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore rename to openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject similarity index 85% rename from openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject rename to openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject index 4d75e03bf3..ea7bf21dc4 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject +++ b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject @@ -5,7 +5,7 @@ "Description": "", "Plugins": [ { - "Name": "OpenPype", + "Name": "Ayon", "Enabled": true } ] diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini deleted file mode 100644 index 8a883cf1db..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/OpenPype.OpenPypeSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png deleted file mode 100644 index abe8a807ef40f00b75d7446d020a2437732c7583..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14594 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 diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png deleted file mode 100644 index 97c4d4326bc16ba6dfb45d35c4362d8bc15900ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85856 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 bCJFu@j}?W0K?Ok;00000NkvXXu0mjf9`4Bp diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index 424addd7bf..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistryModule.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Framework/Notifications/NotificationManager.h" -#include "Widgets/Notifications/SNotificationList.h" - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif - -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index c54e789dca..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs deleted file mode 100644 index f77c1383eb..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -using UnrealBuildTool; - -public class OpenPype : ModuleRules -{ - public OpenPype(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - - PublicIncludePaths.AddRange( - new string[] { - // ... add public include paths required here ... - } - ); - - - PrivateIncludePaths.AddRange( - new string[] { - // ... add other private include paths required here ... - } - ); - - - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - // ... add other public dependencies that you statically link with here ... - } - ); - - - PrivateDependencyModuleNames.AddRange( - new string[] - { - "GameProjectGeneration", - "Projects", - "InputCore", - "UnrealEd", - "LevelEditor", - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "AssetTools" - // ... add private dependencies that you statically link with here ... - } - ); - - - DynamicallyLoadedModuleNames.AddRange( - new string[] - { - // ... add any modules that your module loads dynamically here ... - } - ); - } -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp deleted file mode 100644 index c766f87a8e..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainer.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AssetContainer.h" -#include "AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Engine.h" -#include "Containers/UnrealString.h" - -UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed); -} - -void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - assets.Add(assetPath); - assetsData.Add(AssetData); - UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); - } - } -} - -void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - FString path = UAssetContainer::GetPathName(); - FString lpp = FPackageName::GetLongPackagePath(*path); - - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); - assets.Remove(assetPath); - assetsData.Remove(AssetData); - } - } -} - -void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - - assets.Remove(str); - assets.Add(assetPath); - assetsData.Remove(AssetData); - // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); - } - } -} - diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp deleted file mode 100644 index b943150bdd..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AssetContainerFactory.h" -#include "AssetContainer.h" - -UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp deleted file mode 100644 index abb1975027..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" - -#include "Editor.h" -#include "GameProjectUtils.h" -#include "OPConstants.h" -#include "Commandlets/OPActionResult.h" -#include "ProjectDescriptor.h" - -int32 UOPGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FOPGenerateProjectParams ParsedParams = FOPGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_OP_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_OP_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_OP_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for OpenPype - return 0; -} - - -FOPGenerateProjectParams::FOPGenerateProjectParams(): FOPGenerateProjectParams("") -{ -} - -FOPGenerateProjectParams::FOPGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FOPGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FOPGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FOPGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FOPGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UOPGenerateProjectCommandlet::UOPGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FOP_ActionResult(EOP_ActionResult::ProjectNotCreated, FailReason); - return FOP_ActionResult(); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bLoaded ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotLoaded, FailReason); -} - -void UOPGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor OPPluginDescriptor; - OPPluginDescriptor.bEnabled = true; - OPPluginDescriptor.Name = OPConstants::OP_PluginName; - ProjectDescriptor.Plugins.Add(OPPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = OPConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = OPConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = OPConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = OPConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bSaved ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotSaved, FailReason); -} - -FOPGenerateProjectParams UOPGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FOPGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp deleted file mode 100644 index 6e50ef2221..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - - -#include "Commandlets/OPActionResult.h" -#include "Logging/OP_Log.h" - -EOP_ActionResult::Type& FOP_ActionResult::GetStatus() -{ - return Status; -} - -FText& FOP_ActionResult::GetReason() -{ - return Reason; -} - -FOP_ActionResult::FOP_ActionResult():Status(EOP_ActionResult::Type::Ok) -{ - -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FOP_ActionResult::IsProblem() const -{ - return Status != EOP_ActionResult::Ok; -} - -void FOP_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp deleted file mode 100644 index 29b1068c21..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp deleted file mode 100644 index 9bf7b341c5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "OpenPype.h" - -#include "ISettingsContainer.h" -#include "ISettingsModule.h" -#include "ISettingsSection.h" -#include "LevelEditor.h" -#include "OpenPypePythonBridge.h" -#include "OpenPypeSettings.h" -#include "OpenPypeStyle.h" - - -static const FName OpenPypeTabName("OpenPype"); - -#define LOCTEXT_NAMESPACE "FOpenPypeModule" - -// This function is triggered when the plugin is staring up -void FOpenPypeModule::StartupModule() -{ - if (!IsRunningCommandlet()) { - FOpenPypeStyle::Initialize(); - FOpenPypeStyle::SetIcon("Logo", "openpype40"); - - // Create the Extender that will add content to the menu - FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); - - TSharedPtr MenuExtender = MakeShareable(new FExtender()); - TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); - - MenuExtender->AddMenuExtension( - "LevelEditor", - EExtensionHook::After, - NULL, - FMenuExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddMenuEntry) - ); - ToolbarExtender->AddToolBarExtension( - "Settings", - EExtensionHook::After, - NULL, - FToolBarExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddToobarEntry)); - - - LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); - LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); - - RegisterSettings(); - } -} - -void FOpenPypeModule::ShutdownModule() -{ - FOpenPypeStyle::Shutdown(); -} - - -void FOpenPypeModule::AddMenuEntry(FMenuBuilder& MenuBuilder) -{ - // Create Section - MenuBuilder.BeginSection("OpenPype", TAttribute(FText::FromString("OpenPype"))); - { - // Create a Submenu inside of the Section - MenuBuilder.AddMenuEntry( - FText::FromString("Tools..."), - FText::FromString("Pipeline tools"), - FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup)) - ); - - MenuBuilder.AddMenuEntry( - FText::FromString("Tools dialog..."), - FText::FromString("Pipeline tools dialog"), - FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog)) - ); - } - MenuBuilder.EndSection(); -} - -void FOpenPypeModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) -{ - ToolbarBuilder.BeginSection(TEXT("OpenPype")); - { - ToolbarBuilder.AddToolBarButton( - FUIAction( - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), - NULL, - FIsActionChecked() - - ), - NAME_None, - LOCTEXT("OpenPype_label", "OpenPype"), - LOCTEXT("OpenPype_tooltip", "OpenPype Tools"), - FSlateIcon(FOpenPypeStyle::GetStyleSetName(), "OpenPype.Logo") - ); - } - ToolbarBuilder.EndSection(); -} - -void FOpenPypeModule::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UOpenPypeSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "OpenPype", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FOpenPypeModule::HandleSettingsSaved); - } -} - -bool FOpenPypeModule::HandleSettingsSaved() -{ - UOpenPypeSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - - -void FOpenPypeModule::MenuPopup() -{ - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FOpenPypeModule::MenuDialog() -{ - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp deleted file mode 100644 index 6ebfc528f0..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "OpenPypePythonBridge.h" - -UOpenPypePythonBridge* UOpenPypePythonBridge::Get() -{ - TArray OpenPypePythonBridgeClasses; - GetDerivedClasses(UOpenPypePythonBridge::StaticClass(), OpenPypePythonBridgeClasses); - int32 NumClasses = OpenPypePythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(OpenPypePythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp deleted file mode 100644 index dd4228dfd0..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "OpenPypeSettings.h" - -#include "Interfaces/IPluginManager.h" - -/** - * Mainly is used for initializing default values if the DefaultOpenPypeSettings.ini file does not exist in the saved config - */ -UOpenPypeSettings::UOpenPypeSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/OpenPype.OpenPypeSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp deleted file mode 100644 index 0cc854c5ef..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "OpenPypeStyle.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyle.h" -#include "Styling/SlateStyleRegistry.h" - - -TUniquePtr< FSlateStyleSet > FOpenPypeStyle::OpenPypeStyleInstance = nullptr; - -void FOpenPypeStyle::Initialize() -{ - if (!OpenPypeStyleInstance.IsValid()) - { - OpenPypeStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*OpenPypeStyleInstance); - } -} - -void FOpenPypeStyle::Shutdown() -{ - if (OpenPypeStyleInstance.IsValid()) - { - FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance); - OpenPypeStyleInstance.Reset(); - } -} - -FName FOpenPypeStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("OpenPypeStyle")); - return StyleSetName; -} - -FName FOpenPypeStyle::GetContextName() -{ - static FName ContextName(TEXT("OpenPype")); - return ContextName; -} - -#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) - -const FVector2D Icon40x40(40.0f, 40.0f); - -TUniquePtr< FSlateStyleSet > FOpenPypeStyle::Create() -{ - TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); - Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/OpenPype/Resources")); - - return Style; -} - -void FOpenPypeStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) -{ - FSlateStyleSet* Style = OpenPypeStyleInstance.Get(); - - FString Name(GetContextName().ToString()); - Name = Name + "." + StyleName; - Style->Set(*Name, new FSlateImageBrush(Style->RootToContentDir(ResourcePath, TEXT(".png")), Icon40x40)); - - - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); -} - -#undef IMAGE_BRUSH - -const ISlateStyle& FOpenPypeStyle::Get() -{ - check(OpenPypeStyleInstance); - return *OpenPypeStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h deleted file mode 100644 index 3b0230391c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainer.h +++ /dev/null @@ -1,39 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/NoExportTypes.h" -#include "Engine/AssetUserData.h" -#include "AssetData.h" -#include "AssetContainer.generated.h" - -/** - * - */ -UCLASS(Blueprintable) -class OPENPYPE_API UAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") - TArray assets; - - // There seems to be no reflection option to expose array of FAssetData - /* - UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) - TArray assetsData; - */ -private: - TArray assetsData; - void OnAssetAdded(const FAssetData& AssetData); - void OnAssetRemoved(const FAssetData& AssetData); - void OnAssetRenamed(const FAssetData& AssetData, const FString& str); -}; - - diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h deleted file mode 100644 index 331ce6bb50..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h +++ /dev/null @@ -1,21 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AssetContainerFactory.generated.h" - -/** - * - */ -UCLASS() -class OPENPYPE_API UAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h deleted file mode 100644 index d1129aa070..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "GameProjectUtils.h" -#include "Commandlets/OPActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "OPGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FOPGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FOPGenerateProjectParams(); - FOPGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class OPENPYPE_API UOPGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UOPGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FOPGenerateProjectParams ParseParameters(const FString& Params) const; - FOP_ActionResult TryCreateProject() const; - FOP_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FOP_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h deleted file mode 100644 index 3740c5285a..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h deleted file mode 100644 index 2454344128..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "Engine.h" - - -class FOpenPypeModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterSettings(); - bool HandleSettingsSaved(); - - void AddMenuEntry(FMenuBuilder& MenuBuilder); - void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); - void MenuPopup(); - void MenuDialog(); -}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h deleted file mode 100644 index ef4d1027ea..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "Engine.h" -#include "OpenPypeLib.generated.h" - - -UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypeLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd); - - UFUNCTION(BlueprintCallable, Category = Python) - static TArray GetAllProperties(UClass* cls); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h deleted file mode 100644 index 88defaa773..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "OpenPypeSettings.generated.h" - -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultOpenPypeSettings.ini") - -UCLASS(Config=OpenPypeSettings, DefaultConfig) -class OPENPYPE_API UOpenPypeSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h deleted file mode 100644 index 0e4af129d0..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "CoreMinimal.h" - -class FSlateStyleSet; -class ISlateStyle; - - -class FOpenPypeStyle -{ -public: - static void Initialize(); - static void Shutdown(); - static const ISlateStyle& Get(); - static FName GetStyleSetName(); - static FName GetContextName(); - - static void SetIcon(const FString& StyleName, const FString& ResourcePath); - -private: - static TUniquePtr< FSlateStyleSet > Create(); - static TUniquePtr< FSlateStyleSet > OpenPypeStyleInstance; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/.gitignore rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin similarity index 87% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin index 0fe7b249a8..c93a9b4b68 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin @@ -15,11 +15,6 @@ "IsExperimentalVersion": false, "Installed": true, "Modules": [ - { - "Name": "OpenPype", - "Type": "Editor", - "LoadingPhase": "Default" - }, { "Name": "Ayon", "Type": "Editor", diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultAyonSettings.ini rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py similarity index 93% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py index b85f970699..9ed5a2cb19 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py @@ -16,7 +16,7 @@ if openpype_detected: @unreal.uclass() -class OpenPypeIntegration(unreal.OpenPypePythonBridge): +class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): unreal.log_warning("OpenPype: showing tools popup") diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon128.png rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon40.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon40.png rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon40.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/ayon512.png rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Ayon.Build.cs rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Ayon.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Ayon.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonAssetContainerFactory.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonCommands.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonLib.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonSettings.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp similarity index 93% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp index 91a0c6996b..d88df78735 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp @@ -40,7 +40,7 @@ const FVector2D Icon40x40(40.0f, 40.0f); TSharedRef< FSlateStyleSet > FAyonStyle::Create() { TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/AyonActionResult.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp similarity index 95% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 05d5c8a87d..0d9cddfd1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -5,8 +5,8 @@ #include "AssetRegistry/AssetRegistryModule.h" #include "AssetToolsModule.h" #include "Framework/Notifications/NotificationManager.h" -#include "OpenPypeLib.h" -#include "OpenPypeSettings.h" +#include "AyonLib.h" +#include "AyonSettings.h" #include "Widgets/Notifications/SNotificationList.h" @@ -125,10 +125,10 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); //Get the current settings - const UOpenPypeSettings* Settings = GetMutableDefault(); + const UAyonSettings* Settings = GetMutableDefault(); //Color the base folder - UOpenPypeLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( @@ -142,7 +142,7 @@ void UOpenPypePublishInstance::ColorOpenPypeDirs() { for (const FString& Path : PathList) { - UOpenPypeLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); } } } diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Ayon.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainer.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonAssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonCommands.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonConstants.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonLib.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPythonBridge.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h similarity index 90% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h index 42a724b95a..4f12d1a5f2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonSettings.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h @@ -6,7 +6,7 @@ #include "UObject/Object.h" #include "AyonSettings.generated.h" -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") UCLASS(Config=AyonSettings, DefaultConfig) class AYON_API UAyonSettings : public UObject diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonStyle.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/AyonActionResult.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/Logging/Ayon_Log.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h similarity index 97% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index bce41ef1b1..03a22c6cde 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -6,7 +6,7 @@ UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypePublishInstance : public UPrimaryDataAsset +class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset { GENERATED_UCLASS_BODY() diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h similarity index 88% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 3fdb984411..54dc3e8c1d 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -9,7 +9,7 @@ * */ UCLASS() -class OPENPYPE_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UOpenPypePublishInstanceFactory : public UFactory { GENERATED_BODY() diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject index c8dc1c673e..9cf75ebaf2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject +++ b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject @@ -12,7 +12,7 @@ ] }, { - "Name": "OpenPype", + "Name": "Ayon", "Enabled": true, "Type": "Editor" } diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini deleted file mode 100644 index 8a883cf1db..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/OpenPype.OpenPypeSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png deleted file mode 100644 index abe8a807ef40f00b75d7446d020a2437732c7583..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14594 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 diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png deleted file mode 100644 index 97c4d4326bc16ba6dfb45d35c4362d8bc15900ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85856 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 bCJFu@j}?W0K?Ok;00000NkvXXu0mjf9`4Bp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index c54e789dca..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index 1c51f98b4a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 443d618c9a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp deleted file mode 100644 index b943150bdd..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AssetContainerFactory.h" -#include "AssetContainer.h" - -UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp deleted file mode 100644 index abb1975027..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" - -#include "Editor.h" -#include "GameProjectUtils.h" -#include "OPConstants.h" -#include "Commandlets/OPActionResult.h" -#include "ProjectDescriptor.h" - -int32 UOPGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FOPGenerateProjectParams ParsedParams = FOPGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_OP_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_OP_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_OP_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for OpenPype - return 0; -} - - -FOPGenerateProjectParams::FOPGenerateProjectParams(): FOPGenerateProjectParams("") -{ -} - -FOPGenerateProjectParams::FOPGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FOPGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FOPGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FOPGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FOPGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UOPGenerateProjectCommandlet::UOPGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FOP_ActionResult(EOP_ActionResult::ProjectNotCreated, FailReason); - return FOP_ActionResult(); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bLoaded ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotLoaded, FailReason); -} - -void UOPGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor OPPluginDescriptor; - OPPluginDescriptor.bEnabled = true; - OPPluginDescriptor.Name = OPConstants::OP_PluginName; - ProjectDescriptor.Plugins.Add(OPPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = OPConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = OPConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = OPConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = OPConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FOP_ActionResult UOPGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FOP_ActionResult(bSaved ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotSaved, FailReason); -} - -FOPGenerateProjectParams UOPGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FOPGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp deleted file mode 100644 index 23ae2dd329..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Commandlets/OPActionResult.h" -#include "Logging/OP_Log.h" - -EOP_ActionResult::Type& FOP_ActionResult::GetStatus() -{ - return Status; -} - -FText& FOP_ActionResult::GetReason() -{ - return Reason; -} - -FOP_ActionResult::FOP_ActionResult():Status(EOP_ActionResult::Type::Ok) -{ - -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FOP_ActionResult::IsProblem() const -{ - return Status != EOP_ActionResult::Ok; -} - -void FOP_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp deleted file mode 100644 index 198fb9df0c..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp +++ /dev/null @@ -1,3 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp deleted file mode 100644 index 881814e278..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "OpenPypeCommands.h" - -#define LOCTEXT_NAMESPACE "FOpenPypeModule" - -void FOpenPypeCommands::RegisterCommands() -{ - UI_COMMAND(OpenPypeTools, "OpenPype Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(OpenPypeToolsDialog, "OpenPype Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); -} - -#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp deleted file mode 100644 index 34faba1f49..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "OpenPypeLib.h" - -#include "AssetViewUtils.h" -#include "Misc/Paths.h" -#include "Misc/ConfigCacheIni.h" -#include "UObject/UnrealType.h" - -/** - * Sets color on folder icon on given path - * @param InPath - path to folder - * @param InFolderColor - color of the folder - * @warning This color will appear only after Editor restart. Is there a better way? - */ - -bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all properties on given object - * @param cls - class - * @return TArray of properties - */ -TArray UOpenPypeLib::GetAllProperties(UClass* cls) -{ - TArray Ret; - if (cls != nullptr) - { - for (TFieldIterator It(cls); It; ++It) - { - FProperty* Property = *It; - if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) - { - Ret.Add(Property->GetName()); - } - } - } - return Ret; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp deleted file mode 100644 index 6ebfc528f0..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "OpenPypePythonBridge.h" - -UOpenPypePythonBridge* UOpenPypePythonBridge::Get() -{ - TArray OpenPypePythonBridgeClasses; - GetDerivedClasses(UOpenPypePythonBridge::StaticClass(), OpenPypePythonBridgeClasses); - int32 NumClasses = OpenPypePythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(OpenPypePythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp deleted file mode 100644 index 6562a81138..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "OpenPypeSettings.h" - -#include "Interfaces/IPluginManager.h" -#include "UObject/UObjectGlobals.h" - -/** - * Mainly is used for initializing default values if the DefaultOpenPypeSettings.ini file does not exist in the saved config - */ -UOpenPypeSettings::UOpenPypeSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/OpenPype.OpenPypeSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp deleted file mode 100644 index a4d75e048e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "OpenPypeStyle.h" -#include "OpenPype.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyleRegistry.h" -#include "Slate/SlateGameResources.h" -#include "Interfaces/IPluginManager.h" -#include "Styling/SlateStyleMacros.h" - -#define RootToContentDir Style->RootToContentDir - -TSharedPtr FOpenPypeStyle::OpenPypeStyleInstance = nullptr; - -void FOpenPypeStyle::Initialize() -{ - if (!OpenPypeStyleInstance.IsValid()) - { - OpenPypeStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*OpenPypeStyleInstance); - } -} - -void FOpenPypeStyle::Shutdown() -{ - FSlateStyleRegistry::UnRegisterSlateStyle(*OpenPypeStyleInstance); - ensure(OpenPypeStyleInstance.IsUnique()); - OpenPypeStyleInstance.Reset(); -} - -FName FOpenPypeStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("OpenPypeStyle")); - return StyleSetName; -} - -const FVector2D Icon16x16(16.0f, 16.0f); -const FVector2D Icon20x20(20.0f, 20.0f); -const FVector2D Icon40x40(40.0f, 40.0f); - -TSharedRef< FSlateStyleSet > FOpenPypeStyle::Create() -{ - TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("OpenPypeStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Resources")); - - Style->Set("OpenPype.OpenPypeTools", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); - Style->Set("OpenPype.OpenPypeToolsDialog", new IMAGE_BRUSH(TEXT("openpype40"), Icon40x40)); - - return Style; -} - -void FOpenPypeStyle::ReloadTextures() -{ - if (FSlateApplication::IsInitialized()) - { - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); - } -} - -const ISlateStyle& FOpenPypeStyle::Get() -{ - return *OpenPypeStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h deleted file mode 100644 index 322a23a3e8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "OPActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FOP_ActionResult structure - */ -#define EVALUATE_OP_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future a web document should exists with the mapped error code & what problem occurred & how to repair it... -*/ -UENUM() -namespace EOP_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FOP_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FOP_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EOP_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EOP_ActionResult::Ok - */ - bool IsProblem() const; - EOP_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h deleted file mode 100644 index 3740c5285a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h deleted file mode 100644 index f4587f7a50..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -namespace OPConstants -{ - const FString OP_PluginName = "OpenPype"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h deleted file mode 100644 index 99b0be26f0..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Framework/Commands/Commands.h" -#include "OpenPypeStyle.h" - -class FOpenPypeCommands : public TCommands -{ -public: - - FOpenPypeCommands() - : TCommands(TEXT("OpenPype"), NSLOCTEXT("Contexts", "OpenPype", "OpenPype Tools"), NAME_None, FOpenPypeStyle::GetStyleSetName()) - { - } - - // TCommands<> interface - virtual void RegisterCommands() override; - -public: - TSharedPtr< FUICommandInfo > OpenPypeTools; - TSharedPtr< FUICommandInfo > OpenPypeToolsDialog; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h deleted file mode 100644 index 827f76f56b..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "Engine.h" -#include "OpenPypePythonBridge.generated.h" - -UCLASS(Blueprintable) -class UOpenPypePythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UOpenPypePythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; - -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore new file mode 100644 index 0000000000..b32a6f55e5 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore @@ -0,0 +1,35 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +/Binaries +/Intermediate diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin new file mode 100644 index 0000000000..c93a9b4b68 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin @@ -0,0 +1,24 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "OpenPype", + "Description": "OpenPype Integration", + "Category": "OpenPype.Integration", + "CreatedBy": "Ondrej Samohel", + "CreatedByURL": "https://openpype.io", + "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "MarketplaceURL": "", + "SupportURL": "https://pype.club/", + "CanContainContent": true, + "EngineVersion": "5.0", + "IsExperimentalVersion": false, + "Installed": true, + "Modules": [ + { + "Name": "Ayon", + "Type": "Editor", + "LoadingPhase": "Default" + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini new file mode 100644 index 0000000000..9ad7f55201 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini @@ -0,0 +1,2 @@ +[/Script/Ayon.AyonSettings] +FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini new file mode 100644 index 0000000000..ccebca2f32 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini @@ -0,0 +1,8 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py new file mode 100644 index 0000000000..9ed5a2cb19 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py @@ -0,0 +1,30 @@ +import unreal + +openpype_detected = True +try: + from openpype.pipeline import install_host + from openpype.hosts.unreal.api import UnrealHost + + openpype_host = UnrealHost() +except ImportError as exc: + openpype_host = None + openpype_detected = False + unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + +if openpype_detected: + install_host(openpype_host) + + +@unreal.uclass() +class AyonIntegration(unreal.AyonPythonBridge): + @unreal.ufunction(override=True) + def RunInPython_Popup(self): + unreal.log_warning("OpenPype: showing tools popup") + if openpype_detected: + openpype_host.show_tools_popup() + + @unreal.ufunction(override=True) + def RunInPython_Dialog(self): + unreal.log_warning("OpenPype: showing tools dialog") + if openpype_detected: + openpype_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md new file mode 100644 index 0000000000..cf0aa622c2 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md @@ -0,0 +1,11 @@ +# OpenPype Unreal Integration plugin - UE 5.x + +This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run. + +## How does this work + +Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button +on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are +declared in C++ but needs to be implemented during Unreal Editor +startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor +automatically. diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png new file mode 100644 index 0000000000000000000000000000000000000000..799d849aa3163ecb16be39c641a6ac30324906b9 GIT binary patch literal 2358 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 D;gw7~ literal 0 HcmV?d00001 diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png new file mode 100644 index 0000000000000000000000000000000000000000..990d5917e232a0644820428fb2790943de5ffaa4 GIT binary patch literal 16705 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 - FOpenPypeCommands::Get().OpenPypeTools, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuPopup), + FAyonCommands::Get().AyonTools, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), FCanExecuteAction()); PluginCommands->MapAction( - FOpenPypeCommands::Get().OpenPypeToolsDialog, - FExecuteAction::CreateRaw(this, &FOpenPypeModule::MenuDialog), + FAyonCommands::Get().AyonToolsDialog, + FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), FCanExecuteAction()); UToolMenus::RegisterStartupCallback( - FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOpenPypeModule::RegisterMenus)); + FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); RegisterSettings(); } -void FOpenPypeModule::ShutdownModule() +void FAyonModule::ShutdownModule() { UToolMenus::UnRegisterStartupCallback(this); UToolMenus::UnregisterOwner(this); - FOpenPypeStyle::Shutdown(); + FAyonStyle::Shutdown(); - FOpenPypeCommands::Unregister(); + FAyonCommands::Unregister(); } -void FOpenPypeModule::RegisterSettings() +void FAyonModule::RegisterSettings() { ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); @@ -60,10 +59,10 @@ void FOpenPypeModule::RegisterSettings() // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - UOpenPypeSettings* Settings = GetMutableDefault(); + UAyonSettings* Settings = GetMutableDefault(); // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "OpenPype", "General", + ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", LOCTEXT("RuntimeGeneralSettingsName", "General"), LOCTEXT("RuntimeGeneralSettingsDescription", @@ -75,13 +74,13 @@ void FOpenPypeModule::RegisterSettings() // validate those or just act to settings changes. if (SettingsSection.IsValid()) { - SettingsSection->OnModified().BindRaw(this, &FOpenPypeModule::HandleSettingsSaved); + SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); } } -bool FOpenPypeModule::HandleSettingsSaved() +bool FAyonModule::HandleSettingsSaved() { - UOpenPypeSettings* Settings = GetMutableDefault(); + UAyonSettings* Settings = GetMutableDefault(); bool ResaveSettings = false; // You can put any validation code in here and resave the settings in case an invalid @@ -95,7 +94,7 @@ bool FOpenPypeModule::HandleSettingsSaved() return true; } -void FOpenPypeModule::RegisterMenus() +void FAyonModule::RegisterMenus() { // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner FToolMenuOwnerScoped OwnerScoped(this); @@ -103,21 +102,21 @@ void FOpenPypeModule::RegisterMenus() { UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); { - // FToolMenuSection& Section = Menu->FindOrAddSection("OpenPype"); + // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); FToolMenuSection& Section = Menu->AddSection( - "OpenPype", - TAttribute(FText::FromString("OpenPype")), + "Ayon", + TAttribute(FText::FromString("Ayon")), FToolMenuInsert("Programming", EToolMenuInsertType::Before) ); - Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeTools, PluginCommands); - Section.AddMenuEntryWithCommandList(FOpenPypeCommands::Get().OpenPypeToolsDialog, PluginCommands); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); + Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); } UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); { FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); { FToolMenuEntry& Entry = Section.AddEntry( - FToolMenuEntry::InitToolBarButton(FOpenPypeCommands::Get().OpenPypeTools)); + FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); Entry.SetCommandList(PluginCommands); } } @@ -125,16 +124,16 @@ void FOpenPypeModule::RegisterMenus() } -void FOpenPypeModule::MenuPopup() +void FAyonModule::MenuPopup() { - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); bridge->RunInPython_Popup(); } -void FOpenPypeModule::MenuDialog() +void FAyonModule::MenuDialog() { - UOpenPypePythonBridge* bridge = UOpenPypePythonBridge::Get(); + UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); bridge->RunInPython_Dialog(); } -IMPLEMENT_MODULE(FOpenPypeModule, OpenPype) +IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp similarity index 71% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp index 06dcd67808..3022757dc8 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp @@ -1,36 +1,35 @@ // Fill out your copyright notice in the Description page of Project Settings. -#include "AssetContainer.h" +#include "AyonAssetContainer.h" #include "AssetRegistry/AssetRegistryModule.h" #include "Misc/PackageName.h" -#include "Engine.h" #include "Containers/UnrealString.h" -UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) +UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) : UAssetUserData(ObjectInitializer) { FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); + FString path = UAyonAssetContainer::GetPathName(); + UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); FARFilter Filter; Filter.PackagePaths.Add(FName(*path)); - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed); + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); } -void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) +void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) { TArray split; // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); + FString selfFullPath = UAyonAssetContainer::GetPathName(); FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); + FString assetFName = AssetData.GetObjectPathString(); UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -50,17 +49,17 @@ void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) } } -void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) +void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) { TArray split; // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); + FString selfFullPath = UAyonAssetContainer::GetPathName(); FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); + FString assetFName = AssetData.GetObjectPathString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -68,7 +67,7 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) FString assetDir = FPackageName::GetLongPackagePath(*split[1]); // take interest only in paths starting with path of current container - FString path = UAssetContainer::GetPathName(); + FString path = UAyonAssetContainer::GetPathName(); FString lpp = FPackageName::GetLongPackagePath(*path); if (assetDir.StartsWith(*selfDir)) @@ -83,17 +82,17 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) } } -void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) +void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) { TArray split; // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); + FString selfFullPath = UAyonAssetContainer::GetPathName(); FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); + FString assetFName = AssetData.GetObjectPathString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp new file mode 100644 index 0000000000..086fc1036e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp @@ -0,0 +1,20 @@ +#include "AyonAssetContainerFactory.h" +#include "AyonAssetContainer.h" + +UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonAssetContainer::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); + return AssetContainer; +} + +bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp new file mode 100644 index 0000000000..566ee1dcd1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp @@ -0,0 +1,13 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonCommands.h" + +#define LOCTEXT_NAMESPACE "FAyonModule" + +void FAyonCommands::RegisterCommands() +{ + UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); +} + +#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp similarity index 79% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp index 34faba1f49..7cfa0c9c30 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp @@ -1,9 +1,7 @@ // Copyright 2023, Ayon, All rights reserved. -#include "OpenPypeLib.h" +#include "AyonLib.h" #include "AssetViewUtils.h" -#include "Misc/Paths.h" -#include "Misc/ConfigCacheIni.h" #include "UObject/UnrealType.h" /** @@ -13,7 +11,7 @@ * @warning This color will appear only after Editor restart. Is there a better way? */ -bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) +bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) { if (AssetViewUtils::DoesFolderExist(FolderPath)) { @@ -31,11 +29,11 @@ bool UOpenPypeLib::SetFolderColor(const FString& FolderPath, const FLinearColor& } /** - * Returns all properties on given object + * Returns all poperties on given object * @param cls - class * @return TArray of properties */ -TArray UOpenPypeLib::GetAllProperties(UClass* cls) +TArray UAyonLib::GetAllProperties(UClass* cls) { TArray Ret; if (cls != nullptr) diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp new file mode 100644 index 0000000000..0ed4b2f704 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp @@ -0,0 +1,14 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "AyonPythonBridge.h" + +UAyonPythonBridge* UAyonPythonBridge::Get() +{ + TArray AyonPythonBridgeClasses; + GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); + int32 NumClasses = AyonPythonBridgeClasses.Num(); + if (NumClasses > 0) + { + return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); + } + return nullptr; +}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp new file mode 100644 index 0000000000..da388fbc8f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonSettings.h" + +#include "Interfaces/IPluginManager.h" +#include "UObject/UObjectGlobals.h" + +/** + * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config + */ +UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) +{ + + const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; + + // This has to be probably in the future set using the UE Reflection system + FColor Color; + GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); + + FolderColor = Color; +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp new file mode 100644 index 0000000000..d88df78735 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp @@ -0,0 +1,62 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "AyonStyle.h" +#include "Framework/Application/SlateApplication.h" +#include "Styling/SlateStyleRegistry.h" +#include "Slate/SlateGameResources.h" +#include "Interfaces/IPluginManager.h" +#include "Styling/SlateStyleMacros.h" + +#define RootToContentDir Style->RootToContentDir + +TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; + +void FAyonStyle::Initialize() +{ + if (!AyonStyleInstance.IsValid()) + { + AyonStyleInstance = Create(); + FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); + } +} + +void FAyonStyle::Shutdown() +{ + FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); + ensure(AyonStyleInstance.IsUnique()); + AyonStyleInstance.Reset(); +} + +FName FAyonStyle::GetStyleSetName() +{ + static FName StyleSetName(TEXT("AyonStyle")); + return StyleSetName; +} + +const FVector2D Icon16x16(16.0f, 16.0f); +const FVector2D Icon20x20(20.0f, 20.0f); +const FVector2D Icon40x40(40.0f, 40.0f); + +TSharedRef< FSlateStyleSet > FAyonStyle::Create() +{ + TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); + + Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); + + return Style; +} + +void FAyonStyle::ReloadTextures() +{ + if (FSlateApplication::IsInitialized()) + { + FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); + } +} + +const ISlateStyle& FAyonStyle::Get() +{ + return *AyonStyleInstance; +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp new file mode 100644 index 0000000000..2a137e3ed7 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp @@ -0,0 +1,40 @@ +// Copyright 2023, Ayon, All rights reserved. + +#include "Commandlets/AyonActionResult.h" +#include "Logging/Ayon_Log.h" + +EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() +{ + return Status; +} + +FText& FAyon_ActionResult::GetReason() +{ + return Reason; +} + +FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) +{ + +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FAyon_ActionResult::IsProblem() const +{ + return Status != EAyon_ActionResult::Ok; +} + +void FAyon_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..ed876c8128 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp @@ -0,0 +1,140 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" + +#include "GameProjectUtils.h" +#include "AyonConstants.h" +#include "Commandlets/AyonActionResult.h" +#include "ProjectDescriptor.h" + +int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_Ayon_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for Ayon + return 0; +} + + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") +{ +} + +FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FAyonGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); + return FAyon_ActionResult(); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); +} + +void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor AyonPluginDescriptor; + AyonPluginDescriptor.bEnabled = true; + AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; + ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); +} + +FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FAyonGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp similarity index 72% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index f8d95ac048..0d9cddfd1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/Ayon/Private/AyonPublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,8 +1,9 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "AyonPublishInstance.h" +#include "OpenPypePublishInstance.h" #include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" #include "Framework/Notifications/NotificationManager.h" #include "AyonLib.h" #include "AyonSettings.h" @@ -13,7 +14,7 @@ #define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ VAR.Shrink(); -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) +UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) : UPrimaryDataAsset(ObjectInitializer) { const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< @@ -37,16 +38,16 @@ UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectIniti REMOVE_INVALID_ENTRIES(AssetDataInternal) REMOVE_INVALID_ENTRIES(AssetDataExternal) - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); #ifdef WITH_EDITOR - ColorAyonDirs(); + ColorOpenPypeDirs(); #endif } -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) { TArray split; @@ -59,7 +60,7 @@ void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) return; } - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; if (result) { @@ -71,9 +72,9 @@ void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) } } -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) { - if (Cast(InAssetData.GetAsset()) == nullptr) + if (Cast(InAssetData.GetAsset()) == nullptr) { if (AssetDataInternal.Contains(nullptr)) { @@ -88,13 +89,13 @@ void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) } } -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) { REMOVE_INVALID_ENTRIES(AssetDataInternal); REMOVE_INVALID_ENTRIES(AssetDataExternal); } -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const { FString ThisLeft, ThisRight; this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); @@ -104,20 +105,20 @@ bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const #ifdef WITH_EDITOR -void UAyonPublishInstance::ColorAyonDirs() +void UOpenPypePublishInstance::ColorOpenPypeDirs() { FString PathName = this->GetPathName(); - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; + //Check whether the path contains the defined OpenPype folder + if (!PathName.Contains(TEXT("OpenPype"))) return; //Get the base path for open pype FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); if (PathLeft.IsEmpty() || PathRight.IsEmpty()) { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) return; } @@ -129,7 +130,7 @@ void UAyonPublishInstance::ColorAyonDirs() //Color the base folder UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( "AssetRegistry"); @@ -146,7 +147,7 @@ void UAyonPublishInstance::ColorAyonDirs() } } -void UAyonPublishInstance::SendNotification(const FString& Text) const +void UOpenPypePublishInstance::SendNotification(const FString& Text) const { FNotificationInfo Info{FText::FromString(Text)}; @@ -167,13 +168,13 @@ void UAyonPublishInstance::SendNotification(const FString& Text) const } -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { Super::PostEditChangeProperty(PropertyChangedEvent); if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) + UOpenPypePublishInstance, AssetDataExternal)) { // Check for duplicated assets for (const auto& Asset : AssetDataInternal) @@ -186,10 +187,10 @@ void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& Propert } } - // Check if no UAyonPublishInstance type assets are included + // Check if no UOpenPypePublishInstance type assets are included for (const auto& Asset : AssetDataExternal) { - if (Cast(Asset.Get()) != nullptr) + if (Cast(Asset.Get()) != nullptr) { AssetDataExternal.Remove(Asset); return SendNotification("You are not allowed to add publish instances!"); diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp new file mode 100644 index 0000000000..a32ebe32cb --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -0,0 +1,21 @@ +// Copyright 2023, Ayon, All rights reserved. +#include "OpenPypePublishInstanceFactory.h" +#include "OpenPypePublishInstance.h" + +UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UOpenPypePublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h similarity index 81% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h index b89760099b..bb25430411 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h @@ -3,10 +3,9 @@ #pragma once #include "CoreMinimal.h" -#include "Modules/ModuleManager.h" -class FOpenPypeModule : public IModuleInterface +class FAyonModule : public IModuleInterface { public: virtual void StartupModule() override; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h similarity index 80% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h index 9157569c08..d40642b149 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainer.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h @@ -6,20 +6,17 @@ #include "UObject/NoExportTypes.h" #include "Engine/AssetUserData.h" #include "AssetRegistry/AssetData.h" -#include "AssetContainer.generated.h" +#include "AyonAssetContainer.generated.h" -/** - * - */ UCLASS(Blueprintable) -class OPENPYPE_API UAssetContainer : public UAssetUserData +class AYON_API UAyonAssetContainer : public UAssetUserData { GENERATED_BODY() public: - UAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAssetContainer(); + UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); + // ~UAyonAssetContainer(); UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") TArray assets; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h similarity index 68% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h index 9095f8a3d7..da424cde2e 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/AssetContainerFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h @@ -4,18 +4,15 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "AssetContainerFactory.generated.h" +#include "AyonAssetContainerFactory.generated.h" -/** - * - */ UCLASS() -class OPENPYPE_API UAssetContainerFactory : public UFactory +class AYON_API UAyonAssetContainerFactory : public UFactory { GENERATED_BODY() public: - UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); + UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h new file mode 100644 index 0000000000..9c40dc8241 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h @@ -0,0 +1,24 @@ +// Copyright 2023, Ayon, All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Framework/Commands/Commands.h" +#include "AyonStyle.h" + +class FAyonCommands : public TCommands +{ +public: + + FAyonCommands() + : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) + { + } + + // TCommands<> interface + virtual void RegisterCommands() override; + +public: + TSharedPtr< FUICommandInfo > AyonTools; + TSharedPtr< FUICommandInfo > AyonToolsDialog; +}; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h similarity index 83% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h index f4587f7a50..5fe7c14360 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h @@ -1,9 +1,9 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -namespace OPConstants +namespace AyonConstants { - const FString OP_PluginName = "OpenPype"; + const FString Ayon_PluginName = "Ayon"; const FString PythonScript_PluginName = "PythonScriptPlugin"; const FString SequencerScripting_PluginName = "SequencerScripting"; const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h similarity index 75% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h index ef4d1027ea..da83b448fb 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h @@ -1,12 +1,11 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" -#include "OpenPypeLib.generated.h" +#include "AyonLib.generated.h" UCLASS(Blueprintable) -class OPENPYPE_API UOpenPypeLib : public UBlueprintFunctionLibrary +class AYON_API UAyonLib : public UBlueprintFunctionLibrary { GENERATED_BODY() diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h similarity index 70% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h index 827f76f56b..3c429fd7d3 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h @@ -1,16 +1,15 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" -#include "OpenPypePythonBridge.generated.h" +#include "AyonPythonBridge.generated.h" UCLASS(Blueprintable) -class UOpenPypePythonBridge : public UObject +class UAyonPythonBridge : public UObject { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = Python) - static UOpenPypePythonBridge* Get(); + static UAyonPythonBridge* Get(); UFUNCTION(BlueprintImplementableEvent, Category = Python) void RunInPython_Popup() const; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h similarity index 59% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h index b818fe0e95..4f12d1a5f2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h @@ -1,15 +1,15 @@ -// Copyright 2023, Ayon, All rights reserved. +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "CoreMinimal.h" #include "UObject/Object.h" -#include "OpenPypeSettings.generated.h" +#include "AyonSettings.generated.h" -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultOpenPypeSettings.ini") +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") -UCLASS(Config=OpenPypeSettings, DefaultConfig) -class OPENPYPE_API UOpenPypeSettings : public UObject +UCLASS(Config=AyonSettings, DefaultConfig) +class AYON_API UAyonSettings : public UObject { GENERATED_UCLASS_BODY() diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h similarity index 79% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h index 039abe96ef..58f6af656e 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h @@ -3,7 +3,7 @@ #include "CoreMinimal.h" #include "Styling/SlateStyle.h" -class FOpenPypeStyle +class FAyonStyle { public: static void Initialize(); @@ -15,5 +15,5 @@ public: private: static TSharedRef< class FSlateStyleSet > Create(); - static TSharedPtr< class FSlateStyleSet > OpenPypeStyleInstance; + static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; }; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h similarity index 63% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h index 322a23a3e8..bb995ec452 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h @@ -3,23 +3,23 @@ #pragma once #include "CoreMinimal.h" -#include "OPActionResult.generated.h" +#include "AyonActionResult.generated.h" /** * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FOP_ActionResult structure + * @param ActionResult FAyon_ActionResult structure */ -#define EVALUATE_OP_ACTION_RESULT(ActionResult) \ +#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ if(ActionResult.IsProblem()) \ return ActionResult.GetStatus(); /** * @brief This enum values are humanly readable mapping of error codes. * Here should be all error codes to be possible find what went wrong. -* TODO: In the future a web document should exists with the mapped error code & what problem occurred & how to repair it... +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... */ UENUM() -namespace EOP_ActionResult +namespace EAyon_ActionResult { enum Type { @@ -27,11 +27,11 @@ namespace EOP_ActionResult ProjectNotCreated, ProjectNotLoaded, ProjectNotSaved, - //....Here insert another values + //....Here insert another values //Do not remove! //Usable for looping through enum values - __Last UMETA(Hidden) + __Last UMETA(Hidden) }; } @@ -40,44 +40,44 @@ namespace EOP_ActionResult * @brief This struct holds action result enum and optionally reason of fail */ USTRUCT() -struct FOP_ActionResult +struct FAyon_ActionResult { GENERATED_BODY() public: /** @brief Default constructor usable when there is no problem */ - FOP_ActionResult(); + FAyon_ActionResult(); /** * @brief This constructor initializes variables & attempts to log when is error * @param InEnum Status */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum); + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); /** * @brief This constructor initializes variables & attempts to log when is error * @param InEnum Status * @param InReason Reason of potential fail */ - FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason); + FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); private: /** @brief Action status */ - EOP_ActionResult::Type Status; + EAyon_ActionResult::Type Status; /** @brief Optional reason of fail */ - FText Reason; + FText Reason; public: /** * @brief Checks if there is problematic state - * @return true when status is not equal to EOP_ActionResult::Ok + * @return true when status is not equal to EAyon_ActionResult::Ok */ bool IsProblem() const; - EOP_ActionResult::Type& GetStatus(); + EAyon_ActionResult::Type& GetStatus(); FText& GetReason(); -private: +private: void TryLog() const; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h similarity index 63% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h index 6a6c6406e7..da8e9af661 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h @@ -3,10 +3,10 @@ #include "GameProjectUtils.h" -#include "Commandlets/OPActionResult.h" +#include "Commandlets/AyonActionResult.h" #include "ProjectDescriptor.h" #include "Commandlets/Commandlet.h" -#include "OPGenerateProjectCommandlet.generated.h" +#include "AyonGenerateProjectCommandlet.generated.h" struct FProjectDescriptor; struct FProjectInformation; @@ -15,7 +15,7 @@ struct FProjectInformation; * @brief Structure which parses command line parameters and generates FProjectInformation */ USTRUCT() -struct FOPGenerateProjectParams +struct FAyonGenerateProjectParams { GENERATED_BODY() @@ -25,8 +25,8 @@ private: TArray Switches; public: - FOPGenerateProjectParams(); - FOPGenerateProjectParams(const FString& CommandLineParams); + FAyonGenerateProjectParams(); + FAyonGenerateProjectParams(const FString& CommandLineParams); FProjectInformation GenerateUEProjectInformation() const; @@ -38,7 +38,7 @@ private: }; UCLASS() -class OPENPYPE_API UOPGenerateProjectCommandlet : public UCommandlet +class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet { GENERATED_BODY() @@ -47,15 +47,15 @@ private: FProjectDescriptor ProjectDescriptor; public: - UOPGenerateProjectCommandlet(); + UAyonGenerateProjectCommandlet(); virtual int32 Main(const FString& CommandLineParams) override; private: - FOPGenerateProjectParams ParseParameters(const FString& Params) const; - FOP_ActionResult TryCreateProject() const; - FOP_ActionResult TryLoadProjectDescriptor(); + FAyonGenerateProjectParams ParseParameters(const FString& Params) const; + FAyon_ActionResult TryCreateProject() const; + FAyon_ActionResult TryLoadProjectDescriptor(); void AttachPluginsToProjectDescriptor(); - FOP_ActionResult TrySave(); + FAyon_ActionResult TrySave(); }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h new file mode 100644 index 0000000000..25b33a63e8 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h @@ -0,0 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h similarity index 95% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 4eace68827..03a22c6cde 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -2,15 +2,16 @@ #pragma once #include "Engine.h" -#include "AyonPublishInstance.generated.h" +#include "OpenPypePublishInstance.generated.h" UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset { GENERATED_UCLASS_BODY() public: + /** /** * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is * placed in) @@ -93,8 +94,8 @@ private: #ifdef WITH_EDITOR - void ColorAyonDirs(); - + void ColorOpenPypeDirs(); + void SendNotification(const FString& Text) const; virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h similarity index 65% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 443d618c9a..54dc3e8c1d 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -3,18 +3,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" +#include "OpenPypePublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory +class AYON_API UOpenPypePublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore new file mode 100644 index 0000000000..80814ef0a6 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore @@ -0,0 +1,41 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +/Saved +/DerivedDataCache +/Intermediate +/Binaries +/Content +/Config +/.idea +/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject new file mode 100644 index 0000000000..fe83346624 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject @@ -0,0 +1,20 @@ +{ + "FileVersion": 3, + "EngineAssociation": "5.1", + "Category": "", + "Description": "", + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "Ayon", + "Enabled": true, + "Type": "Editor" + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 86ce0bb033..8b8e02f271 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -229,7 +229,7 @@ def create_unreal_project(project_name: str, print("--- Generating a new project ...") commandlet_cmd = [f'{ue_editor_exe.as_posix()}', f'{cmdlet_project.as_posix()}', - f'-run=OPGenerateProject', + f'-run=AyonGenerateProject', f'{project_file.resolve().as_posix()}'] if dev_mode or preset["dev_mode"]: @@ -319,10 +319,7 @@ def get_path_to_cmdlet_project(ue_version: str) -> Path: cmd_project = Path(os.path.dirname(os.path.abspath(openpype.__file__))) # For now, only tested on Windows (For Linux and Mac it has to be implemented) - if ue_version.split(".")[0] == "4": - cmd_project /= "hosts/unreal/integration/UE_4.7" - elif ue_version.split(".")[0] == "5": - cmd_project /= "hosts/unreal/integration/UE_5.0" + cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" return cmd_project / "CommandletProject/CommandletProject.uproject" @@ -373,7 +370,7 @@ def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: raise RuntimeError("Path to the integration plugin is null!") # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" if not op_plugin_path.is_dir(): return False @@ -394,7 +391,7 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None: raise RuntimeError("Path to the integration plugin is null!") # Create a path to the plugin in the engine - op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/Ayon" if not op_plugin_path.is_dir(): op_plugin_path.mkdir(parents=True, exist_ok=True) @@ -420,7 +417,7 @@ def _build_and_move_plugin(engine_path: Path, if uat_path.is_file(): temp_dir: Path = integration_plugin_path.parent / "Temp" temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = integration_plugin_path / "OpenPype.uplugin" + uplugin_path: Path = integration_plugin_path / "Ayon.uplugin" # in order to successfully build the plugin, # It must be built outside the Engine directory and then moved @@ -431,7 +428,7 @@ def _build_and_move_plugin(engine_path: Path, subprocess.run(build_plugin_cmd) # Copy the contents of the 'Temp' dir into the - # 'OpenPype' directory in the engine + # 'Ayon' directory in the engine dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) # We need to also copy the config folder. diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index d1740124a8..1d8023c4d7 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -93,7 +93,7 @@ class UEProjectGenerationWorker(QtCore.QObject): commandlet_cmd = [ f"{ue_editor_exe.as_posix()}", f"{cmdlet_project.as_posix()}", - "-run=OPGenerateProject", + "-run=AyonGenerateProject", f"{project_file.resolve().as_posix()}", ] @@ -300,7 +300,7 @@ class UEPluginInstallWorker(QtCore.QObject): temp_dir: Path = src_plugin_dir.parent / "Temp" temp_dir.mkdir(exist_ok=True) - uplugin_path: Path = src_plugin_dir / "OpenPype.uplugin" + uplugin_path: Path = src_plugin_dir / "Ayon.uplugin" # in order to successfully build the plugin, # It must be built outside the Engine directory and then moved @@ -332,7 +332,7 @@ class UEPluginInstallWorker(QtCore.QObject): raise RuntimeError(msg) # Copy the contents of the 'Temp' dir into the - # 'OpenPype' directory in the engine + # 'Ayon' directory in the engine dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) @@ -356,7 +356,7 @@ class UEPluginInstallWorker(QtCore.QObject): # Create a path to the plugin in the engine op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \ - "/OpenPype" + "/Ayon" if not op_plugin_path.is_dir(): self.installing.emit("Installing and building the plugin ...") From 10ad412218ce1168abe25e07dee83b2058ba1cb4 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 5 Apr 2023 13:54:00 +0200 Subject: [PATCH 219/918] Added whitespace --- openpype/hosts/unreal/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 2fb55a9b11..6a7c6ba941 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -15,7 +15,7 @@ class UnrealAddon(OpenPypeModule, IHostAddon): """Modify environments to contain all required for implementation.""" # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation - ue_version = app.name.replace("-",".") + ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) From 09f5e3ecc1eb067b60524c66618cbce0e6514e86 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 31 Mar 2023 14:43:10 +0200 Subject: [PATCH 220/918] remove placeholder parent to root at cleanup --- .../maya/api/workfile_template_builder.py | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 4bee0664ef..d65e4c74d2 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -234,26 +234,10 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): return self.get_load_plugin_options(options) def cleanup_placeholder(self, placeholder, failed): - """Hide placeholder, parent them to root - add them to placeholder set and register placeholder's parent - to keep placeholder info available for future use + """Hide placeholder, add them to placeholder set """ - node = placeholder._scene_identifier - node_parent = placeholder.data["parent"] - if node_parent: - cmds.setAttr(node + ".parent", node_parent, type="string") - if cmds.getAttr(node + ".index") < 0: - cmds.setAttr(node + ".index", placeholder.data["index"]) - - holding_sets = cmds.listSets(object=node) - if holding_sets: - for set in holding_sets: - cmds.sets(node, remove=set) - - if cmds.listRelatives(node, p=True): - node = cmds.parent(node, world=True)[0] cmds.sets(node, addElement=PLACEHOLDER_SET) cmds.hide(node) cmds.setAttr(node + ".hiddenInOutliner", True) @@ -286,8 +270,6 @@ class MayaPlaceholderLoadPlugin(PlaceholderPlugin, PlaceholderLoadMixin): elif not cmds.sets(root, q=True): return - if placeholder.data["parent"]: - cmds.parent(nodes_to_parent, placeholder.data["parent"]) # Move loaded nodes to correct index in outliner hierarchy placeholder_form = cmds.xform( placeholder.scene_identifier, From 9aa8aa469fc3a81e714207809af786b520043bf6 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 31 Mar 2023 14:44:18 +0200 Subject: [PATCH 221/918] fix missing var standard --- openpype/client/entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 7054658c64..376157d210 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1216,7 +1216,7 @@ def get_representations( version_ids=version_ids, context_filters=context_filters, names_by_version_ids=names_by_version_ids, - standard=True, + standard=standard, archived=archived, fields=fields ) From 9be576c2147e712db4d257f95c187e951eca40ec Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 31 Mar 2023 14:45:49 +0200 Subject: [PATCH 222/918] fix linked asset import --- .../workfile/workfile_template_builder.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 0ce59de8ad..a3d7340367 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -158,7 +158,7 @@ class AbstractTemplateBuilder(object): def linked_asset_docs(self): if self._linked_asset_docs is None: self._linked_asset_docs = get_linked_assets( - self.current_asset_doc + self.project_name, self.current_asset_doc ) return self._linked_asset_docs @@ -1151,13 +1151,10 @@ class PlaceholderItem(object): return self._log def __repr__(self): - name = None - if hasattr("name", self): - name = self.name - if hasattr("_scene_identifier ", self): - name = self._scene_identifier - - return "< {} {} >".format(self.__class__.__name__, name) + return "< {} {} >".format( + self.__class__.__name__, + self._scene_identifier + ) @property def order(self): @@ -1419,16 +1416,7 @@ class PlaceholderLoadMixin(object): "family": [placeholder.data["family"]] } - elif builder_type != "linked_asset": - context_filters = { - "asset": [re.compile(placeholder.data["asset"])], - "subset": [re.compile(placeholder.data["subset"])], - "hierarchy": [re.compile(placeholder.data["hierarchy"])], - "representation": [placeholder.data["representation"]], - "family": [placeholder.data["family"]] - } - - else: + elif builder_type == "linked_asset": asset_regex = re.compile(placeholder.data["asset"]) linked_asset_names = [] for asset_doc in linked_asset_docs: @@ -1444,6 +1432,15 @@ class PlaceholderLoadMixin(object): "family": [placeholder.data["family"]], } + else: + context_filters = { + "asset": [re.compile(placeholder.data["asset"])], + "subset": [re.compile(placeholder.data["subset"])], + "hierarchy": [re.compile(placeholder.data["hierarchy"])], + "representation": [placeholder.data["representation"]], + "family": [placeholder.data["family"]] + } + return list(get_representations( project_name, context_filters=context_filters From 6f8f61fb4a47e26c67180bf84a169baff45df4f2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 15:10:34 +0100 Subject: [PATCH 223/918] Reinstate settings backwards compatibility. --- .../defaults/project_settings/maya.json | 123 ++++ .../schemas/schema_maya_capture.json | 625 ++++++++++++++++++ 2 files changed, 748 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 24d55de1fd..234a02c6d4 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -789,6 +789,129 @@ "validate_shapes": true }, "ExtractPlayblast": { + "capture_preset": { + "Codec": { + "compression": "png", + "format": "image", + "quality": 95 + }, + "Display Options": { + "override_display": true, + "background": [ + 125, + 125, + 125, + 255 + ], + "backgroundBottom": [ + 125, + 125, + 125, + 255 + ], + "backgroundTop": [ + 125, + 125, + 125, + 255 + ], + "displayGradient": true + }, + "Generic": { + "isolate_view": true, + "off_screen": true, + "pan_zoom": false + }, + "Renderer": { + "rendererName": "vp2Renderer" + }, + "Resolution": { + "width": 1920, + "height": 1080 + }, + "Viewport Options": { + "override_viewport_options": true, + "displayLights": "default", + "displayTextures": true, + "textureMaxResolution": 1024, + "renderDepthOfField": true, + "shadows": true, + "twoSidedLighting": true, + "lineAAEnable": true, + "multiSample": 8, + "useDefaultMaterial": false, + "wireframeOnShaded": false, + "xray": false, + "jointXray": false, + "backfaceCulling": false, + "ssaoEnable": false, + "ssaoAmount": 1, + "ssaoRadius": 16, + "ssaoFilterRadius": 16, + "ssaoSamples": 16, + "fogging": false, + "hwFogFalloff": "0", + "hwFogDensity": 0.0, + "hwFogStart": 0, + "hwFogEnd": 100, + "hwFogAlpha": 0, + "hwFogColorR": 1.0, + "hwFogColorG": 1.0, + "hwFogColorB": 1.0, + "motionBlurEnable": false, + "motionBlurSampleCount": 8, + "motionBlurShutterOpenFraction": 0.2, + "cameras": false, + "clipGhosts": false, + "deformers": false, + "dimensions": false, + "dynamicConstraints": false, + "dynamics": false, + "fluids": false, + "follicles": false, + "greasePencils": false, + "grid": false, + "hairSystems": true, + "handles": false, + "headsUpDisplay": false, + "ikHandles": false, + "imagePlane": true, + "joints": false, + "lights": false, + "locators": false, + "manipulators": false, + "motionTrails": false, + "nCloths": false, + "nParticles": false, + "nRigids": false, + "controlVertices": false, + "nurbsCurves": false, + "hulls": false, + "nurbsSurfaces": false, + "particleInstancers": false, + "pivots": false, + "planes": false, + "pluginShapes": false, + "polymeshes": true, + "strokes": false, + "subdivSurfaces": false, + "textures": false, + "pluginObjects": { + "gpuCacheDisplayFilter": false + } + }, + "Camera Options": { + "displayGateMask": false, + "displayResolution": false, + "displayFilmGate": false, + "displayFieldChart": false, + "displaySafeAction": false, + "displaySafeTitle": false, + "displayFilmPivot": false, + "displayFilmOrigin": false, + "overscan": 1.0 + } + }, "profiles": [ { "task_types": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1909a20cf5..19c169df9c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -4,6 +4,631 @@ "key": "ExtractPlayblast", "label": "Extract Playblast settings", "children": [ + { + "type": "dict", + "key": "capture_preset", + "label": "DEPRECATED! Please use \"Profiles\" below.", + "collapsed": false, + "children": [ + { + "type": "dict", + "key": "Codec", + "children": [ + { + "type": "label", + "label": "Codec" + }, + { + "type": "text", + "key": "compression", + "label": "Encoding" + }, + { + "type": "text", + "key": "format", + "label": "Format" + }, + { + "type": "number", + "key": "quality", + "label": "Quality", + "decimal": 0, + "minimum": 0, + "maximum": 100 + }, + + { + "type": "splitter" + } + ] + }, + { + "type": "dict", + "key": "Display Options", + "children": [ + { + "type": "label", + "label": "Display Options" + }, + { + "type": "boolean", + "key": "override_display", + "label": "Override display options" + }, + { + "type": "color", + "key": "background", + "label": "Background Color: " + }, + { + "type": "color", + "key": "backgroundBottom", + "label": "Background Bottom: " + }, + { + "type": "color", + "key": "backgroundTop", + "label": "Background Top: " + }, + { + "type": "boolean", + "key": "displayGradient", + "label": "Display background gradient" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Generic", + "children": [ + { + "type": "label", + "label": "Generic" + }, + { + "type": "boolean", + "key": "isolate_view", + "label": " Isolate view" + }, + { + "type": "boolean", + "key": "off_screen", + "label": " Off Screen" + }, + { + "type": "boolean", + "key": "pan_zoom", + "label": " 2D Pan/Zoom" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "key": "Renderer", + "children": [ + { + "type": "label", + "label": "Renderer" + }, + { + "type": "enum", + "key": "rendererName", + "label": "Renderer name", + "enum_items": [ + { "vp2Renderer": "Viewport 2.0" } + ] + } + ] + }, + { + "type": "dict", + "key": "Resolution", + "children": [ + { + "type": "splitter" + }, + { + "type": "label", + "label": "Resolution" + }, + { + "type": "number", + "key": "width", + "label": " Width", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + }, + { + "type": "number", + "key": "height", + "label": "Height", + "decimal": 0, + "minimum": 0, + "maximum": 99999 + } + ] + }, + { + "type": "splitter" + }, + { + "type": "dict", + "collapsible": true, + "key": "Viewport Options", + "label": "Viewport Options", + "children": [ + { + "type": "boolean", + "key": "override_viewport_options", + "label": "Override Viewport Options" + }, + { + "type": "enum", + "key": "displayLights", + "label": "Display Lights", + "enum_items": [ + { "default": "Default Lighting"}, + { "all": "All Lights"}, + { "selected": "Selected Lights"}, + { "flat": "Flat Lighting"}, + { "nolights": "No Lights"} + ] + }, + { + "type": "boolean", + "key": "displayTextures", + "label": "Display Textures" + }, + { + "type": "number", + "key": "textureMaxResolution", + "label": "Texture Clamp Resolution", + "decimal": 0 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Display" + }, + { + "type":"boolean", + "key": "renderDepthOfField", + "label": "Depth of Field" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "shadows", + "label": "Display Shadows" + }, + { + "type": "boolean", + "key": "twoSidedLighting", + "label": "Two Sided Lighting" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "lineAAEnable", + "label": "Enable Anti-Aliasing" + }, + { + "type": "number", + "key": "multiSample", + "label": "Anti Aliasing Samples", + "decimal": 0, + "minimum": 0, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "useDefaultMaterial", + "label": "Use Default Material" + }, + { + "type": "boolean", + "key": "wireframeOnShaded", + "label": "Wireframe On Shaded" + }, + { + "type": "boolean", + "key": "xray", + "label": "X-Ray" + }, + { + "type": "boolean", + "key": "jointXray", + "label": "X-Ray Joints" + }, + { + "type": "boolean", + "key": "backfaceCulling", + "label": "Backface Culling" + }, + { + "type": "boolean", + "key": "ssaoEnable", + "label": "Screen Space Ambient Occlusion" + }, + { + "type": "number", + "key": "ssaoAmount", + "label": "SSAO Amount" + }, + { + "type": "number", + "key": "ssaoRadius", + "label": "SSAO Radius" + }, + { + "type": "number", + "key": "ssaoFilterRadius", + "label": "SSAO Filter Radius", + "decimal": 0, + "minimum": 1, + "maximum": 32 + }, + { + "type": "number", + "key": "ssaoSamples", + "label": "SSAO Samples", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "fogging", + "label": "Enable Hardware Fog" + }, + { + "type": "enum", + "key": "hwFogFalloff", + "label": "Hardware Falloff", + "enum_items": [ + { "0": "Linear"}, + { "1": "Exponential"}, + { "2": "Exponential Squared"} + ] + }, + { + "type": "number", + "key": "hwFogDensity", + "label": "Fog Density", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogStart", + "label": "Fog Start" + }, + { + "type": "number", + "key": "hwFogEnd", + "label": "Fog End" + }, + { + "type": "number", + "key": "hwFogAlpha", + "label": "Fog Alpha" + }, + { + "type": "number", + "key": "hwFogColorR", + "label": "Fog Color R", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorG", + "label": "Fog Color G", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "number", + "key": "hwFogColorB", + "label": "Fog Color B", + "decimal": 2, + "minimum": 0, + "maximum": 1 + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "motionBlurEnable", + "label": "Enable Motion Blur" + }, + { + "type": "number", + "key": "motionBlurSampleCount", + "label": "Motion Blur Sample Count", + "decimal": 0, + "minimum": 8, + "maximum": 32 + }, + { + "type": "number", + "key": "motionBlurShutterOpenFraction", + "label": "Shutter Open Fraction", + "decimal": 3, + "minimum": 0.01, + "maximum": 32 + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Show" + }, + { + "type": "boolean", + "key": "cameras", + "label": "Cameras" + }, + { + "type": "boolean", + "key": "clipGhosts", + "label": "Clip Ghosts" + }, + { + "type": "boolean", + "key": "deformers", + "label": "Deformers" + }, + { + "type": "boolean", + "key": "dimensions", + "label": "Dimensions" + }, + { + "type": "boolean", + "key": "dynamicConstraints", + "label": "Dynamic Constraints" + }, + { + "type": "boolean", + "key": "dynamics", + "label": "Dynamics" + }, + { + "type": "boolean", + "key": "fluids", + "label": "Fluids" + }, + { + "type": "boolean", + "key": "follicles", + "label": "Follicles" + }, + { + "type": "boolean", + "key": "greasePencils", + "label": "Grease Pencil" + }, + { + "type": "boolean", + "key": "grid", + "label": "Grid" + }, + { + "type": "boolean", + "key": "hairSystems", + "label": "Hair Systems" + }, + { + "type": "boolean", + "key": "handles", + "label": "Handles" + }, + { + "type": "boolean", + "key": "headsUpDisplay", + "label": "HUD" + }, + { + "type": "boolean", + "key": "ikHandles", + "label": "IK Handles" + }, + { + "type": "boolean", + "key": "imagePlane", + "label": "Image Planes" + }, + { + "type": "boolean", + "key": "joints", + "label": "Joints" + }, + { + "type": "boolean", + "key": "lights", + "label": "Lights" + }, + { + "type": "boolean", + "key": "locators", + "label": "Locators" + }, + { + "type": "boolean", + "key": "manipulators", + "label": "Manipulators" + }, + { + "type": "boolean", + "key": "motionTrails", + "label": "Motion Trails" + }, + { + "type": "boolean", + "key": "nCloths", + "label": "nCloths" + }, + { + "type": "boolean", + "key": "nParticles", + "label": "nParticles" + }, + { + "type": "boolean", + "key": "nRigids", + "label": "nRigids" + }, + { + "type": "boolean", + "key": "controlVertices", + "label": "NURBS CVs" + }, + { + "type": "boolean", + "key": "nurbsCurves", + "label": "NURBS Curves" + }, + { + "type": "boolean", + "key": "hulls", + "label": "NURBS Hulls" + }, + { + "type": "boolean", + "key": "nurbsSurfaces", + "label": "NURBS Surfaces" + }, + { + "type": "boolean", + "key": "particleInstancers", + "label": "Particle Instancers" + }, + { + "type": "boolean", + "key": "pivots", + "label": "Pivots" + }, + { + "type": "boolean", + "key": "planes", + "label": "Planes" + }, + { + "type": "boolean", + "key": "pluginShapes", + "label": "Plugin Shapes" + }, + { + "type": "boolean", + "key": "polymeshes", + "label": "Polygons" + }, + { + "type": "boolean", + "key": "strokes", + "label": "Strokes" + }, + { + "type": "boolean", + "key": "subdivSurfaces", + "label": "Subdiv Surfaces" + }, + { + "type": "boolean", + "key": "textures", + "label": "Texture Placements" + }, + { + "type": "dict-modifiable", + "key": "pluginObjects", + "label": "Plugin Objects", + "object_type": "boolean" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "Camera Options", + "label": "Camera Options", + "children": [ + { + "type": "boolean", + "key": "displayGateMask", + "label": "Display Gate Mask" + }, + { + "type": "boolean", + "key": "displayResolution", + "label": "Display Resolution" + }, + { + "type": "boolean", + "key": "displayFilmGate", + "label": "Display Film Gate" + }, + { + "type": "boolean", + "key": "displayFieldChart", + "label": "Display Field Chart" + }, + { + "type": "boolean", + "key": "displaySafeAction", + "label": "Display Safe Action" + }, + { + "type": "boolean", + "key": "displaySafeTitle", + "label": "Display Safe Title" + }, + { + "type": "boolean", + "key": "displayFilmPivot", + "label": "Display Film Pivot" + }, + { + "type": "boolean", + "key": "displayFilmOrigin", + "label": "Display Film Origin" + }, + { + "type": "number", + "key": "overscan", + "label": "Overscan", + "decimal": 1, + "minimum": 0, + "maximum": 10 + } + ] + } + ] + }, { "type": "list", "key": "profiles", From 9ed7e00254f42544a51702bcc3fe86e4169f05ff Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 16:59:55 +0100 Subject: [PATCH 224/918] Fix missing camera variable. --- openpype/hosts/maya/plugins/publish/collect_review.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 0b3799ac13..3652c0aa40 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -31,6 +31,7 @@ class CollectReview(pyblish.api.InstancePlugin): members = instance.data['setMembers'] self.log.debug('members: {}'.format(members)) cameras = cmds.ls(members, long=True, dag=True, cameras=True) + camera = cameras[0] if cameras else None context = instance.context objectset = context.data['objectsets'] @@ -62,7 +63,7 @@ class CollectReview(pyblish.api.InstancePlugin): data['families'] = ['review'] data["cameras"] = cameras - data['review_camera'] = cameras[0] if cameras else None + data['review_camera'] = camera data['frameStartFtrack'] = instance.data["frameStartHandle"] data['frameEndFtrack'] = instance.data["frameEndHandle"] data['frameStartHandle'] = instance.data["frameStartHandle"] @@ -97,7 +98,7 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['subset'] = legacy_subset_name instance.data["cameras"] = cameras - instance.data['review_camera'] = cameras[0] if cameras else None + instance.data['review_camera'] = camera instance.data['frameStartFtrack'] = \ instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = \ @@ -145,6 +146,9 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data["audio"] = audio_data # Collect focal length. + if camera is None: + return + attr = camera + ".focalLength" if get_attribute_input(attr): start = instance.data["frameStart"] From abea98091aa22f1e6949b70832fd654f953375d5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 17:00:14 +0100 Subject: [PATCH 225/918] Code cosmetics --- openpype/hosts/maya/plugins/create/create_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 594faa7978..156f1e3461 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -45,9 +45,7 @@ class CreateReview(plugin.Creator): )["maya"]["publish"]["ExtractPlayblast"]["profiles"] preset = None - if not profiles: - self.log.warning("No profiles present for extract playblast.") - else: + if profiles: asset_doc = get_asset_by_name(project_name, data["asset"]) task_name = get_current_task_name() task_type = asset_doc["data"]["tasks"][task_name]["type"] @@ -62,6 +60,8 @@ class CreateReview(plugin.Creator): preset = filter_profiles( profiles, filtering_criteria, logger=self.log )["capture_preset"] + else: + self.log.warning("No profiles present for extract playblast.") # Option for using Maya or asset frame range in settings. frame_range = lib.get_frame_range() From f2f42fad308eebdcad7d9ee3267e26a1772bfb33 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 17:03:57 +0100 Subject: [PATCH 226/918] Reinstate backwards compatibility for publishing. --- .../maya/plugins/publish/extract_playblast.py | 20 +++++------ .../maya/plugins/publish/extract_thumbnail.py | 33 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 81007520a8..a9f5062c48 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -34,6 +34,7 @@ class ExtractPlayblast(publish.Extractor): hosts = ["maya"] families = ["review"] optional = True + capture_preset = {} profiles = None def _capture(self, preset): @@ -48,10 +49,6 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("playblast path {}".format(path)) def process(self, instance): - if not self.profiles: - self.log.warning("No profiles present for Extract Playblast") - return - self.log.info("Extracting capture..") # get scene fps @@ -85,12 +82,15 @@ class ExtractPlayblast(publish.Extractor): "task_types": task_type, "subset": subset } - capture_preset = filter_profiles( - self.profiles, filtering_criteria, logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset( - data=capture_preset - ) + capture_preset = self.capture_preset + preset = lib.load_capture_preset(data=self.capture_preset) + if self.profiles: + capture_preset = filter_profiles( + self.profiles, filtering_criteria, logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset(data=capture_preset) + else: + self.log.warning("No profiles present for Extract Playblast") # "isolate_view" will already have been applied at creation, so we'll # ignore it here. diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index cf0f80fa15..8d635d0df2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -25,13 +25,6 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - maya_settings = instance.context.data["project_settings"]["maya"] - profiles = maya_settings["publish"]["ExtractPlayblast"]["profiles"] - - if not profiles: - self.log.warning("No profiles present for Extract Playblast") - return - self.log.info("Extracting capture..") camera = instance.data["review_camera"] @@ -50,12 +43,26 @@ class ExtractThumbnail(publish.Extractor): "task_types": task_type, "subset": subset } - capture_preset = filter_profiles( - profiles, filtering_criteria, logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset( - data=capture_preset - ) + + maya_settings = instance.context.data["project_settings"]["maya"] + plugin_settings = maya_settings["publish"]["ExtractPlayblast"] + + capture_preset = plugin_settings["capture_preset"] + preset = {} + try: + preset = lib.load_capture_preset(data=capture_preset) + except KeyError as ke: + self.log.error("Error loading capture presets: {}".format(str(ke))) + + if plugin_settings["profiles"]: + capture_preset = filter_profiles( + plugin_settings["profiles"], + filtering_criteria, + logger=self.log + )["capture_preset"] + preset = lib.load_capture_preset(data=capture_preset) + else: + self.log.warning("No profiles present for Extract Playblast") # "isolate_view" will already have been applied at creation, so we'll # ignore it here. From 0b3802d9f27c1def5a8dd07553ca205f88d02a85 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 5 Apr 2023 17:08:19 +0100 Subject: [PATCH 227/918] Remove default profile. --- .../defaults/project_settings/maya.json | 131 +----------------- 1 file changed, 1 insertion(+), 130 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 234a02c6d4..8c817b5ba0 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -912,136 +912,7 @@ "overscan": 1.0 } }, - "profiles": [ - { - "task_types": [], - "task_names": [], - "subsets": [], - "capture_preset": { - "Codec": { - "compression": "png", - "format": "image", - "quality": 95 - }, - "Display Options": { - "background": [ - 125, - 125, - 125, - 255 - ], - "backgroundBottom": [ - 125, - 125, - 125, - 255 - ], - "backgroundTop": [ - 125, - 125, - 125, - 255 - ], - "override_display": true, - "displayGradient": true - }, - "Generic": { - "isolate_view": true, - "off_screen": true, - "pan_zoom": false - }, - "Renderer": { - "rendererName": "vp2Renderer" - }, - "Resolution": { - "width": 0, - "height": 0 - }, - "Viewport Options": { - "override_viewport_options": true, - "displayLights": "default", - "displayTextures": true, - "textureMaxResolution": 1024, - "renderDepthOfField": true, - "shadows": true, - "twoSidedLighting": true, - "lineAAEnable": true, - "multiSample": 8, - "useDefaultMaterial": false, - "wireframeOnShaded": false, - "xray": false, - "jointXray": false, - "backfaceCulling": false, - "ssaoEnable": false, - "ssaoAmount": 1, - "ssaoRadius": 16, - "ssaoFilterRadius": 16, - "ssaoSamples": 16, - "fogging": false, - "hwFogFalloff": "0", - "hwFogDensity": 0.0, - "hwFogStart": 0, - "hwFogEnd": 100, - "hwFogAlpha": 0, - "hwFogColorR": 1.0, - "hwFogColorG": 1.0, - "hwFogColorB": 1.0, - "motionBlurEnable": false, - "motionBlurSampleCount": 0, - "motionBlurShutterOpenFraction": 0.2, - "cameras": false, - "clipGhosts": false, - "deformers": false, - "dimensions": false, - "dynamicConstraints": false, - "dynamics": false, - "fluids": false, - "follicles": false, - "greasePencils": false, - "grid": false, - "hairSystems": true, - "handles": false, - "headsUpDisplay": false, - "ikHandles": false, - "imagePlane": true, - "joints": false, - "lights": false, - "locators": false, - "manipulators": false, - "motionTrails": false, - "nCloths": false, - "nParticles": false, - "nRigids": false, - "controlVertices": false, - "nurbsCurves": false, - "hulls": false, - "nurbsSurfaces": false, - "particleInstancers": false, - "pivots": false, - "planes": false, - "pluginShapes": false, - "polymeshes": true, - "strokes": false, - "subdivSurfaces": false, - "textures": false, - "pluginObjects": { - "gpuCacheDisplayFilter": false - } - }, - "Camera Options": { - "displayGateMask": false, - "displayResolution": false, - "displayFilmGate": false, - "displayFieldChart": false, - "displaySafeAction": false, - "displaySafeTitle": false, - "displayFilmPivot": false, - "displayFilmOrigin": false, - "overscan": 1.0 - } - } - } - ] + "profiles": [] }, "ExtractMayaSceneRaw": { "enabled": true, From 442236284bc87cf3a4aff4d3ae622beaaf946c4c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Apr 2023 17:53:42 +0800 Subject: [PATCH 228/918] add docs --- website/docs/artist_hosts_3dsmax.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index 12c1f40181..fffab8ca5d 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -30,7 +30,7 @@ By clicking the icon ```OpenPype Menu``` rolls out. Choose ```OpenPype Menu > Launcher``` to open the ```Launcher``` window. -When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** +When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** and finally **run 3dsmax by its icon** in the tools. ![Menu OpenPype](assets/3dsmax_tray_OP.png) @@ -65,13 +65,13 @@ If not any workfile present simply hit ```Save As``` and keep ```Subversion``` e ![Save As Dialog](assets/3dsmax_SavingFirstFile_OP.png) -OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like +OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like ```workfileName_v001``` ```workfileName_v002``` - etc. + etc. Basically meaning user is free of guessing what is the correct naming and other necessities to keep everything in order and managed. @@ -105,13 +105,13 @@ Before proceeding further please check [Glossary](artist_concepts.md) and [What ### Intro -Current OpenPype integration (ver 3.15.0) supports only ```PointCache``` and ```Camera``` families now. +Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Camera```, ```Geometry``` and ```Redshift Proxy``` families now. **Pointcache** family being basically any geometry outputted as Alembic cache (.abc) format **Camera** family being 3dsmax Camera object with/without animation outputted as native .max, FBX, Alembic format - +**Redshift Proxy** family being Redshift Proxy object with/without animation outputted as rs format(Redshift Proxy's very own format) --- :::note Work in progress @@ -119,7 +119,3 @@ This part of documentation is still work in progress. ::: ## ...to be added - - - - From 4c78f3044118c1623000e5a7f385d1041d0a7713 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 6 Apr 2023 15:42:02 +0200 Subject: [PATCH 229/918] maya: adding maya to global ocio prelaunch hook --- openpype/hooks/pre_ocio_hook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index ff16a8d174..f51e9f48d8 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -13,7 +13,8 @@ class OCIOEnvHook(PreLaunchHook): "blender", "aftereffects", "3dsmax", - "houdini" + "houdini", + "maya" ] def execute(self): From 9c9a1c08399184d863f8f5be4c6688bf183e488d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 6 Apr 2023 16:47:46 +0100 Subject: [PATCH 230/918] Discard vray proxies and aistandin from same variable. --- openpype/hosts/maya/tools/mayalookassigner/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index a8d0f243e9..fe7f460588 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -250,7 +250,7 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): if vp in nodes: vrayproxy_assign_look(vp, subset_name) - nodes = list(set(item["nodes"]).difference(vray_proxies)) + nodes = list(set(nodes).difference(vray_proxies)) else: self.echo( "Could not assign to VRayProxy because vrayformaya plugin " @@ -260,10 +260,12 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): # Assign Arnold Standin look. if cmds.pluginInfo("mtoa", query=True, loaded=True): arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) + for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) - nodes = list(set(item["nodes"]).difference(arnold_standins)) + + nodes = list(set(nodes).difference(arnold_standins)) else: self.echo( "Could not assign to aiStandIn because mtoa plugin is not " From 0cb647c0b20732ced4c1229429be4d256a98c792 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 6 Apr 2023 16:48:40 +0100 Subject: [PATCH 231/918] Hound --- openpype/hosts/maya/tools/mayalookassigner/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/tools/mayalookassigner/app.py b/openpype/hosts/maya/tools/mayalookassigner/app.py index fe7f460588..13da999c2d 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/app.py +++ b/openpype/hosts/maya/tools/mayalookassigner/app.py @@ -260,11 +260,11 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): # Assign Arnold Standin look. if cmds.pluginInfo("mtoa", query=True, loaded=True): arnold_standins = set(cmds.ls(type="aiStandIn", long=True)) - + for standin in arnold_standins: if standin in nodes: arnold_standin.assign_look(standin, subset_name) - + nodes = list(set(nodes).difference(arnold_standins)) else: self.echo( From 33a52f1a5282ba308aeb2861a79f8184642ef362 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 7 Apr 2023 10:43:30 +0100 Subject: [PATCH 232/918] Fix No Lights in project settings. --- .../schemas/projects_schema/schemas/schema_maya_capture.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index a4a986bad8..d468f098e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -176,7 +176,7 @@ { "all": "All Lights"}, { "selected": "Selected Lights"}, { "flat": "Flat Lighting"}, - { "nolights": "No Lights"} + { "none": "No Lights"} ] }, { From 273d87f8b8dec02b80e07da9eea038ac840f319b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 7 Apr 2023 10:44:08 +0100 Subject: [PATCH 233/918] Fix flat lighting and sync labels with project settings. --- openpype/hosts/maya/api/lib.py | 12 ++++++++++-- openpype/hosts/maya/plugins/create/create_review.py | 2 +- .../hosts/maya/plugins/publish/collect_review.py | 4 +--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 931c0f9e5b..f94b32d917 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -112,8 +112,16 @@ FLOAT_FPS = {23.98, 23.976, 29.97, 47.952, 59.94} RENDERLIKE_INSTANCE_FAMILIES = ["rendering", "vrayscene"] -DISPLAY_LIGHTS = [ - "project_settings", "default", "all", "selected", "active", "none" +DISPLAY_LIGHTS_VALUES = [ + "project_settings", "default", "all", "selected", "flat", "none" +] +DISPLAY_LIGHTS_LABELS = [ + "Use Project Settings", + "Default Lighting", + "All Lights", + "Selected Lights", + "Flat Lighting", + "No Lights" ] diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index de92bbb6b5..094c9ebf8c 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -47,6 +47,6 @@ class CreateReview(plugin.Creator): data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency data["panZoom"] = self.panZoom - data["displayLights"] = lib.DISPLAY_LIGHTS + data["displayLights"] = lib.DISPLAY_LIGHTS_LABELS self.data = data diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 516a83de64..3ca45deb3a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -150,10 +150,8 @@ class CollectReview(pyblish.api.InstancePlugin): # Convert enum attribute index to string. index = instance.data.get("displayLights", 0) - display_lights = lib.DISPLAY_LIGHTS[index] + display_lights = lib.DISPLAY_LIGHTS_VALUES[index] if display_lights == "project_settings": - # project_settings/maya/publish/ExtractPlayblast/capture_preset - # /Viewport Options/displayLights settings = instance.context.data["project_settings"] settings = settings["maya"]["publish"]["ExtractPlayblast"] settings = settings["capture_preset"]["Viewport Options"] From d9c67a0bd50fb5c8625632d942c6bf4bf85eb908 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 16:43:53 +0200 Subject: [PATCH 234/918] Improve speed of logging for when its validating a node with many prims. --- .../publish/validate_vdb_output_node.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index f9f88b3bf9..e7908ab119 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -2,6 +2,7 @@ import pyblish.api import hou from openpype.pipeline import PublishValidationError +import clique class ValidateVDBOutputNode(pyblish.api.InstancePlugin): @@ -56,12 +57,21 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): nr_of_prims = len(prims) # All primitives must be hou.VDB - invalid_prim = False + invalid_prims = [] for prim in prims: if not isinstance(prim, hou.VDB): - cls.log.error("Found non-VDB primitive: %s" % prim) - invalid_prim = True - if invalid_prim: + invalid_prims.append(prim) + if invalid_prims: + # Log all invalid primitives in a short readable way, like 0-5 + collections, remainder = clique.assemble( + str(prim.number()) for prim in invalid_prims + ) + collection = collections[0] + cls.log.error("Found non-VDB primitives for '{}', " + "primitive indices: {}".format( + node.path(), + collection.format("{ranges}") + )) return [instance] nr_of_points = len(geometry.points()) From e2e03346fa5592c39fdd4cf3904a479f8f029f75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:35:29 +0200 Subject: [PATCH 235/918] Fix VDB validation --- ..._node.xml => validate_vdb_output_node.xml} | 0 .../publish/validate_vdb_input_node.py | 52 ------------------- .../publish/validate_vdb_output_node.py | 27 +++++----- 3 files changed, 13 insertions(+), 66 deletions(-) rename openpype/hosts/houdini/plugins/publish/help/{validate_vdb_input_node.xml => validate_vdb_output_node.xml} (100%) delete mode 100644 openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py diff --git a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_input_node.xml b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml similarity index 100% rename from openpype/hosts/houdini/plugins/publish/help/validate_vdb_input_node.xml rename to openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py deleted file mode 100644 index 1f9ccc9c42..0000000000 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_input_node.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -from openpype.pipeline import ( - PublishValidationError -) - - -class ValidateVDBInputNode(pyblish.api.InstancePlugin): - """Validate that the node connected to the output node is of type VDB. - - Regardless of the amount of VDBs create the output will need to have an - equal amount of VDBs, points, primitives and vertices - - A VDB is an inherited type of Prim, holds the following data: - - Primitives: 1 - - Points: 1 - - Vertices: 1 - - VDBs: 1 - - """ - - order = pyblish.api.ValidatorOrder + 0.1 - families = ["vdbcache"] - hosts = ["houdini"] - label = "Validate Input Node (VDB)" - - def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError( - self, - "Node connected to the output node is not of type VDB", - title=self.label - ) - - @classmethod - def get_invalid(cls, instance): - - node = instance.data["output_node"] - - prims = node.geometry().prims() - nr_of_prims = len(prims) - - nr_of_points = len(node.geometry().points()) - if nr_of_points != nr_of_prims: - cls.log.error("The number of primitives and points do not match") - return [instance] - - for prim in prims: - if prim.numVertices() != 1: - cls.log.error("Found primitive with more than 1 vertex!") - return [instance] diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index e7908ab119..ee3b9a0a6a 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- import pyblish.api import hou -from openpype.pipeline import PublishValidationError -import clique +from openpype.pipeline import PublishXmlValidationError class ValidateVDBOutputNode(pyblish.api.InstancePlugin): @@ -27,9 +26,9 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError( - "Node connected to the output node is not" " of type VDB!", - title=self.label + raise PublishXmlValidationError( + self, + "Node connected to the output node is not" " of type VDB!" ) @classmethod @@ -62,16 +61,16 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): if not isinstance(prim, hou.VDB): invalid_prims.append(prim) if invalid_prims: - # Log all invalid primitives in a short readable way, like 0-5 - collections, remainder = clique.assemble( - str(prim.number()) for prim in invalid_prims + # TODO Log all invalid primitives in a short readable way, like 0-5 + # This logging can be really slow for many primitives, say 20000+ + # which might be fixed by logging only consecutive ranges + cls.log.error( + "Found non-VDB primitives for '{}', " + "primitive indices: {}".format( + node.path(), + ", ".join(prim.number() for prim in invalid_prims) + ) ) - collection = collections[0] - cls.log.error("Found non-VDB primitives for '{}', " - "primitive indices: {}".format( - node.path(), - collection.format("{ranges}") - )) return [instance] nr_of_points = len(geometry.points()) From 3e71ace6b762806d3b4ee097d4bd523d13dbe627 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:37:35 +0200 Subject: [PATCH 236/918] Fix logic --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index ee3b9a0a6a..a8fb5007cf 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -68,7 +68,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): "Found non-VDB primitives for '{}', " "primitive indices: {}".format( node.path(), - ", ".join(prim.number() for prim in invalid_prims) + ", ".join(str(prim.number()) for prim in invalid_prims) ) ) return [instance] From 3f404002e5abc8eee6778fda6a7363a29273329f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:41:41 +0200 Subject: [PATCH 237/918] Cosmetics + less aggresive message (no exclamation point) --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index a8fb5007cf..dd9ffc2a12 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -28,7 +28,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): if invalid: raise PublishXmlValidationError( self, - "Node connected to the output node is not" " of type VDB!" + "Node connected to the output node is not of type VDB." ) @classmethod From 13b72fa57ccdb1353d515eac1da797e024175774 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 17:59:36 +0200 Subject: [PATCH 238/918] Improve logging speed + readability for large number of primitives --- .../publish/validate_vdb_output_node.py | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index dd9ffc2a12..98a0796fec 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -4,6 +4,39 @@ import hou from openpype.pipeline import PublishXmlValidationError +def group_consecutive_numbers(nums): + """ + Args: + nums (list): List of sorted integer numbers. + + Yields: + str: Group ranges as {start}-{end} if more than one number in the range + else it yields {end} + + """ + start = None + end = None + + def _result(a, b): + if a == b: + return "{}".format(a) + else: + return "{}-{}".format(a, b) + + for num in nums: + if start is None: + start = num + end = num + elif num == end + 1: + end = num + else: + yield _result(start, end) + start = num + end = num + if start is not None: + yield _result(start, end) + + class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. @@ -61,14 +94,15 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): if not isinstance(prim, hou.VDB): invalid_prims.append(prim) if invalid_prims: - # TODO Log all invalid primitives in a short readable way, like 0-5 - # This logging can be really slow for many primitives, say 20000+ - # which might be fixed by logging only consecutive ranges + # Log prim numbers as consecutive ranges so logging isn't very + # slow for large number of primitives cls.log.error( "Found non-VDB primitives for '{}', " "primitive indices: {}".format( node.path(), - ", ".join(str(prim.number()) for prim in invalid_prims) + ", ".join(group_consecutive_numbers( + prim.number() for prim in invalid_prims + )) ) ) return [instance] From 97f13a169b421ec8341f6f3c1b02a1cd5d1b4206 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 7 Apr 2023 18:11:35 +0200 Subject: [PATCH 239/918] Allow output node to be not collected, then correctly show error --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 98a0796fec..b2b5c63799 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -67,7 +67,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - node = instance.data["output_node"] + node = instance.data.get("output_node") if node is None: cls.log.error( "SOP path is not correctly set on " From 414cd0cce113328755103f046e3a5aeef0e432e5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:09:56 +0800 Subject: [PATCH 240/918] add mantra and karma farm publishing for Houdini --- .../plugins/create/create_karma_rop.py | 114 ++++++++++++ .../plugins/create/create_mantra_rop.py | 78 ++++++++ .../publish/collect_instance_frame_data.py | 56 ++++++ .../plugins/publish/collect_instances.py | 2 +- .../plugins/publish/collect_karma_rop.py | 136 ++++++++++++++ .../plugins/publish/collect_mantra_rop.py | 158 ++++++++++++++++ .../plugins/publish/collect_redshift_rop.py | 41 +++- .../plugins/publish/increment_current_file.py | 7 +- .../deadline/abstract_submit_deadline.py | 2 +- .../publish/submit_houdini_render_deadline.py | 175 +++++++----------- .../plugins/publish/submit_publish_job.py | 7 +- 11 files changed, 666 insertions(+), 110 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_karma_rop.py create mode 100644 openpype/hosts/houdini/plugins/create/create_mantra_rop.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_karma_rop.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py new file mode 100644 index 0000000000..f2a6908d01 --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +"""Creator plugin to create Karma ROP.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import BoolDef, EnumDef, NumberDef + + +class CreateKarmaROP(plugin.HoudiniCreator): + """Karma ROP""" + identifier = "io.openpype.creators.houdini.karma_rop" + label = "Karma ROP" + family = "karma_rop" + icon = "magic" + defaults = ["master"] + + def create(self, subset_name, instance_data, pre_create_data): + import hou # noqa + + instance_data.pop("active", None) + instance_data.update({"node_type": "karma"}) + # Add chunk size attribute + instance_data["chunkSize"] = 10 + + instance = super(CreateKarmaROP, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + ext = pre_create_data.get("image_format") + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) + checkpoint = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.checkpoint".format(subset_name) + ) + + usd_directory = "{}{}".format( + hou.text.expandString("$HIP/pyblish/usd_renders/"), + "{}_$RENDERID".format(subset_name) + ) + + parms = { + # Render Frame Range + "trange": 1, + # Karma ROP Setting + "picture": filepath, + # Karma Checkpoint Setting + "productName": checkpoint, + # USD Output Directory + "savetodirectory": usd_directory, + } + + res_x = pre_create_data.get("res_x") + res_y = pre_create_data.get("res_y") + + if self.selected_nodes: + # If camera found in selection + # we will use as render camera + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + camera_node = hou.node(camera) + has_camera = pre_create_data.get("cam_res") + if has_camera: + res_x = camera_node.evalParm("resx") + res_y = camera_node.evalParm("resy") + + if not camera: + self.log.warning("No render camera found in selection") + + + parms.update({ + "camera": camera or "", + "resolutionx": res_x, + "resolutiony": res_y, + }) + + instance_node.setParms(parms) + + # Lock some Avalon attributes + to_lock = ["family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs() + + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default="exr", + label="Image Format Options"), + NumberDef("res_x", + label="width", + default=1920, + decimals=0), + NumberDef("res_y", + label="height", + default=720, + decimals=0), + BoolDef("cam_res", + label="Camera Resolution", + default=False) + ] diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py new file mode 100644 index 0000000000..b1ae996a30 --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""Creator plugin to create Mantra ROP.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import EnumDef + + +class CreateMantraROP(plugin.HoudiniCreator): + """Mantra ROP""" + identifier = "io.openpype.creators.houdini.mantra_rop" + label = "Mantra ROP" + family = "mantra_rop" + icon = "magic" + defaults = ["master"] + + def create(self, subset_name, instance_data, pre_create_data): + import hou # noqa + + instance_data.pop("active", None) + instance_data.update({"node_type": "ifd"}) + # Add chunk size attribute + instance_data["chunkSize"] = 10 + + instance = super(CreateMantraROP, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + ext = pre_create_data.get("image_format") + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) + + parms = { + # Render Frame Range + "trange": 1, + # Mantra ROP Setting + "vm_picture": filepath, + } + + if self.selected_nodes: + # If camera found in selection + # we will use as render camera + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + + if not camera: + self.log.warning("No render camera found in selection") + + parms.update({"camera": camera or ""}) + + instance_node.setParms(parms) + + # Lock some Avalon attributes + to_lock = ["family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateMantraROP, self).get_pre_create_attr_defs() + + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default="exr", + label="Image Format Options") + ] + # Extract Import Plane parameters(Should be in the setting) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py new file mode 100644 index 0000000000..584343cd64 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py @@ -0,0 +1,56 @@ +import hou + +import pyblish.api + + +class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): + """Collect time range frame data for the instance node.""" + + order = pyblish.api.CollectorOrder + 0.001 + label = "Instance Node Frame Range" + hosts = ["houdini"] + + def process(self, instance): + + node_path = instance.data.get("instance_node") + node = hou.node(node_path) if node_path else None + if not node_path or not node: + self.log.debug("No instance node found for instance: " + "{}".format(instance)) + return + + frame_data = self.get_frame_data(node) + if not frame_data: + return + + self.log.info("Collected time data: {}".format(frame_data)) + instance.data.update(frame_data) + + def get_frame_data(self, node): + """Get the frame data: start frame, end frame and steps + Args: + node(hou.Node) + + Returns: + dict + + """ + + data = {} + + if node.parm("trange") is None: + self.log.debug("Node has no 'trange' parameter: " + "{}".format(node.path())) + return data + + if node.evalParm("trange") == 0: + # Ignore 'render current frame' + self.log.debug("Node '{}' has 'Render current frame' set. " + "Time range data ignored.".format(node.path())) + return data + + data["frameStart"] = node.evalParm("f1") + data["frameEnd"] = node.evalParm("f2") + data["byFrameStep"] = node.evalParm("f3") + + return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index bb85630552..3fec2c4673 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -116,6 +116,6 @@ class CollectInstances(pyblish.api.ContextPlugin): data["frameStart"] = node.evalParm("f1") data["frameEnd"] = node.evalParm("f2") - data["steps"] = node.evalParm("f3") + data["byFrameStep"] = node.evalParm("f3") return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py new file mode 100644 index 0000000000..10c97269fc --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -0,0 +1,136 @@ +import re +import os + +import hou +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Karma Render Products + + Collects the instance.data["files"] for the multipart render product. + + Provides: + instance -> files + + """ + + label = "Karma ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["karma_rop"] + + def process(self, instance): + + rop = hou.node(instance.data.get("instance_node")) + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + default_prefix = evalParmNoFrame(rop, "picture") + render_products = [] + + # Default beauty AOV + beauty_product = self.get_render_product_name( + prefix=default_prefix, suffix=None + ) + render_products.append(beauty_product) + + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } + + filenames = list(render_products) + instance.data["files"] = filenames + + for product in render_products: + self.log.debug("Found render product: %s" % product) + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) + + def get_render_product_name(self, prefix, suffix): + if suffix: + # Add ".{suffix}" before the extension + prefix_base, ext = os.path.splitext(prefix) + product_name = prefix_base + "." + suffix + ext + else: + product_name = prefix + + return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py new file mode 100644 index 0000000000..c6cb2a6f49 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -0,0 +1,158 @@ +import re +import os + +import hou +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Mantra Render Products + + Collects the instance.data["files"] for the render products. + + Provides: + instance -> files + + """ + + label = "Mantra ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["mantra_rop"] + + def process(self, instance): + + rop = hou.node(instance.data.get("instance_node")) + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + default_prefix = evalParmNoFrame(rop, "vm_picture") + render_products = [] + + # Default beauty AOV + beauty_product = self.get_render_product_name( + prefix=default_prefix, suffix=None + ) + render_products.append(beauty_product) + + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } + + aov_numbers = rop.evalParm("vm_numaux") + if aov_numbers > 0: + # get the filenames of the AOVs + for i in range(1, aov_numbers + 1): + var = rop.evalParm("vm_variable_plane%d" % i) + if var: + aov_name = "vm_filename_plane%d" % i + has_aov_path = rop.evalParm(aov_name) + if has_aov_path: + aov_prefix = evalParmNoFrame(rop, aov_name) + aov_product = self.get_render_product_name( + prefix=aov_prefix, suffix=None + ) + render_products.append(aov_product) + + files_by_aov[var] = self.generate_expected_files(instance, + aov_product) + for product in render_products: + self.log.debug("Found render product: %s" % product) + filenames = list(render_products) + instance.data["files"] = filenames + + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) + self.log.debug("expectedFiles: %s" % files_by_aov) + + + def get_render_product_name(self, prefix, suffix): + if suffix: + # Add ".{suffix}" before the extension + prefix_base, ext = os.path.splitext(prefix) + product_name = prefix_base + "." + suffix + ext + else: + product_name = prefix + + return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index f1d73d7523..1a0100dd73 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -87,6 +87,10 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): prefix=default_prefix, suffix=beauty_suffix ) render_products.append(beauty_product) + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): @@ -104,11 +108,21 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): aov_product = self.get_render_product_name(aov_prefix, aov_suffix) render_products.append(aov_product) + files_by_aov[aov_suffix] = self.generate_expected_files(instance, + aov_product) + for product in render_products: self.log.debug("Found render product: %s" % product) + filenames = list(render_products) + instance.data["files"] = filenames - filenames = list(render_products) - instance.data["files"] = filenames + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -133,3 +147,26 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): product_name = prefix return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 16d9ef9aec..3c71dd6287 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -14,7 +14,12 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["houdini"] - families = ["workfile"] + families = ["workfile", + "redshift_rop", + "arnold_rop", + "mantra_rop", + "karma_rop", + "usdrender"] optional = True def process(self, context): diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 648eb77007..aea18f6dfd 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -663,7 +663,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): # test if there is instance of workfile waiting # to be published. - assert i.data["publish"] is True, ( + assert i.data.get("publish", True) is True, ( "Workfile (scene) must be published along") return i diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 73ab689c9a..d8de47b596 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -1,19 +1,27 @@ +import hou + import os -import json +import attr import getpass from datetime import datetime - -import requests import pyblish.api -# import hou ??? - from openpype.pipeline import legacy_io from openpype.tests.lib import is_in_tests +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo from openpype.lib import is_running_from_build -class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): +@attr.s +class DeadlinePluginInfo(): + SceneFile = attr.ib(default=None) + OutputDriver = attr.ib(default=None) + Version = attr.ib(default=None) + IgnoreInputs = attr.ib(default=True) + + +class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): """Submit Solaris USD Render ROPs to Deadline. Renders are submitted to a Deadline Web Service as @@ -30,83 +38,55 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder hosts = ["houdini"] families = ["usdrender", - "redshift_rop"] + "redshift_rop", + "mantra_rop", + "karma_rop"] targets = ["local"] + use_published = True - def process(self, instance): + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="Houdini") + instance = self._instance context = instance.context - code = context.data["code"] + filepath = context.data["currentFile"] filename = os.path.basename(filepath) - comment = context.data.get("comment", "") - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - jobname = "%s - %s" % (filename, instance.name) - # Support code prefix label for batch name - batch_name = filename - if code: - batch_name = "{0} - {1}".format(code, batch_name) + job_info.Name = "%s - %s" % (filename, instance.name) + job_info.BatchName = filename + job_info.Plugin = "Houdini" + job_info.UserName = context.data.get( + "deadlineUser", getpass.getuser()) if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") + job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") - # Output driver to render - driver = instance[0] - - # StartFrame to EndFrame by byFrameStep + # Deadline requires integers in frame range frames = "{start}-{end}x{step}".format( start=int(instance.data["frameStart"]), end=int(instance.data["frameEnd"]), step=int(instance.data["byFrameStep"]), ) + job_info.Frames = frames - # Documentation for keys available at: - # https://docs.thinkboxsoftware.com - # /products/deadline/8.0/1_User%20Manual/manual - # /manual-submission.html#job-info-file-options - payload = { - "JobInfo": { - # Top-level group name - "BatchName": batch_name, + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.ChunkSize = instance.data.get("chunkSize", 10) + job_info.Comment = context.data.get("comment") - # Job name, as seen in Monitor - "Name": jobname, - - # Arbitrary username, for visualisation in Monitor - "UserName": deadline_user, - - "Plugin": "Houdini", - "Pool": instance.data.get("primaryPool"), - "secondaryPool": instance.data.get("secondaryPool"), - "Frames": frames, - - "ChunkSize": instance.data.get("chunkSize", 10), - - "Comment": comment - }, - "PluginInfo": { - # Input - "SceneFile": filepath, - "OutputDriver": driver.path(), - - # Mandatory for Deadline - # Houdini version without patch number - "Version": hou.applicationVersionString().rsplit(".", 1)[0], - - "IgnoreInputs": True - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Include critical environment variables with submission + api.Session keys = [ - # Submit along the current Avalon tool setup that we launched - # this application with so the Render Slave can build its own - # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" - "AVALON_TOOLS" + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", + "OPENPYPE_SG_USER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV", + "OPENPYPE_LOG_NO_COLORS", + "OPENPYPE_VERSION" ] # Add OpenPype version if we are running from build. @@ -114,61 +94,50 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): keys.append("OPENPYPE_VERSION") # Add mongo url if it's enabled - if context.data.get("deadlinePassMongoUrl"): + if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) + for key in keys: + value = environment.get(key) + if value: + job_info.EnvironmentKeyValue[key] = value - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) + # to recognize job from PYPE for turning Event On/Off + job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" - # Include OutputFilename entries - # The first entry also enables double-click to preview rendered - # frames from Deadline Monitor - output_data = {} for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) fname = os.path.basename(filepath) - output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/") - output_data["OutputFilename%d" % i] = fname + job_info.OutputDirectory += dirname.replace("\\", "/") + job_info.OutputFilename += fname - # For now ensure destination folder exists otherwise HUSK - # will fail to render the output image. This is supposedly fixed - # in new production builds of Houdini - # TODO Remove this workaround with Houdini 18.0.391+ - if not os.path.exists(dirname): - self.log.info("Ensuring output directory exists: %s" % - dirname) - os.makedirs(dirname) + return job_info - payload["JobInfo"].update(output_data) + def get_plugin_info(self): - self.submit(instance, payload) + instance = self._instance + context = instance.context - def submit(self, instance, payload): + # Output driver to render + driver = hou.node(instance.data["instance_node"]) + hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] - AVALON_DEADLINE = legacy_io.Session.get("AVALON_DEADLINE", - "http://localhost:8082") - assert AVALON_DEADLINE, "Requires AVALON_DEADLINE" + plugin_info = DeadlinePluginInfo( + SceneFile=context.data["currentFile"], + OutputDriver=driver.path(), + Version=hou_major_minor, + IgnoreInputs=True + ) - plugin = payload["JobInfo"]["Plugin"] - self.log.info("Using Render Plugin : {}".format(plugin)) + return attr.asdict(plugin_info) - self.log.info("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(AVALON_DEADLINE) - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) + def process(self, instance): + super(HoudiniSubmitDeadline, self).process(instance) + # TODO: Avoid the need for this logic here, needed for submit publish # Store output dir for unified publisher (filesequence) output_dir = os.path.dirname(instance.data["files"][0]) instance.data["outputDir"] = output_dir - instance.data["deadlineSubmissionJob"] = response.json() + instance.data["toBeRenderedOn"] = "deadline" diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 4765772bcf..cd70201832 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -122,7 +122,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "celaction", "aftereffects", "harmony"] families = ["render.farm", "prerender.farm", - "renderlayer", "imagesequence", "maxrender", "vrayscene"] + "renderlayer", "imagesequence", + "maxrender", "vrayscene", + "mantra_rop", "karma_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE @@ -140,7 +142,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_SERVER", "AVALON_APP_NAME", "OPENPYPE_USERNAME", - "OPENPYPE_SG_USER", + "OPENPYPE_VERSION", + "OPENPYPE_SG_USER" ] # Add OpenPype version if we are running from build. From ecc673295cc4bf8ddb4e725a027050bc67e79b31 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:24:00 +0800 Subject: [PATCH 241/918] hound fix --- openpype/hosts/houdini/plugins/create/create_karma_rop.py | 7 +++---- .../hosts/houdini/plugins/publish/collect_mantra_rop.py | 5 +---- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index f2a6908d01..8d55298926 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -34,12 +34,12 @@ class CreateKarmaROP(plugin.HoudiniCreator): hou.text.expandString("$HIP/pyblish/"), "{}.$F4.{}".format(subset_name, ext) ) - checkpoint = "{}{}".format( + checkpoint = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), "{}.$F4.checkpoint".format(subset_name) ) - usd_directory = "{}{}".format( + usd_directory = "{}{}".format( hou.text.expandString("$HIP/pyblish/usd_renders/"), "{}_$RENDERID".format(subset_name) ) @@ -74,12 +74,11 @@ class CreateKarmaROP(plugin.HoudiniCreator): if not camera: self.log.warning("No render camera found in selection") - parms.update({ "camera": camera or "", "resolutionx": res_x, "resolutiony": res_y, - }) + }) instance_node.setParms(parms) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index c6cb2a6f49..1a881389db 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -22,7 +22,6 @@ def get_top_referenced_parm(parm): else: parm = ref - def evalParmNoFrame(node, parm, pad_character="#"): parameter = node.parm(parm) @@ -108,7 +107,7 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(aov_product) files_by_aov[var] = self.generate_expected_files(instance, - aov_product) + aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) filenames = list(render_products) @@ -121,8 +120,6 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) - self.log.debug("expectedFiles: %s" % files_by_aov) - def get_render_product_name(self, prefix, suffix): if suffix: diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 1a0100dd73..60d3a977fc 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -109,7 +109,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(aov_product) files_by_aov[aov_suffix] = self.generate_expected_files(instance, - aov_product) + aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) From c9a88d06abdf419fc08dde0529dd4f2de51775ad Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:28:38 +0800 Subject: [PATCH 242/918] hound fix --- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 1a881389db..1eb850e52e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -22,6 +22,7 @@ def get_top_referenced_parm(parm): else: parm = ref + def evalParmNoFrame(node, parm, pad_character="#"): parameter = node.parm(parm) @@ -106,8 +107,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): ) render_products.append(aov_product) - files_by_aov[var] = self.generate_expected_files(instance, - aov_product) # noqa + files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa + for product in render_products: self.log.debug("Found render product: %s" % product) filenames = list(render_products) From eee45f42879cfcd117dbd4ab70c0dd0e686cd583 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:29:21 +0800 Subject: [PATCH 243/918] hound fix --- openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 60d3a977fc..82874e546f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -90,7 +90,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): files_by_aov = { "beauty": self.generate_expected_files(instance, beauty_product) - } + } num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): From dfa2ee370580054e9979a070c6ea83dba03524dc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:30:11 +0800 Subject: [PATCH 244/918] hound fix --- openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 82874e546f..15df12e075 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -89,8 +89,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(beauty_product) files_by_aov = { "beauty": self.generate_expected_files(instance, - beauty_product) - } + beauty_product)} num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): From eeaa7fdc55bf2adbd13ef01808559f21c63179b1 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 11 Apr 2023 10:22:22 +0100 Subject: [PATCH 245/918] Update openpype/hosts/maya/plugins/publish/extract_playblast.py --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index a9f5062c48..b2deb71d0b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -85,12 +85,13 @@ class ExtractPlayblast(publish.Extractor): capture_preset = self.capture_preset preset = lib.load_capture_preset(data=self.capture_preset) if self.profiles: - capture_preset = filter_profiles( + profile = filter_profiles( self.profiles, filtering_criteria, logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset(data=capture_preset) + ) + capture_preset = profile.get("capture_preset) or capture_preset else: self.log.warning("No profiles present for Extract Playblast") + preset = lib.load_capture_preset(data=capture_preset) # "isolate_view" will already have been applied at creation, so we'll # ignore it here. From 97d1829b0faba2ba862bb1beaa997368a75d5b60 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 11 Apr 2023 10:24:18 +0100 Subject: [PATCH 246/918] Update openpype/hosts/maya/plugins/publish/extract_playblast.py --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index b2deb71d0b..13e6fb2f0d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -88,7 +88,7 @@ class ExtractPlayblast(publish.Extractor): profile = filter_profiles( self.profiles, filtering_criteria, logger=self.log ) - capture_preset = profile.get("capture_preset) or capture_preset + capture_preset = profile.get("capture_preset") or capture_preset else: self.log.warning("No profiles present for Extract Playblast") preset = lib.load_capture_preset(data=capture_preset) From 6872e32c1eca3b739aed578b6aa059efbf2348bc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Apr 2023 21:45:16 +0200 Subject: [PATCH 247/918] Generate shelves only in UI mode + defer generation to avoid slow Houdini launch on Windows --- openpype/hosts/houdini/api/pipeline.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 45e2f8f87f..62a8fba55e 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -81,7 +81,13 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): # TODO: make sure this doesn't trigger when # opening with last workfile. _set_context_settings() - shelves.generate_shelves() + + if not IS_HEADLESS: + import hdefereval # noqa, hdefereval is only available in ui mode + # Defer generation of shelves due to issue on Windows where shelf + # initialization during start up delays Houdini UI by minutes + # making it extremely slow to launch. + hdefereval.executeDeferred(shelves.generate_shelves) def has_unsaved_changes(self): return hou.hipFile.hasUnsavedChanges() From 06a94f09370b4e359f909f14a1be862364cbafc4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 11 Apr 2023 21:24:57 +0100 Subject: [PATCH 248/918] BigRoy feedback --- .../maya/plugins/create/create_review.py | 7 +++-- .../maya/plugins/publish/extract_playblast.py | 11 +++++-- .../maya/plugins/publish/extract_thumbnail.py | 31 +++++++++---------- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 156f1e3461..972b3a0160 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -42,7 +42,7 @@ class CreateReview(plugin.Creator): project_name = get_current_project_name() profiles = get_project_settings( project_name - )["maya"]["publish"]["ExtractPlayblast"]["profiles"] + )["maya"]["publish"]["ExtractPlayblast"].get("profiles") preset = None if profiles: @@ -57,9 +57,10 @@ class CreateReview(plugin.Creator): "task_types": task_type, "subset": data["subset"] } - preset = filter_profiles( + profile = filter_profiles( profiles, filtering_criteria, logger=self.log - )["capture_preset"] + ) + preset = profile["capture_preset"] if profile else None else: self.log.warning("No profiles present for extract playblast.") diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 13e6fb2f0d..0ce5aa883e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -82,15 +82,20 @@ class ExtractPlayblast(publish.Extractor): "task_types": task_type, "subset": subset } - capture_preset = self.capture_preset - preset = lib.load_capture_preset(data=self.capture_preset) + if self.profiles: profile = filter_profiles( self.profiles, filtering_criteria, logger=self.log ) - capture_preset = profile.get("capture_preset") or capture_preset + capture_preset = profile.get("capture_preset") else: self.log.warning("No profiles present for Extract Playblast") + + # Backward compatibility for deprecated Extract Playblast settings + # without profiles. + if capture_preset is None: + capture_preset = self.capture_preset + preset = lib.load_capture_preset(data=capture_preset) # "isolate_view" will already have been applied at creation, so we'll diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 8d635d0df2..cd4e4694ba 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -44,26 +44,23 @@ class ExtractThumbnail(publish.Extractor): "subset": subset } - maya_settings = instance.context.data["project_settings"]["maya"] - plugin_settings = maya_settings["publish"]["ExtractPlayblast"] - - capture_preset = plugin_settings["capture_preset"] - preset = {} - try: - preset = lib.load_capture_preset(data=capture_preset) - except KeyError as ke: - self.log.error("Error loading capture presets: {}".format(str(ke))) - - if plugin_settings["profiles"]: - capture_preset = filter_profiles( - plugin_settings["profiles"], - filtering_criteria, - logger=self.log - )["capture_preset"] - preset = lib.load_capture_preset(data=capture_preset) + if self.profiles: + profile = filter_profiles( + self.profiles, filtering_criteria, logger=self.log + ) + capture_preset = profile.get("capture_preset") else: self.log.warning("No profiles present for Extract Playblast") + # Backward compatibility for deprecated Extract Playblast settings + # without profiles. + if capture_preset is None: + maya_settings = instance.context.data["project_settings"]["maya"] + plugin_settings = maya_settings["publish"]["ExtractPlayblast"] + capture_preset = plugin_settings["capture_preset"] + + preset = lib.load_capture_preset(data=capture_preset) + # "isolate_view" will already have been applied at creation, so we'll # ignore it here. preset.pop("isolate_view") From 63a168c2f25be5b479da5907c69f28810b06420d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 15:09:40 +0800 Subject: [PATCH 249/918] allows the user to choose image format for arnold rendering option --- .../plugins/create/create_arnold_rop.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 41c4fcfaef..824d891e04 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,5 +1,5 @@ from openpype.hosts.houdini.api import plugin - +from openpype.lib import EnumDef class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" @@ -36,9 +36,11 @@ class CreateArnoldRop(plugin.HoudiniCreator): parm_template_group.hideFolder("Properties", True) instance_node.setParmTemplateGroup(parm_template_group) + ext = pre_create_data.get("image_format") + filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), - "{}.$F4{}".format(subset_name, self.ext) + "{}.$F4{}".format(subset_name, ext) ) parms = { # Render frame range @@ -54,3 +56,18 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Lock any parameters in this list to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateArnoldRop, self).get_pre_create_attr_defs() + + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default=self.ext, + label="Image Format Options") + ] From fd2d365f1e6ad254422759da011387d72e797367 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 15:11:23 +0800 Subject: [PATCH 250/918] hounf --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 1 + openpype/hosts/houdini/plugins/create/create_mantra_rop.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 824d891e04..e012dd467b 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,6 +1,7 @@ from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef + class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index b1ae996a30..0d7ccce099 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -75,4 +75,3 @@ class CreateMantraROP(plugin.HoudiniCreator): default="exr", label="Image Format Options") ] - # Extract Import Plane parameters(Should be in the setting) From 26ba2e6bee906dc1f69a625c24333a0a12264369 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 15:26:03 +0800 Subject: [PATCH 251/918] fix the syntax error --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index e012dd467b..0657d349f9 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -12,7 +12,7 @@ class CreateArnoldRop(plugin.HoudiniCreator): defaults = ["master"] # Default extension - ext = ".exr" + ext = "exr" def create(self, subset_name, instance_data, pre_create_data): import hou @@ -41,7 +41,7 @@ class CreateArnoldRop(plugin.HoudiniCreator): filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), - "{}.$F4{}".format(subset_name, ext) + "{}.$F4.{}".format(subset_name, ext) ) parms = { # Render frame range From ef5658ce09bbc6d96bb45525063915cdff63bc7a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 12 Apr 2023 08:45:53 +0100 Subject: [PATCH 252/918] Refactor fetching capture preset to lib. --- openpype/hosts/maya/api/lib.py | 45 ++++++++++++++ .../maya/plugins/create/create_review.py | 60 ++++++------------- .../maya/plugins/publish/extract_playblast.py | 34 +++-------- .../maya/plugins/publish/extract_thumbnail.py | 36 +++-------- 4 files changed, 78 insertions(+), 97 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 22803a2e3a..a79e7ade0c 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -33,6 +33,7 @@ from openpype.pipeline import ( registered_host, ) from openpype.pipeline.context_tools import get_current_project_asset +from openpype.lib.profiles_filtering import filter_profiles self = sys.modules[__name__] @@ -3812,3 +3813,47 @@ def get_all_children(nodes): iterator.next() # noqa: B305 return list(traversed) + + +def get_capture_preset(task_name, task_type, subset, project_settings, log): + """Get capture preset for playblasting. + + Logic for transitioning from old style capture preset to new capture preset + profiles. + + Args: + task_name (str): Task name. + take_type (str): Task type. + subset (str): Subset name. + project_settings (dict): Project settings. + log (object): Logging object. + """ + filtering_criteria = { + "hosts": "maya", + "families": "review", + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + + plugin_settings = project_settings["maya"]["publish"]["ExtractPlayblast"] + if plugin_settings["profiles"]: + profile = filter_profiles( + plugin_settings["profiles"], + filtering_criteria, + logger=log + ) + capture_preset = profile.get("capture_preset") + else: + log.warning("No profiles present for Extract Playblast") + + # Backward compatibility for deprecated Extract Playblast settings + # without profiles. + if capture_preset is None: + log.debug( + "Falling back to deprecated Extract Playblast capture preset " + "because no new style playblast profiles are defined." + ) + capture_preset = plugin_settings["capture_preset"] + + return capture_preset diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 972b3a0160..eb68bbb257 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -8,7 +8,6 @@ from openpype.hosts.maya.api import ( ) from openpype.settings import get_project_settings from openpype.pipeline import get_current_project_name, get_current_task_name -from openpype.lib.profiles_filtering import filter_profiles from openpype.client import get_asset_by_name @@ -40,29 +39,21 @@ class CreateReview(plugin.Creator): data = OrderedDict(**self.data) project_name = get_current_project_name() - profiles = get_project_settings( - project_name - )["maya"]["publish"]["ExtractPlayblast"].get("profiles") - - preset = None - if profiles: - asset_doc = get_asset_by_name(project_name, data["asset"]) - task_name = get_current_task_name() - task_type = asset_doc["data"]["tasks"][task_name]["type"] - - filtering_criteria = { - "hosts": "maya", - "families": "review", - "task_names": task_name, - "task_types": task_type, - "subset": data["subset"] - } - profile = filter_profiles( - profiles, filtering_criteria, logger=self.log + asset_doc = get_asset_by_name(project_name, data["asset"]) + task_name = get_current_task_name() + preset = lib.get_capture_preset( + task_name, + asset_doc["data"]["tasks"][task_name]["type"], + data["subset"], + get_project_settings(project_name), + self.log + ) + if os.environ.get("OPENPYPE_DEBUG") == "1": + self.log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) ) - preset = profile["capture_preset"] if profile else None - else: - self.log.warning("No profiles present for extract playblast.") # Option for using Maya or asset frame range in settings. frame_range = lib.get_frame_range() @@ -73,25 +64,12 @@ class CreateReview(plugin.Creator): data["fps"] = lib.collect_animation_data(fps=True)["fps"] - data["review_width"] = self.Width - data["review_height"] = self.Height - data["isolate"] = self.isolate data["keepImages"] = self.keepImages - data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency - data["panZoom"] = self.panZoom - - if preset: - if os.environ.get("OPENPYPE_DEBUG") == "1": - self.log.debug( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) - ) - ) - data["review_width"] = preset["Resolution"]["width"] - data["review_height"] = preset["Resolution"]["height"] - data["isolate"] = preset["Generic"]["isolate_view"] - data["imagePlane"] = preset["Viewport Options"]["imagePlane"] - data["panZoom"] = preset["Generic"]["pan_zoom"] + data["review_width"] = preset["Resolution"]["width"] + data["review_height"] = preset["Resolution"]["height"] + data["isolate"] = preset["Generic"]["isolate_view"] + data["imagePlane"] = preset["Viewport Options"]["imagePlane"] + data["panZoom"] = preset["Generic"]["pan_zoom"] self.data = data diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0ce5aa883e..78a8106444 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -7,7 +7,6 @@ import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib -from openpype.lib.profiles_filtering import filter_profiles from maya import cmds @@ -68,33 +67,14 @@ class ExtractPlayblast(publish.Extractor): # get cameras camera = instance.data["review_camera"] - host_name = instance.context.data["hostName"] - family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) - task_name = task_data.get("name") - task_type = task_data.get("type") - subset = instance.data["subset"] - - filtering_criteria = { - "hosts": host_name, - "families": family, - "task_names": task_name, - "task_types": task_type, - "subset": subset - } - - if self.profiles: - profile = filter_profiles( - self.profiles, filtering_criteria, logger=self.log - ) - capture_preset = profile.get("capture_preset") - else: - self.log.warning("No profiles present for Extract Playblast") - - # Backward compatibility for deprecated Extract Playblast settings - # without profiles. - if capture_preset is None: - capture_preset = self.capture_preset + capture_preset = lib.get_capture_preset( + task_data.get("name"), + task_data.get("type"), + instance.data["subset"], + instance.context.data["project_settings"], + self.log + ) preset = lib.load_capture_preset(data=capture_preset) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index cd4e4694ba..e2125e7c44 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -7,7 +7,6 @@ import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib -from openpype.lib.profiles_filtering import filter_profiles from maya import cmds @@ -29,35 +28,14 @@ class ExtractThumbnail(publish.Extractor): camera = instance.data["review_camera"] - host_name = instance.context.data["hostName"] - family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) - task_name = task_data.get("name") - task_type = task_data.get("type") - subset = instance.data["subset"] - - filtering_criteria = { - "hosts": host_name, - "families": family, - "task_names": task_name, - "task_types": task_type, - "subset": subset - } - - if self.profiles: - profile = filter_profiles( - self.profiles, filtering_criteria, logger=self.log - ) - capture_preset = profile.get("capture_preset") - else: - self.log.warning("No profiles present for Extract Playblast") - - # Backward compatibility for deprecated Extract Playblast settings - # without profiles. - if capture_preset is None: - maya_settings = instance.context.data["project_settings"]["maya"] - plugin_settings = maya_settings["publish"]["ExtractPlayblast"] - capture_preset = plugin_settings["capture_preset"] + capture_preset = lib.get_capture_preset( + task_data.get("name"), + task_data.get("type"), + instance.data["subset"], + instance.context.data["project_settings"], + self.log + ) preset = lib.load_capture_preset(data=capture_preset) From 1663a309a67e4abc78435d9b7189cc70bcff6160 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 12 Apr 2023 12:06:54 +0100 Subject: [PATCH 253/918] Fix tile rendering --- .../plugins/publish/submit_maya_deadline.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 062732c059..5542435387 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -327,6 +327,11 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info = copy.deepcopy(payload_job_info) plugin_info = copy.deepcopy(payload_plugin_info) + # Force plugin reload for vray cause the region does not get flushed + # between tile renders. + if plugin_info["Renderer"] == "vray": + job_info.ForceReloadPlugin = True + # if we have sequence of files, we need to create tile job for # every frame job_info.TileJob = True @@ -436,6 +441,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): assembly_payloads = [] output_dir = self.job_info.OutputDirectory[0] + config_files = [] for file in assembly_files: frame = re.search(R_FRAME_NUMBER, file).group("frame") @@ -461,6 +467,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): datetime.now().strftime("%Y_%m_%d_%H_%M_%S") ) ) + config_files.append(config_file) try: if not os.path.isdir(output_dir): os.makedirs(output_dir) @@ -469,8 +476,6 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): self.log.warning("Path is unreachable: " "`{}`".format(output_dir)) - assembly_plugin_info["ConfigFile"] = config_file - with open(config_file, "w") as cf: print("TileCount={}".format(tiles_count), file=cf) print("ImageFileName={}".format(file), file=cf) @@ -479,6 +484,10 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): print("ImageHeight={}".format( instance.data.get("resolutionHeight")), file=cf) + reversed_y = False + if plugin_info["Renderer"] == "arnold": + reversed_y = True + with open(config_file, "a") as cf: # Need to reverse the order of the y tiles, because image # coordinates are calculated from bottom left corner. @@ -489,7 +498,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance.data.get("resolutionWidth"), instance.data.get("resolutionHeight"), payload_plugin_info["OutputFilePrefix"], - reversed_y=True + reversed_y=reversed_y )[1] for k, v in sorted(tiles.items()): print("{}={}".format(k, v), file=cf) @@ -518,6 +527,11 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance.data["assemblySubmissionJobs"] = assembly_job_ids + # Remove config files to avoid confusion about where data is coming + # from in Deadline. + for config_file in config_files: + os.remove(config_file) + def _get_maya_payload(self, data): job_info = copy.deepcopy(self.job_info) @@ -878,8 +892,6 @@ def _format_tiles( out["PluginInfo"]["RegionRight{}".format(tile)] = right # Tile config - cfg["Tile{}".format(tile)] = new_filename - cfg["Tile{}Tile".format(tile)] = new_filename cfg["Tile{}FileName".format(tile)] = new_filename cfg["Tile{}X".format(tile)] = left cfg["Tile{}Y".format(tile)] = top From a15d8fde0145dc9e7d5fb41a248f7b25af5d3592 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Apr 2023 15:01:49 +0200 Subject: [PATCH 254/918] Specify per Creator where it is listed in Tab search + Add a null node in COP2 or SOP network when generated there --- .../hosts/houdini/api/creator_node_shelves.py | 57 +++++++++++++++---- .../plugins/create/create_alembic_camera.py | 8 +++ .../plugins/create/create_composite.py | 16 +++++- .../plugins/create/create_pointcache.py | 9 +++ .../plugins/create/create_vbd_cache.py | 8 +++ 5 files changed, 87 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 3638e14296..bc02b258b7 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -12,26 +12,35 @@ import tempfile import logging import os +from openpype.client import get_asset_by_name from openpype.pipeline import registered_host from openpype.pipeline.create import CreateContext from openpype.resources import get_openpype_icon_filepath import hou +import stateutils +import soptoolutils +import cop2toolutils + log = logging.getLogger(__name__) CREATE_SCRIPT = """ from openpype.hosts.houdini.api.creator_node_shelves import create_interactive -create_interactive("{identifier}") +create_interactive("{identifier}", **kwargs) """ -def create_interactive(creator_identifier): +def create_interactive(creator_identifier, **kwargs): """Create a Creator using its identifier interactively. This is used by the generated shelf tools as callback when a user selects the creator from the node tab search menu. + The `kwargs` should be what Houdini passes to the tool create scripts + context. For more information see: + https://www.sidefx.com/docs/houdini/hom/tool_script.html#arguments + Args: creator_identifier (str): The creator identifier of the Creator plugin to create. @@ -58,6 +67,33 @@ def create_interactive(creator_identifier): host = registered_host() context = CreateContext(host) + creator = context.manual_creators.get(creator_identifier) + if not creator: + raise RuntimeError("Invalid creator identifier: " + "{}".format(creator_identifier)) + + pane = stateutils.activePane(kwargs) + if isinstance(pane, hou.NetworkEditor): + pwd = pane.pwd() + subset_name = creator.get_subset_name( + variant=variant, + task_name=context.get_current_task_name(), + asset_doc=get_asset_by_name( + project_name=context.get_current_project_name(), + asset_name=context.get_current_asset_name() + ), + project_name=context.get_current_project_name(), + host_name=context.host_name + ) + + tool_fn = { + hou.sopNodeTypeCategory(): soptoolutils.genericTool, + hou.cop2NodeTypeCategory(): cop2toolutils.genericTool + }.get(pwd.childTypeCategory()) + + if tool_fn != None: + out_null = tool_fn(kwargs, "null") + out_null.setName("OUT_{}".format(subset_name), unique_name=True) before = context.instances_by_id.copy() @@ -135,12 +171,17 @@ def install(): log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath)) tools = [] + + default_network_categories = [hou.ropNodeTypeCategory()] with shelves_change_block(): for identifier, creator in create_context.manual_creators.items(): - # TODO: Allow the creator plug-in itself to override the categories - # for where they are shown, by e.g. defining - # `Creator.get_network_categories()` + # Allow the creator plug-in itself to override the categories + # for where they are shown with `Creator.get_network_categories()` + if hasattr(creator, "get_network_categories"): + network_categories = creator.get_network_categories() + else: + network_categories = default_network_categories key = "openpype_create.{}".format(identifier) log.debug(f"Registering {key}") @@ -153,17 +194,13 @@ def install(): creator.label ), "help_url": None, - "network_categories": [ - hou.ropNodeTypeCategory(), - hou.sopNodeTypeCategory() - ], + "network_categories": network_categories, "viewer_categories": [], "cop_viewer_categories": [], "network_op_type": None, "viewer_op_type": None, "locations": ["OpenPype"] } - label = "Create {}".format(creator.label) tool = hou.shelves.tool(key) if tool: diff --git a/openpype/hosts/houdini/plugins/create/create_alembic_camera.py b/openpype/hosts/houdini/plugins/create/create_alembic_camera.py index fec64eb4a1..8c8a5e9eed 100644 --- a/openpype/hosts/houdini/plugins/create/create_alembic_camera.py +++ b/openpype/hosts/houdini/plugins/create/create_alembic_camera.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance, CreatorError +import hou + class CreateAlembicCamera(plugin.HoudiniCreator): """Single baked camera from Alembic ROP.""" @@ -47,3 +49,9 @@ class CreateAlembicCamera(plugin.HoudiniCreator): self.lock_parameters(instance_node, to_lock) instance_node.parm("trange").set(1) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.objNodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_composite.py b/openpype/hosts/houdini/plugins/create/create_composite.py index 45af2b0630..9d4f7969bb 100644 --- a/openpype/hosts/houdini/plugins/create/create_composite.py +++ b/openpype/hosts/houdini/plugins/create/create_composite.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- """Creator plugin for creating composite sequences.""" from openpype.hosts.houdini.api import plugin -from openpype.pipeline import CreatedInstance +from openpype.pipeline import CreatedInstance, CreatorError + +import hou class CreateCompositeSequence(plugin.HoudiniCreator): @@ -35,8 +37,20 @@ class CreateCompositeSequence(plugin.HoudiniCreator): "copoutput": filepath } + if self.selected_nodes: + if len(self.selected_nodes) > 1: + raise CreatorError("More than one item selected.") + path = self.selected_nodes[0].path() + parms["coppath"] = path + instance_node.setParms(parms) # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.cop2NodeTypeCategory() + ] diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 6b6b277422..6efa96a42b 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +import hou + class CreatePointCache(plugin.HoudiniCreator): """Alembic ROP to pointcache""" @@ -49,3 +51,10 @@ class CreatePointCache(plugin.HoudiniCreator): # Lock any parameters in this list to_lock = ["prim_to_detail_pattern"] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] + diff --git a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py index 1a5011745f..c015cebd49 100644 --- a/openpype/hosts/houdini/plugins/create/create_vbd_cache.py +++ b/openpype/hosts/houdini/plugins/create/create_vbd_cache.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +import hou + class CreateVDBCache(plugin.HoudiniCreator): """OpenVDB from Geometry ROP""" @@ -34,3 +36,9 @@ class CreateVDBCache(plugin.HoudiniCreator): parms["soppath"] = self.selected_nodes[0].path() instance_node.setParms(parms) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.sopNodeTypeCategory() + ] From c6a0b7ff4546bddd687a617cdb05edd4e88f5447 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 12 Apr 2023 15:23:37 +0200 Subject: [PATCH 255/918] Shush hound --- openpype/hosts/houdini/api/creator_node_shelves.py | 2 +- openpype/hosts/houdini/plugins/create/create_pointcache.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index bc02b258b7..cd14090104 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -91,7 +91,7 @@ def create_interactive(creator_identifier, **kwargs): hou.cop2NodeTypeCategory(): cop2toolutils.genericTool }.get(pwd.childTypeCategory()) - if tool_fn != None: + if tool_fn is not None: out_null = tool_fn(kwargs, "null") out_null.setName("OUT_{}".format(subset_name), unique_name=True) diff --git a/openpype/hosts/houdini/plugins/create/create_pointcache.py b/openpype/hosts/houdini/plugins/create/create_pointcache.py index 6efa96a42b..df74070fee 100644 --- a/openpype/hosts/houdini/plugins/create/create_pointcache.py +++ b/openpype/hosts/houdini/plugins/create/create_pointcache.py @@ -57,4 +57,3 @@ class CreatePointCache(plugin.HoudiniCreator): hou.ropNodeTypeCategory(), hou.sopNodeTypeCategory() ] - From ee99b21e97fcc3236693f2a5dfe846cd815d85d2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:24:28 +0800 Subject: [PATCH 256/918] add override camera resolution options in creator's setting --- .../houdini/plugins/create/create_mantra_rop.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 0d7ccce099..2632f8d6c0 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -2,7 +2,7 @@ """Creator plugin to create Mantra ROP.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance -from openpype.lib import EnumDef +from openpype.lib import EnumDef, BoolDef class CreateMantraROP(plugin.HoudiniCreator): @@ -55,6 +55,9 @@ class CreateMantraROP(plugin.HoudiniCreator): parms.update({"camera": camera or ""}) + custom_res = pre_create_data.get("override_resolution") + if custom_res: + parms.update({"override_camerares": 1}) instance_node.setParms(parms) # Lock some Avalon attributes @@ -73,5 +76,10 @@ class CreateMantraROP(plugin.HoudiniCreator): EnumDef("image_format", image_format_enum, default="exr", - label="Image Format Options") + label="Image Format Options"), + BoolDef("override_resolution", + label="Override Camera Resolution", + tooltip="Override the current camera " + "resolution, recommended for IPR.", + default=False) ] From 80a5982a2460b09cc982fbc1076d26f9aab731e5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:27:33 +0800 Subject: [PATCH 257/918] allows to fully delete the render instances if users click the remove button in creator --- openpype/hosts/houdini/api/plugin.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 340a7f0770..6fb0f8b967 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -247,9 +247,22 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """ for instance in instances: instance_node = hou.node(instance.data.get("instance_node")) + node = instance.data.get("instance_node") + if instance_node: instance_node.destroy() + # for the extra render node from the plugins + # such as vray and redshift + ipr_node = hou.node("{}{}".format(node, + "_IPR")) + if ipr_node: + ipr_node.destroy() + re_node = hou.node("{}{}".format(node, + "_render_element")) + if re_node: + re_node.destroy() + self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): From d925322aa4e270595000b32a7aa00531130bf12c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:28:15 +0800 Subject: [PATCH 258/918] add vray render creator --- .../houdini/plugins/create/create_vray_rop.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/create/create_vray_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py new file mode 100644 index 0000000000..445f55581c --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +"""Creator plugin to create VRay ROP.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import EnumDef, BoolDef + + +class CreateVrayROP(plugin.HoudiniCreator): + """VRay ROP""" + + identifier = "io.openpype.creators.houdini.vray_rop" + label = "VRay ROP" + family = "vray_rop" + icon = "magic" + defaults = ["master"] + + ext = "exr" + + def create(self, subset_name, instance_data, pre_create_data): + import hou # + + instance_data.pop("active", None) + instance_data.update({"node_type": "vray_renderer"}) + # Add chunk size attribute + instance_data["chunkSize"] = 10 + + instance = super(CreateVrayROP, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + # Add IPR for Vray + basename = instance_node.name() + try: + ipr_rop = instance_node.parent().createNode( + "vray", node_name=basename + "_IPR" + ) + except hou.OperationFailed: + raise plugin.OpenPypeCreatorError( + "Cannot create Vray render node. " + "Make sure Vray installed and enabled!" + ) + + ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) + ipr_rop.parm("rop").set(instance_node.path()) + + ext = pre_create_data.get("image_format") + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) + + parms = { + "trange": 1, + "SettingsOutput_img_file_path": filepath, + "SettingsEXR_bits_per_channel": "16" # half precision + } + + if self.selected_nodes: + # set up the render camera from the selected node + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + parms.update({ + "render_camera": camera or "" + }) + + #TODO:Add Render Element + has_re = pre_create_data.get("render_element_enabled") + if has_re: + re_rop = instance_node.parent().createNode( + "vray_render_channels", + node_name=basename + "_render_element" + ) + # move the render element node next to the vray renderer node + re_rop.setPosition(instance_node.position() + hou.Vector2(0, 1)) + re_path = re_rop.path() + parms.update({ + "use_render_channels": 1, + "render_network_render_channels": re_path + }) + else: + parms.update({ + "use_render_channels": 0 + }) + + custom_res = pre_create_data.get("override_resolution") + if custom_res: + parms.update({"override_camerares": 1}) + + instance_node.setParms(parms) + + # lock parameters from AVALON + to_lock = ["family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateVrayROP, self).get_pre_create_attr_defs() + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default=self.ext, + label="Image Format Options"), + BoolDef("override_resolution", + label="Override Camera Resolution", + tooltip="Override the current camera " + "resolution, recommended for IPR.", + default=False), + BoolDef("render_element_enabled", + label="Render Element", + tooltip="Create Render Element Node " + "if enabled", + default=False) + ] +# ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr From 6c242d9285f3d6c924098b04049fd93525614a8b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:30:12 +0800 Subject: [PATCH 259/918] hound --- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 445f55581c..f0430a2892 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -56,11 +56,11 @@ class CreateVrayROP(plugin.HoudiniCreator): parms = { "trange": 1, "SettingsOutput_img_file_path": filepath, - "SettingsEXR_bits_per_channel": "16" # half precision + "SettingsEXR_bits_per_channel": "16" # half precision } if self.selected_nodes: - # set up the render camera from the selected node + # set up the render camera from the selected node camera = None for node in self.selected_nodes: if node.type().name() == "cam": @@ -69,7 +69,7 @@ class CreateVrayROP(plugin.HoudiniCreator): "render_camera": camera or "" }) - #TODO:Add Render Element + # Enable render element has_re = pre_create_data.get("render_element_enabled") if has_re: re_rop = instance_node.parent().createNode( From ae0d6dd1b5d80eec95958da110bc420b5cfc668b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Apr 2023 17:25:41 +0200 Subject: [PATCH 260/918] refactoring of settings for more explicitly communicated attributes --- .../project_settings/aftereffects.json | 6 ++-- .../defaults/project_settings/blender.json | 6 ++-- .../defaults/project_settings/celaction.json | 6 ++-- .../defaults/project_settings/flame.json | 6 ++-- .../defaults/project_settings/fusion.json | 6 ++-- .../defaults/project_settings/global.json | 1 - .../defaults/project_settings/harmony.json | 6 ++-- .../defaults/project_settings/hiero.json | 6 ++-- .../defaults/project_settings/houdini.json | 6 ++-- .../defaults/project_settings/max.json | 6 ++-- .../defaults/project_settings/maya.json | 6 ++-- .../defaults/project_settings/nuke.json | 11 ++---- .../defaults/project_settings/photoshop.json | 6 ++-- .../defaults/project_settings/resolve.json | 6 ++-- .../project_settings/traypublisher.json | 6 ++-- .../defaults/project_settings/tvpaint.json | 6 ++-- .../defaults/project_settings/unreal.json | 6 ++-- .../project_settings/webpublisher.json | 6 ++-- .../schema_project_aftereffects.json | 6 ++-- .../schema_project_blender.json | 6 ++-- .../schema_project_celaction.json | 6 ++-- .../projects_schema/schema_project_flame.json | 6 ++-- .../schema_project_fusion.json | 6 ++-- .../schema_project_global.json | 35 +++++++++++++++++-- .../schema_project_harmony.json | 6 ++-- .../projects_schema/schema_project_hiero.json | 7 ++-- .../schema_project_houdini.json | 6 ++-- .../projects_schema/schema_project_max.json | 6 ++-- .../projects_schema/schema_project_maya.json | 5 ++- .../schema_project_photoshop.json | 6 ++-- .../schema_project_resolve.json | 6 ++-- .../schema_project_traypublisher.json | 6 ++-- .../schema_project_tvpaint.json | 6 ++-- .../schema_project_unreal.json | 6 ++-- .../schema_project_webpublisher.json | 6 ++-- .../schemas/schema_imageio_config.json | 7 ++-- .../schemas/schema_imageio_file_rules.json | 7 ++-- .../schemas/schema_nuke_imageio.json | 19 ++-------- 38 files changed, 139 insertions(+), 133 deletions(-) diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index d1b2309d26..74bd519bbd 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index d9aabea9ad..8328ceeda3 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/celaction.json b/openpype/settings/defaults/project_settings/celaction.json index 10dfd70ac6..0e8b465118 100644 --- a/openpype/settings/defaults/project_settings/celaction.json +++ b/openpype/settings/defaults/project_settings/celaction.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index 5eb6ec2d2a..64021baeef 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": true, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} }, "project": { diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 9130c9322c..fa44bbe3d4 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} }, "ocio": { diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 4c4a7487cf..fccd02d130 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -7,7 +7,6 @@ ] }, "file_rules": { - "enabled": false, "rules": { "example": { "pattern": ".*(beauty).*", diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index 97c9cdf761..e6fb00a700 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index df460e0d86..b7d5d9af23 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} }, "workfile": { diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 8d7f9865c5..2b7192ff99 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 8df4d0ca57..f6462c3d9a 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 6016323b4b..fa3a7bc648 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1,13 +1,13 @@ { "open_workfile_post_initialization": false, "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} }, "colorManagementPreference_v2": { diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 0573df028d..1dd0e5128a 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -9,13 +9,13 @@ } }, "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} }, "viewer": { @@ -27,11 +27,6 @@ "workfile": { "colorManagement": "Nuke", "OCIO_config": "nuke-default", - "customOCIOConfigPath": { - "windows": [], - "darwin": [], - "linux": [] - }, "workingSpaceLUT": "linear", "monitorLut": "sRGB", "int8Lut": "sRGB", diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 9fd4fe54f1..47f397663b 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 3720dc54f4..7379e74200 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 311c5b0cfc..2668b5d638 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index b649d56337..3c930b84eb 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index d83e090fae..d92d3403ed 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/webpublisher.json b/openpype/settings/defaults/project_settings/webpublisher.json index a9dc9012eb..17d61ef028 100644 --- a/openpype/settings/defaults/project_settings/webpublisher.json +++ b/openpype/settings/defaults/project_settings/webpublisher.json @@ -1,12 +1,12 @@ { "imageio": { - "enabled": false, + "activate_host_color_management": true, "ocio_config": { - "enabled": false, + "override_global_config": false, "filepath": [] }, "file_rules": { - "enabled": false, + "override_global_rules": false, "rules": {} } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 2d48e06ccb..7bc20fed87 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 2e4dcb4e31..dbba7dfdd2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json index efecb2a89c..ab3acaf4a2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index 7c839037ad..5b96a49679 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 87856380ac..fad6361119 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -10,12 +10,12 @@ "type": "dict", "label": "Color Management (ImageIO)", "collapsible": true, - "checkbox_key": "enabled", + "is_group": true, "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json index 6f31f4f685..f200c1722f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -27,8 +27,39 @@ ] }, { - "type": "schema", - "name": "schema_imageio_file_rules" + "key": "file_rules", + "type": "dict", + "label": "File Rules", + "collapsible": true, + "children": [ + { + "key": "rules", + "label": "Rules", + "type": "dict-modifiable", + "highlight_content": true, + "collapsible": false, + "object_type": { + "type": "dict", + "children": [ + { + "key": "pattern", + "label": "Regex pattern", + "type": "text" + }, + { + "key": "colorspace", + "label": "Colorspace name", + "type": "text" + }, + { + "key": "ext", + "label": "File extension", + "type": "text" + } + ] + } + } + ] } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index da80648a14..71f8cb4db2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index af3ee713c4..5e42cb0a00 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -9,14 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", - "is_group": true, "collapsible": true, - "checkbox_key": "enabled", + "is_group": true, "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 74ffbbe9f4..14217c944e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index de7b4aca0b..5c4b825872 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 8373a57429..ef32f907ed 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -16,12 +16,11 @@ "label": "Color Management (ImageIO)", "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 95f402ca7c..53e59956eb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index da252dd9b1..16de175933 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 4bce299747..bc80562940 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index b1a31b5c93..a0d94ad7dc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index b330fd600f..87ba3d2d43 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index d0c2145298..f596c89686 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -9,13 +9,13 @@ "key": "imageio", "type": "dict", "label": "Color Management (ImageIO)", + "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json index e7cff969d3..bc65dd7826 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json @@ -3,12 +3,11 @@ "type": "dict", "label": "OCIO config", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "override_global_config", + "label": "Override global OCIO config" }, { "type": "path", @@ -18,4 +17,4 @@ "multipath": true } ] -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json index a171ba1c55..e76c8a326f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json @@ -3,12 +3,11 @@ "type": "dict", "label": "File Rules", "collapsible": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" + "key": "override_global_rules", + "label": "Override global file rules" }, { "key": "rules", @@ -38,4 +37,4 @@ } } ] -} \ No newline at end of file +} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index a46958e616..a986db1ade 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -4,16 +4,11 @@ "label": "Color Management (ImageIO)", "collapsible": true, "is_group": true, - "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "label", - "label": "'Custom OCIO config path' has deprecated.
If you need to set custom config, just enable and add path into 'OCIO config'.
Anatomy keys are supported.." + "key": "activate_host_color_management", + "label": "Enable Color Management in host" }, { "type": "schema", @@ -108,19 +103,9 @@ }, { "cg-config-v1.0.0_aces-v1.3_ocio-v2.1": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)" - }, - { - "custom": "custom" } ] }, - { - "type": "path", - "key": "customOCIOConfigPath", - "label": "Custom OCIO config path", - "multiplatform": true, - "multipath": true - }, { "type": "text", "key": "workingSpaceLUT", From 57e8722d7151e0343f20991dcd48b4a04bca304e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Apr 2023 17:50:46 +0200 Subject: [PATCH 261/918] redefining switches for imageio host activation and config overrides --- openpype/pipeline/colorspace.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index f1a50281d2..ed5c284faf 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -344,28 +344,31 @@ def get_imageio_config( imageio_global, imageio_host = _get_imageio_settings( project_settings, host_name) - # check if host settings group is having enabled key - imageio_enabled = imageio_host.get("enabled") - if imageio_enabled is None: - # it it does not have enabled key, use global settings - imageio_enabled = True + # check if host settings group is having activate_host_color_management + # it it does not have activation key then use global settings + # this is for backward compatibility + # TODO: in future rewrite this to be more explicit + activate_host_color_management = imageio_host.get( + "activate_host_color_management", True) - if not imageio_enabled : + if not activate_host_color_management: # if host settings are disabled return False because # it is expected that no colorspace management is needed return False config_host = imageio_host.get("ocio_config", {}) - if config_host.get("enabled"): + # get config path from either global or host_name + # depending on override flag + # TODO: in future rewrite this to be more explicit + config_data = None + override_global_rules = config_host.get("override_global_rules") + if override_global_rules: config_data = _get_config_data( config_host["filepath"], formatting_data ) else: - config_data = None - - if not config_data: - # get config path from either global or host_name + # get config path from global config_global = imageio_global["ocio_config"] config_data = _get_config_data( config_global["filepath"], formatting_data From 8f057e79d51b2e21586cb8733cf8aaff5c19f4fb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Apr 2023 21:26:25 +0200 Subject: [PATCH 262/918] fixing typo --- openpype/pipeline/colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index ed5c284faf..098e2a02c6 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -362,8 +362,8 @@ def get_imageio_config( # depending on override flag # TODO: in future rewrite this to be more explicit config_data = None - override_global_rules = config_host.get("override_global_rules") - if override_global_rules: + override_global_config = config_host.get("override_global_config") + if override_global_config: config_data = _get_config_data( config_host["filepath"], formatting_data ) From 153810b919efae29db4d7afc84562ac20068e5f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Apr 2023 21:45:53 +0200 Subject: [PATCH 263/918] adding hosts colorspace management switches into code --- openpype/pipeline/colorspace.py | 21 +++++++++++++------- openpype/pipeline/publish/publish_plugins.py | 15 +++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 098e2a02c6..b3774e5e90 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -92,6 +92,11 @@ def get_imageio_colorspace_from_filepath( ) config_data = get_imageio_config( project_name, host_name, project_settings) + + # in case host color management is not enabled + if not config_data: + return None + file_rules = get_imageio_file_rules( project_name, host_name, project_settings) @@ -354,6 +359,10 @@ def get_imageio_config( if not activate_host_color_management: # if host settings are disabled return False because # it is expected that no colorspace management is needed + log.info( + "Colorspace management for host '{}' is disabled.".format( + host_name) + ) return False config_host = imageio_host.get("ocio_config", {}) @@ -456,13 +465,11 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): frules_host = imageio_host.get("file_rules", {}) # compile file rules dictionary - file_rules = {} - if frules_global["enabled"]: - file_rules.update(frules_global["rules"]) - if frules_host and frules_host["enabled"]: - file_rules.update(frules_host["rules"]) - - return file_rules + override_global_rules = frules_host.get("override_global_rules") + if override_global_rules: + return frules_host["rules"] + else: + return frules_global["rules"] def _get_imageio_settings(project_settings, host_name): diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 331235fadc..a3f8413979 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -331,6 +331,11 @@ class ColormanagedPyblishPluginMixin(object): project_settings=project_settings_, anatomy_data=anatomy_data ) + + # in case host color management is not enabled + if not config_data: + return None + file_rules = get_imageio_file_rules( project_name, host_name, project_settings=project_settings_ @@ -385,14 +390,14 @@ class ColormanagedPyblishPluginMixin(object): if colorspace_settings is None: colorspace_settings = self.get_colorspace_settings(context) + # in case host color management is not enabled + if not colorspace_settings: + self.log.warning("Host's colorspace management is disabled.") + return + # unpack colorspace settings config_data, file_rules = colorspace_settings - if not config_data: - # warn in case no colorspace path was defined - self.log.warning("No colorspace management was defined") - return - self.log.info("Config data is : `{}`".format( config_data)) From 6ae7b415330778d9588ef8a8bea2e0ca83f51881 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 12 Apr 2023 22:50:32 +0200 Subject: [PATCH 264/918] fixing tests --- .../openpype/pipeline/publish/test_publish_plugins.py | 10 +++++----- tests/unit/openpype/pipeline/test_colorspace.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/openpype/pipeline/publish/test_publish_plugins.py b/tests/unit/openpype/pipeline/publish/test_publish_plugins.py index 88e0095e34..bbeab2cc90 100644 --- a/tests/unit/openpype/pipeline/publish/test_publish_plugins.py +++ b/tests/unit/openpype/pipeline/publish/test_publish_plugins.py @@ -26,7 +26,7 @@ log = logging.getLogger(__name__) class TestPipelinePublishPlugins(TestPipeline): - """ Testing Pipeline pubish_plugins.py + """ Testing Pipeline publish_plugins.py Example: cd to OpenPype repo root dir @@ -37,7 +37,7 @@ class TestPipelinePublishPlugins(TestPipeline): # files are the same as those used in `test_pipeline_colorspace` TEST_FILES = [ ( - "1d7t9_cVKeZRVF0ppCHiE5MJTTtTlJgBe", + "1YinxOToVyAd3-jAMFgVf7EWQa2x8Ma-O", "test_pipeline_colorspace.zip", "" ) @@ -140,7 +140,7 @@ class TestPipelinePublishPlugins(TestPipeline): config_data, file_rules = plugin.get_colorspace_settings(context) assert config_data["template"] == expected_config_template, ( - "Returned config tempate is not " + "Returned config template is not " f"matching {expected_config_template}" ) assert file_rules == expected_file_rules, ( @@ -193,11 +193,11 @@ class TestPipelinePublishPlugins(TestPipeline): colorspace_data_hiero = representation_hiero.get("colorspaceData") assert colorspace_data_nuke, ( - "Colorspace data were not created in prepresentation" + "Colorspace data were not created in representation" f"matching {representation_nuke}" ) assert colorspace_data_hiero, ( - "Colorspace data were not created in prepresentation" + "Colorspace data were not created in representation" f"matching {representation_hiero}" ) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index d064ca2be4..d0981723ad 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -31,7 +31,7 @@ class TestPipelineColorspace(TestPipeline): TEST_FILES = [ ( - "1d7t9_cVKeZRVF0ppCHiE5MJTTtTlJgBe", + "1YinxOToVyAd3-jAMFgVf7EWQa2x8Ma-O", "test_pipeline_colorspace.zip", "" ) @@ -120,7 +120,7 @@ class TestPipelineColorspace(TestPipeline): ) assert config_data["template"] == expected_template, ( f"Config template \'{config_data['template']}\' doesn't match " - f"expected tempalte \'{expected_template}\'" + f"expected template \'{expected_template}\'" ) def test_parse_colorspace_from_filepath( From 7fe588fdea99f085dddcd7e37ff44961c37fff03 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Apr 2023 14:44:47 +0800 Subject: [PATCH 265/918] roy's comment --- openpype/hosts/houdini/api/plugin.py | 13 ------------- .../houdini/plugins/create/create_vray_rop.py | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 6fb0f8b967..340a7f0770 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -247,22 +247,9 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """ for instance in instances: instance_node = hou.node(instance.data.get("instance_node")) - node = instance.data.get("instance_node") - if instance_node: instance_node.destroy() - # for the extra render node from the plugins - # such as vray and redshift - ipr_node = hou.node("{}{}".format(node, - "_IPR")) - if ipr_node: - ipr_node.destroy() - re_node = hou.node("{}{}".format(node, - "_render_element")) - if re_node: - re_node.destroy() - self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index f0430a2892..74e53eed15 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin to create VRay ROP.""" +import hou + from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance from openpype.lib import EnumDef, BoolDef @@ -17,7 +19,6 @@ class CreateVrayROP(plugin.HoudiniCreator): ext = "exr" def create(self, subset_name, instance_data, pre_create_data): - import hou # instance_data.pop("active", None) instance_data.update({"node_type": "vray_renderer"}) @@ -98,6 +99,21 @@ class CreateVrayROP(plugin.HoudiniCreator): to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) + def remove_instances(self, instances): + for instance in instances: + node = instance.data.get("instance_node") + # for the extra render node from the plugins + # such as vray and redshift + ipr_node = hou.node("{}{}".format(node, "_IPR")) + if ipr_node: + ipr_node.destroy() + re_node = hou.node("{}{}".format(node, + "_render_element")) + if re_node: + re_node.destroy() + + return super(CreateVrayROP, self).remove_instances(instances) + def get_pre_create_attr_defs(self): attrs = super(CreateVrayROP, self).get_pre_create_attr_defs() image_format_enum = [ From 55ccf73ebb7a82ca9e8d8293225f6357426d9707 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Apr 2023 13:31:16 +0200 Subject: [PATCH 266/918] Remove single assembly validation for animation instances --- openpype/hosts/maya/plugins/publish/validate_single_assembly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_single_assembly.py b/openpype/hosts/maya/plugins/publish/validate_single_assembly.py index 8771ca58d1..b768c9c4e8 100644 --- a/openpype/hosts/maya/plugins/publish/validate_single_assembly.py +++ b/openpype/hosts/maya/plugins/publish/validate_single_assembly.py @@ -19,7 +19,7 @@ class ValidateSingleAssembly(pyblish.api.InstancePlugin): order = ValidateContentsOrder hosts = ['maya'] - families = ['rig', 'animation'] + families = ['rig'] label = 'Single Assembly' def process(self, instance): From 99b8cbd9ed3112be46da2413545b4a506abf371e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Apr 2023 22:52:58 +0800 Subject: [PATCH 267/918] wip vray collector and update vray creator --- .../houdini/plugins/create/create_vray_rop.py | 25 +-- .../plugins/publish/collect_vray_rop.py | 147 ++++++++++++++++++ 2 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_vray_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 74e53eed15..cd80ad2d93 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -47,16 +47,8 @@ class CreateVrayROP(plugin.HoudiniCreator): ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) ipr_rop.parm("rop").set(instance_node.path()) - ext = pre_create_data.get("image_format") - - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) - ) - parms = { "trange": 1, - "SettingsOutput_img_file_path": filepath, "SettingsEXR_bits_per_channel": "16" # half precision } @@ -71,8 +63,16 @@ class CreateVrayROP(plugin.HoudiniCreator): }) # Enable render element + ext = pre_create_data.get("image_format") has_re = pre_create_data.get("render_element_enabled") if has_re: + # Vray has its own tag for AOV file output + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.${}.$F4.{}".format(subset_name, + "AOV", + ext) + ) re_rop = instance_node.parent().createNode( "vray_render_channels", node_name=basename + "_render_element" @@ -84,9 +84,15 @@ class CreateVrayROP(plugin.HoudiniCreator): "use_render_channels": 1, "render_network_render_channels": re_path }) + else: + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) parms.update({ - "use_render_channels": 0 + "use_render_channels": 0, + "SettingsOutput_img_file_path": filepath }) custom_res = pre_create_data.get("override_resolution") @@ -137,4 +143,5 @@ class CreateVrayROP(plugin.HoudiniCreator): "if enabled", default=False) ] + # ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py new file mode 100644 index 0000000000..90a6f10585 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -0,0 +1,147 @@ +import re +import os + +import hou +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Vray Render Products + + Collects the instance.data["files"] for the render products. + + Provides: + instance -> files + + """ + + label = "VRay ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["vray_rop"] + + def process(self, instance): + + rop = hou.node(instance.data.get("instance_node")) + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + beauty_product = evalParmNoFrame(rop, "SettingsOutput_img_file_path") + render_products = [] + # Default beauty AOV + render_products.append(beauty_product) + files_by_aov = { + "RGB Color": self.generate_expected_files(instance, + beauty_product) + } + # TODO: add render elements if render element + + for product in render_products: + self.log.debug("Found render product: %s" % product) + filenames = list(render_products) + instance.data["files"] = filenames + self.log.debug("files:{}".format(render_products)) + + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) + self.log.debug("expectedFiles:{}".format(files_by_aov)) + + def get_render_element_name(self, prefix, suffix="AOV"): + """Return the output filename using the AOV prefix and suffix + """ + # need a rewrite + basename = os.path.basename(prefix) + filename, ext = os.path.splitext(basename) + aov_parm = "${%s}" % suffix + # prefix = ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr + prefix = prefix.replace(filename.split("."), + filename.split(".") + + aov_parm) + # find the render element names + """ + children = hou.node("renderelement node").children() + for c in children: + print c # all the aov names + add into aov_list except "channelsContainer" + if c only has channelsContainer please dont do anything + """ + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files From 9a96a6b2e0c74aadb1561733a1aec857cd63d2cd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 13 Apr 2023 16:47:19 +0100 Subject: [PATCH 268/918] KnownPublishError > PublishValidationError --- openpype/hosts/maya/plugins/publish/validate_review.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_review.py b/openpype/hosts/maya/plugins/publish/validate_review.py index 346fb54ac4..12a2e7f86f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_review.py +++ b/openpype/hosts/maya/plugins/publish/validate_review.py @@ -1,7 +1,7 @@ import pyblish.api from openpype.pipeline.publish import ( - ValidateContentsOrder, KnownPublishError + ValidateContentsOrder, PublishValidationError ) @@ -17,11 +17,11 @@ class ValidateReview(pyblish.api.InstancePlugin): # validate required settings if len(cameras) == 0: - raise KnownPublishError( + raise PublishValidationError( "No camera found in review instance: {}".format(instance) ) elif len(cameras) > 2: - raise KnownPublishError( + raise PublishValidationError( "Only a single camera is allowed for a review instance but " "more than one camera found in review instance: {}. " "Cameras found: {}".format(instance, ", ".join(cameras)) From 9a5f86ea1b065a7cf847836b8148e2bc4cb79c59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Apr 2023 18:42:50 +0200 Subject: [PATCH 269/918] add review tag to output of extract sequence if instance is marked for review (#4843) --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 1a21715aa2..8a610cf388 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -144,7 +144,7 @@ class ExtractSequence(pyblish.api.Extractor): # Fill tags and new families from project settings tags = [] - if family_lowered == "review": + if "review" in instance.data["families"]: tags.append("review") # Sequence of one frame From a061c897794a5617e746d2c3387bfb8f81d55569 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 14 Apr 2023 08:38:18 +0100 Subject: [PATCH 270/918] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2ac9f06fcd..46f423023f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3871,6 +3871,8 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): project_settings (dict): Project settings. log (object): Logging object. """ + capture_preset = {} + filtering_criteria = { "hosts": "maya", "families": "review", From 119df6d24e7b9ba6865cb854a0089c71fe3e58e4 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 14 Apr 2023 08:39:42 +0100 Subject: [PATCH 271/918] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 46f423023f..8ca6ade2ec 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3872,7 +3872,6 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): log (object): Logging object. """ capture_preset = {} - filtering_criteria = { "hosts": "maya", "families": "review", From 2ee9c1727045d0c234f9051b9847cd82f58f7ce3 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 14 Apr 2023 12:28:26 +0100 Subject: [PATCH 272/918] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 8ca6ade2ec..39db06f70f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3871,7 +3871,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): project_settings (dict): Project settings. log (object): Logging object. """ - capture_preset = {} + capture_preset = None filtering_criteria = { "hosts": "maya", "families": "review", @@ -3900,4 +3900,4 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): ) capture_preset = plugin_settings["capture_preset"] - return capture_preset + return capture_preset or {} From 3e4c0cb47ea44dc568019a977417086e72168ce7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Apr 2023 20:49:32 +0800 Subject: [PATCH 273/918] add vray collector --- .../houdini/plugins/create/create_vray_rop.py | 5 +- .../plugins/publish/collect_redshift_rop.py | 2 +- .../plugins/publish/collect_vray_rop.py | 56 +++++++++++-------- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index cd80ad2d93..bb2d025cde 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -64,8 +64,8 @@ class CreateVrayROP(plugin.HoudiniCreator): # Enable render element ext = pre_create_data.get("image_format") - has_re = pre_create_data.get("render_element_enabled") - if has_re: + instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa + if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), @@ -82,6 +82,7 @@ class CreateVrayROP(plugin.HoudiniCreator): re_path = re_rop.path() parms.update({ "use_render_channels": 1, + "SettingsOutput_img_file_path": filepath, "render_network_render_channels": re_path }) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 15df12e075..ac55a01209 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -88,7 +88,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): ) render_products.append(beauty_product) files_by_aov = { - "beauty": self.generate_expected_files(instance, + "_": self.generate_expected_files(instance, beauty_product)} num_aovs = rop.evalParm("RS_aov") diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 90a6f10585..f246de6e53 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -78,15 +78,21 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) - beauty_product = evalParmNoFrame(rop, "SettingsOutput_img_file_path") + default_prefix = evalParmNoFrame(rop, "SettingsOutput_img_file_path") render_products = [] - # Default beauty AOV + # TODO: add render elements if render element + + beauty_product = self.get_beauty_render_product(default_prefix) render_products.append(beauty_product) files_by_aov = { - "RGB Color": self.generate_expected_files(instance, - beauty_product) - } - # TODO: add render elements if render element + "RGB Color": self.generate_expected_files(instance, + beauty_product) + } + render_element, aov = self.get_render_element_name(rop, default_prefix) + if render_element is not None: + render_products.append(render_element) + files_by_aov[aov] = self.generate_expected_files(instance, + render_element) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -103,25 +109,31 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"].append(files_by_aov) self.log.debug("expectedFiles:{}".format(files_by_aov)) - def get_render_element_name(self, prefix, suffix="AOV"): + def get_beauty_render_product(self, prefix, suffix=""): + """Return the beauty output filename if render element enabled + """ + aov_parm = ".{}".format(suffix) + beauty_product = None + if aov_parm in prefix: + beauty_product = prefix.replace(aov_parm, "") + else: + beauty_product = prefix + + return beauty_product + + def get_render_element_name(self, node, prefix, suffix=""): """Return the output filename using the AOV prefix and suffix """ # need a rewrite - basename = os.path.basename(prefix) - filename, ext = os.path.splitext(basename) - aov_parm = "${%s}" % suffix - # prefix = ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr - prefix = prefix.replace(filename.split("."), - filename.split(".") + - aov_parm) - # find the render element names - """ - children = hou.node("renderelement node").children() - for c in children: - print c # all the aov names - add into aov_list except "channelsContainer" - if c only has channelsContainer please dont do anything - """ + re_path = node.evalParm("render_network_render_channels") + node_children = hou.node(re_path).children() + for element in node_children: + if element != "channelsContainer": + render_product = prefix.replace(suffix, str(element)) + else: + self.log.debug("skipping non render element output..") + continue + return render_product, str(element) def generate_expected_files(self, instance, path): """Create expected files in instance data""" From bcdaf5c129c0c7a9dabe2d4a29d1c4768d8daf4c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 14 Apr 2023 17:06:16 +0200 Subject: [PATCH 274/918] fixing passing CU secrets in release workflow --- .github/workflows/miletone_release_trigger.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/miletone_release_trigger.yml b/.github/workflows/miletone_release_trigger.yml index 26a2d5833d..4a031be7f9 100644 --- a/.github/workflows/miletone_release_trigger.yml +++ b/.github/workflows/miletone_release_trigger.yml @@ -45,3 +45,6 @@ jobs: token: ${{ secrets.YNPUT_BOT_TOKEN }} user_email: ${{ secrets.CI_EMAIL }} user_name: ${{ secrets.CI_USER }} + cu_api_key: ${{ secrets.CLICKUP_API_KEY }} + cu_team_id: ${{ secrets.CLICKUP_TEAM_ID }} + cu_field_id: ${{ secrets.CLICKUP_RELEASE_FIELD_ID }} From a14f9196bb74ad39d40a07c3db38c666a38a830a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 14 Apr 2023 15:15:13 +0000 Subject: [PATCH 275/918] [Automated] Release --- CHANGELOG.md | 943 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 945 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e22b783c4..5aeb546c14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,948 @@ # Changelog + +## [3.15.4](https://github.com/ynput/OpenPype/tree/3.15.4) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.3...3.15.4) + +### **🆕 New features** + + +
+Maya: Cant assign shaders to the ass file - OP-4859 #4460 + +Support AiStandIn nodes for look assignment. + +Using operators we assign shaders and attribute/parameters to nodes within standins. Initially there is only support for a limited mount of attributes but we can add support as needed; +``` +primaryVisibility +castsShadows +receiveShadows +aiSelfShadows +aiOpaque +aiMatte +aiVisibleInDiffuseTransmission +aiVisibleInSpecularTransmission +aiVisibleInVolume +aiVisibleInDiffuseReflection +aiVisibleInSpecularReflection +aiSubdivUvSmoothing +aiDispHeight +aiDispPadding +aiDispZeroValue +aiStepSize +aiVolumePadding +aiSubdivType +aiSubdivIterations +``` + + +___ + +
+ + +
+Maya: GPU cache representation #4649 + +Implement GPU cache for model, animation and pointcache. + + +___ + +
+ + +
+Houdini: Implement review family with opengl node #3839 + +Implements a first pass for Reviews publishing in Houdini. Resolves #2720 + +Uses the `opengl` ROP node to produce PNG images. + + +___ + +
+ + +
+Maya: Camera focal length visible in review - OP-3278 #4531 + +Camera focal length visible in review. + +Support camera focal length in review; static and dynamic.Resolves #3220 + + +___ + +
+ + +
+Maya: Defining plugins to load on Maya start - OP-4994 #4714 + +Feature to define plugins to load on Maya launch. + + +___ + +
+ + +
+Nuke, DL: Returning Suspended Publishing attribute #4715 + +Old Nuke Publisher's feature for suspended publishing job on render farm was added back to the current Publisher. + + +___ + +
+ + +
+Settings UI: Allow setting a size hint for text fields #4821 + +Text entity have `minimum_lines_count` which allows to change minimum size hint of UI input. + + +___ + +
+ + +
+TrayPublisher: Move 'BatchMovieCreator' settings to 'create' subcategory #4827 + +Moved settings for `BatchMoviewCreator` into subcategory `create` in settings. Changes are made to match other hosts settings chema and structure. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya looks: support for native Redshift texture format #2971 + +Add support for native Redshift textures handling. Closes #2599 + +Uses Redshift's Texture Processor executable to convert textures being used in renders to the Redshift ".rstexbin" format. + + +___ + +
+ + +
+Maya: custom namespace for references #4511 + +Adding an option in Project Settings > Maya > Loader plugins to set custom namespace. If no namespace is set, the default one is used. + + +___ + +
+ + +
+Maya: Set correct framerange with handles on file opening #4664 + +Set the range of playback from the asset data, counting handles, to get the correct data when calling the "collect_animation_data" function. + + +___ + +
+ + +
+Maya: Fix camera update #4751 + +Fix resetting any modelPanel to a different camera when loading a camera and updating. + + +___ + +
+ + +
+Maya: Remove single assembly validation for animation instances #4840 + +Rig groups may now be parented to others groups when `includeParentHierarchy` attribute on the instance is "off". + + +___ + +
+ + +
+Maya: Optional control of display lights on playblast. #4145 + +Optional control of display lights on playblast. + +Giving control to what display lights are on the playblasts. + + +___ + +
+ + +
+Kitsu: note family requirements #4551 + +Allowing to add family requirements to `IntegrateKitsuNote` task status change. + +Adds a `Family requirements` setting to `Integrate Kitsu Note`, so you can add requirements to determine if kitsu task status should be changed based on which families are published or not. For instance you could have the status change only if another subset than workfile is published (but workfile can still be included) by adding an item set to `Not equal` and `workfile`. + + +___ + +
+ + +
+Deactivate closed Kitsu projects on OP #4619 + +Deactivate project on OP when the project is closed on Kitsu. + + +___ + +
+ + +
+Maya: Suggestion to change capture labels. #4691 + +Change capture labels. + + +___ + +
+ + +
+Houdini: Change node type for OpenPypeContext `null` -> `subnet` #4745 + +Change the node type for OpenPype's hidden context node in Houdini from `null` to `subnet`. This fixes #4734 + + +___ + +
+ + +
+General: Extract burnin hosts filters #4749 + +Removed hosts filter from ExtractBurnin plugin. Instance without representations won't cause crash but just skip the instance. We've discovered because Blender already has review but did not create burnins. + + +___ + +
+ + +
+Global: Improve speed of Collect Custom Staging Directory #4768 + +Improve speed of Collect Custom Staging Directory. + + +___ + +
+ + +
+General: Anatomy templates formatting #4773 + +Added option to format only single template from anatomy instead of formatting all of them all the time. Formatting of all templates is causing slowdowns e.g. during publishing of hundreds of instances. + + +___ + +
+ + +
+Harmony: Handle zip files with deeper structure #4782 + +External Harmony zip files might contain one additional level with scene name. + + +___ + +
+ + +
+Unreal: Use common logic to configure executable #4788 + +Unreal Editor location and version was autodetected. This easied configuration in some cases but was not flexible enought. This PR is changing the way Unreal Editor location is set, unifying it with the logic other hosts are using. + + +___ + +
+ + +
+Github: Grammar tweaks + uppercase issue title #4813 + +Tweak some of the grammar in the issue form templates. + + +___ + +
+ + +
+Houdini: Allow creation of publish instances via Houdini TAB menu #4831 + +Register the available Creator's as houdini tools so an artist can add publish instances via the Houdini TAB node search menu from within the network editor. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Fix Collect Render for V-Ray, Redshift and Renderman for missing colorspace #4650 + +Fix Collect Render not working for Redshift, V-Ray and Renderman due to missing `colorspace` argument to `RenderProduct` dataclass. + + +___ + +
+ + +
+Maya: Xgen fixes #4707 + +Fix for Xgen extraction of world parented nodes and validation for required namespace. + + +___ + +
+ + +
+Maya: Fix extract review and thumbnail for Maya 2020 #4744 + +Fix playblasting in Maya 2020 with override viewport options enabled. Fixes #4730. + + +___ + +
+ + +
+Maya: local variable 'arnold_standins' referenced before assignment - OP-5542 #4778 + +MayaLookAssigner erroring when MTOA is not loaded: +``` +# Traceback (most recent call last): +# File "\openpype\hosts\maya\tools\mayalookassigner\app.py", line 272, in on_process_selected +# nodes = list(set(item["nodes"]).difference(arnold_standins)) +# UnboundLocalError: local variable 'arnold_standins' referenced before assignment +``` + + +___ + +
+ + +
+Maya: Fix getting view and display in Maya 2020 - OP-5035 #4795 + +The `view_transform` returns a different format in Maya 2020. Fixes #4540 (hopefully). + + +___ + +
+ + +
+Maya: Fix Look Maya 2020 Py2 support for Extract Look #4808 + +Fix Extract Look supporting python 2.7 for Maya 2020. + + +___ + +
+ + +
+Maya: Fix Validate Mesh Overlapping UVs plugin #4816 + +Fix typo in the code where a maya command returns a `list` instead of `str`. + + +___ + +
+ + +
+Maya: Fix tile rendering with Vray - OP-5566 #4832 + +Fixes tile rendering with Vray. + + +___ + +
+ + +
+Deadline: checking existing frames fails when there is number in file name #4698 + +Previous implementation of validator failed on files with any other number in rendered file names.Used regular expression pattern now handles numbers in the file names (eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr", "Main_beauty.1001.1001.exr") but not numbers behind frames (eg. "Main_beauty.1001.v001.exr") + + +___ + +
+ + +
+Maya: Validate Render Settings. #4735 + +Fixes error message when using attribute validation. + + +___ + +
+ + +
+General: Hero version sites recalculation #4737 + +Sites recalculation in integrate hero version did expect that it is integrated exactly same amount of files as in previous integration. This is not the case in many cases, so the sites recalculation happens in a different way, first are prepared all sites from previous representation files, and all of them are added to each file in new representation. + + +___ + +
+ + +
+Houdini: Fix collect current file #4739 + +Fixes the Workfile publishing getting added into every instance being published from Houdini + + +___ + +
+ + +
+Global: Fix Extract Burnin + Colorspace functions for conflicting python environments with PYTHONHOME #4740 + +This fixes the running of openpype processes from e.g. a host with conflicting python versions that had `PYTHONHOME` said additionally to `PYTHONPATH`, like e.g. Houdini Py3.7 together with OpenPype Py3.9 when using Extract Burnin for a review in #3839This fix applies to Extract Burnin and some of the colorspace functions that use `run_openpype_process` + + +___ + +
+ + +
+Harmony: render what is in timeline in Harmony locally #4741 + +Previously it wasn't possible to render according to what was set in Timeline in scene start/end, just by what it was set in whole timeline.This allows artist to override what is in DB with what they require (with disabled `Validate Scene Settings`). Now artist can extend scene by additional frames, that shouldn't be rendered, but which might be desired.Removed explicit set scene settings (eg. applying frames and resolution directly to the scene after launch), added separate menu item to allow artist to do it themselves. + + +___ + +
+ + +
+Maya: Extract Review settings add Use Background Gradient #4747 + +Add Display Gradient Background toggle in settings to fix support for setting flat background color for reviews. + + +___ + +
+ + +
+Nuke: publisher is offering review on write families on demand #4755 + +Original idea where reviewable toggle will be offered in publisher on demand is fixed and now `review` attribute can be disabled in settings. + + +___ + +
+ + +
+Workfiles: keep Browse always enabled #4766 + +Browse might make sense even if there are no workfiles present, actually in that case it makes the most sense (eg. I want to locate workfile from outside - from Desktop for example). + + +___ + +
+ + +
+Global: label key in instance data is optional #4779 + +Collect OTIO review plugin is not crashing if `label` key is missing in instance data. + + +___ + +
+ + +
+Loader: Fix missing variable #4781 + +There is missing variable `handles` in loader tool after https://github.com/ynput/OpenPype/pull/4746. The variable was renamed to `handles_label` and is initialized to `None` if handles are not available. + + +___ + +
+ + +
+Nuke: Workfile Template builder fixes #4783 + +Popup window after Nuke start is not showing. Knobs with X/Y coordination on nodes where were converted from placeholders are not added if `keepPlaceholders` is witched off. + + +___ + +
+ + +
+Maya: Add family filter 'review' to burnin profile with focal length #4791 + +Avoid profile burnin with `focalLength` key for renders, but use only for playblast reviews. + + +___ + +
+ + +
+add farm instance to the render collector in 3dsMax #4794 + +bug fix for the failure of submitting publish job in 3dsmax + + +___ + +
+ + +
+Publisher: Plugin active attribute is respected #4798 + +Publisher consider plugin's `active` attribute, so the plugin is not processed when `active` is set to `False`. But we use the attribute in `OptionalPyblishPluginMixin` for different purposes, so I've added hack bypass of the active state validation when plugin inherit from the mixin. This is temporary solution which cannot be changed until all hosts use Publisher otherwise global plugins would be broken. Also plugins which have `enabled` set to `False` are filtered out -> this happened only when automated settings were applied and the settings contained `"enabled"` key se to `False`. + + +___ + +
+ + +
+Nuke: settings and optional attribute in publisher for some validators #4811 + +New publisher is supporting optional switch for plugins which is offered in Publisher in Right panel. Some plugins were missing this switch and also settings which would offer the optionality. + + +___ + +
+ + +
+Settings: Version settings popup fix #4822 + +Version completer popup have issues on some platforms, this should fix those edge cases. Also fixed issue when completer stayed shown fater reset (save). + + +___ + +
+ + +
+Hiero/Nuke: adding monitorOut key to settings #4826 + +New versions of Hiero were introduced with new colorspace property for Monitor Out. It have been added into project settings. Also added new config names into settings enumerator option. + + +___ + +
+ + +
+Nuke: removed default workfile template builder preset #4835 + +Default for workfile template builder should have been empty. + + +___ + +
+ + +
+TVPaint: Review can be made from any instance #4843 + +Add `"review"` tag to output of extract sequence if instance is marked for review. At this moment only instances with family `"review"` were able to define input for `ExtractReview` plugin which is not right. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Deadline: Remove unused FramesPerTask job info submission #4657 + +Remove unused `FramesPerTask` job info submission to Deadline. + + +___ + +
+ + +
+Maya: Remove pymel dependency #4724 + +Refactors code written using `pymel` to use standard maya python libraries instead like `maya.cmds` or `maya.api.OpenMaya` + + +___ + +
+ + +
+Remove "preview" data from representation #4759 + +Remove "preview" data from representation + + +___ + +
+ + +
+Maya: Collect Review cleanup code for attached subsets #4720 + +Refactor some code for Maya: Collect Review for attached subsets. + + +___ + +
+ + +
+Refactor: Remove `handles`, `edit_in` and `edit_out` backwards compatibility #4746 + +Removes backward compatibiliy fallback for data called `handles`, `edit_in` and `edit_out`. + + +___ + +
+ +### **📃 Documentation** + + +
+Bump webpack from 5.69.1 to 5.76.1 in /website #4624 + +Bumps [webpack](https://github.com/webpack/webpack) from 5.69.1 to 5.76.1. +
+Release notes +

Sourced from webpack's releases.

+
+

v5.76.1

+

Fixed

+
    +
  • Added assert/strict built-in to NodeTargetPlugin
  • +
+

Revert

+ +

v5.76.0

+

Bugfixes

+ +

Features

+ +

Security

+ +

Repo Changes

+ +

New Contributors

+ +

Full Changelog: https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0

+

v5.75.0

+

Bugfixes

+
    +
  • experiments.* normalize to false when opt-out
  • +
  • avoid NaN%
  • +
  • show the correct error when using a conflicting chunk name in code
  • +
  • HMR code tests existance of window before trying to access it
  • +
  • fix eval-nosources-* actually exclude sources
  • +
  • fix race condition where no module is returned from processing module
  • +
  • fix position of standalong semicolon in runtime code
  • +
+

Features

+
    +
  • add support for @import to extenal CSS when using experimental CSS in node
  • +
+ +
+

... (truncated)

+
+
+Commits +
    +
  • 21be52b Merge pull request #16804 from webpack/chore-patch-release
  • +
  • 1cce945 chore(release): 5.76.1
  • +
  • e76ad9e Merge pull request #16803 from ryanwilsonperkin/revert-16759-real-content-has...
  • +
  • 52b1b0e Revert "Improve performance of hashRegExp lookup"
  • +
  • c989143 Merge pull request #16766 from piranna/patch-1
  • +
  • 710eaf4 Merge pull request #16789 from dmichon-msft/contenthash-hashsalt
  • +
  • 5d64468 Merge pull request #16792 from webpack/update-version
  • +
  • 67af5ec chore(release): 5.76.0
  • +
  • 97b1718 Merge pull request #16781 from askoufis/loader-context-target-type
  • +
  • b84efe6 Merge pull request #16759 from ryanwilsonperkin/real-content-hash-regex-perf
  • +
  • Additional commits viewable in compare view
  • +
+
+
+Maintainer changes +

This version was pushed to npm by evilebottnawi, a new releaser for webpack since your current version.

+
+
+ + +[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.69.1&new-version=5.76.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) + +Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. + +[//]: # (dependabot-automerge-start) +[//]: # (dependabot-automerge-end) + +--- + +
+Dependabot commands and options +
+ +You can trigger Dependabot actions by commenting on this PR: +- `@dependabot rebase` will rebase this PR +- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it +- `@dependabot merge` will merge this PR after your CI passes on it +- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it +- `@dependabot cancel merge` will cancel a previously requested merge and block automerging +- `@dependabot reopen` will reopen this PR if it is closed +- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually +- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) +- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) +- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language +- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language +- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language +- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language + +You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/ynput/OpenPype/network/alerts). + +
+___ + +
+ + +
+Documentation: Add Extract Burnin documentation #4765 + +Add documentation for Extract Burnin global plugin settings. + + +___ + +
+ + +
+Documentation: Move publisher related tips to publisher area #4772 + +Move publisher related tips for After Effects artist documentation to the correct position. + + +___ + +
+ + +
+Documentation: Add extra terminology to the key concepts glossary #4838 + +Tweak some of the key concepts in the documentation. + + +___ + +
+ +### **Merged pull requests** + + +
+Maya: Refactor Extract Look with dedicated processors for maketx #4711 + +Refactor Maya extract look to fix some issues: +- [x] Allow Extraction with maketx with OCIO Color Management enabled in Maya. +- [x] Fix file hashing so it includes arguments to maketx, so that when arguments change it correctly generates a new hash +- [x] Fix maketx destination colorspace when OCIO is enabled +- [x] Use pre-collected colorspaces of the resources instead of trying to retrieve again in Extract Look +- [x] Fix colorspace attributes being reinterpreted by maya on export (fix remapping) - goal is to resolve #2337 +- [x] Fix support for checking config path of maya default OCIO config (due to using `lib.get_color_management_preferences` which remaps that path) +- [x] Merged in #2971 to refactor MakeTX into TextureProcessor and also support generating Redshift `.rstexbin` files. - goal is to resolve #2599 +- [x] Allow custom arguments to `maketx` from OpenPype Settings like mentioned here by @fabiaserra for arguments like: `--monochrome-detect`, `--opaque-detect`, `--checknan`. +- [x] Actually fix the code and make it work. :) (I'll try to keep below checkboxes in sync with my code changes) +- [x] Publishing without texture processor should work (no maketx + no rstexbin) +- [x] Publishing with maketx should work +- [x] Publishing with rstexbin should work +- [x] Test it. (This is just me doing some test-runs, please still test the PR!) + + +___ + +
+ + +
+Maya template builder load all assets linked to the shot #4761 + +Problem +All the assets of the ftrack project are loaded and not those linked to the shot + +How get error +Open maya in the context of shot, then build a new scene with the "Build Workfile from template" button in "OpenPype" menu. +![image](https://user-images.githubusercontent.com/7068597/229124652-573a23d7-a2b2-4d50-81bf-7592c00d24dc.png) + + +___ + +
+ + +
+Global: Do not force instance data with frame ranges of the asset #4383 + +This aims to resolve #4317 + + +___ + +
+ + +
+Cosmetics: Fix some grammar in docstrings and messages (and some code) #4752 + +Tweak some grammar in codebase + + +___ + +
+ + +
+Deadline: Submit publish job fails due root work hardcode - OP-5528 #4775 + +Generating config templates was hardcoded to `root[work]`. This PR fixes that. + + +___ + +
+ + +
+CreateContext: Added option to remove Unknown attributes #4776 + +Added option to remove attributes with UnkownAttrDef on instances. Pop of key will also remove the attribute definition from attribute values, so they're not recreated again. + + +___ + +
+ + + ## [3.15.3](https://github.com/ynput/OpenPype/tree/3.15.3) diff --git a/openpype/version.py b/openpype/version.py index d9e29d691e..1d41f1aa5d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.4-nightly.3" +__version__ = "3.15.4" diff --git a/pyproject.toml b/pyproject.toml index 42ce5aa32c..b97ad8923c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.3" # OpenPype +version = "3.15.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From fcffb919486dfdef7474ccf5aefd264f34c7a8f2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Fri, 14 Apr 2023 17:39:53 +0200 Subject: [PATCH 276/918] After Effects: fix handles KeyError (#4727) * get handles from context if not in asset * fix linting errors * get frameStart, frameEnd, handleStart and handleEnd from context --------- Co-authored-by: clement hector Co-authored-by: Thomas Fricard --- .../aftereffects/plugins/publish/collect_workfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 3c5013b3bd..c21c3623c3 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -53,10 +53,10 @@ class CollectWorkfile(pyblish.api.ContextPlugin): "active": True, "asset": asset_entity["name"], "task": task, - "frameStart": asset_entity["data"]["frameStart"], - "frameEnd": asset_entity["data"]["frameEnd"], - "handleStart": asset_entity["data"]["handleStart"], - "handleEnd": asset_entity["data"]["handleEnd"], + "frameStart": context.data['frameStart'], + "frameEnd": context.data['frameEnd'], + "handleStart": context.data['handleStart'], + "handleEnd": context.data['handleEnd'], "fps": asset_entity["data"]["fps"], "resolutionWidth": asset_entity["data"].get( "resolutionWidth", From 43b86f47c304a7dac4325a6b59d0247b53f619a0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 14 Apr 2023 17:42:42 +0200 Subject: [PATCH 277/918] :recycle: replace OpenPype/Avalon for Ayon --- openpype/hosts/unreal/README.md | 4 +- openpype/hosts/unreal/addon.py | 8 +- openpype/hosts/unreal/api/__init__.py | 2 +- openpype/hosts/unreal/api/helpers.py | 12 +- openpype/hosts/unreal/api/pipeline.py | 138 +++++++++--------- openpype/hosts/unreal/api/plugin.py | 4 +- openpype/hosts/unreal/api/rendering.py | 14 +- openpype/hosts/unreal/api/tools_ui.py | 2 +- .../unreal/hooks/pre_workfile_preparation.py | 10 +- .../integration/UE_4.27/Ayon/Ayon.uplugin | 14 +- .../Ayon/Content/Python/init_unreal.py | 26 ++-- .../unreal/integration/UE_4.27/Ayon/README.md | 12 +- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 6 +- .../Ayon/Private/OpenPypePublishInstance.cpp | 4 +- .../OpenPypePublishInstanceFactory.cpp | 2 + .../Ayon/Source/Ayon/Public/AyonSettings.h | 8 +- .../Ayon/Public/OpenPypePublishInstance.h | 6 +- .../Public/OpenPypePublishInstanceFactory.h | 2 + .../integration/UE_5.0/Ayon/Ayon.uplugin | 14 +- .../UE_5.0/Ayon/Content/Python/init_unreal.py | 26 ++-- .../unreal/integration/UE_5.0/Ayon/README.md | 12 +- .../Ayon/Private/OpenPypePublishInstance.cpp | 2 + .../OpenPypePublishInstanceFactory.cpp | 2 + .../Ayon/Public/OpenPypePublishInstance.h | 4 +- .../Public/OpenPypePublishInstanceFactory.h | 2 + .../integration/UE_5.1/Ayon/Ayon.uplugin | 14 +- .../UE_5.1/Ayon/Content/Python/init_unreal.py | 26 ++-- .../unreal/integration/UE_5.1/Ayon/README.md | 12 +- .../Ayon/Private/OpenPypePublishInstance.cpp | 2 + .../OpenPypePublishInstanceFactory.cpp | 2 + .../Ayon/Public/OpenPypePublishInstance.h | 4 +- .../Public/OpenPypePublishInstanceFactory.h | 2 + openpype/hosts/unreal/lib.py | 17 ++- .../unreal/plugins/create/create_camera.py | 2 +- .../unreal/plugins/create/create_layout.py | 2 +- .../unreal/plugins/create/create_look.py | 4 +- .../unreal/plugins/create/create_render.py | 10 +- .../plugins/create/create_staticmeshfbx.py | 2 +- .../unreal/plugins/create/create_uasset.py | 2 +- .../plugins/load/load_alembic_animation.py | 12 +- .../unreal/plugins/load/load_animation.py | 10 +- .../hosts/unreal/plugins/load/load_camera.py | 16 +- .../plugins/load/load_geometrycache_abc.py | 10 +- .../hosts/unreal/plugins/load/load_layout.py | 14 +- .../plugins/load/load_layout_existing.py | 12 +- .../plugins/load/load_skeletalmesh_abc.py | 12 +- .../plugins/load/load_skeletalmesh_fbx.py | 12 +- .../plugins/load/load_staticmesh_abc.py | 13 +- .../plugins/load/load_staticmesh_fbx.py | 16 +- .../hosts/unreal/plugins/load/load_uasset.py | 16 +- .../publish/collect_render_instances.py | 10 +- openpype/hosts/unreal/ue_workers.py | 4 +- openpype/pipeline/__init__.py | 2 + openpype/pipeline/constants.py | 2 +- 54 files changed, 306 insertions(+), 292 deletions(-) diff --git a/openpype/hosts/unreal/README.md b/openpype/hosts/unreal/README.md index 0a69b9e0cf..d131105659 100644 --- a/openpype/hosts/unreal/README.md +++ b/openpype/hosts/unreal/README.md @@ -4,6 +4,6 @@ Supported Unreal Engine version is 4.26+ (mainly because of major Python changes ### Project naming Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are -invalid. If OpenPype detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` +invalid. If Ayon detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` will become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters. -Longer names will issue warning in Unreal Editor that there might be possible side effects. \ No newline at end of file +Longer names will issue warning in Unreal Editor that there might be possible side effects. diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 6a7c6ba941..0c42755d37 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -13,16 +13,16 @@ class UnrealAddon(OpenPypeModule, IHostAddon): def add_implementation_envs(self, env, app): """Modify environments to contain all required for implementation.""" - # Set OPENPYPE_UNREAL_PLUGIN required for Unreal implementation + # Set AYON_UNREAL_PLUGIN required for Unreal implementation ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) - if not env.get("OPENPYPE_UNREAL_PLUGIN") or \ - env.get("OPENPYPE_UNREAL_PLUGIN") != unreal_plugin_path: - env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path + if not env.get("AYON_UNREAL_PLUGIN") or \ + env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: + env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings defaults = { diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index 2618a7677c..de0fce13d5 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Unreal Editor OpenPype host API.""" +"""Unreal Editor Ayon host API.""" from .plugin import ( UnrealActorCreator, diff --git a/openpype/hosts/unreal/api/helpers.py b/openpype/hosts/unreal/api/helpers.py index 0b6f07f52f..e9ab3fb4c5 100644 --- a/openpype/hosts/unreal/api/helpers.py +++ b/openpype/hosts/unreal/api/helpers.py @@ -2,15 +2,15 @@ import unreal # noqa -class OpenPypeUnrealException(Exception): +class AyonUnrealException(Exception): pass @unreal.uclass() -class OpenPypeHelpers(unreal.OpenPypeLib): - """Class wrapping some useful functions for OpenPype. +class AyonHelpers(unreal.AyonLib): + """Class wrapping some useful functions for Ayon. - This class is extending native BP class in OpenPype Integration Plugin. + This class is extending native BP class in Ayon Integration Plugin. """ @@ -29,13 +29,13 @@ class OpenPypeHelpers(unreal.OpenPypeLib): Example: - OpenPypeHelpers().set_folder_color( + AyonHelpers().set_folder_color( "/Game/Path", unreal.LinearColor(a=1.0, r=1.0, g=0.5, b=0) ) Note: This will take effect only after Editor is restarted. I couldn't - find a way to refresh it. Also this saves the color definition + find a way to refresh it. Also, this saves the color definition into the project config, binding this path with color. So if you delete this path and later re-create, it will set this color again. diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 1a7c626984..0d8922d2e6 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -14,7 +14,7 @@ from openpype.pipeline import ( register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, ) from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -22,12 +22,13 @@ from openpype.host import HostBase, ILoadHost, IPublishHost import unreal # noqa +# Rename to Ayon once parent module renames logger = logging.getLogger("openpype.hosts.unreal") -OPENPYPE_CONTAINERS = "OpenPypeContainers" -CONTEXT_CONTAINER = "OpenPype/context.json" +AYON_CONTAINERS = "AyonContainers" +CONTEXT_CONTAINER = "Ayon/context.json" UNREAL_VERSION = semver.VersionInfo( - *os.getenv("OPENPYPE_UNREAL_VERSION").split(".") + *os.getenv("AYON_UNREAL_VERSION").split(".") ) HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.unreal.__file__)) @@ -53,14 +54,14 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): def get_containers(self): return ls() - def show_tools_popup(self): + @staticmethod + def show_tools_popup(): """Show tools popup with actions leading to show other tools.""" - show_tools_popup() - def show_tools_dialog(self): + @staticmethod + def show_tools_dialog(): """Show tools dialog with actions leading to show other tools.""" - show_tools_dialog() def update_context_data(self, data, changes): @@ -72,9 +73,10 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): with open(op_ctx, "w+") as f: json.dump(data, f) break - except IOError: + except IOError as e: if i == attempts - 1: - raise Exception("Failed to write context data. Aborting.") + raise Exception( + "Failed to write context data. Aborting.") from e unreal.log_warning("Failed to write context data. Retrying...") i += 1 time.sleep(3) @@ -95,19 +97,30 @@ def install(): print("-=" * 40) logo = '''. . - ____________ - / \\ __ \\ - \\ \\ \\/_\\ \\ - \\ \\ _____/ ______ - \\ \\ \\___// \\ \\ - \\ \\____\\ \\ \\_____\\ - \\/_____/ \\/______/ PYPE Club . + · + │ + ·∙/ + ·-∙•∙-· + / \\ /∙· / \\ + ∙ \\ │ / ∙ + \\ \\ · / / + \\\\ ∙ ∙ // + \\\\/ \\// + ___ + │ │ + │ │ + │ │ + │___│ + -· + + ·-─═─-∙ A Y O N ∙-─═─-· + by YNPUT . ''' print(logo) - print("installing OpenPype for Unreal ...") + print("installing Ayon for Unreal ...") print("-=" * 40) - logger.info("installing OpenPype for Unreal") + logger.info("installing Ayon for Unreal") pyblish.api.register_host("unreal") pyblish.api.register_plugin_path(str(PUBLISH_PATH)) register_loader_plugin_path(str(LOAD_PATH)) @@ -117,7 +130,7 @@ def install(): def uninstall(): - """Uninstall Unreal configuration for Avalon.""" + """Uninstall Unreal configuration for Ayon.""" pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) deregister_loader_plugin_path(str(LOAD_PATH)) deregister_creator_plugin_path(str(CREATE_PATH)) @@ -125,14 +138,14 @@ def uninstall(): def _register_callbacks(): """ - TODO: Implement callbacks if supported by UE4 + TODO: Implement callbacks if supported by UE """ pass def _register_events(): """ - TODO: Implement callbacks if supported by UE4 + TODO: Implement callbacks if supported by UE """ pass @@ -146,32 +159,30 @@ def ls(): """ ar = unreal.AssetRegistryHelpers.get_asset_registry() # UE 5.1 changed how class name is specified - class_name = ["/Script/OpenPype", "AssetContainer"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else "AssetContainer" # noqa - openpype_containers = ar.get_assets_by_class(class_name, True) + class_name = ["/Script/Ayon", "AyonAssetContainer"] if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 else "AyonAssetContainer" # noqa + ayon_containers = ar.get_assets_by_class(class_name, True) # get_asset_by_class returns AssetData. To get all metadata we need to # load asset. get_tag_values() work only on metadata registered in # Asset Registry Project settings (and there is no way to set it with # python short of editing ini configuration file). - for asset_data in openpype_containers: + for asset_data in ayon_containers: asset = asset_data.get_asset() data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) data["objectName"] = asset_data.asset_name - data = cast_map_to_str_dict(data) - - yield data + yield cast_map_to_str_dict(data) def ls_inst(): ar = unreal.AssetRegistryHelpers.get_asset_registry() # UE 5.1 changed how class name is specified class_name = [ - "/Script/OpenPype", - "OpenPypePublishInstance" + "/Script/Ayon", + "AyonPublishInstance" ] if ( UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor > 0 - ) else "OpenPypePublishInstance" # noqa + ) else "AyonPublishInstance" # noqa instances = ar.get_assets_by_class(class_name, True) # get_asset_by_class returns AssetData. To get all metadata we need to @@ -182,13 +193,11 @@ def ls_inst(): asset = asset_data.get_asset() data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) data["objectName"] = asset_data.asset_name - data = cast_map_to_str_dict(data) - - yield data + yield cast_map_to_str_dict(data) def parse_container(container): - """To get data from container, AssetContainer must be loaded. + """To get data from container, AyonAssetContainer must be loaded. Args: container(str): path to container @@ -217,7 +226,7 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): Unreal doesn't support *groups* of assets that you can add metadata to. But it does support folders that helps to organize asset. Unfortunately those folders are just that - you cannot add any additional information - to them. OpenPype Integration Plugin is providing way out - Implementing + to them. Ayon Integration Plugin is providing way out - Implementing `AssetContainer` Blueprint class. This class when added to folder can handle metadata on it using standard :func:`unreal.EditorAssetLibrary.set_metadata_tag()` and @@ -226,30 +235,30 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): those assets is available as `assets` property. This is list of strings starting with asset type and ending with its path: - `Material /Game/OpenPype/Test/TestMaterial.TestMaterial` + `Material /Game/Ayon/Test/TestMaterial.TestMaterial` """ # 1 - create directory for container root = "/Game" - container_name = "{}{}".format(name, suffix) + container_name = f"{name}{suffix}" new_name = move_assets_to_path(root, container_name, nodes) # 2 - create Asset Container there - path = "{}/{}".format(root, new_name) + path = f"{root}/{new_name}" create_container(container=container_name, path=path) namespace = path data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "name": new_name, "namespace": namespace, "loader": str(loader), "representation": context["representation"]["_id"], } # 3 - imprint data - imprint("{}/{}".format(path, container_name), data) + imprint(f"{path}/{container_name}", data) return path @@ -257,7 +266,7 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): """Bundles *nodes* into *container*. Marking it with metadata as publishable instance. If assets are provided, - they are moved to new path where `OpenPypePublishInstance` class asset is + they are moved to new path where `AyonPublishInstance` class asset is created and imprinted with metadata. This can then be collected for publishing by Pyblish for example. @@ -271,7 +280,7 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): suffix (str): suffix string to append to instance name """ - container_name = "{}{}".format(name, suffix) + container_name = f"{name}{suffix}" # if we specify assets, create new folder and move them there. If not, # just create empty folder @@ -280,10 +289,10 @@ def instantiate(root, name, data, assets=None, suffix="_INS"): else: new_name = create_folder(root, name) - path = "{}/{}".format(root, new_name) + path = f"{root}/{new_name}" create_publish_instance(instance=container_name, path=path) - imprint("{}/{}".format(path, container_name), data) + imprint(f"{path}/{container_name}", data) def imprint(node, data): @@ -299,7 +308,7 @@ def imprint(node, data): loaded_asset, key, str(value) ) - with unreal.ScopedEditorTransaction("OpenPype containerising"): + with unreal.ScopedEditorTransaction("Ayon containerising"): unreal.EditorAssetLibrary.save_asset(node) @@ -366,11 +375,11 @@ def create_folder(root: str, name: str) -> str: eal = unreal.EditorAssetLibrary index = 1 while True: - if eal.does_directory_exist("{}/{}".format(root, name)): - name = "{}{}".format(name, index) + if eal.does_directory_exist(f"{root}/{name}"): + name = f"{name}{index}" index += 1 else: - eal.make_directory("{}/{}".format(root, name)) + eal.make_directory(f"{root}/{name}") break return name @@ -403,9 +412,7 @@ def move_assets_to_path(root: str, name: str, assets: List[str]) -> str: unreal.log(assets) for asset in assets: loaded = eal.load_asset(asset) - eal.rename_asset( - asset, "{}/{}/{}".format(root, name, loaded.get_name()) - ) + eal.rename_asset(asset, f"{root}/{name}/{loaded.get_name()}") return name @@ -435,14 +442,13 @@ def create_container(container: str, path: str) -> unreal.Object: factory = unreal.AssetContainerFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(container, path, None, factory) - return asset + return tools.create_asset(container, path, None, factory) def create_publish_instance(instance: str, path: str) -> unreal.Object: - """Helper function to create OpenPype Publish Instance on given path. + """Helper function to create Ayon Publish Instance on given path. - This behaves similarly as :func:`create_openpype_container`. + This behaves similarly as :func:`create_ayon_container`. Args: path (str): Path where to create Publish Instance. @@ -460,10 +466,9 @@ def create_publish_instance(instance: str, path: str) -> unreal.Object: ) """ - factory = unreal.OpenPypePublishInstanceFactory() + factory = unreal.AyonPublishInstanceFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() - asset = tools.create_asset(instance, path, None, factory) - return asset + return tools.create_asset(instance, path, None, factory) def cast_map_to_str_dict(umap) -> dict: @@ -494,11 +499,14 @@ def get_subsequences(sequence: unreal.LevelSequence): """ tracks = sequence.get_master_tracks() - subscene_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - break + subscene_track = next( + ( + t + for t in tracks + if t.get_class() == unreal.MovieSceneSubTrack.static_class() + ), + None, + ) if subscene_track is not None and subscene_track.get_sections(): return subscene_track.get_sections() return [] diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index d60050a696..26ef69af86 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -31,7 +31,7 @@ from openpype.pipeline import ( @six.add_metaclass(ABCMeta) class UnrealBaseCreator(Creator): """Base class for Unreal creator plugins.""" - root = "/Game/OpenPype/PublishInstances" + root = "/Game/Ayon/AyonPublishInstances" suffix = "_INS" @staticmethod @@ -243,5 +243,5 @@ class UnrealActorCreator(UnrealBaseCreator): class Loader(LoaderPlugin, ABC): - """This serves as skeleton for future OpenPype specific functionality""" + """This serves as skeleton for future Ayon specific functionality""" pass diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index 29e4747f6e..fac1459416 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -2,6 +2,7 @@ import os import unreal +import openpype.pipeline from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline @@ -40,7 +41,7 @@ def start_rendering(): # instances = pipeline.ls_inst() instances = [ a for a in assets - if a.get_class().get_name() == "OpenPypePublishInstance"] + if a.get_class().get_name() == "AyonPublishInstance"] inst_data = [] @@ -50,11 +51,12 @@ def start_rendering(): inst_data.append(data) try: - project = os.environ.get("AVALON_PROJECT") + project = openpype.pipeline.get_current_project_name() anatomy = Anatomy(project) root = anatomy.roots['renders'] - except Exception: - raise Exception("Could not find render root in anatomy settings.") + except Exception as e: + raise Exception( + "Could not find render root in anatomy settings.") from e render_dir = f"{root}/{project}" @@ -103,11 +105,11 @@ def start_rendering(): job = queue.allocate_new_job(unreal.MoviePipelineExecutorJob) job.sequence = unreal.SoftObjectPath(i["master_sequence"]) job.map = unreal.SoftObjectPath(i["master_level"]) - job.author = "OpenPype" + job.author = "Ayon" # User data could be used to pass data to the job, that can be # read in the job's OnJobFinished callback. We could, - # for instance, pass the AvalonPublishInstance's path to the job. + # for instance, pass the AyonPublishInstance's path to the job. # job.user_data = "" settings = job.get_configuration().find_or_add_setting_by_class( diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py index 8531472142..5a4c689918 100644 --- a/openpype/hosts/unreal/api/tools_ui.py +++ b/openpype/hosts/unreal/api/tools_ui.py @@ -64,7 +64,7 @@ class ToolsDialog(QtWidgets.QDialog): def __init__(self, *args, **kwargs): super(ToolsDialog, self).__init__(*args, **kwargs) - self.setWindowTitle("OpenPype tools") + self.setWindowTitle("Ayon tools") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) self.setWindowIcon(icon) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index b628d89b2c..cc43bd2ca6 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -186,15 +186,15 @@ class UnrealPrelaunchHook(PreLaunchHook): project_path.mkdir(parents=True, exist_ok=True) - # Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for + # Set "AYON_UNREAL_PLUGIN" to current process environment for # execution of `create_unreal_project` - if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): + if self.launch_context.env.get("AYON_UNREAL_PLUGIN"): self.log.info(( f"{self.signature} using Ayon plugin from " - f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}" + f"{self.launch_context.env.get('AYON_UNREAL_PLUGIN')}" )) - env_key = "OPENPYPE_UNREAL_PLUGIN" + env_key = "AYON_UNREAL_PLUGIN" if self.launch_context.env.get(env_key): os.environ[env_key] = self.launch_context.env[env_key] @@ -213,7 +213,7 @@ class UnrealPrelaunchHook(PreLaunchHook): engine_path, project_path) - self.launch_context.env["OPENPYPE_UNREAL_VERSION"] = engine_version + self.launch_context.env["AYON_UNREAL_VERSION"] = engine_version # Append project file to launch arguments self.launch_context.launch_args.append( f"\"{project_file.as_posix()}\"") diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin index 299a5edc6a..0838da5577 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin @@ -2,14 +2,14 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "OpenPype", - "Description": "OpenPype Integration", - "Category": "OpenPype.Integration", + "FriendlyName": "Ayon", + "Description": "Ayon Integration", + "Category": "Ayon.Integration", "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://openpype.io", - "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "CreatedByURL": "https://ayon.ynput.io", + "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", "MarketplaceURL": "", - "SupportURL": "https://pype.club/", + "SupportURL": "https://ynput.io/", "EngineVersion": "4.27", "CanContainContent": true, "Installed": true, @@ -20,4 +20,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py index 9ed5a2cb19..43d6b8b7cf 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py @@ -1,30 +1,30 @@ import unreal -openpype_detected = True +ayon_detected = True try: from openpype.pipeline import install_host from openpype.hosts.unreal.api import UnrealHost - openpype_host = UnrealHost() + ayon_host = UnrealHost() except ImportError as exc: - openpype_host = None - openpype_detected = False - unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + ayon_host = None + ayon_detected = False + unreal.log_error(f"OpenPype: cannot load Ayon [ {exc} ]") -if openpype_detected: - install_host(openpype_host) +if ayon_detected: + install_host(ayon_host) @unreal.uclass() class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): - unreal.log_warning("OpenPype: showing tools popup") - if openpype_detected: - openpype_host.show_tools_popup() + unreal.log_warning("Ayon: showing tools popup") + if ayon_detected: + ayon_host.show_tools_popup() @unreal.ufunction(override=True) def RunInPython_Dialog(self): - unreal.log_warning("OpenPype: showing tools dialog") - if openpype_detected: - openpype_host.show_tools_dialog() + unreal.log_warning("Ayon: showing tools dialog") + if ayon_detected: + ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md index a08c1ada39..77ae8c7e98 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md @@ -1,11 +1,3 @@ -# OpenPype Unreal Integration plugin - UE 4.x +# Ayon Unreal Integration plugin - UE 4.x -This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run. - -## How does this work - -Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button -on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are -declared in c++ but needs to be implemented during Unreal Editor -startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor -automatically. +This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp index d91dc94db1..509b7268ba 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp @@ -9,12 +9,12 @@ */ UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) { - - const FString ConfigFilePath = OPENPYPE_SETTINGS_FILEPATH; + + const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; // This has to be probably in the future set using the UE Reflection system FColor Color; GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); FolderColor = Color; -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 548bc4c399..320285591e 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "OpenPypePublishInstance.h" @@ -43,7 +45,7 @@ UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& Obj #ifdef WITH_EDITOR ColorOpenPypeDirs(); #endif - + } void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp index a32ebe32cb..4b4492bd20 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h index 0902019c72..7a93f107c5 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h @@ -5,19 +5,19 @@ #include "CoreMinimal.h" #include "AyonSettings.generated.h" -#define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") +#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") UCLASS(Config=AyonSettings, DefaultConfig) class AYON_API UAyonSettings : public UObject { GENERATED_UCLASS_BODY() - + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) FColor GetFolderFColor() const { return FolderColor; } - + UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) FLinearColor GetFolderFLinearColor() const { @@ -28,4 +28,4 @@ protected: UPROPERTY(config, EditAnywhere, Category = Folders) FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file +}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 8f2dca5d69..2f3b6aa596 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "Engine.h" @@ -48,7 +50,7 @@ public: /** * Function for returning all the assets in the container combined. - * + * * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are * returning raw pointers. Seems like an issue in UE5 * @@ -94,7 +96,7 @@ private: #ifdef WITH_EDITOR void ColorOpenPypeDirs(); - + void SendNotification(const FString& Text) const; virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 54dc3e8c1d..5a02a51d1c 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin index c93a9b4b68..70ed8f6b9a 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin @@ -2,14 +2,14 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "OpenPype", - "Description": "OpenPype Integration", - "Category": "OpenPype.Integration", + "FriendlyName": "Ayon", + "Description": "Ayon Integration", + "Category": "Ayon.Integration", "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://openpype.io", - "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "CreatedByURL": "https://ayon.ynput.io", + "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", "MarketplaceURL": "", - "SupportURL": "https://pype.club/", + "SupportURL": "https://ynput.io/", "CanContainContent": true, "EngineVersion": "5.0", "IsExperimentalVersion": false, @@ -21,4 +21,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py index 9ed5a2cb19..c0b1d0ce5d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py @@ -1,30 +1,30 @@ import unreal -openpype_detected = True +ayon_detected = True try: from openpype.pipeline import install_host from openpype.hosts.unreal.api import UnrealHost - openpype_host = UnrealHost() + ayon_host = UnrealHost() except ImportError as exc: - openpype_host = None - openpype_detected = False - unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + ayon_host = None + ayon_detected = False + unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") -if openpype_detected: - install_host(openpype_host) +if ayon_detected: + install_host(ayon_host) @unreal.uclass() class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): - unreal.log_warning("OpenPype: showing tools popup") - if openpype_detected: - openpype_host.show_tools_popup() + unreal.log_warning("Ayon: showing tools popup") + if ayon_detected: + ayon_host.show_tools_popup() @unreal.ufunction(override=True) def RunInPython_Dialog(self): - unreal.log_warning("OpenPype: showing tools dialog") - if openpype_detected: - openpype_host.show_tools_dialog() + unreal.log_warning("Ayon: showing tools dialog") + if ayon_detected: + ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md index cf0aa622c2..865c8cafea 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md @@ -1,11 +1,3 @@ -# OpenPype Unreal Integration plugin - UE 5.x +# Ayon Unreal Integration plugin - UE 5.0 -This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run. - -## How does this work - -Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button -on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are -declared in C++ but needs to be implemented during Unreal Editor -startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor -automatically. +This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 0d9cddfd1c..7a65fd0c98 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp index a32ebe32cb..4b4492bd20 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 03a22c6cde..544cb6d915 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "Engine.h" @@ -49,7 +51,7 @@ public: /** * Function for returning all the assets in the container combined. - * + * * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are * returning raw pointers. Seems like an issue in UE5 * diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 54dc3e8c1d..5a02a51d1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin index c93a9b4b68..70ed8f6b9a 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin @@ -2,14 +2,14 @@ "FileVersion": 3, "Version": 1, "VersionName": "1.0", - "FriendlyName": "OpenPype", - "Description": "OpenPype Integration", - "Category": "OpenPype.Integration", + "FriendlyName": "Ayon", + "Description": "Ayon Integration", + "Category": "Ayon.Integration", "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://openpype.io", - "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", + "CreatedByURL": "https://ayon.ynput.io", + "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", "MarketplaceURL": "", - "SupportURL": "https://pype.club/", + "SupportURL": "https://ynput.io/", "CanContainContent": true, "EngineVersion": "5.0", "IsExperimentalVersion": false, @@ -21,4 +21,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py index 9ed5a2cb19..c0b1d0ce5d 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py @@ -1,30 +1,30 @@ import unreal -openpype_detected = True +ayon_detected = True try: from openpype.pipeline import install_host from openpype.hosts.unreal.api import UnrealHost - openpype_host = UnrealHost() + ayon_host = UnrealHost() except ImportError as exc: - openpype_host = None - openpype_detected = False - unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) + ayon_host = None + ayon_detected = False + unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") -if openpype_detected: - install_host(openpype_host) +if ayon_detected: + install_host(ayon_host) @unreal.uclass() class AyonIntegration(unreal.AyonPythonBridge): @unreal.ufunction(override=True) def RunInPython_Popup(self): - unreal.log_warning("OpenPype: showing tools popup") - if openpype_detected: - openpype_host.show_tools_popup() + unreal.log_warning("Ayon: showing tools popup") + if ayon_detected: + ayon_host.show_tools_popup() @unreal.ufunction(override=True) def RunInPython_Dialog(self): - unreal.log_warning("OpenPype: showing tools dialog") - if openpype_detected: - openpype_host.show_tools_dialog() + unreal.log_warning("Ayon: showing tools dialog") + if ayon_detected: + ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md index cf0aa622c2..417d490548 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md @@ -1,11 +1,3 @@ -# OpenPype Unreal Integration plugin - UE 5.x +# Ayon Unreal Integration plugin - UE 5.1 -This is plugin for Unreal Editor, creating menu for [OpenPype](https://github.com/getavalon) tools to run. - -## How does this work - -Plugin is creating basic menu items in **Window/OpenPype** section of Unreal Editor main menu and a button -on the main toolbar with associated menu. Clicking on those menu items is calling callbacks that are -declared in C++ but needs to be implemented during Unreal Editor -startup in `Plugins/OpenPype/Content/Python/init_unreal.py` - this should be executed by Unreal Editor -automatically. +This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 0d9cddfd1c..7a65fd0c98 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp index a32ebe32cb..4b4492bd20 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 03a22c6cde..544cb6d915 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "Engine.h" @@ -49,7 +51,7 @@ public: /** * Function for returning all the assets in the container combined. - * + * * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are * returning raw pointers. Seems like an issue in UE5 * diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h index 54dc3e8c1d..5a02a51d1c 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h @@ -1,4 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 681d55e3b1..aa5b09fda8 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -189,7 +189,7 @@ def create_unreal_project(project_name: str, As there is no way I know to create a project via command line, this is easiest option. Unreal project file is basically a JSON file. If we find - the `OPENPYPE_UNREAL_PLUGIN` environment variable we assume this is the + the `AYON_UNREAL_PLUGIN` environment variable we assume this is the location of the Integration Plugin and we copy its content to the project folder and enable this plugin. @@ -203,8 +203,7 @@ def create_unreal_project(project_name: str, sources. This will trigger automatically if `Binaries` directory is not found in plugin folders as this indicates this is only source distribution of the plugin. Dev mode - is also set by preset file `unreal/project_setup.json` in - **OPENPYPE_CONFIG**. + is also set in Settings. env (dict, optional): Environment to use. If not set, `os.environ`. Throws: @@ -324,9 +323,11 @@ def get_path_to_uat(engine_path: Path) -> Path: def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path(os.path.dirname(os.path.abspath(openpype.__file__))) + cmd_project = Path(os.path.dirname( + os.path.abspath(os.getenv("OPENPYPE_ROOT")))) - # For now, only tested on Windows (For Linux and Mac it has to be implemented) + # For now, only tested on Windows (For Linux and Mac + # it has to be implemented) cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" return cmd_project / "CommandletProject/CommandletProject.uproject" @@ -372,7 +373,7 @@ def get_build_id(engine_path: Path, ue_version: str) -> str: def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: env = env or os.environ - integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(integration_plugin_path): raise RuntimeError("Path to the integration plugin is null!") @@ -393,7 +394,7 @@ def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: def try_installing_plugin(engine_path: Path, env: dict = None) -> None: env = env or os.environ - integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(integration_plugin_path): raise RuntimeError("Path to the integration plugin is null!") @@ -420,7 +421,7 @@ def _build_and_move_plugin(engine_path: Path, uat_path: Path = get_path_to_uat(engine_path) env = env or os.environ - integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + integration_plugin_path: Path = Path(env.get("AYON_UNREAL_PLUGIN", "")) if uat_path.is_file(): temp_dir: Path = integration_plugin_path.parent / "Temp" diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 642924e2d6..73afb6cefd 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -11,7 +11,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateCamera(UnrealAssetCreator): """Create Camera.""" - identifier = "io.openpype.creators.unreal.camera" + identifier = "io.ayon.creators.unreal.camera" label = "Camera" family = "camera" icon = "fa.camera" diff --git a/openpype/hosts/unreal/plugins/create/create_layout.py b/openpype/hosts/unreal/plugins/create/create_layout.py index 1d2e800a13..e5c7b8ee19 100644 --- a/openpype/hosts/unreal/plugins/create/create_layout.py +++ b/openpype/hosts/unreal/plugins/create/create_layout.py @@ -7,7 +7,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateLayout(UnrealActorCreator): """Layout output for character rigs.""" - identifier = "io.openpype.creators.unreal.layout" + identifier = "io.ayon.creators.unreal.layout" label = "Layout" family = "layout" icon = "cubes" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index f6c73e47e6..e15b57b2ee 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -14,7 +14,7 @@ from openpype.lib import UILabelDef class CreateLook(UnrealAssetCreator): """Shader connections defining shape look.""" - identifier = "io.openpype.creators.unreal.look" + identifier = "io.ayon.creators.unreal.look" label = "Look" family = "look" icon = "paint-brush" @@ -30,7 +30,7 @@ class CreateLook(UnrealAssetCreator): selected_asset = selection[0] - look_directory = "/Game/OpenPype/Looks" + look_directory = "/Game/Ayon/Looks" # Create the folder folder_name = create_folder(look_directory, subset_name) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 5834d2e7a7..2f434d0a60 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -14,7 +14,7 @@ from openpype.lib import UILabelDef class CreateRender(UnrealAssetCreator): """Create instance for sequence for rendering""" - identifier = "io.openpype.creators.unreal.render" + identifier = "io.ayon.creators.unreal.render" label = "Render" family = "render" icon = "eye" @@ -45,22 +45,22 @@ class CreateRender(UnrealAssetCreator): # The asset name is the third element of the path which # contains the map. # To take the asset name, we remove from the path the prefix - # "/Game/OpenPype/" and then we split the path by "/". + # "/Game/Ayon/" and then we split the path by "/". sel_path = selected_asset_path - asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] + asset_name = sel_path.replace("/Game/Ayon/", "").split("/")[0] # Get the master sequence and the master level. # There should be only one sequence and one level in the directory. ar_filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[f"/Game/Ayon/{asset_name}"], recursive_paths=False) sequences = ar.get_assets(ar_filter) master_seq = sequences[0].get_asset().get_path_name() master_seq_obj = sequences[0].get_asset() ar_filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], + package_paths=[f"/Game/Ayon/{asset_name}"], recursive_paths=False) levels = ar.get_assets(ar_filter) master_lvl = levels[0].get_asset().get_path_name() diff --git a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py index 1acf7084d1..80816d8386 100644 --- a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py @@ -7,7 +7,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateStaticMeshFBX(UnrealAssetCreator): """Create Static Meshes as FBX geometry.""" - identifier = "io.openpype.creators.unreal.staticmeshfbx" + identifier = "io.ayon.creators.unreal.staticmeshfbx" label = "Static Mesh (FBX)" family = "unrealStaticMesh" icon = "cube" diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index 70f17d478b..c78518e86b 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -12,7 +12,7 @@ from openpype.hosts.unreal.api.plugin import ( class CreateUAsset(UnrealAssetCreator): """Create UAsset.""" - identifier = "io.openpype.creators.unreal.uasset" + identifier = "io.ayon.creators.unreal.uasset" label = "UAsset" family = "uasset" icon = "cube" diff --git a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py index 496b6056ea..52eea4122a 100644 --- a/openpype/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_alembic_animation.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -68,8 +68,8 @@ class AnimationAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and openpype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -97,8 +97,8 @@ class AnimationAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -109,7 +109,7 @@ class AnimationAlembicLoader(plugin.Loader): "family": context["representation"]["context"]["family"] } unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 1fe0bef462..c1fc7e1e32 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -11,7 +11,7 @@ from unreal import MovieSceneSkeletalAnimationSection from openpype.pipeline.context_tools import get_current_project_asset from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -139,9 +139,9 @@ class AnimationFBXLoader(plugin.Loader): Returns: list(str): list of container content """ - # Create directory for asset and avalon container + # Create directory for asset and Ayon container hierarchy = context.get('asset').get('data').get('parents') - root = "/Game/OpenPype" + root = "/Game/Ayon" asset = context.get('asset').get('name') suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" @@ -223,8 +223,8 @@ class AnimationFBXLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 2496440e5f..c082562775 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -8,7 +8,7 @@ from unreal import EditorLevelLibrary from unreal import EditorLevelUtils from openpype.client import get_assets, get_asset_by_name from openpype.pipeline import ( - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin @@ -100,9 +100,9 @@ class CameraLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and avalon container + # Create directory for asset and Ayon container hierarchy = context.get('asset').get('data').get('parents') - root = "/Game/OpenPype" + root = "/Game/Ayon" hierarchy_dir = root hierarchy_dir_list = [] for h in hierarchy: @@ -291,8 +291,8 @@ class CameraLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -320,7 +320,7 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/OpenPype" + root = "/Game/ayon" asset_dir = container.get('namespace') @@ -378,7 +378,7 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. - root = "/Game/OpenPype" + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] filter = unreal.ARFilter( @@ -511,7 +511,7 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. - root = "/Game/OpenPype" + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] filter = unreal.ARFilter( diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 6ac3531b40..74101d6a53 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -83,8 +83,8 @@ class PointCacheAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -118,8 +118,8 @@ class PointCacheAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 63d415a52b..0f25677484 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -19,7 +19,7 @@ from openpype.pipeline import ( loaders_from_representation, load_container, get_representation_path, - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, legacy_io, ) from openpype.pipeline.context_tools import get_current_project_asset @@ -37,7 +37,7 @@ class LayoutLoader(plugin.Loader): label = "Load Layout" icon = "code-fork" color = "orange" - ASSET_ROOT = "/Game/OpenPype" + ASSET_ROOT = "/Game/Ayon" def _get_asset_containers(self, path): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -634,7 +634,7 @@ class LayoutLoader(plugin.Loader): data = get_current_project_settings() create_sequences = data["unreal"]["level_sequences_for_layouts"] - # Create directory for asset and avalon container + # Create directory for asset and Ayon container hierarchy = context.get('asset').get('data').get('parents') root = self.ASSET_ROOT hierarchy_dir = root @@ -749,8 +749,8 @@ class LayoutLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -781,7 +781,7 @@ class LayoutLoader(plugin.Loader): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/OpenPype" + root = "/Game/Ayon" asset_dir = container.get('namespace') context = representation.get("context") @@ -867,7 +867,7 @@ class LayoutLoader(plugin.Loader): data = get_current_project_settings() create_sequences = data["unreal"]["level_sequences_for_layouts"] - root = "/Game/OpenPype" + root = "/Game/Ayon" path = Path(container.get("namespace")) containers = unreal_pipeline.ls() diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 092b273ded..96ee8cfc25 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -10,7 +10,7 @@ from openpype.pipeline import ( loaders_from_representation, load_container, get_representation_path, - AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin @@ -28,7 +28,7 @@ class ExistingLayoutLoader(plugin.Loader): label = "Load Layout on Existing Scene" icon = "code-fork" color = "orange" - ASSET_ROOT = "/Game/OpenPype" + ASSET_ROOT = "/Game/Ayon" delete_unmatched_assets = True @@ -59,8 +59,8 @@ class ExistingLayoutLoader(plugin.Loader): container = obj.get_asset() data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -416,8 +416,8 @@ class ExistingLayoutLoader(plugin.Loader): container=container_name, path=curr_level_path) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": curr_level_path, "container_name": container_name, diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index e316d255e9..7591d5582f 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -70,8 +70,8 @@ class SkeletalMeshAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and openpype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -98,8 +98,8 @@ class SkeletalMeshAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -110,7 +110,7 @@ class SkeletalMeshAlembicLoader(plugin.Loader): "family": context["representation"]["context"]["family"] } unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 227c5c9292..e9676cde3a 100644 --- a/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -42,8 +42,8 @@ class SkeletalMeshFBXLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" if options and options.get("asset_dir"): root = options["asset_dir"] asset = context.get('asset').get('name') @@ -103,8 +103,8 @@ class SkeletalMeshFBXLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -115,7 +115,7 @@ class SkeletalMeshFBXLoader(plugin.Loader): "family": context["representation"]["context"]["family"] } unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index c7841cef53..c435b8843d 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -75,8 +75,8 @@ class StaticMeshAlembicLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -108,8 +108,8 @@ class StaticMeshAlembicLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -119,8 +119,7 @@ class StaticMeshAlembicLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py index 351c686095..e416256486 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -4,7 +4,7 @@ import os from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -68,8 +68,8 @@ class StaticMeshFBXLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" if options and options.get("asset_dir"): root = options["asset_dir"] asset = context.get('asset').get('name') @@ -81,7 +81,8 @@ class StaticMeshFBXLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{root}/{asset}/{name}", suffix="" + ) container_name += suffix @@ -96,8 +97,8 @@ class StaticMeshFBXLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -107,8 +108,7 @@ class StaticMeshFBXLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index eccfc7b445..b1a4fc6971 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -5,7 +5,7 @@ import shutil from openpype.pipeline import ( get_representation_path, - AVALON_CONTAINER_ID + AYON_CONTAINER_ID ) from openpype.hosts.unreal.api import plugin from openpype.hosts.unreal.api import pipeline as unreal_pipeline @@ -38,8 +38,8 @@ class UAssetLoader(plugin.Loader): list(str): list of container content """ - # Create directory for asset and OpenPype container - root = "/Game/OpenPype/Assets" + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -49,7 +49,8 @@ class UAssetLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(root, asset, name), suffix="") + f"{root}/{asset}/{name}", suffix="" + ) container_name += suffix @@ -67,8 +68,8 @@ class UAssetLoader(plugin.Loader): container=container_name, path=asset_dir) data = { - "schema": "openpype:container-2.0", - "id": AVALON_CONTAINER_ID, + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, "asset": asset, "namespace": asset_dir, "container_name": container_name, @@ -78,8 +79,7 @@ class UAssetLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( - "{}/{}".format(asset_dir, container_name), data) + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index cb28f4bf60..6697a6b90d 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -3,6 +3,7 @@ from pathlib import Path import unreal +from openpype.pipeline import get_current_project_name from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline import pyblish.api @@ -81,12 +82,13 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): self.log.debug(f"new instance data: {new_data}") try: - project = os.environ.get("AVALON_PROJECT") + project = get_current_project_name() anatomy = Anatomy(project) root = anatomy.roots['renders'] - except Exception: - raise Exception( - "Could not find render root in anatomy settings.") + except Exception as e: + raise Exception(( + "Could not find render root " + "in anatomy settings.")) from e render_dir = f"{root}/{project}/{s.get('output')}" render_path = Path(render_dir) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 1d8023c4d7..e7a690ac9c 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -286,7 +286,7 @@ class UEPluginInstallWorker(QtCore.QObject): def _build_and_move_plugin(self, plugin_build_path: Path): uat_path: Path = ue_lib.get_path_to_uat(self.engine_path) - src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(src_plugin_dir): msg = "Path to the integration plugin is null!" @@ -347,7 +347,7 @@ class UEPluginInstallWorker(QtCore.QObject): dir_util.remove_tree(temp_dir.as_posix()) def run(self): - src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + src_plugin_dir = Path(self.env.get("AYON_UNREAL_PLUGIN", "")) if not os.path.isdir(src_plugin_dir): msg = "Path to the integration plugin is null!" diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 7a2ef59a5a..d656d58adc 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -1,5 +1,6 @@ from .constants import ( AVALON_CONTAINER_ID, + AYON_CONTAINER_ID, HOST_WORKFILE_EXTENSIONS, ) @@ -99,6 +100,7 @@ uninstall = uninstall_host __all__ = ( "AVALON_CONTAINER_ID", + "AYON_CONTAINER_ID", "HOST_WORKFILE_EXTENSIONS", # --- MongoDB --- diff --git a/openpype/pipeline/constants.py b/openpype/pipeline/constants.py index e6496cbf95..755a5fb380 100644 --- a/openpype/pipeline/constants.py +++ b/openpype/pipeline/constants.py @@ -1,5 +1,5 @@ # Metadata ID of loaded container into scene -AVALON_CONTAINER_ID = "pyblish.avalon.container" +AVALON_CONTAINER_ID = AYON_CONTAINER_ID = "pyblish.avalon.container" # TODO get extensions from host implementations HOST_WORKFILE_EXTENSIONS = { From 1e0670d0860192d529a215477f711be51c437ddb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Apr 2023 17:03:28 +0800 Subject: [PATCH 278/918] fix the vray collector and hound fix --- .../plugins/publish/collect_redshift_rop.py | 2 +- .../plugins/publish/collect_vray_rop.py | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index ac55a01209..353a3756db 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -89,7 +89,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(beauty_product) files_by_aov = { "_": self.generate_expected_files(instance, - beauty_product)} + beauty_product)} num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index f246de6e53..263abb375e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -85,14 +85,16 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): beauty_product = self.get_beauty_render_product(default_prefix) render_products.append(beauty_product) files_by_aov = { - "RGB Color": self.generate_expected_files(instance, - beauty_product) - } - render_element, aov = self.get_render_element_name(rop, default_prefix) - if render_element is not None: - render_products.append(render_element) - files_by_aov[aov] = self.generate_expected_files(instance, - render_element) + "RGB Color": self.generate_expected_files(instance, + beauty_product)} + + if instance.data.get("RenderElement", True): + render_element = self.get_render_element_name(rop, default_prefix) + if render_element: + for aov, renderpass in render_element.items(): + render_products.append(renderpass) + files_by_aov[aov] = self.generate_expected_files(instance, + renderpass) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -124,16 +126,17 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): def get_render_element_name(self, node, prefix, suffix=""): """Return the output filename using the AOV prefix and suffix """ + render_element_dict = {} # need a rewrite re_path = node.evalParm("render_network_render_channels") - node_children = hou.node(re_path).children() - for element in node_children: - if element != "channelsContainer": - render_product = prefix.replace(suffix, str(element)) - else: - self.log.debug("skipping non render element output..") - continue - return render_product, str(element) + if re_path: + node_children = hou.node(re_path).children() + for element in node_children: + if element.shaderName() != "vray:SettingsRenderChannels": + aov = str(element) + render_product = prefix.replace(suffix, aov) + render_element_dict[aov] = render_product + return render_element_dict def generate_expected_files(self, instance, path): """Create expected files in instance data""" From 664637219d9e673b3d1823cce0ee6a04502ef99a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Apr 2023 17:04:48 +0800 Subject: [PATCH 279/918] hound fix --- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 263abb375e..6ec9e0b37e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -93,8 +93,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): if render_element: for aov, renderpass in render_element.items(): render_products.append(renderpass) - files_by_aov[aov] = self.generate_expected_files(instance, - renderpass) + files_by_aov[aov] = self.generate_expected_files(instance, renderpass) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) From b8ee128bc09922c97bedd71780111ab707db1043 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 17 Apr 2023 12:00:31 +0200 Subject: [PATCH 280/918] imageio: adding `use_ocio_config` toggle --- openpype/hooks/pre_ocio_hook.py | 21 +++++++++++++++-- openpype/pipeline/colorspace.py | 23 +++++++++++++++++++ .../project_settings/aftereffects.json | 1 + .../defaults/project_settings/blender.json | 1 + .../defaults/project_settings/fusion.json | 1 + .../defaults/project_settings/hiero.json | 1 + .../defaults/project_settings/houdini.json | 1 + .../defaults/project_settings/max.json | 1 + .../defaults/project_settings/maya.json | 1 + .../defaults/project_settings/nuke.json | 1 + .../defaults/project_settings/unreal.json | 1 + .../schema_project_aftereffects.json | 5 ++++ .../schema_project_blender.json | 5 ++++ .../schema_project_fusion.json | 5 ++++ .../projects_schema/schema_project_hiero.json | 5 ++++ .../schema_project_houdini.json | 5 ++++ .../projects_schema/schema_project_max.json | 5 ++++ .../projects_schema/schema_project_maya.json | 5 ++++ .../schema_project_unreal.json | 5 ++++ .../schemas/schema_nuke_imageio.json | 5 ++++ 20 files changed, 96 insertions(+), 2 deletions(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index f51e9f48d8..9038d57e9e 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -1,6 +1,9 @@ from openpype.lib import PreLaunchHook -from openpype.pipeline.colorspace import get_imageio_config +from openpype.pipeline.colorspace import ( + get_imageio_config, + is_host_use_ocio_config_activated +) from openpype.pipeline.template_data import get_template_data_with_names @@ -14,7 +17,12 @@ class OCIOEnvHook(PreLaunchHook): "aftereffects", "3dsmax", "houdini", - "maya" + "maya", + "nuke", + "nukex", + "nukeassist", + "nukestudio", + "hiero" ] def execute(self): @@ -37,6 +45,15 @@ class OCIOEnvHook(PreLaunchHook): ) if config_data: + use_config_path = is_host_use_ocio_config_activated( + project_name=self.data["project_name"], + host_name=self.host_name, + host_name=self.data["project_settings"] + ) + if not use_config_path: + self.log.info("Using of OCIO config path was not activated...") + return + ocio_path = config_data["path"] self.log.info(f"Setting OCIO config path: {ocio_path}") diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index b3774e5e90..5520dab627 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -392,6 +392,29 @@ def get_imageio_config( return config_data +def is_host_use_ocio_config_activated( + project_name, host_name, project_settings=None +): + """Check if host OCIO config path is activated + + Args: + project_name (str): project name + host_name (str): host name + + Returns: + bool: True if activated + """ + project_settings = project_settings or get_project_settings(project_name) + + # get colorspace settings + _, imageio_host = _get_imageio_settings( + project_settings, host_name) + + # check if host settings is having use_ocio_config + if imageio_host.get("use_ocio_config", False): + return True + + def _get_config_data(path_list, anatomy_data): """Return first existing path in path list. diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 74bd519bbd..5b6dffe67e 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -1,6 +1,7 @@ { "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 8328ceeda3..f1a3286488 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,6 +1,7 @@ { "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index fa44bbe3d4..ede907e415 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -1,6 +1,7 @@ { "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index b7d5d9af23..e2b5933b6d 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -1,6 +1,7 @@ { "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 2b7192ff99..fca782b2b8 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,6 +1,7 @@ { "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index f6462c3d9a..a9625cc539 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,6 +1,7 @@ { "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index fa3a7bc648..60d5b5ad67 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -2,6 +2,7 @@ "open_workfile_post_initialization": false, "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 1dd0e5128a..cea458d289 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -10,6 +10,7 @@ }, "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index d92d3403ed..60471f28c9 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,6 +1,7 @@ { "imageio": { "activate_host_color_management": true, + "use_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 7bc20fed87..35371f3505 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -17,6 +17,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index dbba7dfdd2..793ac5e908 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -17,6 +17,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index fad6361119..b088f3f034 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -17,6 +17,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index 5e42cb0a00..d09a9efa25 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -17,6 +17,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 14217c944e..24e741ff66 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -17,6 +17,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 5c4b825872..aa336d0791 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -17,6 +17,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index ef32f907ed..534afe2e12 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -22,6 +22,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 87ba3d2d43..bfcb4d7fe6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -17,6 +17,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index a986db1ade..1122eb1949 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -10,6 +10,11 @@ "key": "activate_host_color_management", "label": "Enable Color Management in host" }, + { + "type": "boolean", + "key": "use_ocio_config", + "label": "Use OCIO config file in host" + }, { "type": "schema", "name": "schema_imageio_config" From 4148788761dc0c966bd83af00cbb2cd1e235d79a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 17 Apr 2023 12:16:37 +0200 Subject: [PATCH 281/918] removing obsolete settings all of those settings were now driven form global hook`pre_ocio_hook.py` which is activating OCIO environment variable --- .../defaults/project_settings/hiero.json | 5 ---- .../defaults/project_settings/maya.json | 10 -------- .../schema_project_fusion.json | 25 ------------------- .../projects_schema/schema_project_hiero.json | 14 ----------- .../projects_schema/schema_project_maya.json | 22 ---------------- 5 files changed, 76 deletions(-) diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index e2b5933b6d..a1ca0e8933 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -12,11 +12,6 @@ }, "workfile": { "ocioConfigName": "nuke-default", - "ocioconfigpath": { - "windows": [], - "darwin": [], - "linux": [] - }, "workingSpace": "linear", "sixteenBitLut": "sRGB", "eightBitLut": "sRGB", diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 60d5b5ad67..b33636a446 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -13,21 +13,11 @@ }, "colorManagementPreference_v2": { "enabled": true, - "configFilePath": { - "windows": [], - "darwin": [], - "linux": [] - }, "renderSpace": "ACEScg", "displayName": "sRGB", "viewName": "ACES 1.0 SDR-video" }, "colorManagementPreference": { - "configFilePath": { - "windows": [], - "darwin": [], - "linux": [] - }, "renderSpace": "scene-linear Rec 709/sRGB", "viewTransform": "sRGB gamma" } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index b088f3f034..d488c9f551 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -29,31 +29,6 @@ { "type": "schema", "name": "schema_imageio_file_rules" - }, - { - "key": "ocio", - "type": "dict", - "label": "OpenColorIO (OCIO)", - "collapsible": true, - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Set OCIO variable for Fusion" - }, - { - "type": "label", - "label": "'configFilePath' will be deprecated.
Please move values to : project_settings/{app}/imageio/ocio_config/filepath." - }, - { - "type": "path", - "key": "configFilePath", - "label": "OCIO Config File Path", - "multiplatform": true, - "multipath": true - } - ] } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index d09a9efa25..0bd88c6e11 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -36,10 +36,6 @@ "label": "Workfile", "collapsible": false, "children": [ - { - "type": "label", - "label": "'ocioconfigpath' will be deprecated.
Please move values to : project_settings/{app}/imageio/ocio_config/filepath." - }, { "type": "form", "children": [ @@ -65,19 +61,9 @@ }, { "cg-config-v1.0.0_aces-v1.3_ocio-v2.1": "cg-config-v1.0.0_aces-v1.3_ocio-v2.1 (14)" - }, - { - "custom": "custom" } ] }, - { - "type": "path", - "key": "ocioconfigpath", - "label": "Custom OCIO path", - "multiplatform": true, - "multipath": true - }, { "type": "text", "key": "workingSpace", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 534afe2e12..cb2292319a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -47,17 +47,6 @@ "key": "enabled", "label": "Use Color Management Preference v2" }, - { - "type": "label", - "label": "'configFilePath' will be deprecated.
Please move values to : project_settings/{app}/imageio/ocio_config/filepath." - }, - { - "type": "path", - "key": "configFilePath", - "label": "OCIO Config File Path", - "multiplatform": true, - "multipath": true - }, { "type": "text", "key": "renderSpace", @@ -81,17 +70,6 @@ "label": "Color Management Preference (legacy)", "collapsible": true, "children": [ - { - "type": "label", - "label": "'configFilePath' will be deprecated.
Please move values to : project_settings/{app}/imageio/ocio_config/filepath." - }, - { - "type": "path", - "key": "configFilePath", - "label": "OCIO Config File Path", - "multiplatform": true, - "multipath": true - }, { "type": "text", "key": "renderSpace", From 5a39139aaa0321bea3635cc8d8b9ef144af87282 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 17 Apr 2023 12:27:27 +0200 Subject: [PATCH 282/918] fix duplicated keyword input --- openpype/hooks/pre_ocio_hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 9038d57e9e..d65433fba6 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -48,7 +48,7 @@ class OCIOEnvHook(PreLaunchHook): use_config_path = is_host_use_ocio_config_activated( project_name=self.data["project_name"], host_name=self.host_name, - host_name=self.data["project_settings"] + project_settings=self.data["project_settings"] ) if not use_config_path: self.log.info("Using of OCIO config path was not activated...") From 30d0b35c9a80960ee3f620c521c96fc729982d58 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 17 Apr 2023 12:28:02 +0200 Subject: [PATCH 283/918] removing obsolete code in nuke host since we are using OCIO env var there is no need to set this per attribute --- openpype/hosts/nuke/api/lib.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 71643a2fd0..57c3207463 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2013,18 +2013,6 @@ class WorkfileSettings(object): ''' workfile_settings = imageio_host["workfile"] - # get config data if imageio is enabled - config_data = None - if imageio_host.get("enabled"): - # switch ocio config to custom config - workfile_settings["OCIO_config"] = "custom" - workfile_settings["colorManagement"] = "OCIO" - - # get resolved ocio config path - config_data = get_imageio_config( - legacy_io.active_project(), "nuke" - ) - # first set OCIO if self._root_node["colorManagement"].value() \ not in str(workfile_settings["colorManagement"]): @@ -2034,6 +2022,7 @@ class WorkfileSettings(object): # we dont need the key anymore workfile_settings.pop("colorManagement") + # second set ocio version if self._root_node["OCIO_config"].value() \ not in str(workfile_settings["OCIO_config"]): @@ -2043,14 +2032,6 @@ class WorkfileSettings(object): # we dont need the key anymore workfile_settings.pop("OCIO_config") - # third set ocio custom path - if config_data: - self._root_node["customOCIOConfigPath"].setValue( - str(config_data["path"]).replace("\\", "/") - ) - # backward compatibility, remove in case it exists - workfile_settings.pop("customOCIOConfigPath") - # then set the rest for knob, value in workfile_settings.items(): # skip unfilled ocio config path From ef55dd932d57694d5e872a3f6a9fe4f0cb77370b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 17 Apr 2023 13:00:01 +0200 Subject: [PATCH 284/918] :art: move startup script logic to hook --- .../hosts/max/hooks/force_startup_script.py | 24 +++++++++++++++++++ .../system_settings/applications.json | 4 +--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/hooks/force_startup_script.py diff --git a/openpype/hosts/max/hooks/force_startup_script.py b/openpype/hosts/max/hooks/force_startup_script.py new file mode 100644 index 0000000000..4fcf4fef21 --- /dev/null +++ b/openpype/hosts/max/hooks/force_startup_script.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""Pre-launch to force 3ds max startup script.""" +from openpype.lib import PreLaunchHook +import os + + +class ForceStartupScript(PreLaunchHook): + """Inject OpenPype environment to 3ds max. + + Note that this works in combination whit 3dsmax startup script that + is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH + environment. + + Hook `GlobalHostDataHook` must be executed before this hook. + """ + app_groups = ["3dsmax"] + order = 11 + + def execute(self): + startup_args = [ + "-U", + "MAXScript", + f"{os.getenv('OPENPYPE_ROOT')}\\openpype\\hosts\\max\\startup\\startup.ms"] # noqa + self.launch_context.launch_args.append(startup_args) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index d25e21a66e..6a0fb45698 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -133,9 +133,7 @@ "linux": [] }, "arguments": { - "windows": [ - "-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms" - ], + "windows": [], "darwin": [], "linux": [] }, From f7026c46948b22128ea43a3bf3a6558fa3215453 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 17 Apr 2023 13:06:07 +0200 Subject: [PATCH 285/918] :recycle: delete ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR --- openpype/settings/defaults/system_settings/applications.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 6a0fb45698..df5b5e07c6 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -119,9 +119,7 @@ "label": "3ds max", "icon": "{}/app_icons/3dsmax.png", "host_name": "max", - "environment": { - "ADSK_3DSMAX_STARTUPSCRIPTS_ADDON_DIR": "{OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup" - }, + "environment": {}, "variants": { "2023": { "use_python_2": false, From b05afaa8371d211d20431e4d4b245807438ef784 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Apr 2023 14:53:15 +0200 Subject: [PATCH 286/918] Global: Optimize anatomy formatting by only formatting used templates instead (#4784) * TemplatesDict can create different type of template * anatomy templates can be formatted on their own * return objected templates on get item * '_rootless_path' is public classmethod 'rootless_path_from_result' * 'AnatomyStringTemplate' expect anatomy templates * remove key getters * fix typo 'create_ojected_templates' -> 'create_objected_templates' * Fix type of argument * Fix long line * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Optimize formatting to use single template formatting instead of formatting full anatomy * Use format strict + code cosmetics * Get template from the formatted data * Update openpype/plugins/publish/integrate_legacy.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Cosmetics * Move template obj definition for path up + rename to `path_template_obj` * Refactor more cases from `anatomy.format` to template obj `.format_strict` * Refactor more cases from `anatomy.format` to template obj `.format_strict` * Refactor more cases from `anatomy.format` to template obj `.format_strict` --------- Co-authored-by: Jakub Trllo Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../avalon_uri_processor.py | 4 +-- .../publish/extract_workfile_location.py | 5 ++-- .../unreal/hooks/pre_workfile_preparation.py | 4 +-- openpype/lib/usdlib.py | 4 +-- .../deadline/abstract_submit_deadline.py | 4 +-- .../plugins/publish/submit_publish_job.py | 12 ++++---- openpype/pipeline/context_tools.py | 7 +++-- openpype/pipeline/delivery.py | 20 ++++++------- openpype/pipeline/workfile/path_resolving.py | 4 +-- .../plugins/publish/collect_resources_path.py | 12 ++++---- openpype/plugins/publish/integrate.py | 22 ++++++-------- .../plugins/publish/integrate_hero_version.py | 30 +++++++++---------- openpype/plugins/publish/integrate_legacy.py | 13 ++++---- .../plugins/publish/integrate_thumbnail.py | 6 ++-- .../push_to_project/control_integrate.py | 4 +-- openpype/tools/texture_copy/app.py | 4 +-- openpype/tools/workfiles/save_as_dialog.py | 8 ++--- 17 files changed, 79 insertions(+), 84 deletions(-) diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py index d7d1c79d73..48019e0a82 100644 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py +++ b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py @@ -128,14 +128,14 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase): if not asset_doc: raise RuntimeError("Invalid asset name: '%s'" % asset) - formatted_anatomy = anatomy.format({ + template_obj = anatomy.templates_obj["publish"]["path"] + path = template_obj.format_strict({ "project": PROJECT, "asset": asset_doc["name"], "subset": subset, "representation": ext, "version": 0 # stub version zero }) - path = formatted_anatomy["publish"]["path"] # Remove the version folder subset_folder = os.path.dirname(os.path.dirname(path)) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py index 18bf0394ae..9ff84e32fb 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -27,11 +27,12 @@ class ExtractWorkfileUrl(pyblish.api.ContextPlugin): rep_name = instance.data.get("representations")[0].get("name") template_data["representation"] = rep_name template_data["ext"] = rep_name - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) filepath = os.path.normpath(template_filled) self.log.info("Using published scene for render {}".format( filepath)) + break if not filepath: self.log.info("Texture batch doesn't contain workfile.") diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 5dae7eef09..efbacc3b16 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -61,10 +61,10 @@ class UnrealPrelaunchHook(PreLaunchHook): project_name=project_doc["name"] ) # Fill templates - filled_anatomy = anatomy.format(workdir_data) + template_obj = anatomy.templates_obj[workfile_template_key]["file"] # Return filename - return filled_anatomy[workfile_template_key]["file"] + return template_obj.format_strict(workdir_data) def exec_plugin_install(self, engine_path: Path, env: dict = None): # set up the QThread and worker with necessary signals diff --git a/openpype/lib/usdlib.py b/openpype/lib/usdlib.py index 20703ee308..5ef1d38f87 100644 --- a/openpype/lib/usdlib.py +++ b/openpype/lib/usdlib.py @@ -327,7 +327,8 @@ def get_usd_master_path(asset, subset, representation): else: asset_doc = get_asset_by_name(project_name, asset, fields=["name"]) - formatted_result = anatomy.format( + template_obj = anatomy.templates_obj["publish"]["path"] + path = template_obj.format_strict( { "project": { "name": project_name, @@ -340,7 +341,6 @@ def get_usd_master_path(asset, subset, representation): } ) - path = formatted_result["publish"]["path"] # Remove the version folder subset_folder = os.path.dirname(os.path.dirname(path)) master_folder = os.path.join(subset_folder, "master") diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 648eb77007..558a637e4b 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -534,8 +534,8 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): template_data["comment"] = None anatomy = instance.context.data['anatomy'] - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) file_path = os.path.normpath(template_filled) self.log.info("Using published scene for render {}".format(file_path)) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 4765772bcf..f80bd40133 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -1202,10 +1202,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): template_data["family"] = "render" template_data["version"] = version - anatomy_filled = anatomy.format(template_data) - - if "folder" in anatomy.templates["render"]: - publish_folder = anatomy_filled["render"]["folder"] + render_templates = anatomy.templates_obj["render"] + if "folder" in render_templates: + publish_folder = render_templates["folder"].format_strict( + template_data + ) else: # solve deprecated situation when `folder` key is not underneath # `publish` anatomy @@ -1215,8 +1216,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): " key underneath `publish` (in global of for project `{}`)." ).format(project_name)) - file_path = anatomy_filled["render"]["path"] - # Directory + file_path = render_templates["path"].format_strict(template_data) publish_folder = os.path.dirname(file_path) return publish_folder diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 6610fd7da7..dede2b8fce 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -463,9 +463,7 @@ def get_workdir_from_session(session=None, template_key=None): session = legacy_io.Session project_name = session["AVALON_PROJECT"] host_name = session["AVALON_APP"] - anatomy = Anatomy(project_name) template_data = get_template_data_from_session(session) - anatomy_filled = anatomy.format(template_data) if not template_key: task_type = template_data["task"]["type"] @@ -474,7 +472,10 @@ def get_workdir_from_session(session=None, template_key=None): host_name, project_name=project_name ) - path = anatomy_filled[template_key]["folder"] + + anatomy = Anatomy(project_name) + template_obj = anatomy.templates_obj[template_key]["folder"] + path = template_obj.format_strict(template_data) if path: path = os.path.normpath(path) return path diff --git a/openpype/pipeline/delivery.py b/openpype/pipeline/delivery.py index 8cf9a43aac..500f54040a 100644 --- a/openpype/pipeline/delivery.py +++ b/openpype/pipeline/delivery.py @@ -1,5 +1,6 @@ """Functions useful for delivery of published representations.""" import os +import copy import shutil import glob import clique @@ -146,12 +147,11 @@ def deliver_single_file( report_items["Source file was not found"].append(msg) return report_items, 0 - anatomy_filled = anatomy.format(anatomy_data) if format_dict: - template_result = anatomy_filled["delivery"][template_name] - delivery_path = template_result.rootless.format(**format_dict) - else: - delivery_path = anatomy_filled["delivery"][template_name] + anatomy_data = copy.deepcopy(anatomy_data) + anatomy_data["root"] = format_dict["root"] + template_obj = anatomy.templates_obj["delivery"][template_name] + delivery_path = template_obj.format_strict(anatomy_data) # Backwards compatibility when extension contained `.` delivery_path = delivery_path.replace("..", ".") @@ -269,14 +269,12 @@ def deliver_sequence( frame_indicator = "@####@" + anatomy_data = copy.deepcopy(anatomy_data) anatomy_data["frame"] = frame_indicator - anatomy_filled = anatomy.format(anatomy_data) - if format_dict: - template_result = anatomy_filled["delivery"][template_name] - delivery_path = template_result.rootless.format(**format_dict) - else: - delivery_path = anatomy_filled["delivery"][template_name] + anatomy_data["root"] = format_dict["root"] + template_obj = anatomy.templates_obj["delivery"][template_name] + delivery_path = template_obj.format_strict(anatomy_data) delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) delivery_folder = os.path.dirname(delivery_path) diff --git a/openpype/pipeline/workfile/path_resolving.py b/openpype/pipeline/workfile/path_resolving.py index 801cb7223c..15689f4d99 100644 --- a/openpype/pipeline/workfile/path_resolving.py +++ b/openpype/pipeline/workfile/path_resolving.py @@ -132,9 +132,9 @@ def get_workdir_with_workdir_data( project_settings ) - anatomy_filled = anatomy.format(workdir_data) + template_obj = anatomy.templates_obj[template_key]["folder"] # Output is TemplateResult object which contain useful data - output = anatomy_filled[template_key]["folder"] + output = template_obj.format_strict(workdir_data) if output: return output.normalized() return output diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 4a5f9f1cc2..f96dd0ae18 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -83,10 +83,11 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "hierarchy": instance.data["hierarchy"] }) - anatomy_filled = anatomy.format(template_data) - - if "folder" in anatomy.templates["publish"]: - publish_folder = anatomy_filled["publish"]["folder"] + publish_templates = anatomy.templates_obj["publish"] + if "folder" in publish_templates: + publish_folder = publish_templates["folder"].format_strict( + template_data + ) else: # solve deprecated situation when `folder` key is not underneath # `publish` anatomy @@ -95,8 +96,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): " key underneath `publish` (in global of for project `{}`)." ).format(anatomy.project_name)) - file_path = anatomy_filled["publish"]["path"] - # Directory + file_path = publish_templates["path"].format_strict(template_data) publish_folder = os.path.dirname(file_path) publish_folder = os.path.normpath(publish_folder) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 07131ec3ae..65ce30412c 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -665,8 +665,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # - template_data (Dict[str, Any]): source data used to fill template # - to add required data to 'repre_context' not used for # formatting - # - anatomy_filled (Dict[str, Any]): filled anatomy of last file - # - to fill 'publishDir' on instance.data -> not ideal + path_template_obj = anatomy.templates_obj[template_name]["path"] # Treat template with 'orignalBasename' in special way if "{originalBasename}" in template: @@ -700,8 +699,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data["originalBasename"], _ = os.path.splitext( src_file_name) - anatomy_filled = anatomy.format(template_data) - dst = anatomy_filled[template_name]["path"] + dst = path_template_obj.format_strict(template_data) src = os.path.join(stagingdir, src_file_name) transfers.append((src, dst)) if repre_context is None: @@ -761,8 +759,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data["udim"] = index else: template_data["frame"] = index - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_filled = path_template_obj.format_strict( + template_data + ) dst_filepaths.append(template_filled) if repre_context is None: self.log.debug( @@ -798,8 +797,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if is_udim: template_data["udim"] = repre["udim"][0] # Construct destination filepath from template - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_filled = path_template_obj.format_strict(template_data) repre_context = template_filled.used_values dst = os.path.normpath(template_filled) @@ -810,11 +808,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # todo: Are we sure the assumption each representation # ends up in the same folder is valid? if not instance.data.get("publishDir"): - instance.data["publishDir"] = ( - anatomy_filled - [template_name] - ["folder"] - ) + template_obj = anatomy.templates_obj[template_name]["folder"] + template_filled = template_obj.format_strict(template_data) + instance.data["publishDir"] = template_filled for key in self.db_representation_context_keys: # Also add these values to the context even if not used by the diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 80141e88fe..b71207c24f 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -291,6 +291,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): )) try: src_to_dst_file_paths = [] + path_template_obj = anatomy.templates_obj[template_key]["path"] for repre_info in published_repres.values(): # Skip if new repre does not have published repre files @@ -303,9 +304,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_data.pop("version", None) # Get filled path to repre context - anatomy_filled = anatomy.format(anatomy_data) - template_filled = anatomy_filled[template_key]["path"] - + template_filled = path_template_obj.format_strict(anatomy_data) repre_data = { "path": str(template_filled), "template": hero_template @@ -343,8 +342,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # Get head and tail for collection frame_splitter = "_-_FRAME_SPLIT_-_" anatomy_data["frame"] = frame_splitter - _anatomy_filled = anatomy.format(anatomy_data) - _template_filled = _anatomy_filled[template_key]["path"] + _template_filled = path_template_obj.format_strict( + anatomy_data + ) head, tail = _template_filled.split(frame_splitter) padding = int( anatomy.templates[template_key]["frame_padding"] @@ -520,24 +520,24 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): }) if "folder" in anatomy.templates[template_key]: - anatomy_filled = anatomy.format(template_data) - publish_folder = anatomy_filled[template_key]["folder"] + template_obj = anatomy.templates_obj[template_key]["folder"] + publish_folder = template_obj.format_strict(template_data) else: # This is for cases of Deprecated anatomy without `folder` # TODO remove when all clients have solved this issue - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "TEMP" - }) - anatomy_filled = anatomy.format(template_data) - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy self.log.warning(( "Deprecation warning: Anatomy does not have set `folder`" " key underneath `publish` (in global of for project `{}`)." ).format(anatomy.project_name)) + # solve deprecated situation when `folder` key is not underneath + # `publish` anatomy + template_data.update({ + "frame": "FRAME_TEMP", + "representation": "TEMP" + }) + template_obj = anatomy.templates_obj[template_key]["path"] + file_path = template_obj.format_strict(template_data) - file_path = anatomy_filled[template_key]["path"] # Directory publish_folder = os.path.dirname(file_path) diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index 3f1f6ad0c9..c67ce62bf6 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -480,8 +480,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): else: template_data["udim"] = src_padding_exp % i - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_obj = anatomy.templates_obj[template_name]["path"] + template_filled = template_obj.format_strict(template_data) if repre_context is None: repre_context = template_filled.used_values test_dest_files.append( @@ -587,8 +587,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre.get("udim"): template_data["udim"] = repre["udim"][0] src = os.path.join(stagingdir, fname) - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled[template_name]["path"] + template_obj = anatomy.templates_obj[template_name]["path"] + template_filled = template_obj.format_strict(template_data) repre_context = template_filled.used_values dst = os.path.normpath(template_filled) @@ -600,9 +600,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if not instance.data.get("publishDir"): instance.data["publishDir"] = ( - anatomy_filled - [template_name] - ["folder"] + anatomy.templates_obj[template_name]["folder"] + .format_strict(template_data) ) if repre.get("udim"): repre_context["udim"] = repre.get("udim") # store list diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 809a1782e0..16cc47d432 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -271,9 +271,9 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): "thumbnail_type": "thumbnail" }) - anatomy_filled = anatomy.format(template_data) - thumbnail_template = anatomy.templates["publish"]["thumbnail"] - template_filled = anatomy_filled["publish"]["thumbnail"] + template_obj = anatomy.templates_obj["publish"]["thumbnail"] + template_filled = template_obj.format_strict(template_data) + thumbnail_template = template_filled.template dst_full_path = os.path.normpath(str(template_filled)) self.log.debug("Copying file .. {} -> {}".format( diff --git a/openpype/tools/push_to_project/control_integrate.py b/openpype/tools/push_to_project/control_integrate.py index bb95fdb26f..37a0512d59 100644 --- a/openpype/tools/push_to_project/control_integrate.py +++ b/openpype/tools/push_to_project/control_integrate.py @@ -1050,8 +1050,8 @@ class ProjectPushItemProcess: repre_format_data["ext"] = ext[1:] break - tmp_result = anatomy.format(formatting_data) - folder_path = tmp_result[template_name]["folder"] + template_obj = anatomy.templates_obj[template_name]["folder"] + folder_path = template_obj.format_strict(formatting_data) repre_context = folder_path.used_values folder_path_rootless = folder_path.rootless repre_filepaths = [] diff --git a/openpype/tools/texture_copy/app.py b/openpype/tools/texture_copy/app.py index a695bb8c4d..a5a9f7349a 100644 --- a/openpype/tools/texture_copy/app.py +++ b/openpype/tools/texture_copy/app.py @@ -47,8 +47,8 @@ class TextureCopy: "hierarchy": hierarchy } anatomy = Anatomy(project_name) - anatomy_filled = anatomy.format(template_data) - return anatomy_filled['texture']['path'] + template_obj = anatomy.templates_obj["texture"]["path"] + return template_obj.format_strict(template_data) def _get_version(self, path): versions = [0] diff --git a/openpype/tools/workfiles/save_as_dialog.py b/openpype/tools/workfiles/save_as_dialog.py index aa881e7946..9f1d1060da 100644 --- a/openpype/tools/workfiles/save_as_dialog.py +++ b/openpype/tools/workfiles/save_as_dialog.py @@ -60,8 +60,8 @@ class CommentMatcher(object): temp_data["version"] = "<>" temp_data["ext"] = "<>" - formatted = anatomy.format(temp_data) - fname_pattern = formatted[template_key]["file"] + template_obj = anatomy.templates_obj[template_key]["file"] + fname_pattern = template_obj.format_strict(temp_data) fname_pattern = re.escape(fname_pattern) # Replace comment and version with something we can match with regex @@ -375,8 +375,8 @@ class SaveAsDialog(QtWidgets.QDialog): data["ext"] = data["ext"].lstrip(".") - anatomy_filled = self.anatomy.format(data) - return anatomy_filled[self.template_key]["file"] + template_obj = self.anatomy.templates_obj[self.template_key]["file"] + return template_obj.format_strict(data) def refresh(self): extensions = list(self._extensions) From db72ed2e634389e4f57ee555585efb1b934c3e32 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 15:46:47 +0800 Subject: [PATCH 287/918] the resolution and the frame range set correctly before saving the scene --- openpype/hosts/max/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index dacc402318..957c674518 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -52,8 +52,11 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): def context_setting(): return lib.set_context_setting() + rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) + rt.callbacks.addScript(rt.Name('filePreSave'), + context_setting) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? From 973caf2d6de18b1f63384aef8089a0908878b4ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 15:56:13 +0800 Subject: [PATCH 288/918] not adding the before save callback --- openpype/hosts/max/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 957c674518..50fe30b299 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -55,8 +55,6 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) - rt.callbacks.addScript(rt.Name('filePreSave'), - context_setting) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? From 5f78ba9bb8a4c5747618b6f669d9c0ddd287a363 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 16:06:20 +0800 Subject: [PATCH 289/918] adding callback to reset the resolution when opening the file --- openpype/hosts/max/api/pipeline.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 50fe30b299..ac841d395f 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -56,6 +56,9 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) + rt.callbacks.addScript(rt.Name('filePostOpen'), + context_setting) + def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? return True From 9d8ed55ea5577aa3abbbad768573778f1e3cf03a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 18 Apr 2023 11:07:07 +0100 Subject: [PATCH 290/918] Fix nested model instances. --- .../maya/plugins/publish/collect_review.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index fcb188734f..7ea91afdfc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -36,6 +36,30 @@ class CollectReview(pyblish.api.InstancePlugin): context = instance.context objectset = context.data['objectsets'] + # Convert enum attribute index to string for Display Lights. + index = instance.data.get("displayLights", 0) + display_lights = lib.DISPLAY_LIGHTS_VALUES[index] + if display_lights == "project_settings": + settings = instance.context.data["project_settings"] + settings = settings["maya"]["publish"]["ExtractPlayblast"] + settings = settings["capture_preset"]["Viewport Options"] + display_lights = settings["displayLights"] + + # Collect camera focal length. + burninDataMembers = instance.data.get("burninDataMembers", {}) + if camera is not None: + attr = camera + ".focalLength" + if lib.get_attribute_input(attr): + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + 1 + time_range = range(int(start), int(end)) + focal_length = [cmds.getAttr(attr, time=t) for t in time_range] + else: + focal_length = cmds.getAttr(attr) + + burninDataMembers["focalLength"] = focal_length + + # Account for nested instances like model. reviewable_subsets = list(set(members) & set(objectset)) if reviewable_subsets: if len(reviewable_subsets) > 1: @@ -77,6 +101,8 @@ class CollectReview(pyblish.api.InstancePlugin): data["isolate"] = instance.data["isolate"] data["panZoom"] = instance.data.get("panZoom", False) data["panel"] = instance.data["panel"] + data["displayLights"] = display_lights + data["burninDataMembers"] = burninDataMembers # The review instance must be active cmds.setAttr(str(instance) + '.active', 1) @@ -103,6 +129,8 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data["frameStartHandle"] instance.data['frameEndFtrack'] = \ instance.data["frameEndHandle"] + instance.data["displayLights"] = display_lights + instance.data["burninDataMembers"] = burninDataMembers # make ftrack publishable instance.data.setdefault("families", []).append('ftrack') @@ -144,33 +172,3 @@ class CollectReview(pyblish.api.InstancePlugin): audio_data.append(get_audio_node_data(node)) instance.data["audio"] = audio_data - - # Convert enum attribute index to string. - index = instance.data.get("displayLights", 0) - display_lights = lib.DISPLAY_LIGHTS_VALUES[index] - if display_lights == "project_settings": - settings = instance.context.data["project_settings"] - settings = settings["maya"]["publish"]["ExtractPlayblast"] - settings = settings["capture_preset"]["Viewport Options"] - display_lights = settings["displayLights"] - instance.data["displayLights"] = display_lights - - # Collect focal length. - if camera is None: - return - - attr = camera + ".focalLength" - if lib.get_attribute_input(attr): - start = instance.data["frameStart"] - end = instance.data["frameEnd"] + 1 - focal_length = [ - cmds.getAttr(attr, time=t) for t in range(int(start), int(end)) - ] - else: - focal_length = cmds.getAttr(attr) - - key = "focalLength" - try: - instance.data["burninDataMembers"][key] = focal_length - except KeyError: - instance.data["burninDataMembers"] = {key: focal_length} From 38cc309d43613e78d94ee7e9bbf53b1044b713be Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 18 Apr 2023 11:34:35 +0100 Subject: [PATCH 291/918] Fix #4851 --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 7ea91afdfc..5c190a4a7b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -92,6 +92,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['frameEndFtrack'] = instance.data["frameEndHandle"] data['frameStartHandle'] = instance.data["frameStartHandle"] data['frameEndHandle'] = instance.data["frameEndHandle"] + data['handleStart'] = instance.data["handleStart"] + data['handleEnd'] = instance.data["handleEnd"] data["frameStart"] = instance.data["frameStart"] data["frameEnd"] = instance.data["frameEnd"] data['step'] = instance.data['step'] From 30eedb646e5a0c784dc5a2e0c485ac834f352f4b Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 18 Apr 2023 12:53:52 +0200 Subject: [PATCH 292/918] Patchelf version locked --- Dockerfile.centos7 | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index 5eb2f478ea..b35bde1589 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -53,6 +53,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n # we need to build our own patchelf WORKDIR /temp-patchelf RUN git clone https://github.com/NixOS/patchelf.git . \ + && git checkout 0.17.0 \ && source scl_source enable devtoolset-7 \ && ./bootstrap.sh \ && ./configure \ From b8e69a5b0171a807e5523b94f2f685d654714641 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 15:21:08 +0200 Subject: [PATCH 293/918] Support .abc files directly for Arnold standin look assignment --- .../maya/tools/mayalookassigner/alembic.py | 97 +++++++++++++++++++ .../tools/mayalookassigner/arnold_standin.py | 6 ++ .../tools/mayalookassigner/vray_proxies.py | 90 +---------------- 3 files changed, 104 insertions(+), 89 deletions(-) create mode 100644 openpype/hosts/maya/tools/mayalookassigner/alembic.py diff --git a/openpype/hosts/maya/tools/mayalookassigner/alembic.py b/openpype/hosts/maya/tools/mayalookassigner/alembic.py new file mode 100644 index 0000000000..6885e923d3 --- /dev/null +++ b/openpype/hosts/maya/tools/mayalookassigner/alembic.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Tools for loading looks to vray proxies.""" +import os +from collections import defaultdict +import logging + +import six + +import alembic.Abc + + +log = logging.getLogger(__name__) + + +def get_alembic_paths_by_property(filename, attr, verbose=False): + # type: (str, str, bool) -> dict + """Return attribute value per objects in the Alembic file. + + Reads an Alembic archive hierarchy and retrieves the + value from the `attr` properties on the objects. + + Args: + filename (str): Full path to Alembic archive to read. + attr (str): Id attribute. + verbose (bool): Whether to verbosely log missing attributes. + + Returns: + dict: Mapping of node full path with its id + + """ + # Normalize alembic path + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + filename = str(filename) # path must be string + + try: + archive = alembic.Abc.IArchive(filename) + except RuntimeError: + # invalid alembic file - probably vrmesh + log.warning("{} is not an alembic file".format(filename)) + return {} + root = archive.getTop() + + iterator = list(root.children) + obj_ids = {} + + for obj in iterator: + name = obj.getFullName() + + # include children for coming iterations + iterator.extend(obj.children) + + props = obj.getProperties() + if props.getNumProperties() == 0: + # Skip those without properties, e.g. '/materials' in a gpuCache + continue + + # THe custom attribute is under the properties' first container under + # the ".arbGeomParams" + prop = props.getProperty(0) # get base property + + _property = None + try: + geo_params = prop.getProperty('.arbGeomParams') + _property = geo_params.getProperty(attr) + except KeyError: + if verbose: + log.debug("Missing attr on: {0}".format(name)) + continue + + if not _property.isConstant(): + log.warning("Id not constant on: {0}".format(name)) + + # Get first value sample + value = _property.getValue()[0] + + obj_ids[name] = value + + return obj_ids + + +def get_alembic_ids_cache(path): + # type: (str) -> dict + """Build a id to node mapping in Alembic file. + + Nodes without IDs are ignored. + + Returns: + dict: Mapping of id to nodes in the Alembic. + + """ + node_ids = get_alembic_paths_by_property(path, attr="cbId") + id_nodes = defaultdict(list) + for node, _id in six.iteritems(node_ids): + id_nodes[_id].append(node) + + return dict(six.iteritems(id_nodes)) diff --git a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py index 7eeeb72553..0ce2b21dcd 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/openpype/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -9,6 +9,7 @@ from openpype.pipeline import legacy_io from openpype.client import get_last_version_by_subset_name from openpype.hosts.maya import api from . import lib +from .alembic import get_alembic_ids_cache log = logging.getLogger(__name__) @@ -68,6 +69,11 @@ def get_nodes_by_id(standin): (dict): Dictionary with node full name/path and id. """ path = cmds.getAttr(standin + ".dso") + + if path.endswith(".abc"): + # Support alembic files directly + return get_alembic_ids_cache(path) + json_path = None for f in os.listdir(os.path.dirname(path)): if f.endswith(".json"): diff --git a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py index 1d2ec5fd87..c875fec7f0 100644 --- a/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py +++ b/openpype/hosts/maya/tools/mayalookassigner/vray_proxies.py @@ -1,108 +1,20 @@ # -*- coding: utf-8 -*- """Tools for loading looks to vray proxies.""" -import os from collections import defaultdict import logging -import six - -import alembic.Abc from maya import cmds from openpype.client import get_last_version_by_subset_name from openpype.pipeline import legacy_io import openpype.hosts.maya.lib as maya_lib from . import lib +from .alembic import get_alembic_ids_cache log = logging.getLogger(__name__) -def get_alembic_paths_by_property(filename, attr, verbose=False): - # type: (str, str, bool) -> dict - """Return attribute value per objects in the Alembic file. - - Reads an Alembic archive hierarchy and retrieves the - value from the `attr` properties on the objects. - - Args: - filename (str): Full path to Alembic archive to read. - attr (str): Id attribute. - verbose (bool): Whether to verbosely log missing attributes. - - Returns: - dict: Mapping of node full path with its id - - """ - # Normalize alembic path - filename = os.path.normpath(filename) - filename = filename.replace("\\", "/") - filename = str(filename) # path must be string - - try: - archive = alembic.Abc.IArchive(filename) - except RuntimeError: - # invalid alembic file - probably vrmesh - log.warning("{} is not an alembic file".format(filename)) - return {} - root = archive.getTop() - - iterator = list(root.children) - obj_ids = {} - - for obj in iterator: - name = obj.getFullName() - - # include children for coming iterations - iterator.extend(obj.children) - - props = obj.getProperties() - if props.getNumProperties() == 0: - # Skip those without properties, e.g. '/materials' in a gpuCache - continue - - # THe custom attribute is under the properties' first container under - # the ".arbGeomParams" - prop = props.getProperty(0) # get base property - - _property = None - try: - geo_params = prop.getProperty('.arbGeomParams') - _property = geo_params.getProperty(attr) - except KeyError: - if verbose: - log.debug("Missing attr on: {0}".format(name)) - continue - - if not _property.isConstant(): - log.warning("Id not constant on: {0}".format(name)) - - # Get first value sample - value = _property.getValue()[0] - - obj_ids[name] = value - - return obj_ids - - -def get_alembic_ids_cache(path): - # type: (str) -> dict - """Build a id to node mapping in Alembic file. - - Nodes without IDs are ignored. - - Returns: - dict: Mapping of id to nodes in the Alembic. - - """ - node_ids = get_alembic_paths_by_property(path, attr="cbId") - id_nodes = defaultdict(list) - for node, _id in six.iteritems(node_ids): - id_nodes[_id].append(node) - - return dict(six.iteritems(id_nodes)) - - def assign_vrayproxy_shaders(vrayproxy, assignments): # type: (str, dict) -> None """Assign shaders to content of Vray Proxy. From f082b85fced9a98fdfdc529f43d792afa680ed50 Mon Sep 17 00:00:00 2001 From: 64qam Date: Tue, 18 Apr 2023 15:21:42 +0200 Subject: [PATCH 294/918] Update Dockerfile.centos7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- Dockerfile.centos7 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index b35bde1589..ce1a624a4f 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -52,8 +52,7 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n # we need to build our own patchelf WORKDIR /temp-patchelf -RUN git clone https://github.com/NixOS/patchelf.git . \ - && git checkout 0.17.0 \ +RUN git clone -b 0.17.0 --single-branch https://github.com/NixOS/patchelf.git . \ && source scl_source enable devtoolset-7 \ && ./bootstrap.sh \ && ./configure \ From 3cf5667787141512dfaf27896666f86968b54c51 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 21:34:01 +0800 Subject: [PATCH 295/918] adding vray_rop families into houdini deadline submission and submit publish job --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 3 ++- .../modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 2bddb28792..6a62ee0ea8 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -41,7 +41,8 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "redshift_rop", "arnold_rop", "mantra_rop", - "karma_rop"] + "karma_rop", + "vray_rop"] targets = ["local"] use_published = True diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 58c9ffc9a0..c382a7e7ec 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -125,7 +125,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "renderlayer", "imagesequence", "vrayscene", "maxrender", "arnold_rop", "mantra_rop", - "karma_rop"] + "karma_rop", "vray_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE From 8e3cc04cdbc14de43cc9c7a5153791df1f7156ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 21:56:07 +0800 Subject: [PATCH 296/918] add farm instance to the render creators --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_karma_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_mantra_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_vray_rop.py | 2 ++ 5 files changed, 10 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 0657d349f9..382279e812 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -23,6 +23,8 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 1 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateArnoldRop, self).create( subset_name, diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 8d55298926..4326a98af4 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -20,6 +20,8 @@ class CreateKarmaROP(plugin.HoudiniCreator): instance_data.update({"node_type": "karma"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateKarmaROP, self).create( subset_name, diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 2632f8d6c0..7ccb554be0 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -20,6 +20,8 @@ class CreateMantraROP(plugin.HoudiniCreator): instance_data.update({"node_type": "ifd"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateMantraROP, self).create( subset_name, diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 2cbe9bfda1..1fb9ab2f67 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -19,6 +19,8 @@ class CreateRedshiftROP(plugin.HoudiniCreator): instance_data.update({"node_type": "Redshift_ROP"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True # Clear the family prefix from the subset subset = subset_name diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index bb2d025cde..40981da430 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -24,6 +24,8 @@ class CreateVrayROP(plugin.HoudiniCreator): instance_data.update({"node_type": "vray_renderer"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateVrayROP, self).create( subset_name, From dec2521c05b6998540a9dfeeb7f2cfa2afdb206d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:29:04 +0200 Subject: [PATCH 297/918] Do not change time slider ranges in `get_frame_range` function --- openpype/hosts/maya/api/lib.py | 90 ++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 61ea3d59df..a78ac184c2 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2153,17 +2153,23 @@ def set_scene_resolution(width, height, pixelAspect): cmds.setAttr("%s.pixelAspect" % control_node, pixelAspect) -def get_frame_range(): - """Get the current assets frame range and handles.""" +def get_frame_range(include_animation_range=False): + """Get the current assets frame range and handles. + + Args: + include_animation_range (bool, optional): Whether to include + `animationStart` and `animationEnd` keys to define the outer + range of the timeline. It is excluded by default. + + Returns: + dict: Asset's expected frame range values. + + """ # Set frame start/end project_name = get_current_project_name() - task_name = get_current_task_name() asset_name = get_current_asset_name() asset = get_asset_by_name(project_name, asset_name) - settings = get_project_settings(project_name) - include_handles_settings = settings["maya"]["include_handles"] - current_task = asset.get("data").get("tasks").get(task_name) frame_start = asset["data"].get("frameStart") frame_end = asset["data"].get("frameEnd") @@ -2175,32 +2181,39 @@ def get_frame_range(): handle_start = asset["data"].get("handleStart") or 0 handle_end = asset["data"].get("handleEnd") or 0 - animation_start = frame_start - animation_end = frame_end - - include_handles = include_handles_settings["include_handles_default"] - for item in include_handles_settings["per_task_type"]: - if current_task["type"] in item["task_type"]: - include_handles = item["include_handles"] - break - if include_handles: - animation_start -= int(handle_start) - animation_end += int(handle_end) - - cmds.playbackOptions( - minTime=frame_start, - maxTime=frame_end, - animationStartTime=animation_start, - animationEndTime=animation_end - ) - cmds.currentTime(frame_start) - - return { + frame_range = { "frameStart": frame_start, "frameEnd": frame_end, "handleStart": handle_start, - "handleEnd": handle_end + "handleEnd": handle_end, } + if include_animation_range: + # The animation range values are only included to define whether + # the Maya time slider should include the handles or not. + # Some usages of this function use the full dictionary to define + # instance attributes for which we want to exclude the animation + # keys. That is why these are excluded by default. + task_name = get_current_task_name() + settings = get_project_settings(project_name) + include_handles_settings = settings["maya"]["include_handles"] + current_task = asset.get("data").get("tasks").get(task_name) + + animation_start = frame_start + animation_end = frame_end + + include_handles = include_handles_settings["include_handles_default"] + for item in include_handles_settings["per_task_type"]: + if current_task["type"] in item["task_type"]: + include_handles = item["include_handles"] + break + if include_handles: + animation_start -= int(handle_start) + animation_end += int(handle_end) + + frame_range["animationStart"] = animation_start + frame_range["animationEnd"] = animation_end + + return frame_range def reset_frame_range(playback=True, render=True, fps=True): @@ -2219,18 +2232,19 @@ def reset_frame_range(playback=True, render=True, fps=True): ) set_scene_fps(fps) - frame_range = get_frame_range() - - frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) - frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) + frame_range = get_frame_range(include_animation_range=True) + frame_start = frame_range["frameStart"] + frame_end = frame_range["frameEnd"] + animation_start = frame_range["animationStart"] + animation_end = frame_range["animationEnd"] if playback: - cmds.playbackOptions(minTime=frame_start) - cmds.playbackOptions(maxTime=frame_end) - cmds.playbackOptions(animationStartTime=frame_start) - cmds.playbackOptions(animationEndTime=frame_end) - cmds.playbackOptions(minTime=frame_start) - cmds.playbackOptions(maxTime=frame_end) + cmds.playbackOptions( + minTime=frame_start, + maxTime=frame_end, + animationStartTime=animation_start, + animationEndTime=animation_end + ) cmds.currentTime(frame_start) if render: From 8c1abf2b5b96dd3ed875b717f240402b14f71711 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:30:02 +0200 Subject: [PATCH 298/918] Allow potential case that frame range might not be defined on an asset. - Warning will still be printed from `get_frame_range` function --- openpype/hosts/maya/api/lib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index a78ac184c2..e78da3d801 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2233,6 +2233,10 @@ def reset_frame_range(playback=True, render=True, fps=True): set_scene_fps(fps) frame_range = get_frame_range(include_animation_range=True) + if not frame_range: + # No frame range data found for asset + return + frame_start = frame_range["frameStart"] frame_end = frame_range["frameEnd"] animation_start = frame_range["animationStart"] From 3c801ca1187ac3d1cf1d0da50cf27eceaae5fa30 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:39:07 +0200 Subject: [PATCH 299/918] Fix imports --- openpype/hosts/maya/api/setdress.py | 8 +++----- openpype/hosts/maya/plugins/load/load_assembly.py | 15 ++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index 159bfe9eb3..0bb1f186eb 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -28,7 +28,9 @@ from openpype.pipeline import ( ) from openpype.hosts.maya.api.lib import ( matrix_equals, - unique_namespace + unique_namespace, + get_container_transforms, + DEFAULT_MATRIX ) log = logging.getLogger("PackageLoader") @@ -183,8 +185,6 @@ def _add(instance, representation_id, loaders, namespace, root="|"): """ - from openpype.hosts.maya.lib import get_container_transforms - # Process within the namespace with namespaced(namespace, new=False) as namespace: @@ -379,8 +379,6 @@ def update_scene(set_container, containers, current_data, new_data, new_file): """ - from openpype.hosts.maya.lib import DEFAULT_MATRIX, get_container_transforms - set_namespace = set_container['namespace'] project_name = legacy_io.active_project() diff --git a/openpype/hosts/maya/plugins/load/load_assembly.py b/openpype/hosts/maya/plugins/load/load_assembly.py index 902f38695c..275f21be5d 100644 --- a/openpype/hosts/maya/plugins/load/load_assembly.py +++ b/openpype/hosts/maya/plugins/load/load_assembly.py @@ -1,8 +1,14 @@ +import maya.cmds as cmds + from openpype.pipeline import ( load, remove_container ) +from openpype.hosts.maya.api.pipeline import containerise +from openpype.hosts.maya.api.lib import unique_namespace +from openpype.hosts.maya.api import setdress + class AssemblyLoader(load.LoaderPlugin): @@ -16,9 +22,6 @@ class AssemblyLoader(load.LoaderPlugin): def load(self, context, name, namespace, data): - from openpype.hosts.maya.api.pipeline import containerise - from openpype.hosts.maya.api.lib import unique_namespace - asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", @@ -26,8 +29,6 @@ class AssemblyLoader(load.LoaderPlugin): suffix="_", ) - from openpype.hosts.maya.api import setdress - containers = setdress.load_package( filepath=self.fname, name=name, @@ -50,15 +51,11 @@ class AssemblyLoader(load.LoaderPlugin): def update(self, container, representation): - from openpype import setdress return setdress.update_package(container, representation) def remove(self, container): """Remove all sub containers""" - from openpype import setdress - import maya.cmds as cmds - # Remove all members member_containers = setdress.get_contained_containers(container) for member_container in member_containers: From 0ad5442cd4280d6ee63deaf9cde9b55c37d3fc35 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 10:52:04 +0200 Subject: [PATCH 300/918] Update openpype/hosts/maya/api/lib.py --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index e78da3d801..c3de2c327f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2185,7 +2185,7 @@ def get_frame_range(include_animation_range=False): "frameStart": frame_start, "frameEnd": frame_end, "handleStart": handle_start, - "handleEnd": handle_end, + "handleEnd": handle_end } if include_animation_range: # The animation range values are only included to define whether From 839377696b970410b131bcd2129f5458dda2e56d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 15:29:20 +0200 Subject: [PATCH 301/918] Implement `switch` method on loaders --- openpype/hosts/houdini/plugins/load/load_alembic.py | 3 +++ openpype/hosts/houdini/plugins/load/load_alembic_archive.py | 3 +++ openpype/hosts/houdini/plugins/load/load_bgeo.py | 3 +++ openpype/hosts/houdini/plugins/load/load_camera.py | 3 +++ openpype/hosts/houdini/plugins/load/load_image.py | 3 +++ openpype/hosts/houdini/plugins/load/load_usd_layer.py | 3 +++ openpype/hosts/houdini/plugins/load/load_usd_reference.py | 3 +++ openpype/hosts/houdini/plugins/load/load_vdb.py | 3 +++ 8 files changed, 24 insertions(+) diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py index 96e666b255..c6f0ebf2f9 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic.py @@ -104,3 +104,6 @@ class AbcLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py index b960073e12..47d2e1b896 100644 --- a/openpype/hosts/houdini/plugins/load/load_alembic_archive.py +++ b/openpype/hosts/houdini/plugins/load/load_alembic_archive.py @@ -73,3 +73,6 @@ class AbcArchiveLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_bgeo.py b/openpype/hosts/houdini/plugins/load/load_bgeo.py index b298d423bc..86e8675c02 100644 --- a/openpype/hosts/houdini/plugins/load/load_bgeo.py +++ b/openpype/hosts/houdini/plugins/load/load_bgeo.py @@ -106,3 +106,6 @@ class BgeoLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py index 059ad11a76..6365508f4e 100644 --- a/openpype/hosts/houdini/plugins/load/load_camera.py +++ b/openpype/hosts/houdini/plugins/load/load_camera.py @@ -192,3 +192,6 @@ class CameraLoader(load.LoaderPlugin): new_node.moveToGoodPosition() return new_node + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py index c78798e58a..26bc569c53 100644 --- a/openpype/hosts/houdini/plugins/load/load_image.py +++ b/openpype/hosts/houdini/plugins/load/load_image.py @@ -125,3 +125,6 @@ class ImageLoader(load.LoaderPlugin): prefix, padding, suffix = first_fname.rsplit(".", 2) fname = ".".join([prefix, "$F{}".format(len(padding)), suffix]) return os.path.join(root, fname).replace("\\", "/") + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_usd_layer.py b/openpype/hosts/houdini/plugins/load/load_usd_layer.py index 2e5079925b..1f0ec25128 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_layer.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_layer.py @@ -79,3 +79,6 @@ class USDSublayerLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_usd_reference.py b/openpype/hosts/houdini/plugins/load/load_usd_reference.py index c4371db39b..f66d05395e 100644 --- a/openpype/hosts/houdini/plugins/load/load_usd_reference.py +++ b/openpype/hosts/houdini/plugins/load/load_usd_reference.py @@ -79,3 +79,6 @@ class USDReferenceLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/openpype/hosts/houdini/plugins/load/load_vdb.py b/openpype/hosts/houdini/plugins/load/load_vdb.py index c558a7a0e7..87900502c5 100644 --- a/openpype/hosts/houdini/plugins/load/load_vdb.py +++ b/openpype/hosts/houdini/plugins/load/load_vdb.py @@ -102,3 +102,6 @@ class VdbLoader(load.LoaderPlugin): node = container["node"] node.destroy() + + def switch(self, container, representation): + self.update(container, representation) From 1a10e0fc74e8d79bada3d1a2ab595250b6d43a92 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 18:01:43 +0200 Subject: [PATCH 302/918] Hide animation instance in creator + add inventory action to recreate animation publish instance for loaded rigs --- openpype/hosts/maya/api/lib.py | 52 +++++++++++++++++++ .../maya/plugins/create/create_animation.py | 6 +++ .../rig_recreate_animation_instance.py | 37 +++++++++++++ .../hosts/maya/plugins/load/load_reference.py | 44 ++-------------- .../defaults/project_settings/maya.json | 2 +- 5 files changed, 101 insertions(+), 40 deletions(-) create mode 100644 openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 61ea3d59df..db8195ac40 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -32,6 +32,10 @@ from openpype.pipeline import ( load_container, registered_host, ) +from openpype.pipeline.create import ( + legacy_create, + get_legacy_creator_by_name, +) from openpype.pipeline.context_tools import ( get_current_asset_name, get_current_project_asset, @@ -3913,3 +3917,51 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): capture_preset = plugin_settings["capture_preset"] return capture_preset or {} + + +def create_rig_animation_instance(nodes, context, namespace, log=None): + """Create an animation publish instance for loaded rigs. + + See the RecreateRigAnimationInstance inventory action on how to use this + for loaded rig containers. + + Arguments: + nodes (list): Member nodes of the rig instance. + context (dict): Representation context of the rig container + namespace (str): Namespace of the rig container + log (logging.Logger, optional): Logger to log to if provided + + Returns: + None + + """ + output = next((node for node in nodes if + node.endswith("out_SET")), None) + controls = next((node for node in nodes if + node.endswith("controls_SET")), None) + + assert output, "No out_SET in rig, this is a bug." + assert controls, "No controls_SET in rig, this is a bug." + + # Find the roots amongst the loaded nodes + roots = cmds.ls(nodes, assemblies=True, long=True) or \ + get_highest_in_hierarchy(nodes) + assert roots, "No root nodes in rig, this is a bug." + + asset = legacy_io.Session["AVALON_ASSET"] + dependency = str(context["representation"]["_id"]) + + if log: + log.info("Creating subset: {}".format(namespace)) + + # Create the animation instance + creator_plugin = get_legacy_creator_by_name("CreateAnimation") + with maintained_selection(): + cmds.select([output, controls] + roots, noExpand=True) + legacy_create( + creator_plugin, + name=namespace, + asset=asset, + options={"useSelection": True}, + data={"dependencies": dependency} + ) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index f992ff2c1a..095cbcdd64 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -7,6 +7,12 @@ from openpype.hosts.maya.api import ( class CreateAnimation(plugin.Creator): """Animation output for character rigs""" + # We hide the animation creator from the UI since the creation of it + # is automated upon loading a rig. There's an inventory action to recreate + # it for loaded rigs if by chance someone deleted the animation instance. + # Note: This setting is actually applied from project settings + enabled = False + name = "animationDefault" label = "Animation" family = "animation" diff --git a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py new file mode 100644 index 0000000000..fe4a123dfe --- /dev/null +++ b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -0,0 +1,37 @@ +from openpype.pipeline import ( + InventoryAction, + get_representation_context +) +from openpype.hosts.maya.api.lib import ( + create_rig_animation_instance, + get_container_members, +) + + +class RecreateRigAnimationInstance(InventoryAction): + """Recreate animation publish instance for loaded rigs""" + + label = "Recreate rig animation instance" + icon = "industry" + color = "#55DDAA" + + @staticmethod + def is_compatible(container): + return ( + container.get("loader") == "ReferenceLoader" + and container.get("name", "").startswith("rig") + ) + + def process(self, containers): + + for container in containers: + # todo: delete an existing entry if it exist or skip creation + + namespace = container["namespace"] + representation_id = container["representation"] + context = get_representation_context(representation_id) + nodes = get_container_members(container) + + create_rig_animation_instance(nodes, context, namespace) + + diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index c2b321b789..0dbdb03bb7 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -4,16 +4,12 @@ import contextlib from maya import cmds from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io -from openpype.pipeline.create import ( - legacy_create, - get_legacy_creator_by_name, -) import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import ( maintained_selection, get_container_members, - parent_nodes + parent_nodes, + create_rig_animation_instance ) @@ -114,9 +110,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): icon = "code-fork" color = "orange" - # Name of creator class that will be used to create animation instance - animation_creator_name = "CreateAnimation" - def process_reference(self, context, name, namespace, options): import maya.cmds as cmds @@ -220,37 +213,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): self._lock_camera_transforms(members) def _post_process_rig(self, name, namespace, context, options): - - output = next((node for node in self if - node.endswith("out_SET")), None) - controls = next((node for node in self if - node.endswith("controls_SET")), None) - - assert output, "No out_SET in rig, this is a bug." - assert controls, "No controls_SET in rig, this is a bug." - - # Find the roots amongst the loaded nodes - roots = cmds.ls(self[:], assemblies=True, long=True) - assert roots, "No root nodes in rig, this is a bug." - - asset = legacy_io.Session["AVALON_ASSET"] - dependency = str(context["representation"]["_id"]) - - self.log.info("Creating subset: {}".format(namespace)) - - # Create the animation instance - creator_plugin = get_legacy_creator_by_name( - self.animation_creator_name + nodes = self[:] + create_rig_animation_instance( + nodes, context, namespace, log=self.log ) - with maintained_selection(): - cmds.select([output, controls] + roots, noExpand=True) - legacy_create( - creator_plugin, - name=namespace, - asset=asset, - options={"useSelection": True}, - data={"dependencies": dependency} - ) def _lock_camera_transforms(self, nodes): cameras = cmds.ls(nodes, type="camera") diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 5960547d46..91712e6672 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -554,7 +554,7 @@ "publish_mip_map": true }, "CreateAnimation": { - "enabled": true, + "enabled": false, "write_color_sets": false, "write_face_sets": false, "include_parent_hierarchy": false, From fbc0430bb21f0c6af39e907986873cfac83d625f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 18:06:25 +0200 Subject: [PATCH 303/918] Tweak color + icon --- .../maya/plugins/inventory/rig_recreate_animation_instance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py index fe4a123dfe..90b4d3eab8 100644 --- a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py +++ b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -12,8 +12,8 @@ class RecreateRigAnimationInstance(InventoryAction): """Recreate animation publish instance for loaded rigs""" label = "Recreate rig animation instance" - icon = "industry" - color = "#55DDAA" + icon = "wrench" + color = "#888888" @staticmethod def is_compatible(container): From 5b7d419e18087354b576c1f73452df2c74a170f1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 18:07:08 +0200 Subject: [PATCH 304/918] Cosmetics --- openpype/hosts/maya/api/lib.py | 6 ++++-- .../plugins/inventory/rig_recreate_animation_instance.py | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index db8195ac40..f3c079506b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3944,8 +3944,10 @@ def create_rig_animation_instance(nodes, context, namespace, log=None): assert controls, "No controls_SET in rig, this is a bug." # Find the roots amongst the loaded nodes - roots = cmds.ls(nodes, assemblies=True, long=True) or \ - get_highest_in_hierarchy(nodes) + roots = ( + cmds.ls(nodes, assemblies=True, long=True) or + get_highest_in_hierarchy(nodes) + ) assert roots, "No root nodes in rig, this is a bug." asset = legacy_io.Session["AVALON_ASSET"] diff --git a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py index 90b4d3eab8..39bc59fbbf 100644 --- a/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py +++ b/openpype/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -33,5 +33,3 @@ class RecreateRigAnimationInstance(InventoryAction): nodes = get_container_members(container) create_rig_animation_instance(nodes, context, namespace) - - From b82279f9d7e47e18c9b7f7e8e9755b8f658a4dac Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 21:58:16 +0200 Subject: [PATCH 305/918] Fix default so namespace behaves like before #4511 --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 5960547d46..a535f8d4c9 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1459,7 +1459,7 @@ ] }, "reference_loader": { - "namespace": "{asset_name}_{subset}_##", + "namespace": "{asset_name}_{subset}_##_", "group_name": "_GRP" } }, From b3044398fc9181db2d2230f9f0f5cc1de7e9d297 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 19 Apr 2023 23:43:47 +0200 Subject: [PATCH 306/918] Improve validation report + allow to select the invalid node --- openpype/hosts/houdini/api/action.py | 46 +++++++ .../publish/help/validate_vdb_output_node.xml | 25 ++-- .../publish/validate_vdb_output_node.py | 112 ++++++++++++------ 3 files changed, 135 insertions(+), 48 deletions(-) create mode 100644 openpype/hosts/houdini/api/action.py diff --git a/openpype/hosts/houdini/api/action.py b/openpype/hosts/houdini/api/action.py new file mode 100644 index 0000000000..27e8ce55bb --- /dev/null +++ b/openpype/hosts/houdini/api/action.py @@ -0,0 +1,46 @@ +import pyblish.api +import hou + +from openpype.pipeline.publish import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid nodes in Maya when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + hou.clearAllSelected() + if invalid: + self.log.info("Selecting invalid nodes: {}".format( + ", ".join(node.path() for node in invalid) + )) + for node in invalid: + node.setSelected(True) + node.setCurrent(True) + else: + self.log.info("No invalid nodes found.") diff --git a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml index 0f92560bf7..eb83bfffe3 100644 --- a/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml +++ b/openpype/hosts/houdini/plugins/publish/help/validate_vdb_output_node.xml @@ -1,21 +1,28 @@ -Scene setting +Invalid VDB -## Invalid input node +## Invalid VDB output + +All primitives of the output geometry must be VDBs, no other primitive +types are allowed. That means that regardless of the amount of VDBs in the +geometry it will have an equal amount of VDBs, points, primitives and +vertices since each VDB primitive is one point, one vertex and one VDB. + +This validation only checks the geometry on the first frame of the export +frame range. + -VDB input must have the same number of VDBs, points, primitives and vertices as output. -### __Detailed Info__ (optional) +### Detailed Info + +ROP node `{rop_path}` is set to export SOP path `{sop_path}`. + +{message} -A VDB is an inherited type of Prim, holds the following data: - - Primitives: 1 - - Points: 1 - - Vertices: 1 - - VDBs: 1 \ No newline at end of file diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index b2b5c63799..3fa75e5822 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import pyblish.api import hou + from openpype.pipeline import PublishXmlValidationError +from openpype.hosts.houdini.api.action import SelectInvalidAction def group_consecutive_numbers(nums): @@ -40,8 +42,13 @@ def group_consecutive_numbers(nums): class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. - Regardless of the amount of VDBs create the output will need to have an - equal amount of VDBs, points, primitives and vertices + All primitives of the output geometry must be VDBs, no other primitive + types are allowed. That means that regardless of the amount of VDBs in the + geometry it will have an equal amount of VDBs, points, primitives and + vertices since each VDB primitive is one point, one vertex and one VDB. + + This validation only checks the geometry on the first frame of the export + frame range for optimization purposes. A VDB is an inherited type of Prim, holds the following data: - Primitives: 1 @@ -55,64 +62,91 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): families = ["vdbcache"] hosts = ["houdini"] label = "Validate Output Node (VDB)" + actions = [SelectInvalidAction] def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: + invalid_nodes, message = self.get_invalid_with_message(instance) + if invalid_nodes: raise PublishXmlValidationError( self, - "Node connected to the output node is not of type VDB." + "Node connected to the output node is not of type VDB.", + formatting_data={ + "message": message, + "rop_path": instance.data.get("instance_node"), + "sop_path": instance.data.get("output_node") + } ) @classmethod - def get_invalid(cls, instance): + def get_invalid_with_message(cls, instance): node = instance.data.get("output_node") if node is None: - cls.log.error( + instance_node = instance.data.get("instance_node") + error = ( "SOP path is not correctly set on " - "ROP node '%s'." % instance.data.get("instance_node") + "ROP node `%s`." % instance_node ) - return [instance] + return [instance_node, error] frame = instance.data.get("frameStart", 0) + node.cook(force=True, frame_range=(frame, frame)) geometry = node.geometryAtFrame(frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? - cls.log.error( + error = ( "SOP node has no geometry data. " "Is it cooked? %s" % node.path() ) - return [node] + return [node, error] - prims = geometry.prims() - nr_of_prims = len(prims) - - # All primitives must be hou.VDB - invalid_prims = [] - for prim in prims: - if not isinstance(prim, hou.VDB): - invalid_prims.append(prim) - if invalid_prims: - # Log prim numbers as consecutive ranges so logging isn't very - # slow for large number of primitives - cls.log.error( - "Found non-VDB primitives for '{}', " - "primitive indices: {}".format( - node.path(), - ", ".join(group_consecutive_numbers( - prim.number() for prim in invalid_prims - )) - ) + num_prims = geometry.intrinsicValue("primitivecount") + num_points = geometry.intrinsicValue("pointcount") + if num_prims == 0 and num_points == 0: + # Since we are only checking the first frame it doesn't mean there + # won't be VDB prims in a few frames. As such we'll assume for now + # the user knows what he or she is doing + cls.log.warning( + "SOP node `{}` has no primitives on start frame {}. " + "Validation is skipped and it is assumed elsewhere in the " + "frame range VDB prims and only VDB prims will exist." + "".format(node.path(), int(frame)) ) - return [instance] + return [None, None] - nr_of_points = len(geometry.points()) - if nr_of_points != nr_of_prims: - cls.log.error("The number of primitives and points do not match") - return [instance] + num_vdb_prims = geometry.countPrimType(hou.primType.VDB) + cls.log.debug("Detected {} VDB primitives".format(num_vdb_prims)) + if num_prims != num_vdb_prims: + # There's at least one primitive that is not a VDB. + # Search them and report them to the artist. + prims = geometry.prims() + invalid_prims = [prim for prim in prims + if not isinstance(prim, hou.VDB)] + if invalid_prims: + # Log prim numbers as consecutive ranges so logging isn't very + # slow for large number of primitives + error = ( + "Found non-VDB primitives for `{}`. " + "Primitive indices {} are not VDB primitives.".format( + node.path(), + ", ".join(group_consecutive_numbers( + prim.number() for prim in invalid_prims + )) + ) + ) + return [node, error] - for prim in prims: - if prim.numVertices() != 1: - cls.log.error("Found primitive with more than 1 vertex!") - return [instance] + if num_points != num_vdb_prims: + # We have points unrelated to the VDB primitives. + error = ( + "The number of primitives and points do not match in '{}'. " + "This likely means you have unconnected points, which we do " + "not allow in the VDB output.".format(node.path())) + return [node, error] + + return [None, None] + + @classmethod + def get_invalid(cls, instance): + nodes, _ = cls.get_invalid_with_message(instance) + return nodes From bb24b823649c3cf124fafb9c465a9fd5709d193a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:05:07 +0200 Subject: [PATCH 307/918] Fix type bug --- .../houdini/plugins/publish/validate_vdb_output_node.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 3fa75e5822..def9595e9a 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -67,13 +67,18 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): def process(self, instance): invalid_nodes, message = self.get_invalid_with_message(instance) if invalid_nodes: + + # instance_node is str, but output_node is hou.Node so we convert + output = instance.data.get("output_node") + output_path = output.path() if output else None + raise PublishXmlValidationError( self, "Node connected to the output node is not of type VDB.", formatting_data={ "message": message, "rop_path": instance.data.get("instance_node"), - "sop_path": instance.data.get("output_node") + "sop_path": output_path } ) From 9484bd4a51c465957b49c83c915b0995f1a4de98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:05:47 +0200 Subject: [PATCH 308/918] Force geometry update, otherwise manual update mode will fail to get the geometry correctly --- .../publish/validate_vdb_output_node.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index def9595e9a..43da4b0528 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import contextlib + import pyblish.api import hou @@ -39,6 +41,23 @@ def group_consecutive_numbers(nums): yield _result(start, end) +@contextlib.contextmanager +def update_mode_context(mode): + original = hou.updateModeSetting() + try: + hou.setUpdateMode(mode) + yield + finally: + hou.setUpdateMode(original) + + +def get_geometry_at_frame(sop_node, frame, force=True): + """Return geometry at frame but force a cooked value.""" + with update_mode_context(hou.updateMode.AutoUpdate): + sop_node.cook(force=force, frame_range=(frame, frame)) + return sop_node.geometryAtFrame(frame) + + class ValidateVDBOutputNode(pyblish.api.InstancePlugin): """Validate that the node connected to the output node is of type VDB. @@ -95,8 +114,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): return [instance_node, error] frame = instance.data.get("frameStart", 0) - node.cook(force=True, frame_range=(frame, frame)) - geometry = node.geometryAtFrame(frame) + geometry = get_geometry_at_frame(node, frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? error = ( From cbd88a616c0420448c3cb3b9028d6e15482a314c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:06:10 +0200 Subject: [PATCH 309/918] Tweak formatting, fix type bug for instance node --- .../houdini/plugins/publish/validate_vdb_output_node.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index 43da4b0528..bd1fb0b887 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -109,17 +109,17 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): instance_node = instance.data.get("instance_node") error = ( "SOP path is not correctly set on " - "ROP node `%s`." % instance_node + "ROP node `{}`.".format(instance_node) ) - return [instance_node, error] + return [hou.node(instance_node), error] frame = instance.data.get("frameStart", 0) geometry = get_geometry_at_frame(node, frame) if geometry is None: # No geometry data on this node, maybe the node hasn't cooked? error = ( - "SOP node has no geometry data. " - "Is it cooked? %s" % node.path() + "SOP node `{}` has no geometry data. " + "Was it unable to cook?".format(node.path()) ) return [node, error] From 175db5407403dcb8e0b3a3f7a49b39463b2ceb56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 00:09:50 +0200 Subject: [PATCH 310/918] Tweak logged message for non-UI report --- .../hosts/houdini/plugins/publish/validate_vdb_output_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py index bd1fb0b887..674782179c 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py +++ b/openpype/hosts/houdini/plugins/publish/validate_vdb_output_node.py @@ -93,7 +93,7 @@ class ValidateVDBOutputNode(pyblish.api.InstancePlugin): raise PublishXmlValidationError( self, - "Node connected to the output node is not of type VDB.", + "Invalid VDB content: {}".format(message), formatting_data={ "message": message, "rop_path": instance.data.get("instance_node"), From 16b169205ef8816099d1d94ff263069298d406cc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 01:29:51 +0200 Subject: [PATCH 311/918] Allow camera path to not be set correctly in review instance until validation --- .../plugins/publish/collect_review_data.py | 10 +++--- .../plugins/publish/validate_scene_review.py | 33 ++++++++++++++----- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py index e321dcb2fa..3ab93dc491 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py @@ -18,6 +18,9 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): instance.data["handleStart"] = 0 instance.data["handleEnd"] = 0 + # Enable ftrack functionality + instance.data.setdefault("families", []).append('ftrack') + # Get the camera from the rop node to collect the focal length ropnode_path = instance.data["instance_node"] ropnode = hou.node(ropnode_path) @@ -25,8 +28,9 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): camera_path = ropnode.parm("camera").eval() camera_node = hou.node(camera_path) if not camera_node: - raise RuntimeError("No valid camera node found on review node: " - "{}".format(camera_path)) + self.log.warning("No valid camera node found on review node: " + "{}".format(camera_path)) + return # Collect focal length. focal_length_parm = camera_node.parm("focal") @@ -48,5 +52,3 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): # Store focal length in `burninDataMembers` burnin_members = instance.data.setdefault("burninDataMembers", {}) burnin_members["focalLength"] = focal_length - - instance.data.setdefault("families", []).append('ftrack') diff --git a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py index ade01d4b90..58d8a37240 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py +++ b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py @@ -16,13 +16,17 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): label = "Scene Setting for review" def process(self, instance): - invalid = self.get_invalid_scene_path(instance) report = [] + instance_node = hou.node(instance.data.get("instance_node")) + + invalid = self.get_invalid_scene_path(instance_node) if invalid: - report.append( - "Scene path does not exist: '%s'" % invalid[0], - ) + report.append(invalid) + + invalid = self.get_invalid_camera_path(instance_node) + if invalid: + report.append(invalid) invalid = self.get_invalid_resolution(instance) if invalid: @@ -33,13 +37,24 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): "\n\n".join(report), title=self.label) - def get_invalid_scene_path(self, instance): - - node = hou.node(instance.data.get("instance_node")) - scene_path_parm = node.parm("scenepath") + def get_invalid_scene_path(self, rop_node): + scene_path_parm = rop_node.parm("scenepath") scene_path_node = scene_path_parm.evalAsNode() if not scene_path_node: - return [scene_path_parm.evalAsString()] + path = scene_path_parm.evalAsString() + return "Scene path does not exist: '{}'".format(path) + + def get_invalid_camera_path(self, rop_node): + camera_path_parm = rop_node.parm("camera") + camera_node = camera_path_parm.evalAsNode() + path = camera_path_parm.evalAsString() + if not camera_node: + return "Camera path does not exist: '{}'".format(path) + type_name = camera_node.type().name() + if type_name != "cam": + return "Camera path is not a camera: '{}' (type: {})".format( + path, type_name + ) def get_invalid_resolution(self, instance): node = hou.node(instance.data.get("instance_node")) From 0424f66164717b5127f89612f0d83b7865bece63 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 01:34:01 +0200 Subject: [PATCH 312/918] Re-use instance node --- .../houdini/plugins/publish/validate_scene_review.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py index 58d8a37240..a44b7e1597 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_scene_review.py +++ b/openpype/hosts/houdini/plugins/publish/validate_scene_review.py @@ -28,7 +28,7 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): if invalid: report.append(invalid) - invalid = self.get_invalid_resolution(instance) + invalid = self.get_invalid_resolution(instance_node) if invalid: report.extend(invalid) @@ -56,18 +56,17 @@ class ValidateSceneReview(pyblish.api.InstancePlugin): path, type_name ) - def get_invalid_resolution(self, instance): - node = hou.node(instance.data.get("instance_node")) + def get_invalid_resolution(self, rop_node): # The resolution setting is only used when Override Camera Resolution # is enabled. So we skip validation if it is disabled. - override = node.parm("tres").eval() + override = rop_node.parm("tres").eval() if not override: return invalid = [] - res_width = node.parm("res1").eval() - res_height = node.parm("res2").eval() + res_width = rop_node.parm("res1").eval() + res_height = rop_node.parm("res2").eval() if res_width == 0: invalid.append("Override Resolution width is set to zero.") if res_height == 0: From a8e5a0c5fc37a1236f70dc5c890e3160f8b5f6f3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 20 Apr 2023 12:10:55 +0200 Subject: [PATCH 313/918] :art: calculate hash for tx texture --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 520951a5e6..3cc95a0b2e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -280,7 +280,7 @@ class MakeTX(TextureProcessor): # Do nothing if the source file is already a .tx file. return TextureResult( path=source, - file_hash=None, # todo: unknown texture hash? + file_hash=source_hash(source), colorspace=colorspace, transfer_mode=COPY ) From ef192d3edd1da53736ed54f176e662923c718e7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:16:40 +0200 Subject: [PATCH 314/918] Add `get_network_categories` to `CreateUSD` --- openpype/hosts/houdini/plugins/create/create_usd.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_usd.py b/openpype/hosts/houdini/plugins/create/create_usd.py index 51ed8237c5..e05d254863 100644 --- a/openpype/hosts/houdini/plugins/create/create_usd.py +++ b/openpype/hosts/houdini/plugins/create/create_usd.py @@ -3,6 +3,8 @@ from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +import hou + class CreateUSD(plugin.HoudiniCreator): """Universal Scene Description""" @@ -13,7 +15,6 @@ class CreateUSD(plugin.HoudiniCreator): enabled = False def create(self, subset_name, instance_data, pre_create_data): - import hou # noqa instance_data.pop("active", None) instance_data.update({"node_type": "usd"}) @@ -43,3 +44,9 @@ class CreateUSD(plugin.HoudiniCreator): "id", ] self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + return [ + hou.ropNodeTypeCategory(), + hou.lopNodeTypeCategory() + ] From 96b1b3e19d6a3e7dd7387b4477224c208eeaba90 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:28:44 +0200 Subject: [PATCH 315/918] Implement `get_network_categories` on Houdini base creator plugin --- .../hosts/houdini/api/creator_node_shelves.py | 13 ++++++++----- openpype/hosts/houdini/api/plugin.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index cd14090104..8a15d902b5 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -172,16 +172,19 @@ def install(): log.debug("Writing OpenPype Creator nodes to shelf: {}".format(filepath)) tools = [] - default_network_categories = [hou.ropNodeTypeCategory()] with shelves_change_block(): for identifier, creator in create_context.manual_creators.items(): # Allow the creator plug-in itself to override the categories # for where they are shown with `Creator.get_network_categories()` - if hasattr(creator, "get_network_categories"): - network_categories = creator.get_network_categories() - else: - network_categories = default_network_categories + if not hasattr(creator, "get_network_categories"): + log.debug("Creator {} has no `get_network_categories` method " + "and will not be added to TAB search.") + continue + + network_categories = creator.get_network_categories() + if not network_categories: + continue key = "openpype_create.{}".format(identifier) log.debug(f"Registering {key}") diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 340a7f0770..1e7eaa7e22 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -276,3 +276,19 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): color = hou.Color((0.616, 0.871, 0.769)) node.setUserData('nodeshape', shape) node.setColor(color) + + def get_network_categories(self): + """Return in which network view type this creator should show. + + The node type categories returned here will be used to define where + the creator will show up in the TAB search for nodes in Houdini's + Network View. + + This can be overridden in inherited classes to define where that + particular Creator should be visible in the TAB search. + + Returns: + list: List of houdini node type categories + + """ + return [hou.ropNodeTypeCategory()] From 3cbeda17a8cfefb31fdf2b35314b53779334867c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:29:08 +0200 Subject: [PATCH 316/918] Support auto `null` node in LOPs --- openpype/hosts/houdini/api/creator_node_shelves.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 8a15d902b5..96e843b3a9 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -20,6 +20,7 @@ from openpype.resources import get_openpype_icon_filepath import hou import stateutils import soptoolutils +import loptoolutils import cop2toolutils @@ -88,7 +89,8 @@ def create_interactive(creator_identifier, **kwargs): tool_fn = { hou.sopNodeTypeCategory(): soptoolutils.genericTool, - hou.cop2NodeTypeCategory(): cop2toolutils.genericTool + hou.cop2NodeTypeCategory(): cop2toolutils.genericTool, + hou.lopNodeTypeCategory(): loptoolutils.genericTool }.get(pwd.childTypeCategory()) if tool_fn is not None: From 0941469c248c5d0503c8c40fadb0b1a280b55d94 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:31:37 +0200 Subject: [PATCH 317/918] Move variable to module level --- openpype/hosts/houdini/api/creator_node_shelves.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 96e843b3a9..1cc28add86 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -26,6 +26,13 @@ import cop2toolutils log = logging.getLogger(__name__) +CATEGORY_GENERIC_TOOL = { + hou.sopNodeTypeCategory(): soptoolutils.genericTool, + hou.cop2NodeTypeCategory(): cop2toolutils.genericTool, + hou.lopNodeTypeCategory(): loptoolutils.genericTool +} + + CREATE_SCRIPT = """ from openpype.hosts.houdini.api.creator_node_shelves import create_interactive create_interactive("{identifier}", **kwargs) @@ -87,12 +94,7 @@ def create_interactive(creator_identifier, **kwargs): host_name=context.host_name ) - tool_fn = { - hou.sopNodeTypeCategory(): soptoolutils.genericTool, - hou.cop2NodeTypeCategory(): cop2toolutils.genericTool, - hou.lopNodeTypeCategory(): loptoolutils.genericTool - }.get(pwd.childTypeCategory()) - + tool_fn = CATEGORY_GENERIC_TOOL.get(pwd.childTypeCategory()) if tool_fn is not None: out_null = tool_fn(kwargs, "null") out_null.setName("OUT_{}".format(subset_name), unique_name=True) From 9012b9f18f45562c03ecbf7c9d1ac807a0019f93 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 12:34:14 +0200 Subject: [PATCH 318/918] Add todo for later --- openpype/hosts/houdini/api/creator_node_shelves.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/houdini/api/creator_node_shelves.py b/openpype/hosts/houdini/api/creator_node_shelves.py index 1cc28add86..7c6122cffe 100644 --- a/openpype/hosts/houdini/api/creator_node_shelves.py +++ b/openpype/hosts/houdini/api/creator_node_shelves.py @@ -80,6 +80,10 @@ def create_interactive(creator_identifier, **kwargs): raise RuntimeError("Invalid creator identifier: " "{}".format(creator_identifier)) + # TODO: Once more elaborate unique create behavior should exist per Creator + # instead of per network editor area then we should move this from here + # to a method on the Creators for which this could be the default + # implementation. pane = stateutils.activePane(kwargs) if isinstance(pane, hou.NetworkEditor): pwd = pane.pwd() From 1ab4243d58fc24e932edb10dd932719561d033fc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 14:03:29 +0200 Subject: [PATCH 319/918] Tweak rig publish + load documentation, add documentation for Recreate rig animation instance action --- website/docs/artist_hosts_maya.md | 70 +++++++++++++----- ...ory_action_recreate_animation_instance.png | Bin 0 -> 46819 bytes 2 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 website/docs/assets/maya-inventory_action_recreate_animation_instance.png diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 0a551f0213..6b2abcb58b 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -238,12 +238,12 @@ For resolution and frame range, use **OpenPype → Set Frame Range** and Creating and publishing rigs with OpenPype follows similar workflow as with other data types. Create your rig and mark parts of your hierarchy in sets to -help OpenPype validators and extractors to check it and publish it. +help OpenPype validators and extractors to check and publish it. ### Preparing rig for publish When creating rigs, it is recommended (and it is in fact enforced by validators) -to separate bones or driving objects, their controllers and geometry so they are +to separate bone or driven objects, their controllers and geometry so they are easily managed. Currently OpenPype doesn't allow to publish model at the same time as its rig so for demonstration purposes, I'll first create simple model for robotic arm, just made out of simple boxes and I'll publish it. @@ -252,41 +252,48 @@ arm, just made out of simple boxes and I'll publish it. For more information about publishing models, see [Publishing models](artist_hosts_maya.md#publishing-models). -Now lets start with empty scene. Load your model - **OpenPype → Load...**, right +Now let's start with empty scene. Load your model - **OpenPype → Load...**, right click on it and select **Reference (abc)**. -I've created few bones and their controllers in two separate -groups - `rig_GRP` and `controls_GRP`. Naming is not important - just adhere to -your naming conventions. +I've created a few bones in `rig_GRP`, their controllers in `controls_GRP` and +placed the rig's output geometry in `geometry_GRP`. Naming of the groups is not important - just adhere to +your naming conventions. Then I parented everything into a single top group named `arm_rig`. -Then I've put everything into `arm_rig` group. - -When you've prepared your hierarchy, it's time to create *Rig instance* in OpenPype. -Select your whole rig hierarchy and go **OpenPype → Create...**. Select **Rig**. -Set is created in your scene to mark rig parts for export. Notice that it has -two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` +With the prepared hierarchy it is time to create a *Rig instance* in OpenPype. +Select the top group of your rig and go to **OpenPype → Create...**. Select **Rig**. +A publish set for your rig is created in your scene to mark rig parts for export. +Notice that it has two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` and geometry to `out_SET`. You should end up with something like this: ![Maya - Rig Hierarchy Example](assets/maya-rig_hierarchy_example.jpg) +:::note controls_SET and out_SET contents +It is totally allowed to put the `geometry_GRP` in the `out_SET` as opposed to +the individual meshes - it's even **recommended**. However, the `controls_SET` +requires the individual controls in it that the artist is supposed to animate +and manipulate so the publish validators can accurately check the rig's +controls. +::: + ### Publishing rigs -Publishing rig is done in same way as publishing everything else. Save your scene -and go **OpenPype → Publish**. When you run validation you'll mostly run at first into -few issues. Although number of them will seem to be intimidating at first, you'll -find out they are mostly minor things easily fixed. +Publishing rigs is done in a same way as publishing everything else. Save your scene +and go **OpenPype → Publish**. When you run validation you'll most likely run into +a few issues at first. Although a number of them will seem to be intimidating you +will find out they are mostly minor things, easily fixed and are there to optimize +your rig for consistency and safe usage by the artist. -* **Non Duplicate Instance Members (ID)** - This will most likely fail because when +- **Non Duplicate Instance Members (ID)** - This will most likely fail because when creating rigs, we usually duplicate few parts of it to reuse them. But duplication will duplicate also ID of original object and OpenPype needs every object to have unique ID. This is easily fixed by **Repair** action next to validator name. click on little up arrow on right side of validator name and select **Repair** form menu. -* **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as +- **Joints Hidden** - This is enforcing joints (bones) to be hidden for user as animator usually doesn't need to see them and they clutter his viewports. So well behaving rig should have them hidden. **Repair** action will help here also. -* **Rig Controllers** will check if there are no transforms on unlocked attributes +- **Rig Controllers** will check if there are no transforms on unlocked attributes of controllers. This is needed because animator should have ease way to reset rig to it's default position. It also check that those attributes doesn't have any incoming connections from other parts of scene to ensure that published rig doesn't @@ -297,6 +304,19 @@ have any missing dependencies. You can load rig with [Loader](artist_tools_loader). Go **OpenPype → Load...**, select your rig, right click on it and **Reference** it. +### Animation instances + +Whenever you load a rig an animation publish instance is automatically created +for it. This means that if you load a rig you don't need to create a pointcache +instance yourself to publish the geometry. This is all cleanly prepared for you +when loading a published rig. + +:::tip Missing animation instance for your loaded rig? +Did you accidentally delete the animation instance for a loaded rig? You can +recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance) +inventory action. +::: + ## Point caches OpenPype is using Alembic format for point caches. Workflow is very similar as other data types. @@ -646,3 +666,15 @@ Select 1 container of type `animation` or `pointcache`, then 1+ container of any The action searches the selected containers for 1 animation container of type `animation` or `pointcache`. This animation container will be connected to the rest of the selected containers. Matching geometries between containers is done by comparing the attribute `cbId`. The connection between geometries is done with a live blendshape. + +### Recreate rig animation instance + +This action can regenerate an animation instance for a loaded rig, for example +for when it was accidentally deleted by the user. + +![Maya - Inventory Action Recreate Rig Animation Instance](assets/maya-inventory_action_recreate_animation_instance.png) + +#### Usage + +Select 1 or more container of type `rig` for which you want to recreate the +animation instance. \ No newline at end of file diff --git a/website/docs/assets/maya-inventory_action_recreate_animation_instance.png b/website/docs/assets/maya-inventory_action_recreate_animation_instance.png new file mode 100644 index 0000000000000000000000000000000000000000..42a6f269648df6f9ef0420e96e3ab137c7321a0a GIT binary patch literal 46819 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�il6%M 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 HVbuQsK%Is3 literal 0 HcmV?d00001 From a8abe2dda8f46ea3d9bb73cd63655c0ed8b7fadd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 14:04:40 +0200 Subject: [PATCH 320/918] Fix typo --- website/docs/artist_hosts_maya.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 6b2abcb58b..e36ccb77d2 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -243,7 +243,7 @@ help OpenPype validators and extractors to check and publish it. ### Preparing rig for publish When creating rigs, it is recommended (and it is in fact enforced by validators) -to separate bone or driven objects, their controllers and geometry so they are +to separate bones or driven objects, their controllers and geometry so they are easily managed. Currently OpenPype doesn't allow to publish model at the same time as its rig so for demonstration purposes, I'll first create simple model for robotic arm, just made out of simple boxes and I'll publish it. @@ -257,11 +257,11 @@ click on it and select **Reference (abc)**. I've created a few bones in `rig_GRP`, their controllers in `controls_GRP` and placed the rig's output geometry in `geometry_GRP`. Naming of the groups is not important - just adhere to -your naming conventions. Then I parented everything into a single top group named `arm_rig`. +your naming conventions. Then I parented everything into a single top group named `arm_rig`. With the prepared hierarchy it is time to create a *Rig instance* in OpenPype. Select the top group of your rig and go to **OpenPype → Create...**. Select **Rig**. -A publish set for your rig is created in your scene to mark rig parts for export. +A publish set for your rig is created in your scene to mark rig parts for export. Notice that it has two subsets - `controls_SET` and `out_SET`. Put your controls into `controls_SET` and geometry to `out_SET`. You should end up with something like this: @@ -269,19 +269,19 @@ and geometry to `out_SET`. You should end up with something like this: :::note controls_SET and out_SET contents It is totally allowed to put the `geometry_GRP` in the `out_SET` as opposed to -the individual meshes - it's even **recommended**. However, the `controls_SET` +the individual meshes - it's even **recommended**. However, the `controls_SET` requires the individual controls in it that the artist is supposed to animate -and manipulate so the publish validators can accurately check the rig's +and manipulate so the publish validators can accurately check the rig's controls. ::: ### Publishing rigs Publishing rigs is done in a same way as publishing everything else. Save your scene -and go **OpenPype → Publish**. When you run validation you'll most likely run into -a few issues at first. Although a number of them will seem to be intimidating you +and go **OpenPype → Publish**. When you run validation you'll most likely run into +a few issues at first. Although a number of them will seem to be intimidating you will find out they are mostly minor things, easily fixed and are there to optimize -your rig for consistency and safe usage by the artist. +your rig for consistency and safe usage by the artist. - **Non Duplicate Instance Members (ID)** - This will most likely fail because when creating rigs, we usually duplicate few parts of it to reuse them. But duplication @@ -312,8 +312,8 @@ instance yourself to publish the geometry. This is all cleanly prepared for you when loading a published rig. :::tip Missing animation instance for your loaded rig? -Did you accidentally delete the animation instance for a loaded rig? You can -recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance) +Did you accidentally delete the animation instance for a loaded rig? You can +recreate it using the [**Recreate rig animation instance**](artist_hosts_maya.md#recreate-rig-animation-instance) inventory action. ::: @@ -677,4 +677,4 @@ for when it was accidentally deleted by the user. #### Usage Select 1 or more container of type `rig` for which you want to recreate the -animation instance. \ No newline at end of file +animation instance. From 95c802047cff3dc211c7f0ad037497befbff0c14 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 20 Apr 2023 16:40:49 +0200 Subject: [PATCH 321/918] Don't make ExtractOpenGL optional --- .../hosts/houdini/plugins/publish/extract_opengl.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/extract_opengl.py b/openpype/hosts/houdini/plugins/publish/extract_opengl.py index c26d0813a6..6c36dec5f5 100644 --- a/openpype/hosts/houdini/plugins/publish/extract_opengl.py +++ b/openpype/hosts/houdini/plugins/publish/extract_opengl.py @@ -2,27 +2,20 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish from openpype.hosts.houdini.api.lib import render_rop import hou -class ExtractOpenGL(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractOpenGL(publish.Extractor): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract OpenGL" families = ["review"] hosts = ["houdini"] - optional = True def process(self, instance): - if not self.is_active(instance.data): - return ropnode = hou.node(instance.data.get("instance_node")) output = ropnode.evalParm("picture") From f05f7510b4256964de741b6fd982327da9e4e1aa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Apr 2023 21:39:49 +0200 Subject: [PATCH 322/918] adding slate condition to plugin --- openpype/plugins/publish/validate_sequence_frames.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/plugins/publish/validate_sequence_frames.py b/openpype/plugins/publish/validate_sequence_frames.py index 0dba99b07c..239008ee21 100644 --- a/openpype/plugins/publish/validate_sequence_frames.py +++ b/openpype/plugins/publish/validate_sequence_frames.py @@ -49,7 +49,12 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): collection = collections[0] frames = list(collection.indexes) + if instance.data.get("slate"): + # Slate is not part of the frame range + frames = frames[1:] + current_range = (frames[0], frames[-1]) + required_range = (instance.data["frameStart"], instance.data["frameEnd"]) From aa2d683dd9402268d355d74df452ce72e8c09e6a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Apr 2023 21:49:58 +0200 Subject: [PATCH 323/918] adding test routine for the slate condition --- .../publish/test_validate_sequence_frames.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py b/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py index 58d9de011d..17e47c9f64 100644 --- a/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py +++ b/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py @@ -180,5 +180,23 @@ class TestValidateSequenceFrames(BaseTest): plugin.process(instance) assert ("Missing frames: [1002]" in str(excinfo.value)) + def test_validate_sequence_frames_slate(self, instance, plugin): + representations = [ + { + "ext": "exr", + "files": [ + "Main_beauty.1000.exr", + "Main_beauty.1001.exr", + "Main_beauty.1002.exr", + "Main_beauty.1003.exr" + ] + } + ] + instance.data["slate"] = True + instance.data["representations"] = representations + instance.data["frameEnd"] = 1003 + + plugin.process(instance) + test_case = TestValidateSequenceFrames() From 23105fcf9052804dcbfa034b63301faf8d6c884e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 20 Apr 2023 22:00:41 +0200 Subject: [PATCH 324/918] Update openpype/pipeline/colorspace.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fabià Serra Arrizabalaga --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 5520dab627..ceb4572b38 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -350,7 +350,7 @@ def get_imageio_config( project_settings, host_name) # check if host settings group is having activate_host_color_management - # it it does not have activation key then use global settings + # it it does not have activation key then default it to True so it uses global settings # this is for backward compatibility # TODO: in future rewrite this to be more explicit activate_host_color_management = imageio_host.get( From 3793830b3db1d340f4e594ea34b8209d383067ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 20 Apr 2023 22:05:50 +0200 Subject: [PATCH 325/918] Update openpype/pipeline/colorspace.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fabià Serra Arrizabalaga --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index ceb4572b38..5dd7f01009 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -411,7 +411,7 @@ def is_host_use_ocio_config_activated( project_settings, host_name) # check if host settings is having use_ocio_config - if imageio_host.get("use_ocio_config", False): + return imageio_host.get("use_ocio_config", False) return True From b66aaf2bdff6bd8ee013ecf9d96b63fbdb9f573c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Apr 2023 22:09:48 +0200 Subject: [PATCH 326/918] keep consistency in returning types --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index ceb4572b38..29794aa7aa 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -363,7 +363,7 @@ def get_imageio_config( "Colorspace management for host '{}' is disabled.".format( host_name) ) - return False + return {} config_host = imageio_host.get("ocio_config", {}) From 92c148a42e2f3dfb4c67edbd8d2e387b1cd2fe72 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 20 Apr 2023 22:22:33 +0200 Subject: [PATCH 327/918] hound --- openpype/pipeline/colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index eecaa64705..e8d88bf533 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -350,7 +350,8 @@ def get_imageio_config( project_settings, host_name) # check if host settings group is having activate_host_color_management - # it it does not have activation key then default it to True so it uses global settings + # it it does not have activation key then default it to True so it uses + # global settings # this is for backward compatibility # TODO: in future rewrite this to be more explicit activate_host_color_management = imageio_host.get( @@ -412,7 +413,6 @@ def is_host_use_ocio_config_activated( # check if host settings is having use_ocio_config return imageio_host.get("use_ocio_config", False) - return True def _get_config_data(path_list, anatomy_data): From 35c1e4cd89d6694d729cb11604ed1c21e5c44f3c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 10:58:18 +0200 Subject: [PATCH 328/918] Converting `app_groups` to `hosts` --- openpype/hooks/pre_ocio_hook.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index d65433fba6..7c67be5cfe 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -11,7 +11,7 @@ class OCIOEnvHook(PreLaunchHook): """Set OCIO environment variable for hosts that use OpenColorIO.""" order = 0 - app_groups = [ + hosts = [ "fusion", "blender", "aftereffects", @@ -19,9 +19,6 @@ class OCIOEnvHook(PreLaunchHook): "houdini", "maya", "nuke", - "nukex", - "nukeassist", - "nukestudio", "hiero" ] From 5e012d6be254dfa68721d5f08a981cafecbc8842 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Apr 2023 17:29:47 +0800 Subject: [PATCH 329/918] add validator for resolution setting --- openpype/hosts/max/api/pipeline.py | 3 - .../publish/validate_resolution_setting.py | 59 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_resolution_setting.py diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index ac841d395f..50fe30b299 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -56,9 +56,6 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): rt.callbacks.addScript(rt.Name('systemPostNew'), context_setting) - rt.callbacks.addScript(rt.Name('filePostOpen'), - context_setting) - def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? return True diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py new file mode 100644 index 0000000000..43a3a3a278 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -0,0 +1,59 @@ +import pyblish.api +from openpype.pipeline import ( + PublishValidationError, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api.lib import reset_scene_resolution + +from openpype.pipeline.context_tools import ( + get_current_project_asset, + get_current_project +) + + +class ValidateResolutionSetting(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validate the resolution setting aligned with DB""" + + order = pyblish.api.ValidatorOrder- 0.01 + families = ["maxrender"] + hosts = ["max"] + label = "Validate Resolution Setting" + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + width, height = self.get_db_resolution(instance) + current_width = rt.renderwidth + current_height = rt.renderHeight + if current_width != width and current_height != height: + raise PublishValidationError("Resolution Setting" + " not aligned with DB") + if current_width != width: + raise PublishValidationError("Width in Resolution Setting " + "not aligned with DB") + + if current_height != height: + raise PublishValidationError("Height in Resolution Setting " + "not aligned with DB") + + + def get_db_resolution(self, instance): + data = ["data.resolutionWidth", "data.resolutionHeight"] + project_resolution = get_current_project(fields=data) + project_resolution_data = project_resolution["data"] + asset_resolution = get_current_project_asset(fields=data) + asset_resolution_data = asset_resolution["data"] + # Set project resolution + project_width = int(project_resolution_data.get("resolutionWidth", 1920)) + project_height = int(project_resolution_data.get("resolutionHeight", 1080)) + width = int(asset_resolution_data.get("resolutionWidth", project_width)) + height = int(asset_resolution_data.get("resolutionHeight", project_height)) + + return width, height + + @classmethod + def repair(cls, instance): + reset_scene_resolution() From 843fd5f1b920e4a22cf94595eeb2c4c945ad0cbd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Apr 2023 11:31:52 +0200 Subject: [PATCH 330/918] Nuke: Legacy convertor skips deprecation warnings (#4846) * convert legacy checks for AVALON_TAB to avoid deprecation warnings * simplify 'get_avalon_knob_data' --- openpype/hosts/nuke/api/lib.py | 13 ++++++------- .../hosts/nuke/plugins/create/convert_legacy.py | 7 +++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index fe3a2d2bd1..64fa32a383 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -495,17 +495,17 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): data (dict) """ + data = {} + if AVALON_TAB not in node.knobs(): + return data + # check if lists if not isinstance(prefix, list): - prefix = list([prefix]) - - data = dict() + prefix = [prefix] # loop prefix for p in prefix: # check if the node is avalon tracked - if AVALON_TAB not in node.knobs(): - continue try: # check if data available on the node test = node[AVALON_DATA_GROUP].value() @@ -516,8 +516,7 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): if create: node = set_avalon_knob_data(node) return get_avalon_knob_data(node) - else: - return {} + return {} # get data from filtered knobs data.update({k.replace(p, ''): node[k].value() diff --git a/openpype/hosts/nuke/plugins/create/convert_legacy.py b/openpype/hosts/nuke/plugins/create/convert_legacy.py index c143e4cb27..377e9f78f6 100644 --- a/openpype/hosts/nuke/plugins/create/convert_legacy.py +++ b/openpype/hosts/nuke/plugins/create/convert_legacy.py @@ -2,7 +2,8 @@ from openpype.pipeline.create.creator_plugins import SubsetConvertorPlugin from openpype.hosts.nuke.api.lib import ( INSTANCE_DATA_KNOB, get_node_data, - get_avalon_knob_data + get_avalon_knob_data, + AVALON_TAB, ) from openpype.hosts.nuke.api.plugin import convert_to_valid_instaces @@ -17,13 +18,15 @@ class LegacyConverted(SubsetConvertorPlugin): legacy_found = False # search for first available legacy item for node in nuke.allNodes(recurseGroups=True): - if node.Class() in ["Viewer", "Dot"]: continue if get_node_data(node, INSTANCE_DATA_KNOB): continue + if AVALON_TAB not in node.knobs(): + continue + # get data from avalon knob avalon_knob_data = get_avalon_knob_data( node, ["avalon:", "ak:"], create=False) From dac51f41ec5d97384698faf0f19e4ba398d96be6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Apr 2023 17:32:11 +0800 Subject: [PATCH 331/918] hound fix --- .../publish/validate_resolution_setting.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 43a3a3a278..ce1f8a975d 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -16,7 +16,7 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Validate the resolution setting aligned with DB""" - order = pyblish.api.ValidatorOrder- 0.01 + order = pyblish.api.ValidatorOrder - 0.01 families = ["maxrender"] hosts = ["max"] label = "Validate Resolution Setting" @@ -39,7 +39,6 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, raise PublishValidationError("Height in Resolution Setting " "not aligned with DB") - def get_db_resolution(self, instance): data = ["data.resolutionWidth", "data.resolutionHeight"] project_resolution = get_current_project(fields=data) @@ -47,10 +46,17 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, asset_resolution = get_current_project_asset(fields=data) asset_resolution_data = asset_resolution["data"] # Set project resolution - project_width = int(project_resolution_data.get("resolutionWidth", 1920)) - project_height = int(project_resolution_data.get("resolutionHeight", 1080)) - width = int(asset_resolution_data.get("resolutionWidth", project_width)) - height = int(asset_resolution_data.get("resolutionHeight", project_height)) + project_width = int( + project_resolution_data.get("resolutionWidth", 1920) + ) + project_height = int( + project_resolution_data.get("resolutionHeight", 1080)) + width = int( + asset_resolution_data.get("resolutionWidth", project_width) + ) + height = int( + asset_resolution_data.get("resolutionHeight", project_height) + ) return width, height From 90f12d15e050e0136eaec50a53d2049385e3fcf6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Apr 2023 17:33:08 +0800 Subject: [PATCH 332/918] hound fix --- .../max/plugins/publish/validate_resolution_setting.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index ce1f8a975d..9424b24380 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -47,16 +47,13 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, asset_resolution_data = asset_resolution["data"] # Set project resolution project_width = int( - project_resolution_data.get("resolutionWidth", 1920) - ) + project_resolution_data.get("resolutionWidth", 1920)) project_height = int( project_resolution_data.get("resolutionHeight", 1080)) width = int( - asset_resolution_data.get("resolutionWidth", project_width) - ) + asset_resolution_data.get("resolutionWidth", project_width)) height = int( - asset_resolution_data.get("resolutionHeight", project_height) - ) + asset_resolution_data.get("resolutionHeight", project_height)) return width, height From 63af34e9ab76e1e1ccc15067c188bb1648fd279e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 11:52:34 +0200 Subject: [PATCH 333/918] rename `use_ocio_config` to `set_ocio_config` --- openpype/hooks/pre_ocio_hook.py | 4 ++-- openpype/pipeline/colorspace.py | 6 +++--- .../settings/defaults/project_settings/aftereffects.json | 2 +- openpype/settings/defaults/project_settings/blender.json | 2 +- openpype/settings/defaults/project_settings/fusion.json | 2 +- openpype/settings/defaults/project_settings/hiero.json | 2 +- openpype/settings/defaults/project_settings/houdini.json | 2 +- openpype/settings/defaults/project_settings/max.json | 2 +- openpype/settings/defaults/project_settings/maya.json | 2 +- openpype/settings/defaults/project_settings/nuke.json | 2 +- openpype/settings/defaults/project_settings/unreal.json | 2 +- .../projects_schema/schema_project_aftereffects.json | 2 +- .../schemas/projects_schema/schema_project_blender.json | 2 +- .../schemas/projects_schema/schema_project_fusion.json | 2 +- .../schemas/projects_schema/schema_project_hiero.json | 2 +- .../schemas/projects_schema/schema_project_houdini.json | 2 +- .../schemas/projects_schema/schema_project_max.json | 2 +- .../schemas/projects_schema/schema_project_maya.json | 2 +- .../schemas/projects_schema/schema_project_unreal.json | 2 +- .../projects_schema/schemas/schema_nuke_imageio.json | 2 +- 20 files changed, 23 insertions(+), 23 deletions(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 7c67be5cfe..e09460db14 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -2,7 +2,7 @@ from openpype.lib import PreLaunchHook from openpype.pipeline.colorspace import ( get_imageio_config, - is_host_use_ocio_config_activated + is_set_ocio_config_activated ) from openpype.pipeline.template_data import get_template_data_with_names @@ -42,7 +42,7 @@ class OCIOEnvHook(PreLaunchHook): ) if config_data: - use_config_path = is_host_use_ocio_config_activated( + use_config_path = is_set_ocio_config_activated( project_name=self.data["project_name"], host_name=self.host_name, project_settings=self.data["project_settings"] diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index e8d88bf533..a1714bc75e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -393,7 +393,7 @@ def get_imageio_config( return config_data -def is_host_use_ocio_config_activated( +def is_set_ocio_config_activated( project_name, host_name, project_settings=None ): """Check if host OCIO config path is activated @@ -411,8 +411,8 @@ def is_host_use_ocio_config_activated( _, imageio_host = _get_imageio_settings( project_settings, host_name) - # check if host settings is having use_ocio_config - return imageio_host.get("use_ocio_config", False) + # check if host settings is having set_ocio_config + return imageio_host.get("set_ocio_config", False) def _get_config_data(path_list, anatomy_data): diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 5b6dffe67e..c30356335b 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -1,7 +1,7 @@ { "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index f1a3286488..1969cd8346 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,7 +1,7 @@ { "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index ede907e415..c80936d402 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -1,7 +1,7 @@ { "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index a1ca0e8933..e876d1727d 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -1,7 +1,7 @@ { "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index fca782b2b8..dd3fc87b80 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,7 +1,7 @@ { "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index a9625cc539..89ba7a702d 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,7 +1,7 @@ { "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 9b86a04bd9..d50441d961 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -411,7 +411,7 @@ }, "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 0f7c5fdaef..119a240ad5 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -10,7 +10,7 @@ }, "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 60471f28c9..71c3498f1e 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,7 +1,7 @@ { "imageio": { "activate_host_color_management": true, - "use_ocio_config": false, + "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 35371f3505..777d185275 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -19,7 +19,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 793ac5e908..8872cd123e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -19,7 +19,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index d488c9f551..41f464589c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -19,7 +19,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index 0bd88c6e11..55d29e8b07 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -19,7 +19,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 24e741ff66..4782295006 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -19,7 +19,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index aa336d0791..d57f603641 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -19,7 +19,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index ef6e3f9171..a99b650401 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -59,7 +59,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index bfcb4d7fe6..d5c58e6a5d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -19,7 +19,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index 1122eb1949..4bc741703a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -12,7 +12,7 @@ }, { "type": "boolean", - "key": "use_ocio_config", + "key": "set_ocio_config", "label": "Use OCIO config file in host" }, { From bae3ca9a3be102c926fda14eaa497e3b49ec63ea Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 11:57:22 +0200 Subject: [PATCH 334/918] change label to reflect attribute name --- .../schemas/projects_schema/schema_project_aftereffects.json | 2 +- .../schemas/projects_schema/schema_project_blender.json | 2 +- .../entities/schemas/projects_schema/schema_project_fusion.json | 2 +- .../entities/schemas/projects_schema/schema_project_hiero.json | 2 +- .../schemas/projects_schema/schema_project_houdini.json | 2 +- .../entities/schemas/projects_schema/schema_project_max.json | 2 +- .../entities/schemas/projects_schema/schema_project_maya.json | 2 +- .../entities/schemas/projects_schema/schema_project_unreal.json | 2 +- .../schemas/projects_schema/schemas/schema_nuke_imageio.json | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 777d185275..148c1840e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -20,7 +20,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 8872cd123e..fe6ee94654 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -20,7 +20,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 41f464589c..f97a3a3a40 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -20,7 +20,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index 55d29e8b07..a46611dc8b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -20,7 +20,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 4782295006..d254b92269 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -20,7 +20,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index d57f603641..1141cefb40 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -20,7 +20,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index a99b650401..37f864a71c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -60,7 +60,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index d5c58e6a5d..4ff4bddc47 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -20,7 +20,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index 4bc741703a..c69a4c4f4b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -13,7 +13,7 @@ { "type": "boolean", "key": "set_ocio_config", - "label": "Use OCIO config file in host" + "label": "Set OCIO config file in host" }, { "type": "schema", From a2f79419bcb51731546e5422292e51cbd66bd52f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 11:59:41 +0200 Subject: [PATCH 335/918] Clear publisher comment on successful publish or on window close (#4885) --- openpype/tools/publisher/window.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 8826e0f849..0615157e1b 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -284,6 +284,9 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "publish.has_validated.changed", self._on_publish_validated_change ) + controller.event_system.add_callback( + "publish.finished.changed", self._on_publish_finished_change + ) controller.event_system.add_callback( "publish.process.stopped", self._on_publish_stop ) @@ -400,6 +403,7 @@ class PublisherWindow(QtWidgets.QDialog): # TODO capture changes and ask user if wants to save changes on close if not self._controller.host_context_has_changed: self._save_changes(False) + self._comment_input.setText("") # clear comment self._reset_on_show = True self._controller.clear_thumbnail_temp_dir_path() super(PublisherWindow, self).closeEvent(event) @@ -777,6 +781,11 @@ class PublisherWindow(QtWidgets.QDialog): if event["value"]: self._validate_btn.setEnabled(False) + def _on_publish_finished_change(self, event): + if event["value"]: + # Successful publish, remove comment from UI + self._comment_input.setText("") + def _on_publish_stop(self): self._set_publish_overlay_visibility(False) self._reset_btn.setEnabled(True) From 5b1854e9022ed7e6fc994b08ed160543572851c2 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Fri, 21 Apr 2023 18:17:01 +0800 Subject: [PATCH 336/918] Add fps as instance.data in collect review in Houdini. (#4888) * add fps as instance data in collect review data * Trllo's feedback --- openpype/hosts/houdini/plugins/publish/collect_review_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/publish/collect_review_data.py b/openpype/hosts/houdini/plugins/publish/collect_review_data.py index e321dcb2fa..8118e6d558 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_review_data.py +++ b/openpype/hosts/houdini/plugins/publish/collect_review_data.py @@ -17,6 +17,7 @@ class CollectHoudiniReviewData(pyblish.api.InstancePlugin): # which isn't the actual frame range that this instance renders. instance.data["handleStart"] = 0 instance.data["handleEnd"] = 0 + instance.data["fps"] = instance.context.data["fps"] # Get the camera from the rop node to collect the focal length ropnode_path = instance.data["instance_node"] From cac990cd3cb707fa3528b2f302fb5791a783b678 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 12:20:10 +0200 Subject: [PATCH 337/918] Code: Tweak docstrings and return type hints (#4875) * Tweak docstrings and return type hints * Remove test import of `typing` * Fix indentations * Fix indentations * Fix typos * Update openpype/client/entities.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * `fields` as `Optional` iterable of strings. --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/client/entities.py | 229 +++++++++++++++++++++--------------- 1 file changed, 135 insertions(+), 94 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 376157d210..8004dc3019 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -69,6 +69,19 @@ def convert_ids(in_ids): def get_projects(active=True, inactive=False, fields=None): + """Yield all project entity documents. + + Args: + active (Optional[bool]): Include active projects. Defaults to True. + inactive (Optional[bool]): Include inactive projects. + Defaults to False. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Yields: + dict: Project entity data which can be reduced to specified 'fields'. + None is returned if project with specified filters was not found. + """ mongodb = get_project_database() for project_name in mongodb.collection_names(): if project_name in ("system.indexes",): @@ -81,6 +94,20 @@ def get_projects(active=True, inactive=False, fields=None): def get_project(project_name, active=True, inactive=True, fields=None): + """Return project entity document by project name. + + Args: + project_name (str): Name of project. + active (Optional[bool]): Allow active project. Defaults to True. + inactive (Optional[bool]): Allow inactive project. Defaults to True. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Project entity data which can be reduced to + specified 'fields'. None is returned if project with specified + filters was not found. + """ # Skip if both are disabled if not active and not inactive: return None @@ -124,17 +151,18 @@ def get_whole_project(project_name): def get_asset_by_id(project_name, asset_id, fields=None): - """Receive asset data by it's id. + """Receive asset data by its id. Args: project_name (str): Name of project where to look for queried entities. asset_id (Union[str, ObjectId]): Asset's id. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - dict: Asset entity data. - None: Asset was not found by id. + Union[Dict, None]: Asset entity data which can be reduced to + specified 'fields'. None is returned if asset with specified + filters was not found. """ asset_id = convert_id(asset_id) @@ -147,17 +175,18 @@ def get_asset_by_id(project_name, asset_id, fields=None): def get_asset_by_name(project_name, asset_name, fields=None): - """Receive asset data by it's name. + """Receive asset data by its name. Args: project_name (str): Name of project where to look for queried entities. asset_name (str): Asset's name. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - dict: Asset entity data. - None: Asset was not found by name. + Union[Dict, None]: Asset entity data which can be reduced to + specified 'fields'. None is returned if asset with specified + filters was not found. """ if not asset_name: @@ -195,8 +224,8 @@ def _get_assets( parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. standard (bool): Query standard assets (type 'asset'). archived (bool): Query archived assets (type 'archived_asset'). - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Query cursor as iterable which returns asset documents matching @@ -261,8 +290,8 @@ def get_assets( asset_names (Iterable[str]): Name assets that should be found. parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. archived (bool): Add also archived assets. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Query cursor as iterable which returns asset documents matching @@ -300,8 +329,8 @@ def get_archived_assets( be found. asset_names (Iterable[str]): Name assets that should be found. parent_ids (Iterable[Union[str, ObjectId]]): Parent asset ids. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Query cursor as iterable which returns asset documents matching @@ -356,17 +385,18 @@ def get_asset_ids_with_subsets(project_name, asset_ids=None): def get_subset_by_id(project_name, subset_id, fields=None): - """Single subset entity data by it's id. + """Single subset entity data by its id. Args: project_name (str): Name of project where to look for queried entities. subset_id (Union[str, ObjectId]): Id of subset which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If subset with specified filters was not found. - Dict: Subset document which can be reduced to specified 'fields'. + Union[Dict, None]: Subset entity data which can be reduced to + specified 'fields'. None is returned if subset with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -379,20 +409,19 @@ def get_subset_by_id(project_name, subset_id, fields=None): def get_subset_by_name(project_name, subset_name, asset_id, fields=None): - """Single subset entity data by it's name and it's version id. + """Single subset entity data by its name and its version id. Args: project_name (str): Name of project where to look for queried entities. subset_name (str): Name of subset. asset_id (Union[str, ObjectId]): Id of parent asset. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - Union[None, Dict[str, Any]]: None if subset with specified filters was - not found or dict subset document which can be reduced to - specified 'fields'. - + Union[Dict, None]: Subset entity data which can be reduced to + specified 'fields'. None is returned if subset with specified + filters was not found. """ if not subset_name: return None @@ -434,8 +463,8 @@ def get_subsets( names_by_asset_ids (dict[ObjectId, List[str]]): Complex filtering using asset ids and list of subset names under the asset. archived (bool): Look for archived subsets too. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching subsets. @@ -520,17 +549,18 @@ def get_subset_families(project_name, subset_ids=None): def get_version_by_id(project_name, version_id, fields=None): - """Single version entity data by it's id. + """Single version entity data by its id. Args: project_name (str): Name of project where to look for queried entities. version_id (Union[str, ObjectId]): Id of version which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ version_id = convert_id(version_id) @@ -546,18 +576,19 @@ def get_version_by_id(project_name, version_id, fields=None): def get_version_by_name(project_name, version, subset_id, fields=None): - """Single version entity data by it's name and subset id. + """Single version entity data by its name and subset id. Args: project_name (str): Name of project where to look for queried entities. - version (int): name of version entity (it's version). + version (int): name of version entity (its version). subset_id (Union[str, ObjectId]): Id of version which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -574,7 +605,7 @@ def get_version_by_name(project_name, version, subset_id, fields=None): def version_is_latest(project_name, version_id): - """Is version the latest from it's subset. + """Is version the latest from its subset. Note: Hero versions are considered as latest. @@ -680,8 +711,8 @@ def get_versions( versions (Iterable[int]): Version names (as integers). Filter ignored if 'None' is passed. hero (bool): Look also for hero versions. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching versions. @@ -705,12 +736,13 @@ def get_hero_version_by_subset_id(project_name, subset_id, fields=None): project_name (str): Name of project where to look for queried entities. subset_id (Union[str, ObjectId]): Subset id under which is hero version. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If hero version for passed subset id does not exists. - Dict: Hero version entity data. + Union[Dict, None]: Hero version entity data which can be reduced to + specified 'fields'. None is returned if hero version with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -730,17 +762,18 @@ def get_hero_version_by_subset_id(project_name, subset_id, fields=None): def get_hero_version_by_id(project_name, version_id, fields=None): - """Hero version by it's id. + """Hero version by its id. Args: project_name (str): Name of project where to look for queried entities. version_id (Union[str, ObjectId]): Hero version id. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If hero version with passed id was not found. - Dict: Hero version entity data. + Union[Dict, None]: Hero version entity data which can be reduced to + specified 'fields'. None is returned if hero version with specified + filters was not found. """ version_id = convert_id(version_id) @@ -773,8 +806,8 @@ def get_hero_versions( should look for hero versions. Filter ignored if 'None' is passed. version_ids (Iterable[Union[str, ObjectId]]): Hero version ids. Filter ignored if 'None' is passed. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor|list: Iterable yielding hero versions matching passed filters. @@ -801,8 +834,8 @@ def get_output_link_versions(project_name, version_id, fields=None): project_name (str): Name of project where to look for queried entities. version_id (Union[str, ObjectId]): Version id which can be used as input link for other versions. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Iterable: Iterable cursor yielding versions that are used as input @@ -828,8 +861,8 @@ def get_last_versions(project_name, subset_ids, fields=None): Args: project_name (str): Name of project where to look for queried entities. subset_ids (Iterable[Union[str, ObjectId]]): List of subset ids. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: dict[ObjectId, int]: Key is subset id and value is last version name. @@ -913,12 +946,13 @@ def get_last_version_by_subset_id(project_name, subset_id, fields=None): Args: project_name (str): Name of project where to look for queried entities. subset_id (Union[str, ObjectId]): Id of version which should be found. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ subset_id = convert_id(subset_id) @@ -945,12 +979,13 @@ def get_last_version_by_subset_name( asset_id (Union[str, ObjectId]): Asset id which is parent of passed subset name. asset_name (str): Asset name which is parent of passed subset name. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If version with specified filters was not found. - Dict: Version document which can be reduced to specified 'fields'. + Union[Dict, None]: Version entity data which can be reduced to + specified 'fields'. None is returned if version with specified + filters was not found. """ if not asset_id and not asset_name: @@ -972,18 +1007,18 @@ def get_last_version_by_subset_name( def get_representation_by_id(project_name, representation_id, fields=None): - """Representation entity data by it's id. + """Representation entity data by its id. Args: project_name (str): Name of project where to look for queried entities. representation_id (Union[str, ObjectId]): Representation id. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If representation with specified filters was not found. - Dict: Representation entity data which can be reduced - to specified 'fields'. + Union[Dict, None]: Representation entity data which can be reduced to + specified 'fields'. None is returned if representation with + specified filters was not found. """ if not representation_id: @@ -1004,19 +1039,19 @@ def get_representation_by_id(project_name, representation_id, fields=None): def get_representation_by_name( project_name, representation_name, version_id, fields=None ): - """Representation entity data by it's name and it's version id. + """Representation entity data by its name and its version id. Args: project_name (str): Name of project where to look for queried entities. representation_name (str): Representation name. version_id (Union[str, ObjectId]): Id of parent version entity. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If representation with specified filters was not found. - Dict: Representation entity data which can be reduced - to specified 'fields'. + Union[dict[str, Any], None]: Representation entity data which can be + reduced to specified 'fields'. None is returned if representation + with specified filters was not found. """ version_id = convert_id(version_id) @@ -1202,8 +1237,8 @@ def get_representations( names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering using version ids and list of names under the version. archived (bool): Output will also contain archived representations. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching representations. @@ -1247,8 +1282,8 @@ def get_archived_representations( representation context fields. names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering using version ids and list of names under the version. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: Cursor: Iterable cursor yielding all matching representations. @@ -1377,8 +1412,8 @@ def get_thumbnail_id_from_source(project_name, src_type, src_id): src_id (Union[str, ObjectId]): Id of source entity. Returns: - ObjectId: Thumbnail id assigned to entity. - None: If Source entity does not have any thumbnail id assigned. + Union[ObjectId, None]: Thumbnail id assigned to entity. If Source + entity does not have any thumbnail id assigned. """ if not src_type or not src_id: @@ -1397,14 +1432,14 @@ def get_thumbnails(project_name, thumbnail_ids, fields=None): """Receive thumbnails entity data. Thumbnail entity can be used to receive binary content of thumbnail based - on it's content and ThumbnailResolvers. + on its content and ThumbnailResolvers. Args: project_name (str): Name of project where to look for queried entities. thumbnail_ids (Iterable[Union[str, ObjectId]]): Ids of thumbnail entities. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: cursor: Cursor of queried documents. @@ -1429,12 +1464,13 @@ def get_thumbnail(project_name, thumbnail_id, fields=None): Args: project_name (str): Name of project where to look for queried entities. thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. Returns: - None: If thumbnail with specified id was not found. - Dict: Thumbnail entity data which can be reduced to specified 'fields'. + Union[Dict, None]: Thumbnail entity data which can be reduced to + specified 'fields'.None is returned if thumbnail with specified + filters was not found. """ if not thumbnail_id: @@ -1458,8 +1494,13 @@ def get_workfile_info( project_name (str): Name of project where to look for queried entities. asset_id (Union[str, ObjectId]): Id of asset entity. task_name (str): Task name on asset. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. All + fields are returned if 'None' is passed. + + Returns: + Union[Dict, None]: Workfile entity data which can be reduced to + specified 'fields'.None is returned if workfile with specified + filters was not found. """ if not asset_id or not task_name or not filename: From b751c539c3d3f0d2aa9ed6846bac01ce1ad91eb5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 12:22:11 +0200 Subject: [PATCH 338/918] Publisher: Make sure to reset asset widget when hidden and reshown (#4886) * Make sure to reset asset widget when hidden and reshown * change '_soft_reset_enabled' only on controller reset --------- Co-authored-by: Jakub Trllo --- openpype/tools/publisher/widgets/assets_widget.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/publisher/widgets/assets_widget.py b/openpype/tools/publisher/widgets/assets_widget.py index 3c559af259..a750d8d540 100644 --- a/openpype/tools/publisher/widgets/assets_widget.py +++ b/openpype/tools/publisher/widgets/assets_widget.py @@ -211,6 +211,10 @@ class AssetsDialog(QtWidgets.QDialog): layout.addWidget(asset_view, 1) layout.addLayout(btns_layout, 0) + controller.event_system.add_callback( + "controller.reset.finished", self._on_controller_reset + ) + asset_view.double_clicked.connect(self._on_ok_clicked) filter_input.textChanged.connect(self._on_filter_change) ok_btn.clicked.connect(self._on_ok_clicked) @@ -245,6 +249,10 @@ class AssetsDialog(QtWidgets.QDialog): new_pos.setY(new_pos.y() - int(self.height() / 2)) self.move(new_pos) + def _on_controller_reset(self): + # Change reset enabled so model is reset on show event + self._soft_reset_enabled = True + def showEvent(self, event): """Refresh asset model on show.""" super(AssetsDialog, self).showEvent(event) From d5ccdcbaab3b7946ad62730d968498ab0e19f612 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 13:21:46 +0200 Subject: [PATCH 339/918] fixing nightly workflow --- .github/workflows/nightly_merge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml index f1850762d9..3f8c75dce3 100644 --- a/.github/workflows/nightly_merge.yml +++ b/.github/workflows/nightly_merge.yml @@ -25,5 +25,5 @@ jobs: - name: Invoke pre-release workflow uses: benc-uk/workflow-dispatch@v1 with: - workflow: Nightly Prerelease + workflow: prerelease.yml token: ${{ secrets.YNPUT_BOT_TOKEN }} From edccc0f9e915d05843dbd0e1b1dc1513cc464aa3 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 21 Apr 2023 11:23:24 +0000 Subject: [PATCH 340/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 1d41f1aa5d..b9090cd8a1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.4" +__version__ = "3.15.5-nightly.1" From d03200238bbb1a0e57f14e88fe39902daed6c98f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 13:27:10 +0200 Subject: [PATCH 341/918] prerelease step with workflow dispatch for update bug. --- .github/workflows/prerelease.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index e8c619c6eb..8c5c733c08 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -65,3 +65,9 @@ jobs: source_ref: 'main' target_branch: 'develop' commit_message_template: '[Automated] Merged {source_ref} into {target_branch}' + + - name: Invoke Update bug report workflow + uses: benc-uk/workflow-dispatch@v1 + with: + workflow: update_bug_report.yml + token: ${{ secrets.YNPUT_BOT_TOKEN }} \ No newline at end of file From 34b1ad105b76e7d69094741f668927b96d406f4d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:18:41 +0200 Subject: [PATCH 342/918] implemented collector for review instances to fix extract review issues (#4891) --- .../plugins/publish/collect_review_frames.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py b/openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py new file mode 100644 index 0000000000..6b41c0dd21 --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/collect_review_frames.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +import pyblish.api + + +class CollectReviewInfo(pyblish.api.InstancePlugin): + """Collect data required for review instances. + + ExtractReview plugin requires frame start/end, fps on instance data which + are missing on instances from TrayPublishes. + + Warning: + This is temporary solution to "make it work". Contains removed changes + from https://github.com/ynput/OpenPype/pull/4383 reduced only for + review instances. + """ + + label = "Collect Review Info" + order = pyblish.api.CollectorOrder + 0.491 + families = ["review"] + hosts = ["traypublisher"] + + def process(self, instance): + asset_entity = instance.data.get("assetEntity") + if instance.data.get("frameStart") is not None or not asset_entity: + self.log.debug("Missing required data on instance") + return + + asset_data = asset_entity["data"] + # Store collected data for logging + collected_data = {} + for key in ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + ): + if key in instance.data or key not in asset_data: + continue + value = asset_data[key] + collected_data[key] = value + instance.data[key] = value + self.log.debug("Collected data: {}".format(str(collected_data))) From a31e90c53255d4cd84983218f4647aa50ae14649 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 15:41:38 +0200 Subject: [PATCH 343/918] renaming variable according to attribute --- openpype/hooks/pre_ocio_hook.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index e09460db14..bcff31fc93 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -42,16 +42,21 @@ class OCIOEnvHook(PreLaunchHook): ) if config_data: - use_config_path = is_set_ocio_config_activated( + set_config_path = is_set_ocio_config_activated( project_name=self.data["project_name"], host_name=self.host_name, project_settings=self.data["project_settings"] ) - if not use_config_path: - self.log.info("Using of OCIO config path was not activated...") + if not set_config_path: + self.log.info( + "Setting of OCIO environment with " + "config path was not activated..." + ) return ocio_path = config_data["path"] - self.log.info(f"Setting OCIO config path: {ocio_path}") + self.log.info( + f"Setting OCIO environment to config path: {ocio_path}") + self.launch_context.env["OCIO"] = ocio_path From 3fe4710b2115a5618ec898c94587f52d5522dbf6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 15:42:19 +0200 Subject: [PATCH 344/918] Maya: refactor colorspace preferecies with new settings --- openpype/hosts/maya/api/lib.py | 147 ++++++++---------- .../defaults/project_settings/maya.json | 14 +- .../projects_schema/schema_project_maya.json | 61 ++++++-- 3 files changed, 127 insertions(+), 95 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 61ea3d59df..58537db5f0 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1,6 +1,7 @@ """Standalone helper functions""" import os +from pprint import pformat import sys import platform import uuid @@ -3177,75 +3178,6 @@ def iter_shader_edits(relationships, shader_nodes, nodes_by_id, label=None): def set_colorspace(): """Set Colorspace from project configuration """ - project_name = os.getenv("AVALON_PROJECT") - imageio = get_project_settings(project_name)["maya"]["imageio"] - - # Maya 2022+ introduces new OCIO v2 color management settings that - # can override the old color managenement preferences. OpenPype has - # separate settings for both so we fall back when necessary. - use_ocio_v2 = imageio["colorManagementPreference_v2"]["enabled"] - required_maya_version = 2022 - maya_version = int(cmds.about(version=True)) - maya_supports_ocio_v2 = maya_version >= required_maya_version - if use_ocio_v2 and not maya_supports_ocio_v2: - # Fallback to legacy behavior with a warning - log.warning("Color Management Preference v2 is enabled but not " - "supported by current Maya version: {} (< {}). Falling " - "back to legacy settings.".format( - maya_version, required_maya_version) - ) - use_ocio_v2 = False - - if use_ocio_v2: - root_dict = imageio["colorManagementPreference_v2"] - else: - root_dict = imageio["colorManagementPreference"] - - if not isinstance(root_dict, dict): - msg = "set_colorspace(): argument should be dictionary" - log.error(msg) - - log.debug(">> root_dict: {}".format(root_dict)) - - # enable color management - cmds.colorManagementPrefs(e=True, cmEnabled=True) - cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True) - - # set config path - custom_ocio_config = False - if root_dict.get("configFilePath"): - unresolved_path = root_dict["configFilePath"] - ocio_paths = unresolved_path[platform.system().lower()] - - resolved_path = None - for ocio_p in ocio_paths: - resolved_path = str(ocio_p).format(**os.environ) - if not os.path.exists(resolved_path): - continue - - if resolved_path: - filepath = str(resolved_path).replace("\\", "/") - cmds.colorManagementPrefs(e=True, configFilePath=filepath) - cmds.colorManagementPrefs(e=True, cmConfigFileEnabled=True) - log.debug("maya '{}' changed to: {}".format( - "configFilePath", resolved_path)) - custom_ocio_config = True - else: - cmds.colorManagementPrefs(e=True, cmConfigFileEnabled=False) - cmds.colorManagementPrefs(e=True, configFilePath="") - - # If no custom OCIO config file was set we make sure that Maya 2022+ - # either chooses between Maya's newer default v2 or legacy config based - # on OpenPype setting to use ocio v2 or not. - if maya_supports_ocio_v2 and not custom_ocio_config: - if use_ocio_v2: - # Use Maya 2022+ default OCIO v2 config - log.info("Setting default Maya OCIO v2 config") - cmds.colorManagementPrefs(edit=True, configFilePath="") - else: - # Set the Maya default config file path - log.info("Setting default Maya OCIO v1 legacy config") - cmds.colorManagementPrefs(edit=True, configFilePath="legacy") # set color spaces for rendering space and view transforms def _colormanage(**kwargs): @@ -3262,17 +3194,74 @@ def set_colorspace(): except RuntimeError as exc: log.error(exc) - if use_ocio_v2: - _colormanage(renderingSpaceName=root_dict["renderSpace"]) - _colormanage(displayName=root_dict["displayName"]) - _colormanage(viewName=root_dict["viewName"]) - else: - _colormanage(renderingSpaceName=root_dict["renderSpace"]) - if maya_supports_ocio_v2: - _colormanage(viewName=root_dict["viewTransform"]) - _colormanage(displayName="legacy") + project_name = os.getenv("AVALON_PROJECT") + imageio = get_project_settings(project_name)["maya"]["imageio"] + + # ocio compatibility variables + ocio_v2_maya_version = 2022 + maya_version = int(cmds.about(version=True)) + ocio_v2_support = use_ocio_v2 = maya_version >= ocio_v2_maya_version + + root_dict = {} + use_workfile_settings = imageio.get("workfile", {}).get("enabled") + + if use_workfile_settings: + # TODO: deprecated code from 3.15.5 - remove + # Maya 2022+ introduces new OCIO v2 color management settings that + # can override the old color management preferences. OpenPype has + # separate settings for both so we fall back when necessary. + use_ocio_v2 = imageio["colorManagementPreference_v2"]["enabled"] + if use_ocio_v2 and not ocio_v2_support: + # Fallback to legacy behavior with a warning + log.warning( + "Color Management Preference v2 is enabled but not " + "supported by current Maya version: {} (< {}). Falling " + "back to legacy settings.".format( + maya_version, ocio_v2_maya_version) + ) + + if use_ocio_v2: + root_dict = imageio["colorManagementPreference_v2"] else: - _colormanage(viewTransformName=root_dict["viewTransform"]) + root_dict = imageio["colorManagementPreference"] + + if not isinstance(root_dict, dict): + msg = "set_colorspace(): argument should be dictionary" + log.error(msg) + + else: + root_dict = imageio["workfile"] + + log.debug(">> root_dict: {}".format(pformat(root_dict))) + + if root_dict: + # enable color management + cmds.colorManagementPrefs(e=True, cmEnabled=True) + cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True) + + # backward compatibility + # TODO: deprecated code from 3.15.5 - refactor to use new settings + view_name = root_dict.get("viewTransform") + if view_name is None: + view_name = root_dict.get("viewName") + + if use_ocio_v2: + # Use Maya 2022+ default OCIO v2 config + log.info("Setting default Maya OCIO v2 config") + cmds.colorManagementPrefs(edit=True, configFilePath="") + + # set rendering space and view transform + _colormanage(renderingSpaceName=root_dict["renderSpace"]) + _colormanage(viewName=view_name) + _colormanage(displayName=root_dict["displayName"]) + else: + # Set the Maya default config file path + log.info("Setting default Maya OCIO v1 legacy config") + cmds.colorManagementPrefs(edit=True, configFilePath="legacy") + + # set rendering space and view transform + _colormanage(renderingSpaceName=root_dict["renderSpace"]) + _colormanage(viewTransformName=view_name) @contextlib.contextmanager diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index d50441d961..b09ed146bf 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -420,6 +420,12 @@ "override_global_rules": false, "rules": {} }, + "workfile": { + "enabled": false, + "renderSpace": "ACEScg", + "displayName": "sRGB", + "viewName": "ACES 1.0 SDR-video" + }, "colorManagementPreference_v2": { "enabled": true, "renderSpace": "ACEScg", @@ -448,6 +454,10 @@ "destination-path": [] } }, + "include_handles": { + "include_handles_default": false, + "per_task_type": [] + }, "scriptsmenu": { "name": "OpenPype Tools", "definition": [ @@ -1546,10 +1556,6 @@ } ] }, - "include_handles": { - "include_handles_default": false, - "per_task_type": [] - }, "templated_workfile_build": { "profiles": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 37f864a71c..b5366bb0a7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -71,16 +71,16 @@ "name": "schema_imageio_file_rules" }, { - "key": "colorManagementPreference_v2", + "key": "workfile", "type": "dict", - "label": "Color Management Preference v2 (Maya 2022+)", + "label": "Workfile", "collapsible": true, "checkbox_key": "enabled", "children": [ { "type": "boolean", "key": "enabled", - "label": "Use Color Management Preference v2" + "label": "Enabled" }, { "type": "text", @@ -100,20 +100,57 @@ ] }, { - "key": "colorManagementPreference", - "type": "dict", - "label": "Color Management Preference (legacy)", + "type": "collapsible-wrap", + "label": "[Deprecated] please migrate all to 'Workfile' and enable it.", "collapsible": true, + "collapsed": true, "children": [ { - "type": "text", - "key": "renderSpace", - "label": "Rendering Space" + "key": "colorManagementPreference_v2", + "type": "dict", + "label": "[DEPRECATED] Color Management Preference v2 (Maya 2022+)", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Use Color Management Preference v2" + }, + { + "type": "text", + "key": "renderSpace", + "label": "Rendering Space" + }, + { + "type": "text", + "key": "displayName", + "label": "Display" + }, + { + "type": "text", + "key": "viewName", + "label": "View" + } + ] }, { - "type": "text", - "key": "viewTransform", - "label": "Viewer Transform" + "key": "colorManagementPreference", + "type": "dict", + "label": "[DEPRECATED] Color Management Preference (legacy)", + "collapsible": true, + "children": [ + { + "type": "text", + "key": "renderSpace", + "label": "Rendering Space" + }, + { + "type": "text", + "key": "viewTransform", + "label": "Viewer Transform (workfile/viewName)" + } + ] } ] } From 7aaa5f767c196bb9b95e3225e0347e84f11660f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 21 Apr 2023 15:42:51 +0200 Subject: [PATCH 345/918] removing old settings --- openpype/settings/defaults/project_settings/fusion.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index c80936d402..ba2abd467f 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -9,14 +9,6 @@ "file_rules": { "override_global_rules": false, "rules": {} - }, - "ocio": { - "enabled": false, - "configFilePath": { - "windows": [], - "darwin": [], - "linux": [] - } } }, "copy_fusion_settings": { From cf7e704964d1db85476e0d2eacc7e5c53485a6ef Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Apr 2023 18:43:01 +0200 Subject: [PATCH 346/918] Collect `currentFile` context data separate from workfile instance (#4883) --- .../plugins/publish/collect_current_file.py | 32 +++-------------- .../plugins/publish/collect_workfile.py | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_workfile.py diff --git a/openpype/hosts/houdini/plugins/publish/collect_current_file.py b/openpype/hosts/houdini/plugins/publish/collect_current_file.py index caf679f98b..7b55778803 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/collect_current_file.py @@ -4,15 +4,14 @@ import hou import pyblish.api -class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): +class CollectHoudiniCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.01 + order = pyblish.api.CollectorOrder - 0.1 label = "Houdini Current File" hosts = ["houdini"] - families = ["workfile"] - def process(self, instance): + def process(self, context): """Inject the current working file""" current_file = hou.hipFile.path() @@ -34,26 +33,5 @@ class CollectHoudiniCurrentFile(pyblish.api.InstancePlugin): "saved correctly." ) - instance.context.data["currentFile"] = current_file - - folder, file = os.path.split(current_file) - filename, ext = os.path.splitext(file) - - instance.data.update({ - "setMembers": [current_file], - "frameStart": instance.context.data['frameStart'], - "frameEnd": instance.context.data['frameEnd'], - "handleStart": instance.context.data['handleStart'], - "handleEnd": instance.context.data['handleEnd'] - }) - - instance.data['representations'] = [{ - 'name': ext.lstrip("."), - 'ext': ext.lstrip("."), - 'files': file, - "stagingDir": folder, - }] - - self.log.info('Collected instance: {}'.format(file)) - self.log.info('Scene path: {}'.format(current_file)) - self.log.info('staging Dir: {}'.format(folder)) + context.data["currentFile"] = current_file + self.log.info('Current workfile path: {}'.format(current_file)) diff --git a/openpype/hosts/houdini/plugins/publish/collect_workfile.py b/openpype/hosts/houdini/plugins/publish/collect_workfile.py new file mode 100644 index 0000000000..a6e94ec29e --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_workfile.py @@ -0,0 +1,36 @@ +import os + +import pyblish.api + + +class CollectWorkfile(pyblish.api.InstancePlugin): + """Inject workfile representation into instance""" + + order = pyblish.api.CollectorOrder - 0.01 + label = "Houdini Workfile Data" + hosts = ["houdini"] + families = ["workfile"] + + def process(self, instance): + + current_file = instance.context.data["currentFile"] + folder, file = os.path.split(current_file) + filename, ext = os.path.splitext(file) + + instance.data.update({ + "setMembers": [current_file], + "frameStart": instance.context.data['frameStart'], + "frameEnd": instance.context.data['frameEnd'], + "handleStart": instance.context.data['handleStart'], + "handleEnd": instance.context.data['handleEnd'] + }) + + instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] + + self.log.info('Collected instance: {}'.format(file)) + self.log.info('staging Dir: {}'.format(folder)) From f4ee2a7537ad393ed6991ea835e5cdb95d77f8c6 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 22 Apr 2023 03:25:51 +0000 Subject: [PATCH 347/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index b9090cd8a1..b43cc436bb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.5-nightly.1" +__version__ = "3.15.5-nightly.2" From e3b5aa0f3fd48ae5b9cf3753d636593b069de809 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 24 Apr 2023 16:22:19 +0800 Subject: [PATCH 348/918] clarify the directory for each renderer's rop --- .../houdini/plugins/create/create_arnold_rop.py | 4 ++-- .../houdini/plugins/create/create_karma_rop.py | 8 ++++---- .../houdini/plugins/create/create_mantra_rop.py | 4 ++-- .../hosts/houdini/plugins/create/create_vray_rop.py | 13 +++++++------ .../houdini/plugins/publish/collect_mantra_rop.py | 4 +++- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 382279e812..2ae6727ce4 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -42,8 +42,8 @@ class CreateArnoldRop(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/renders/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) parms = { # Render frame range diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 4326a98af4..e2fe7f40be 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -33,8 +33,8 @@ class CreateKarmaROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/render/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) checkpoint = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), @@ -42,8 +42,8 @@ class CreateKarmaROP(plugin.HoudiniCreator): ) usd_directory = "{}{}".format( - hou.text.expandString("$HIP/pyblish/usd_renders/"), - "{}_$RENDERID".format(subset_name) + hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), + "{}_$RENDERID".format(subset_name, subset_name, ext) ) parms = { diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 7ccb554be0..83332ec775 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -33,8 +33,8 @@ class CreateMantraROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/render/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) parms = { diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 40981da430..e4875d5b0d 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -70,10 +70,11 @@ class CreateVrayROP(plugin.HoudiniCreator): if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.${}.$F4.{}".format(subset_name, - "AOV", - ext) + hou.text.expandString("$HIP/pyblish/renders/"), + "{}/{}.${}.$F4.{}".format(subset_name, + subset_name, + "AOV", + ext) ) re_rop = instance_node.parent().createNode( "vray_render_channels", @@ -90,8 +91,8 @@ class CreateVrayROP(plugin.HoudiniCreator): else: filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/renders/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) parms.update({ "use_render_channels": 0, diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 1eb850e52e..8f06eb12cf 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -99,8 +99,10 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): var = rop.evalParm("vm_variable_plane%d" % i) if var: aov_name = "vm_filename_plane%d" % i + aov_boolean = "vm_usefile_plane%d" % i + aov_enabled = rop.evalParm(aov_boolean) has_aov_path = rop.evalParm(aov_name) - if has_aov_path: + if has_aov_path and aov_enabled == 1: aov_prefix = evalParmNoFrame(rop, aov_name) aov_product = self.get_render_product_name( prefix=aov_prefix, suffix=None From 36a5beaa7b6f2ac084f3cef546a2f9077e481d12 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 24 Apr 2023 12:05:10 +0200 Subject: [PATCH 349/918] :bug: few fixes --- openpype/hosts/unreal/api/pipeline.py | 2 +- openpype/hosts/unreal/lib.py | 38 +++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0d8922d2e6..bb45fa8c01 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -439,7 +439,7 @@ def create_container(container: str, path: str) -> unreal.Object: ) """ - factory = unreal.AssetContainerFactory() + factory = unreal.AyonAssetContainerFactory() tools = unreal.AssetToolsHelpers().get_asset_tools() return tools.create_asset(container, path, None, factory) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index aa5b09fda8..38976c3ef1 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -317,18 +317,46 @@ def get_path_to_uat(engine_path: Path) -> Path: if platform.system().lower() == "windows": return engine_path / "Engine/Build/BatchFiles/RunUAT.bat" - if platform.system().lower() == "linux" \ - or platform.system().lower() == "darwin": + if platform.system().lower() in ["linux", "darwin"]: return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path(os.path.dirname( - os.path.abspath(os.getenv("OPENPYPE_ROOT")))) + cmd_project = Path( + os.path.abspath(os.getenv("OPENPYPE_ROOT"))) # For now, only tested on Windows (For Linux and Mac # it has to be implemented) - cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" + cmd_project /= f"openpype/hosts/unreal/integration/UE_{ue_version}" + + # if the integration doesn't exist for current engine version + # try to find the closest to it. + if cmd_project.exists(): + return cmd_project / "CommandletProject/CommandletProject.uproject" + + major, minor = ue_version.split(".") + integration_paths = [p for p in cmd_project.parent.iterdir() + if p.is_dir()] + + compatible_versions = [cmd_project] + for i in integration_paths: + + # parse version from path + i_major, i_minor = re.search( + r"(?P\d+).(?P\d+)$", i.name).groups() + + # consider versions with different major so different that they + # are incompatible + if int(major) != int(i_major): + continue + + compatible_versions.append(i) + + sorted(set(compatible_versions)) + + + + return cmd_project / "CommandletProject/CommandletProject.uproject" From 17cb32beda9947770fd7a0bc17ec13973df5d0b1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 24 Apr 2023 12:10:15 +0200 Subject: [PATCH 350/918] :bug: workaround for alembic --- .../unreal/plugins/load/load_geometrycache_abc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 74101d6a53..8b1b9d8f9e 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -22,7 +22,7 @@ class PointCacheAlembicLoader(plugin.Loader): color = "orange" def get_task( - self, filename, asset_dir, asset_name, replace, frame_start, frame_end + self, filename, asset_dir, asset_name, replace, frame_start=None, frame_end=None ): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() @@ -51,8 +51,10 @@ class PointCacheAlembicLoader(plugin.Loader): conversion_settings.set_editor_property( 'rotation', unreal.Vector(x=-90.0, y=0.0, z=180.0)) - sampling_settings.set_editor_property('frame_start', frame_start) - sampling_settings.set_editor_property('frame_end', frame_end) + if frame_start is not None: + sampling_settings.set_editor_property('frame_start', frame_start) + if frame_end is not None: + sampling_settings.set_editor_property('frame_end', frame_end) options.geometry_cache_settings = gc_settings options.conversion_settings = conversion_settings @@ -145,9 +147,9 @@ class PointCacheAlembicLoader(plugin.Loader): name = container["asset_name"] source_path = get_representation_path(representation) destination_path = container["namespace"] + representation["context"] - task = self.get_task(source_path, destination_path, name, True) - + task = self.get_task(source_path, destination_path, name, False) # do import fbx and replace existing data unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) From 60d386b127badba113199c94111bd76de1dee041 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 24 Apr 2023 12:53:17 +0200 Subject: [PATCH 351/918] :bug: fix missing review flag on instance with pre-render --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 536a0698f3..6697a1e59a 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -190,7 +190,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, # make sure rendered sequence on farm will # be used for extract review - if not instance.data["review"]: + if not instance.data.get("review"): instance.data["useSequenceForReview"] = False self.log.debug("instance.data: {}".format(pformat(instance.data))) From ed1fd82ff21877eb517c14865ce48da7149637e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Apr 2023 13:16:38 +0200 Subject: [PATCH 352/918] Scene inventory: Model refresh fix with cherry picking (#4895) * fix bug in model refresh * fix signal callbacks * rename '_refresh_callback' to '_on_refresh_request' --- openpype/tools/sceneinventory/model.py | 169 +++++++++++++----------- openpype/tools/sceneinventory/window.py | 9 +- 2 files changed, 98 insertions(+), 80 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 63d2945145..5cc849bb9e 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -199,90 +199,103 @@ class InventoryModel(TreeModel): """Refresh the model""" host = registered_host() - if not items: # for debugging or testing, injecting items from outside + # for debugging or testing, injecting items from outside + if items is None: if isinstance(host, ILoadHost): items = host.get_containers() - else: + elif hasattr(host, "ls"): items = host.ls() + else: + items = [] self.clear() - - if self._hierarchy_view and selected: - if not hasattr(host.pipeline, "update_hierarchy"): - # If host doesn't support hierarchical containers, then - # cherry-pick only. - self.add_items((item for item in items - if item["objectName"] in selected)) - return - - # Update hierarchy info for all containers - items_by_name = {item["objectName"]: item - for item in host.pipeline.update_hierarchy(items)} - - selected_items = set() - - def walk_children(names): - """Select containers and extend to chlid containers""" - for name in [n for n in names if n not in selected_items]: - selected_items.add(name) - item = items_by_name[name] - yield item - - for child in walk_children(item["children"]): - yield child - - items = list(walk_children(selected)) # Cherry-picked and extended - - # Cut unselected upstream containers - for item in items: - if not item.get("parent") in selected_items: - # Parent not in selection, this is root item. - item["parent"] = None - - parents = [self._root_item] - - # The length of `items` array is the maximum depth that a - # hierarchy could be. - # Take this as an easiest way to prevent looping forever. - maximum_loop = len(items) - count = 0 - while items: - if count > maximum_loop: - self.log.warning("Maximum loop count reached, possible " - "missing parent node.") - break - - _parents = list() - for parent in parents: - _unparented = list() - - def _children(): - """Child item provider""" - for item in items: - if item.get("parent") == parent.get("objectName"): - # (NOTE) - # Since `self._root_node` has no "objectName" - # entry, it will be paired with root item if - # the value of key "parent" is None, or not - # having the key. - yield item - else: - # Not current parent's child, try next - _unparented.append(item) - - self.add_items(_children(), parent) - - items[:] = _unparented - - # Parents of next level - for group_node in parent.children(): - _parents += group_node.children() - - parents[:] = _parents - count += 1 - - else: + if not selected or not self._hierarchy_view: self.add_items(items) + return + + if ( + not hasattr(host, "pipeline") + or not hasattr(host.pipeline, "update_hierarchy") + ): + # If host doesn't support hierarchical containers, then + # cherry-pick only. + self.add_items(( + item + for item in items + if item["objectName"] in selected + )) + return + + # TODO find out what this part does. Function 'update_hierarchy' is + # available only in 'blender' at this moment. + + # Update hierarchy info for all containers + items_by_name = { + item["objectName"]: item + for item in host.pipeline.update_hierarchy(items) + } + + selected_items = set() + + def walk_children(names): + """Select containers and extend to chlid containers""" + for name in [n for n in names if n not in selected_items]: + selected_items.add(name) + item = items_by_name[name] + yield item + + for child in walk_children(item["children"]): + yield child + + items = list(walk_children(selected)) # Cherry-picked and extended + + # Cut unselected upstream containers + for item in items: + if not item.get("parent") in selected_items: + # Parent not in selection, this is root item. + item["parent"] = None + + parents = [self._root_item] + + # The length of `items` array is the maximum depth that a + # hierarchy could be. + # Take this as an easiest way to prevent looping forever. + maximum_loop = len(items) + count = 0 + while items: + if count > maximum_loop: + self.log.warning("Maximum loop count reached, possible " + "missing parent node.") + break + + _parents = list() + for parent in parents: + _unparented = list() + + def _children(): + """Child item provider""" + for item in items: + if item.get("parent") == parent.get("objectName"): + # (NOTE) + # Since `self._root_node` has no "objectName" + # entry, it will be paired with root item if + # the value of key "parent" is None, or not + # having the key. + yield item + else: + # Not current parent's child, try next + _unparented.append(item) + + self.add_items(_children(), parent) + + items[:] = _unparented + + # Parents of next level + for group_node in parent.children(): + _parents += group_node.children() + + parents[:] = _parents + count += 1 def add_items(self, items, parent=None): """Add the items to the model. diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 89424fd746..6ee1c0d38e 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -107,8 +107,8 @@ class SceneInventoryWindow(QtWidgets.QDialog): view.hierarchy_view_changed.connect( self._on_hierarchy_view_change ) - view.data_changed.connect(self.refresh) - refresh_button.clicked.connect(self.refresh) + view.data_changed.connect(self._on_refresh_request) + refresh_button.clicked.connect(self._on_refresh_request) update_all_button.clicked.connect(self._on_update_all) self._update_all_button = update_all_button @@ -139,6 +139,11 @@ class SceneInventoryWindow(QtWidgets.QDialog): """ + def _on_refresh_request(self): + """Signal callback to trigger 'refresh' without any arguments.""" + + self.refresh() + def refresh(self, items=None): with preserve_expanded_rows( tree_view=self._view, From ea83a40f8b5e25e0528775f2a5c6689391ac278e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 15:24:04 +0200 Subject: [PATCH 353/918] Attribute is already set in `parameters` above --- .../substancepainter/plugins/publish/validate_ouput_maps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py index e3d4c733e1..1f4dbaba13 100644 --- a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -34,7 +34,6 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin): parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) parameters["dithering"] = False # no dithering (faster) - config["exportParameters"][0]["parameters"]["sizeLog2"] = [1, 1] result = substance_painter.export.export_project_textures(config) if result.status != substance_painter.export.ExportStatus.Success: From 2ff7d7ee1d8e24412bb50be1c2da12886d104e0a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 15:24:30 +0200 Subject: [PATCH 354/918] Cosmetics --- .../substancepainter/plugins/publish/validate_ouput_maps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py index 1f4dbaba13..b57cf4c5a2 100644 --- a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -31,7 +31,7 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin): # generate the smallest size / fastest export as possible config = copy.deepcopy(config) parameters = config["exportParameters"][0]["parameters"] - parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) + parameters["sizeLog2"] = [1, 1] # output 2x2 images (smallest) parameters["paddingAlgorithm"] = "passthrough" # no dilation (faster) parameters["dithering"] = False # no dithering (faster) From 042efaae33c495999ad5b0fdfedbff0feab77af3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 15:34:15 +0200 Subject: [PATCH 355/918] Implement output template extensions override --- .../plugins/create/create_textures.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/substancepainter/plugins/create/create_textures.py b/openpype/hosts/substancepainter/plugins/create/create_textures.py index 6070a06367..dece4b2cc1 100644 --- a/openpype/hosts/substancepainter/plugins/create/create_textures.py +++ b/openpype/hosts/substancepainter/plugins/create/create_textures.py @@ -91,7 +91,34 @@ class CreateTextures(Creator): EnumDef("exportFileFormat", items={ None: "Based on output template", - # TODO: implement extensions + # TODO: Get available extensions from substance API + "bmp": "bmp", + "ico": "ico", + "jpeg": "jpeg", + "jng": "jng", + "pbm": "pbm", + "pgm": "pgm", + "png": "png", + "ppm": "ppm", + "tga": "targa", + "tif": "tiff", + "wap": "wap", + "wbmp": "wbmp", + "xpm": "xpm", + "gif": "gif", + "hdr": "hdr", + "exr": "exr", + "j2k": "j2k", + "jp2": "jp2", + "pfm": "pfm", + "webp": "webp", + # TODO: Unsure why jxr format fails to export + # "jxr": "jpeg-xr", + # TODO: File formats that combine the exported textures + # like psd are not correctly supported due to + # publishing only a single file + # "psd": "psd", + # "sbsar": "sbsar", }, default=None, label="File type"), From a1b264de9b2b910f1c7a5b7aadd0b931103fcb5d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 16:19:44 +0200 Subject: [PATCH 356/918] Fix houdini workfile icon --- openpype/hosts/houdini/plugins/create/create_workfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 0c6d840810..5f5aa306ee 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -14,7 +14,7 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): identifier = "io.openpype.creators.houdini.workfile" label = "Workfile" family = "workfile" - icon = "document" + icon = "file-o" default_variant = "Main" @@ -90,4 +90,4 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): for created_inst, _changes in update_list: if created_inst["creator_identifier"] == self.identifier: workfile_data = {"workfile": created_inst.data_to_store()} - imprint(op_ctx, workfile_data, update=True) + imprint(op_ctx, workfile_data, update=True) \ No newline at end of file From e2fc8564e6e2fe64b47d3d8561f0f288dec35b98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 16:23:41 +0200 Subject: [PATCH 357/918] Fix accidental newline at end of file removal --- openpype/hosts/houdini/plugins/create/create_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 5f5aa306ee..9884fca325 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -90,4 +90,4 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): for created_inst, _changes in update_list: if created_inst["creator_identifier"] == self.identifier: workfile_data = {"workfile": created_inst.data_to_store()} - imprint(op_ctx, workfile_data, update=True) \ No newline at end of file + imprint(op_ctx, workfile_data, update=True) From ebcd48d13875f472a4c5d1eddc9e4a834b37133d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 24 Apr 2023 17:36:26 +0200 Subject: [PATCH 358/918] Publisher: Keep track about current context and fix context selection widget (#4892) * keep track about last context so it can be updated on context change * don't use '_asset_name' attribute for validation of selected asset * use current context after publisher window close --- .../tools/publisher/widgets/create_widget.py | 39 ++++++++++++++++++- openpype/tools/publisher/window.py | 3 ++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index ef9c5b98fe..db20b21ed7 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -282,6 +282,9 @@ class CreateWidget(QtWidgets.QWidget): thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) + controller.event_system.add_callback( + "main.window.closed", self._on_main_window_close + ) controller.event_system.add_callback( "plugins.refresh.finished", self._on_plugins_refresh ) @@ -316,6 +319,10 @@ class CreateWidget(QtWidgets.QWidget): self._first_show = True self._last_thumbnail_path = None + self._last_current_context_asset = None + self._last_current_context_task = None + self._use_current_context = True + @property def current_asset_name(self): return self._controller.current_asset_name @@ -356,12 +363,39 @@ class CreateWidget(QtWidgets.QWidget): if check_prereq: self._invalidate_prereq() + def _on_main_window_close(self): + """Publisher window was closed.""" + + # Use current context on next refresh + self._use_current_context = True + def refresh(self): + current_asset_name = self._controller.current_asset_name + current_task_name = self._controller.current_task_name + # Get context before refresh to keep selection of asset and # task widgets asset_name = self._get_asset_name() task_name = self._get_task_name() + # Replace by current context if last loaded context was + # 'current context' before reset + if ( + self._use_current_context + or ( + self._last_current_context_asset + and asset_name == self._last_current_context_asset + and task_name == self._last_current_context_task + ) + ): + asset_name = current_asset_name + task_name = current_task_name + + # Store values for future refresh + self._last_current_context_asset = current_asset_name + self._last_current_context_task = current_task_name + self._use_current_context = False + self._prereq_available = False # Disable context widget so refresh of asset will use context asset @@ -398,7 +432,10 @@ class CreateWidget(QtWidgets.QWidget): prereq_available = False creator_btn_tooltips.append("Creator is not selected") - if self._context_change_is_enabled() and self._asset_name is None: + if ( + self._context_change_is_enabled() + and self._get_asset_name() is None + ): # QUESTION how to handle invalid asset? prereq_available = False creator_btn_tooltips.append("Context is not selected") diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 0615157e1b..e94979142a 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -406,6 +406,9 @@ class PublisherWindow(QtWidgets.QDialog): self._comment_input.setText("") # clear comment self._reset_on_show = True self._controller.clear_thumbnail_temp_dir_path() + # Trigger custom event that should be captured only in UI + # - backend (controller) must not be dependent on this event topic!!! + self._controller.event_system.emit("main.window.closed", {}, "window") super(PublisherWindow, self).closeEvent(event) def leaveEvent(self, event): From afa3f563e43be117af30bd2896983b7bd7027d9f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 24 Apr 2023 15:41:57 +0000 Subject: [PATCH 359/918] [Automated] Release --- CHANGELOG.md | 303 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 305 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aeb546c14..16deaaa4fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,309 @@ # Changelog +## [3.15.5](https://github.com/ynput/OpenPype/tree/3.15.5) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.4...3.15.5) + +### **🚀 Enhancements** + + +
+Maya: Playblast profiles #4777 + +Support playblast profiles.This enables studios to customize what playblast settings should be on a per task and/or subset basis. For example `modeling` should have `Wireframe On Shaded` enabled, while all other tasks should have it disabled. + + +___ + +
+ + +
+Maya: Support .abc files directly for Arnold standin look assignment #4856 + +If `.abc` file is loaded into arnold standin support look assignment through the `cbId` attributes in the alembic file. + + +___ + +
+ + +
+Maya: Hide animation instance in creator #4872 + +- Hide animation instance in creator +- Add inventory action to recreate animation publish instance for loaded rigs + + +___ + +
+ + +
+Unreal: Render Creator enhancements #4477 + +Improvements to the creator for render family + +This PR introduces some enhancements to the creator for the render family in Unreal Engine: +- Added the option to create a new, empty sequence for the render. +- Added the option to not include the whole hierarchy for the selected sequence. +- Improvements of the error messages. + + +___ + +
+ + +
+Unreal: Added settings for rendering #4575 + +Added settings for rendering in Unreal Engine. + +Two settings has been added: +- Pre roll frames, to set how many frames are used to load the scene before starting the actual rendering. +- Configuration path, to allow to save a preset of settings from Unreal, and use it for rendering. + + +___ + +
+ + +
+Global: Optimize anatomy formatting by only formatting used templates instead #4784 + +Optimization to not format full anatomy when only a single template is used. Instead format only the single template instead. + + +___ + +
+ + +
+Patchelf version locked #4853 + +For Centos dockerfile it is necessary to lock the patchelf version to the older, otherwise the build process fails. + +___ + +
+ + +
+Houdini: Implement `switch` method on loaders #4866 + +Implement `switch` method on loaders + + +___ + +
+ + +
+Code: Tweak docstrings and return type hints #4875 + +Tweak docstrings and return type hints for functions in `openpype.client.entities`. + + +___ + +
+ + +
+Publisher: Clear comment on successful publish and on window close #4885 + +Clear comment text field on successful publish and on window close. + + +___ + +
+ + +
+Publisher: Make sure to reset asset widget when hidden and reshown #4886 + +Make sure to reset asset widget when hidden and reshown. Without this the asset list would never refresh in the set asset widget when changing context on an existing instance and thus would not show new assets from after the first time launching that widget. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Fix nested model instances. #4852 + +Fix nested model instance under review instance, where data collection was not including "Display Lights" and "Focal Length". + + +___ + +
+ + +
+Maya: Make default namespace naming backwards compatible #4873 + +Namespaces of loaded references are now _by default_ back to what they were before #4511 + + +___ + +
+ + +
+Nuke: Legacy convertor skips deprecation warnings #4846 + +Nuke legacy convertor was triggering deprecated function which is causing a lot of logs which slows down whole process. Changed the convertor to skip all nodes without `AVALON_TAB` to avoid the warnings. + + +___ + +
+ + +
+3dsmax: move startup script logic to hook #4849 + +Startup script for OpenPype was interfering with Open Last Workfile feature. Moving this loggic from simple command line argument in the Settings to pre-launch hook is solving the order of command line arguments and making both features work. + + +___ + +
+ + +
+Maya: Don't change time slider ranges in `get_frame_range` #4858 + +Don't change time slider ranges in `get_frame_range` + + +___ + +
+ + +
+Maya: Looks - calculate hash for tx texture #4878 + +Texture hash is calculated for textures used in published look and it is used as key in dictionary. In recent changes, this hash is not calculated for TX files, resulting in `None` value as key in dictionary, crashing publishing. This PR is adding texture hash for TX files to solve that issue. + + +___ + +
+ + +
+Houdini: Collect `currentFile` context data separate from workfile instance #4883 + +Fix publishing without an active workfile instance due to missing `currentFile` data.Now collect `currentFile` into context in houdini through context plugin no matter the active instances. + + +___ + +
+ + +
+Nuke: fixed broken slate workflow once published on deadline #4887 + +Slate workflow is now working as expected and Validate Sequence Frames is not raising the once slate frame is included. + + +___ + +
+ + +
+Add fps as instance.data in collect review in Houdini. #4888 + +fix the bug of failing to publish extract review in HoudiniOriginal error: +```python + File "OpenPype\build\exe.win-amd64-3.9\openpype\plugins\publish\extract_review.py", line 516, in prepare_temp_data + "fps": float(instance.data["fps"]), +KeyError: 'fps' +``` + + +___ + +
+ + +
+TrayPublisher: Fill missing data for instances with review #4891 + +Fill required data to instance in traypublisher if instance has review family. The data are required by ExtractReview and it would be complicated to do proper fix at this moment! The collector does for review instances what did https://github.com/ynput/OpenPype/pull/4383 + + +___ + +
+ + +
+Publisher: Keep track about current context and fix context selection widget #4892 + +Change selected context to current context on reset. Fix bug when context widget is re-enabled. + + +___ + +
+ + +
+Scene inventory: Model refresh fix with cherry picking #4895 + +Fix cherry pick issue in scene inventory. + + +___ + +
+ + +
+Nuke: Pre-render and missing review flag on instance causing crash #4897 + +If instance created in nuke was missing `review` flag, collector crashed. + + +___ + +
+ +### **Merged pull requests** + + +
+After Effects: fix handles KeyError #4727 + +Sometimes when publishing with AE (we only saw this error on AE 2023), we got a KeyError for the handles in the "Collect Workfile" step. So I did get the handles from the context if ther's no handles in the asset entity. + + +___ + +
+ + + + ## [3.15.4](https://github.com/ynput/OpenPype/tree/3.15.4) diff --git a/openpype/version.py b/openpype/version.py index b43cc436bb..02537af762 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.5-nightly.2" +__version__ = "3.15.5" diff --git a/pyproject.toml b/pyproject.toml index b97ad8923c..2f40d58f56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.4" # OpenPype +version = "3.15.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 55957621645e3ddb6e313916509cbcad275a76e8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 17:47:33 +0200 Subject: [PATCH 360/918] Fusion: Simplify creator icons code (#4899) * Simplify setting creator icons * Use font-awesome 5 explicitly --- openpype/hosts/fusion/plugins/create/create_saver.py | 6 +----- openpype/hosts/fusion/plugins/create/create_workfile.py | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 56085b0a06..cedc4029fa 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,7 +1,5 @@ import os -import qtawesome - from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, @@ -28,6 +26,7 @@ class CreateSaver(Creator): family = "render" default_variants = ["Main", "Mask"] description = "Fusion Saver to generate image sequence" + icon = "fa5.eye" instance_attributes = ["reviewable"] @@ -89,9 +88,6 @@ class CreateSaver(Creator): self._add_instance_to_context(created_instance) - def get_icon(self): - return qtawesome.icon("fa.eye", color="white") - def update_instances(self, update_list): for created_inst, _changes in update_list: new_data = created_inst.data_to_store() diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 0bb3a0d3d4..40721ea88a 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -1,5 +1,3 @@ -import qtawesome - from openpype.hosts.fusion.api import ( get_current_comp ) @@ -15,6 +13,7 @@ class FusionWorkfileCreator(AutoCreator): identifier = "workfile" family = "workfile" label = "Workfile" + icon = "fa5.file" default_variant = "Main" @@ -104,6 +103,3 @@ class FusionWorkfileCreator(AutoCreator): existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name - - def get_icon(self): - return qtawesome.icon("fa.file-o", color="white") From 3a096bcf8bf4ff60ead25495a63ec2bcf6054d18 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Apr 2023 17:51:40 +0200 Subject: [PATCH 361/918] Use explicit font awesome 5 name --- openpype/hosts/houdini/plugins/create/create_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_workfile.py b/openpype/hosts/houdini/plugins/create/create_workfile.py index 9884fca325..1a8537adcd 100644 --- a/openpype/hosts/houdini/plugins/create/create_workfile.py +++ b/openpype/hosts/houdini/plugins/create/create_workfile.py @@ -14,7 +14,7 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): identifier = "io.openpype.creators.houdini.workfile" label = "Workfile" family = "workfile" - icon = "file-o" + icon = "fa5.file" default_variant = "Main" From 0ef59fcb39a033a11b94e0d3884b1b48029e75eb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 25 Apr 2023 08:18:46 +0200 Subject: [PATCH 362/918] adding ci user and email --- .github/workflows/update_bug_report.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update_bug_report.yml b/.github/workflows/update_bug_report.yml index 9f44d7c7a6..7a1bfb7bfd 100644 --- a/.github/workflows/update_bug_report.yml +++ b/.github/workflows/update_bug_report.yml @@ -18,6 +18,8 @@ jobs: uses: ynput/gha-populate-form-version@main with: github_token: ${{ secrets.YNPUT_BOT_TOKEN }} + github_user: ${{ secrets.CI_USER }} + github_email: ${{ secrets.CI_EMAIL }} registry: github dropdown: _version limit_to: 100 From 0567701ddb827f9644e9f9631f56d4b3c73d01c5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 25 Apr 2023 06:32:26 +0000 Subject: [PATCH 363/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c4073ed1af..fe86a8400b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,10 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.5 + - 3.15.5-nightly.2 + - 3.15.5-nightly.1 + - 3.15.4 - 3.15.4-nightly.3 - 3.15.4-nightly.2 - 3.15.4-nightly.1 @@ -131,10 +135,6 @@ body: - 3.13.1-nightly.2 - 3.13.1-nightly.1 - 3.13.0 - - 3.13.0-nightly.1 - - 3.12.3-nightly.3 - - 3.12.3-nightly.2 - - 3.12.3-nightly.1 validations: required: true - type: dropdown @@ -166,8 +166,8 @@ body: label: Are there any labels you wish to add? description: Please search labels and identify those related to your bug. options: - - label: I have added the relevant labels to the bug report. - required: true + - label: I have added the relevant labels to the bug report. + required: true - type: textarea id: logs attributes: From 4ed1c1f65d6f99ece0f35c404e6ca40c3ee2c5fd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Apr 2023 10:29:12 +0200 Subject: [PATCH 364/918] Enhancement: Fix PySide 6.5 support for loader (#4900) * Reverse inheritance order to avoid PySide6.5 bug `PYSIDE-2294` & `PYSIDE-2304` * Fix PySide6 support --- openpype/tools/loader/model.py | 2 +- openpype/tools/publisher/widgets/list_view_widgets.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 14671e341f..e5d8400031 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -123,7 +123,7 @@ class BaseRepresentationModel(object): self.remote_provider = remote_provider -class SubsetsModel(TreeModel, BaseRepresentationModel): +class SubsetsModel(BaseRepresentationModel, TreeModel): doc_fetched = QtCore.Signal() refreshed = QtCore.Signal(bool) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 227ae7bda9..cb5a203130 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -1039,7 +1039,8 @@ class InstanceListView(AbstractInstanceView): proxy_index = proxy_model.mapFromSource(select_indexes[0]) selection_model.setCurrentIndex( proxy_index, - selection_model.ClearAndSelect | selection_model.Rows + QtCore.QItemSelectionModel.ClearAndSelect + | QtCore.QItemSelectionModel.Rows ) return From 38347ece5a7e60f23d643568e1268e3900f8fa21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 25 Apr 2023 10:37:49 +0200 Subject: [PATCH 365/918] Publisher: Small style changes (#4894) * border hover has color without alpha * changed border radius to 0.2em * removed border from scroll area * variant hint button has 0.5em width * inputs in attribute definitions have smaller padding * label is shown only to value inputs and added tooltips * change spacing for attribute befinitions * align labels to right * implemented 'ComboBox' which ignores wheel events and has styled delegate * PixmalLabel has minimum sizeHint * cards are smaller * renamed 'Options' to 'Context' * implemented active state changes in card view * set object name of main window to "PublishWindow" * plugin don't have to pass 'title' to an error * fix PySide6 support for custom keysequences * check for exact match for all bindings * added validation of exact match for save shortcut --- openpype/pipeline/publish/publish_plugins.py | 2 +- openpype/style/data.json | 2 +- openpype/style/style.css | 29 ++++++-- openpype/tools/attribute_defs/widgets.py | 10 ++- openpype/tools/publisher/constants.py | 5 +- openpype/tools/publisher/control.py | 11 ++- .../publisher/widgets/card_view_widgets.py | 72 ++++++++++++++++--- .../tools/publisher/widgets/create_widget.py | 4 ++ .../publisher/widgets/list_view_widgets.py | 5 +- .../publisher/widgets/precreate_widget.py | 14 +++- openpype/tools/publisher/widgets/widgets.py | 33 +++++++-- openpype/tools/publisher/window.py | 19 +++-- openpype/tools/utils/__init__.py | 2 + openpype/tools/utils/widgets.py | 34 ++++++++- 14 files changed, 208 insertions(+), 34 deletions(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 331235fadc..a38896ec8e 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -45,7 +45,7 @@ class PublishValidationError(Exception): def __init__(self, message, title=None, description=None, detail=None): self.message = message - self.title = title or "< Missing title >" + self.title = title self.description = description or message self.detail = detail super(PublishValidationError, self).__init__(message) diff --git a/openpype/style/data.json b/openpype/style/data.json index 404ca6944c..bea2a3d407 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -48,7 +48,7 @@ "bg-view-selection-hover": "rgba(92, 173, 214, .8)", "border": "#373D48", - "border-hover": "rgba(168, 175, 189, .3)", + "border-hover": "rgb(92, 99, 111)", "border-focus": "rgb(92, 173, 214)", "restart-btn-bg": "#458056", diff --git a/openpype/style/style.css b/openpype/style/style.css index da477eeefa..29abb1d351 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -35,6 +35,11 @@ QWidget:disabled { color: {color:font-disabled}; } +/* Some DCCs have set borders to solid color */ +QScrollArea { + border: none; +} + QLabel { background: transparent; } @@ -42,7 +47,7 @@ QLabel { /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; - border-radius: 0.3em; + border-radius: 0.2em; background: {color:bg-inputs}; padding: 0.1em; } @@ -226,7 +231,7 @@ QMenu::separator { /* Combobox */ QComboBox { border: 1px solid {color:border}; - border-radius: 3px; + border-radius: 0.2em; padding: 1px 3px 1px 3px; background: {color:bg-inputs}; } @@ -474,7 +479,6 @@ QAbstractItemView:disabled{ } QAbstractItemView::item:hover { - /* color: {color:bg-view-hover}; */ background: {color:bg-view-hover}; } @@ -743,7 +747,7 @@ OverlayMessageWidget QWidget { #TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { background: transparent; - border-radius: 0.3em; + border-radius: 0.2em; } #TypeEditor:focus, #ToolEditor:focus, #NameEditor:focus, #NumberEditor:focus { @@ -860,7 +864,13 @@ OverlayMessageWidget QWidget { background: {color:bg-view-hover}; } -/* New Create/Publish UI */ +/* Publisher UI (Create/Publish) */ +#PublishWindow QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { + padding: 1px; +} +#PublishWindow QComboBox { + padding: 1px 1px 1px 0.2em; +} PublisherTabsWidget { background: {color:publisher:tab-bg}; } @@ -944,6 +954,7 @@ PixmapButton:disabled { border-top-left-radius: 0px; padding-top: 0.5em; padding-bottom: 0.5em; + width: 0.5em; } #VariantInput[state="new"], #VariantInput[state="new"]:focus, #VariantInput[state="new"]:hover { border-color: {color:publisher:success}; @@ -1072,7 +1083,7 @@ ValidationArtistMessage QLabel { #AssetNameInputWidget { background: {color:bg-inputs}; border: 1px solid {color:border}; - border-radius: 0.3em; + border-radius: 0.2em; } #AssetNameInputWidget QWidget { @@ -1465,6 +1476,12 @@ CreateNextPageOverlay { } /* Attribute Definition widgets */ +AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { + padding: 1px; +} +AttributeDefinitionsWidget QComboBox { + padding: 1px 1px 1px 0.2em; +} InViewButton, InViewButton:disabled { background: transparent; } diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 0d4e1e88a9..d46c238da1 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -1,4 +1,3 @@ -import uuid import copy from qtpy import QtWidgets, QtCore @@ -126,7 +125,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): row = 0 for attr_def in attr_defs: - if not isinstance(attr_def, UIDef): + if attr_def.is_value_def: if attr_def.key in self._current_keys: raise KeyError( "Duplicated key \"{}\"".format(attr_def.key)) @@ -144,11 +143,16 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols - if attr_def.label: + if attr_def.is_value_def and attr_def.label: label_widget = QtWidgets.QLabel(attr_def.label, self) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) layout.addWidget( label_widget, row, 0, 1, expand_cols ) diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 5d23886aa8..660fccecf1 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -2,7 +2,7 @@ from qtpy import QtCore, QtGui # ID of context item in instance view CONTEXT_ID = "context" -CONTEXT_LABEL = "Options" +CONTEXT_LABEL = "Context" # Not showed anywhere - used as identifier CONTEXT_GROUP = "__ContextGroup__" @@ -15,6 +15,9 @@ VARIANT_TOOLTIP = ( "\nnumerical characters (0-9) dot (\".\") or underscore (\"_\")." ) +INPUTS_LAYOUT_HSPACING = 4 +INPUTS_LAYOUT_VSPACING = 2 + # Roles for instance views INSTANCE_ID_ROLE = QtCore.Qt.UserRole + 1 SORT_VALUE_ROLE = QtCore.Qt.UserRole + 2 diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 7754e4aa02..4b083d4bc8 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -163,7 +163,7 @@ class AssetDocsCache: return copy.deepcopy(self._full_asset_docs_by_name[asset_name]) -class PublishReport: +class PublishReportMaker: """Report for single publishing process. Report keeps current state of publishing and currently processed plugin. @@ -784,6 +784,13 @@ class PublishValidationErrors: # Make sure the cached report is cleared plugin_id = self._plugins_proxy.get_plugin_id(plugin) + if not error.title: + if hasattr(plugin, "label") and plugin.label: + plugin_label = plugin.label + else: + plugin_label = plugin.__name__ + error.title = plugin_label + self._error_items.append( ValidationErrorItem.from_result(plugin_id, error, instance) ) @@ -1674,7 +1681,7 @@ class PublisherController(BasePublisherController): # pyblish.api.Context self._publish_context = None # Pyblish report - self._publish_report = PublishReport(self) + self._publish_report = PublishReportMaker(self) # Store exceptions of validation error self._publish_validation_errors = PublishValidationErrors() diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 0734e1bc27..13715bc73c 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -9,7 +9,7 @@ Only one item can be selected at a time. ``` : Icon. Can have Warning icon when context is not right ┌──────────────────────┐ -│ Options │ +│ Context │ │ ────────── │ │ [x]│ │ [x]│ @@ -202,7 +202,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget): class InstanceGroupWidget(BaseGroupWidget): """Widget wrapping instances under group.""" - active_changed = QtCore.Signal() + active_changed = QtCore.Signal(str, str, bool) def __init__(self, group_icons, *args, **kwargs): super(InstanceGroupWidget, self).__init__(*args, **kwargs) @@ -253,13 +253,16 @@ class InstanceGroupWidget(BaseGroupWidget): instance, group_icon, self ) widget.selected.connect(self._on_widget_selection) - widget.active_changed.connect(self.active_changed) + widget.active_changed.connect(self._on_active_changed) self._widgets_by_id[instance.id] = widget self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 self._update_ordered_item_ids() + def _on_active_changed(self, instance_id, value): + self.active_changed.emit(self.group_name, instance_id, value) + class CardWidget(BaseClickableFrame): """Clickable card used as bigger button.""" @@ -332,7 +335,7 @@ class ContextCardWidget(CardWidget): icon_layout.addWidget(icon_widget) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 5, 10, 5) + layout.setContentsMargins(0, 2, 10, 2) layout.addLayout(icon_layout, 0) layout.addWidget(label_widget, 1) @@ -363,7 +366,7 @@ class ConvertorItemCardWidget(CardWidget): icon_layout.addWidget(icon_widget) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 5, 10, 5) + layout.setContentsMargins(0, 2, 10, 2) layout.addLayout(icon_layout, 0) layout.addWidget(label_widget, 1) @@ -377,7 +380,7 @@ class ConvertorItemCardWidget(CardWidget): class InstanceCardWidget(CardWidget): """Card widget representing instance.""" - active_changed = QtCore.Signal() + active_changed = QtCore.Signal(str, bool) def __init__(self, instance, group_icon, parent): super(InstanceCardWidget, self).__init__(parent) @@ -424,7 +427,7 @@ class InstanceCardWidget(CardWidget): top_layout.addWidget(expand_btn, 0) layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 5, 10, 5) + layout.setContentsMargins(0, 2, 10, 2) layout.addLayout(top_layout) layout.addWidget(detail_widget) @@ -445,6 +448,10 @@ class InstanceCardWidget(CardWidget): def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) + @property + def is_active(self): + return self._active_checkbox.isChecked() + def set_active(self, new_value): """Set instance as active.""" checkbox_value = self._active_checkbox.isChecked() @@ -515,7 +522,7 @@ class InstanceCardWidget(CardWidget): return self.instance["active"] = new_value - self.active_changed.emit() + self.active_changed.emit(self._id, new_value) def _on_expend_clicked(self): self._set_expanded() @@ -584,6 +591,45 @@ class InstanceCardView(AbstractInstanceView): result.setWidth(width) return result + def _toggle_instances(self, value): + if not self._active_toggle_enabled: + return + + widgets = self._get_selected_widgets() + changed = False + for widget in widgets: + if not isinstance(widget, InstanceCardWidget): + continue + + is_active = widget.is_active + if value == -1: + widget.set_active(not is_active) + changed = True + continue + + _value = bool(value) + if is_active is not _value: + widget.set_active(_value) + changed = True + + if changed: + self.active_changed.emit() + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Space: + self._toggle_instances(-1) + return True + + elif event.key() == QtCore.Qt.Key_Backspace: + self._toggle_instances(0) + return True + + elif event.key() == QtCore.Qt.Key_Return: + self._toggle_instances(1) + return True + + return super(InstanceCardView, self).keyPressEvent(event) + def _get_selected_widgets(self): output = [] if ( @@ -742,7 +788,15 @@ class InstanceCardView(AbstractInstanceView): for widget in self._widgets_by_group.values(): widget.update_instance_values() - def _on_active_changed(self): + def _on_active_changed(self, group_name, instance_id, value): + group_widget = self._widgets_by_group[group_name] + instance_widget = group_widget.get_widget_by_item_id(instance_id) + if instance_widget.is_selected: + for widget in self._get_selected_widgets(): + if isinstance(widget, InstanceCardWidget): + widget.set_active(value) + else: + self._select_item_clear(instance_id, group_name, instance_widget) self.active_changed.emit() def _on_widget_selection(self, instance_id, group_name, selection_type): diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index db20b21ed7..30980af03d 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -22,6 +22,8 @@ from ..constants import ( CREATOR_IDENTIFIER_ROLE, CREATOR_THUMBNAIL_ENABLED_ROLE, CREATOR_SORT_ROLE, + INPUTS_LAYOUT_HSPACING, + INPUTS_LAYOUT_VSPACING, ) SEPARATORS = ("---separator---", "---") @@ -198,6 +200,8 @@ class CreateWidget(QtWidgets.QWidget): variant_subset_layout = QtWidgets.QFormLayout(variant_subset_widget) variant_subset_layout.setContentsMargins(0, 0, 0, 0) + variant_subset_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + variant_subset_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) variant_subset_layout.addRow("Variant", variant_widget) variant_subset_layout.addRow("Subset", subset_name_input) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index cb5a203130..557e6559c8 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -11,7 +11,7 @@ selection can be enabled disabled using checkbox or keyboard key presses: - Backspace - disable selection ``` -|- Options +|- Context |- [x] | |- [x] | |- [x] @@ -486,6 +486,9 @@ class InstanceListView(AbstractInstanceView): group_widget.set_expanded(expanded) def _on_toggle_request(self, toggle): + if not self._active_toggle_enabled: + return + selected_instance_ids = self._instance_view.get_selected_instance_ids() if toggle == -1: active = None diff --git a/openpype/tools/publisher/widgets/precreate_widget.py b/openpype/tools/publisher/widgets/precreate_widget.py index 3037a0e12d..3bf0bc3657 100644 --- a/openpype/tools/publisher/widgets/precreate_widget.py +++ b/openpype/tools/publisher/widgets/precreate_widget.py @@ -2,6 +2,8 @@ from qtpy import QtWidgets, QtCore from openpype.tools.attribute_defs import create_widget_for_attr_def +from ..constants import INPUTS_LAYOUT_HSPACING, INPUTS_LAYOUT_VSPACING + class PreCreateWidget(QtWidgets.QWidget): def __init__(self, parent): @@ -81,6 +83,8 @@ class AttributesWidget(QtWidgets.QWidget): layout = QtWidgets.QGridLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) self._layout = layout @@ -117,8 +121,16 @@ class AttributesWidget(QtWidgets.QWidget): col_num = 2 - expand_cols - if attr_def.label: + if attr_def.is_value_def and attr_def.label: label_widget = QtWidgets.QLabel(attr_def.label, self) + tooltip = attr_def.tooltip + if tooltip: + label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) self._layout.addWidget( label_widget, row, 0, 1, expand_cols ) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index d2ce1fbcb2..cd1f1f5a96 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -9,7 +9,7 @@ import collections from qtpy import QtWidgets, QtCore, QtGui import qtawesome -from openpype.lib.attribute_definitions import UnknownDef, UIDef +from openpype.lib.attribute_definitions import UnknownDef from openpype.tools.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm @@ -36,6 +36,8 @@ from .icons import ( from ..constants import ( VARIANT_TOOLTIP, ResetKeySequence, + INPUTS_LAYOUT_HSPACING, + INPUTS_LAYOUT_VSPACING, ) @@ -1098,6 +1100,8 @@ class GlobalAttrsWidget(QtWidgets.QWidget): btns_layout.addWidget(cancel_btn) main_layout = QtWidgets.QFormLayout(self) + main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) main_layout.addRow("Variant", variant_input) main_layout.addRow("Asset", asset_value_widget) main_layout.addRow("Task", task_value_widget) @@ -1346,6 +1350,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout.setColumnStretch(0, 0) content_layout.setColumnStretch(1, 1) content_layout.setAlignment(QtCore.Qt.AlignTop) + content_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) row = 0 for attr_def, attr_instances, values in result: @@ -1371,9 +1377,19 @@ class CreatorAttrsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols - label = attr_def.label or attr_def.key + label = None + if attr_def.is_value_def: + label = attr_def.label or attr_def.key if label: label_widget = QtWidgets.QLabel(label, self) + tooltip = attr_def.tooltip + if tooltip: + label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) content_layout.addWidget( label_widget, row, 0, 1, expand_cols ) @@ -1474,6 +1490,8 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): attr_def_layout = QtWidgets.QGridLayout(attr_def_widget) attr_def_layout.setColumnStretch(0, 0) attr_def_layout.setColumnStretch(1, 1) + attr_def_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + attr_def_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.addWidget(attr_def_widget, 0) @@ -1501,12 +1519,19 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): expand_cols = 1 col_num = 2 - expand_cols - label = attr_def.label or attr_def.key + label = None + if attr_def.is_value_def: + label = attr_def.label or attr_def.key if label: label_widget = QtWidgets.QLabel(label, content_widget) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) attr_def_layout.addWidget( label_widget, row, 0, 1, expand_cols ) @@ -1517,7 +1542,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): ) row += 1 - if isinstance(attr_def, UIDef): + if not attr_def.is_value_def: continue widget.value_changed.connect(self._input_value_changed) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index e94979142a..b3471163ae 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -46,6 +46,8 @@ class PublisherWindow(QtWidgets.QDialog): def __init__(self, parent=None, controller=None, reset_on_show=None): super(PublisherWindow, self).__init__(parent) + self.setObjectName("PublishWindow") + self.setWindowTitle("OpenPype publisher") icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) @@ -440,15 +442,24 @@ class PublisherWindow(QtWidgets.QDialog): event.accept() return - if event.matches(QtGui.QKeySequence.Save): + save_match = event.matches(QtGui.QKeySequence.Save) + if save_match == QtGui.QKeySequence.ExactMatch: if not self._controller.publish_has_started: self._save_changes(True) event.accept() return - if ResetKeySequence.matches( - QtGui.QKeySequence(event.key() | event.modifiers()) - ): + # PySide6 Support + if hasattr(event, "keyCombination"): + reset_match_result = ResetKeySequence.matches( + QtGui.QKeySequence(event.keyCombination()) + ) + else: + reset_match_result = ResetKeySequence.matches( + QtGui.QKeySequence(event.modifiers() | event.key()) + ) + + if reset_match_result == QtGui.QKeySequence.ExactMatch: if not self.controller.publish_is_running: self.reset() event.accept() diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 4292e2d726..4149763f80 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -1,6 +1,7 @@ from .widgets import ( FocusSpinBox, FocusDoubleSpinBox, + ComboBox, CustomTextComboBox, PlaceholderLineEdit, BaseClickableFrame, @@ -38,6 +39,7 @@ from .overlay_messages import ( __all__ = ( "FocusSpinBox", "FocusDoubleSpinBox", + "ComboBox", "CustomTextComboBox", "PlaceholderLineEdit", "BaseClickableFrame", diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index b416c56797..bae89aeb09 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -41,7 +41,28 @@ class FocusDoubleSpinBox(QtWidgets.QDoubleSpinBox): super(FocusDoubleSpinBox, self).wheelEvent(event) -class CustomTextComboBox(QtWidgets.QComboBox): +class ComboBox(QtWidgets.QComboBox): + """Base of combobox with pre-implement changes used in tools. + + Combobox is using styled delegate by default so stylesheets are propagated. + + Items are not changed on scroll until the combobox is in focus. + """ + + def __init__(self, *args, **kwargs): + super(ComboBox, self).__init__(*args, **kwargs) + delegate = QtWidgets.QStyledItemDelegate() + self.setItemDelegate(delegate) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + self._delegate = delegate + + def wheelEvent(self, event): + if self.hasFocus(): + return super(ComboBox, self).wheelEvent(event) + + +class CustomTextComboBox(ComboBox): """Combobox which can have different text showed.""" def __init__(self, *args, **kwargs): @@ -253,6 +274,9 @@ class PixmapLabel(QtWidgets.QLabel): self._empty_pixmap = QtGui.QPixmap(0, 0) self._source_pixmap = pixmap + self._last_width = 0 + self._last_height = 0 + def set_source_pixmap(self, pixmap): """Change source image.""" self._source_pixmap = pixmap @@ -263,6 +287,12 @@ class PixmapLabel(QtWidgets.QLabel): size += size % 2 return size, size + def minimumSizeHint(self): + width, height = self._get_pix_size() + if width != self._last_width or height != self._last_height: + self._set_resized_pix() + return QtCore.QSize(width, height) + def _set_resized_pix(self): if self._source_pixmap is None: self.setPixmap(self._empty_pixmap) @@ -276,6 +306,8 @@ class PixmapLabel(QtWidgets.QLabel): QtCore.Qt.SmoothTransformation ) ) + self._last_width = width + self._last_height = height def resizeEvent(self, event): self._set_resized_pix() From a724bd1c77ca9ded191967648e96c2adda8619ea Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 26 Apr 2023 03:25:35 +0000 Subject: [PATCH 366/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 02537af762..080fd6eece 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.5" +__version__ = "3.15.6-nightly.1" From 5d14869180d0c04c744edcf5f88abcca22cbb579 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 18:35:14 +0800 Subject: [PATCH 367/918] validator and selected nodes use for containers --- .../hosts/max/plugins/create/create_model.py | 8 ++-- .../plugins/publish/validate_usd_plugin.py | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_usd_plugin.py diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py index a78a30e0c7..e7ae3af9db 100644 --- a/openpype/hosts/max/plugins/create/create_model.py +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -12,7 +12,6 @@ class CreateModel(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) instance = super(CreateModel, self).create( subset_name, instance_data, @@ -20,7 +19,10 @@ class CreateModel(plugin.MaxCreator): container = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - for obj in sel_obj: - obj.parent = container + sel_obj = None + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py new file mode 100644 index 0000000000..8a92263884 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt + + +class ValidateUSDPlugin(pyblish.api.InstancePlugin): + """Validates if USD plugin is installed or loaded in Max + """ + + order = pyblish.api.ValidatorOrder - 0.01 + families = ["model"] + hosts = ["max"] + label = "USD Plugin" + + def process(self, instance): + #usdimport.dli + #usdexport.dle + plugin_mgr = rt.pluginManager + plugin_count = plugin_mgr.pluginDllCount + plugin_info = self.get_plugins(plugin_mgr, + plugin_count) + usd_import = "usdimport.dli" + if usd_import not in plugin_info: + raise PublishValidationError("USD Plugin {}" + " not found".format(usd_import)) + usd_export = "usdexport.dle" + if usd_export not in plugin_info: + raise PublishValidationError("USD Plugin {}" + " not found".format(usd_export)) + + def get_plugins(self, manager, count): + plugin_info_list = list() + for p in range(1, count + 1): + plugin_info = manager.pluginDllName(p) + plugin_info_list.append(plugin_info) + + return plugin_info_list From 12c9d10ba1faebae7c71bcb1f15fdd293d946e29 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 18:36:27 +0800 Subject: [PATCH 368/918] hound fix --- openpype/hosts/max/plugins/publish/validate_usd_plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py index 8a92263884..747147020a 100644 --- a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py @@ -14,8 +14,6 @@ class ValidateUSDPlugin(pyblish.api.InstancePlugin): label = "USD Plugin" def process(self, instance): - #usdimport.dli - #usdexport.dle plugin_mgr = rt.pluginManager plugin_count = plugin_mgr.pluginDllCount plugin_info = self.get_plugins(plugin_mgr, From fdbe5ac3a1b033bcf4ec7e28b916106914fda951 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 18:45:39 +0800 Subject: [PATCH 369/918] adjustment --- openpype/hosts/max/plugins/publish/validate_model_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index dd9c8de2cf..dd782674ff 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -32,7 +32,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): "{}".format(container)) con = rt.getNodeByName(container) - selection_list = list(con.Children) + selection_list = list(con.Children) or rt.getCurrentSelection() for sel in selection_list: if rt.classOf(sel) in rt.Camera.classes: invalid.append(sel) From 7b6df29203ea2b093bc68bc4f9aec0fa6a167b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 12:48:51 +0200 Subject: [PATCH 370/918] Feature: Blender hook to execute python scripts at launch --- .../hooks/pre_add_run_python_script_arg.py | 61 +++++++++++++++++++ website/docs/dev_blender.md | 61 +++++++++++++++++++ website/sidebars.js | 1 + 3 files changed, 123 insertions(+) create mode 100644 openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py create mode 100644 website/docs/dev_blender.md diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py new file mode 100644 index 0000000000..7cf7b0f852 --- /dev/null +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -0,0 +1,61 @@ +from pathlib import Path + +from openpype.lib import PreLaunchHook +from openpype.settings.lib import get_project_settings + + +class AddPythonScriptToLaunchArgs(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + # Append after file argument + order = 15 + app_groups = [ + "blender", + ] + + def execute(self): + # Check enabled in settings + project_name = self.data["project_name"] + project_settings = get_project_settings(project_name) + host_name = self.application.host_name + host_settings = project_settings.get(host_name) + if not host_settings: + self.log.info(f"""Host "{host_name}" doesn\'t have settings""") + return None + + # Add path to workfile to arguments + for python_script_path in self.launch_context.data.get( + "python_scripts", [] + ): + self.log.info( + f"Adding python script {python_script_path} to launch" + ) + # Test script path exists + if not Path(python_script_path).exists(): + raise ValueError( + f"Python script {python_script_path} doesn't exist." + ) + + if "--" in self.launch_context.launch_args: + # Insert before separator + separator_index = self.launch_context.launch_args.index("--") + self.launch_context.launch_args.insert( + separator_index, + "-P", + ) + self.launch_context.launch_args.insert( + separator_index + 1, + Path(python_script_path).as_posix(), + ) + else: + self.launch_context.launch_args.extend( + ["-P", Path(python_script_path).as_posix()] + ) + + # Ensure separator + if "--" not in self.launch_context.launch_args: + self.launch_context.launch_args.append("--") + + self.launch_context.launch_args.extend( + [*self.launch_context.data.get("script_args", [])] + ) diff --git a/website/docs/dev_blender.md b/website/docs/dev_blender.md new file mode 100644 index 0000000000..228447fb64 --- /dev/null +++ b/website/docs/dev_blender.md @@ -0,0 +1,61 @@ +--- +id: dev_blender +title: Blender integration +sidebar_label: Blender integration +toc_max_heading_level: 4 +--- + +## Run python script at launch +In case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows: + +```python +from openpype.hosts.blender.hooks.pre_add_run_python_script_arg import AddPythonScriptToLaunchArgs +from openpype.lib import PreLaunchHook + + +class MyHook(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + order = AddPythonScriptToLaunchArgs.order - 1 + app_groups = [ + "blender", + ] + + def execute(self): + self.launch_context.data.setdefault("python_scripts", []).append( + "/path/to/my_script.py" + ) +``` + +You can write a bare python script, as you could run into the [Text Editor](https://docs.blender.org/manual/en/latest/editors/text_editor.html). + +### Python script with arguments +#### Adding arguments +In case you need to pass arguments to your script, you can append them to `self.launch_context.data["script_args"]`: + +```python +self.launch_context.data.setdefault("script_args", []).append( + "--my-arg", + "value", + ) +``` + +#### Parsing arguments +You can parse arguments in your script using [argparse](https://docs.python.org/3/library/argparse.html) as follows: + +```python +import argparse + +parser = argparse.ArgumentParser( + description="Parsing arguments for my_script.py" +) +parser.add_argument( + "--my-arg", + nargs="?", + help="My argument", +) +args, unknown = arg_parser.parse_known_args( + sys.argv[sys.argv.index("--") + 1 :] +) +print(args.my_arg) +``` diff --git a/website/sidebars.js b/website/sidebars.js index 93887e00f6..c204c3fb45 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -179,6 +179,7 @@ module.exports = { ] }, "dev_deadline", + "dev_blender", "dev_colorspace" ] }; From 6de1710810b028949c86276088a988ccb83f06e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 12:53:20 +0200 Subject: [PATCH 371/918] clean Path --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 7cf7b0f852..9ae96327ca 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -31,7 +31,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): f"Adding python script {python_script_path} to launch" ) # Test script path exists - if not Path(python_script_path).exists(): + python_script_path = Path(python_script_path) + if not python_script_path.exists(): raise ValueError( f"Python script {python_script_path} doesn't exist." ) @@ -45,11 +46,11 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ) self.launch_context.launch_args.insert( separator_index + 1, - Path(python_script_path).as_posix(), + python_script_path.as_posix(), ) else: self.launch_context.launch_args.extend( - ["-P", Path(python_script_path).as_posix()] + ["-P", python_script_path.as_posix()] ) # Ensure separator From cdf9a10aa19b39c698a57b97abbb5858b6571de6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 19:10:55 +0800 Subject: [PATCH 372/918] roy's comment --- openpype/hosts/max/api/lib.py | 9 ++++++++- .../max/plugins/create/create_redshift_proxy.py | 7 ++++--- .../max/plugins/publish/extract_redshift_proxy.py | 3 +-- .../publish/validate_renderer_redshift_proxy.py | 14 +++++++++----- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ad9a450cad..27d4598a3a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -128,7 +128,14 @@ def get_all_children(parent, node_type=None): def get_current_renderer(): - """get current renderer""" + """ + Notes: + Get current renderer for Max + + Returns: + "{Current Renderer}:{Current Renderer}" + e.g. "Redshift_Renderer:Redshift_Renderer" + """ return rt.renderers.production diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index ca0891fc5b..1bddbdafae 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -18,6 +18,7 @@ class CreateRedshiftProxy(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance container = rt.getNodeByName(instance.data.get("instance_node")) - - for obj in sel_obj: - obj.parent = container + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 0a3579d687..eb1673c4fa 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -45,7 +45,6 @@ class ExtractRedshiftProxy(publish.Extractor): representation = { 'name': 'rs', 'ext': 'rs', - # need to count the files 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa "stagingDir": stagingdir, } @@ -53,7 +52,7 @@ class ExtractRedshiftProxy(publish.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, stagingdir)) - # TODO: set sequence + def get_rsfiles(self, instance, startFrame, endFrame): rs_filenames = [] rs_name = instance.data["name"] diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index 3a921c386e..c834f12ae2 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -22,12 +22,13 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): invalid = self.get_all_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" - " before using this!") + " before using the Redshift proxy instance") invalid = self.get_current_renderer(instance) if invalid: - raise PublishValidationError("Current Renderer is not Redshift") + raise PublishValidationError("The Redshift proxy extraction discontinued" + "since the current renderer is not Redshift") - def get_all_renderer(self, instance): + def get_redshift_renderer(self, instance): invalid = list() max_renderers_list = str(rt.RendererClass.classes) if "Redshift_Renderer" not in max_renderers_list: @@ -46,5 +47,8 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - if "Redshift_Renderer" in str(rt.RendererClass.classes[2]()): - rt.renderers.production = rt.RendererClass.classes[2]() + renderer_count = len(rt.RendererClass.classes) + for r in range(renderer_count): + if "Redshift_Renderer" in str(rt.RendererClass.classes[r]()): + rt.renderers.production = rt.RendererClass.classes[r]() + break From a20d37c68045d73b5f442f503dcbaf31bb8892b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 19:13:33 +0800 Subject: [PATCH 373/918] hound fix --- .../hosts/max/plugins/publish/extract_redshift_proxy.py | 1 - .../max/plugins/publish/validate_renderer_redshift_proxy.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index eb1673c4fa..3b44099609 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -52,7 +52,6 @@ class ExtractRedshiftProxy(publish.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, stagingdir)) - def get_rsfiles(self, instance, startFrame, endFrame): rs_filenames = [] rs_name = instance.data["name"] diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index c834f12ae2..6f8a92a93c 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -22,11 +22,11 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): invalid = self.get_all_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" - " before using the Redshift proxy instance") + " before using the Redshift proxy instance") # noqa invalid = self.get_current_renderer(instance) if invalid: - raise PublishValidationError("The Redshift proxy extraction discontinued" - "since the current renderer is not Redshift") + raise PublishValidationError("The Redshift proxy extraction" + "discontinued since the current renderer is not Redshift") # noqa def get_redshift_renderer(self, instance): invalid = list() From 610a65420d521b26a3c44792368cbd4b9cec6219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 14:02:18 +0200 Subject: [PATCH 374/918] remove useless settings --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 9ae96327ca..ff3683baa9 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -14,15 +14,6 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ] def execute(self): - # Check enabled in settings - project_name = self.data["project_name"] - project_settings = get_project_settings(project_name) - host_name = self.application.host_name - host_settings = project_settings.get(host_name) - if not host_settings: - self.log.info(f"""Host "{host_name}" doesn\'t have settings""") - return None - # Add path to workfile to arguments for python_script_path in self.launch_context.data.get( "python_scripts", [] From 4fe7ce64a2a834adcdd2763a22b8c93d532c0ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 14:05:39 +0200 Subject: [PATCH 375/918] changes from comments --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index ff3683baa9..0f959b8f54 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -1,7 +1,6 @@ from pathlib import Path from openpype.lib import PreLaunchHook -from openpype.settings.lib import get_project_settings class AddPythonScriptToLaunchArgs(PreLaunchHook): @@ -14,10 +13,11 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ] def execute(self): + if not self.launch_context.data.get("python_scripts"): + return + # Add path to workfile to arguments - for python_script_path in self.launch_context.data.get( - "python_scripts", [] - ): + for python_script_path in self.launch_context.data["python_scripts"]: self.log.info( f"Adding python script {python_script_path} to launch" ) From edec4a2b1995adbeda6f5b681b9488f7fb3bcb08 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 20:17:36 +0800 Subject: [PATCH 376/918] roy's comment --- .../publish/validate_renderer_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index 6f8a92a93c..bc82f82f3b 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -19,7 +19,7 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): actions = [RepairAction] def process(self, instance): - invalid = self.get_all_renderer(instance) + invalid = self.get_redshift_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" " before using the Redshift proxy instance") # noqa @@ -47,8 +47,8 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - renderer_count = len(rt.RendererClass.classes) - for r in range(renderer_count): - if "Redshift_Renderer" in str(rt.RendererClass.classes[r]()): - rt.renderers.production = rt.RendererClass.classes[r]() + for Renderer in rt.RendererClass.classes: + renderer = Renderer() + if "Redshift_Renderer" in str(renderer): + rt.renderers.production = renderer break From 668fc9f10f65c4805c6577d51c7bab22478f5d7b Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 26 Apr 2023 18:28:24 +0200 Subject: [PATCH 377/918] Preparation for the submission of the plugin to marketplace, fixed a bug with the cmdlet path not being valid. --- .../hosts/unreal/hooks/pre_workfile_preparation.py | 3 +++ openpype/hosts/unreal/integration/README.md | 10 ++++++++++ .../Ayon/Source/Ayon/Private/AyonAssetContainer.cpp | 1 - .../integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h | 2 -- .../UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h | 1 - .../UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h | 1 - .../Ayon/Source/Ayon/Public/OpenPypePublishInstance.h | 1 - .../unreal/integration/UE_4.27/BuildPlugin_4-27.bat | 1 + .../integration/UE_4.27/BuildPlugin_4-27_Window.bat | 1 + .../Ayon/Source/Ayon/Public/OpenPypePublishInstance.h | 1 - .../unreal/integration/UE_5.0/BuildPlugin_5-0.bat | 1 + .../integration/UE_5.0/BuildPlugin_5-0_Window.bat | 1 + .../Source/Ayon/Private/OpenPypePublishInstance.cpp | 2 +- .../Ayon/Source/Ayon/Public/OpenPypePublishInstance.h | 1 - .../unreal/integration/UE_5.1/BuildPlugin_5-1.bat | 1 + .../integration/UE_5.1/BuildPlugin_5-1_Window.bat | 1 + openpype/hosts/unreal/lib.py | 5 ++--- 17 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 openpype/hosts/unreal/integration/README.md create mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat create mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat create mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index f01609d314..085f80209d 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -107,6 +107,9 @@ class UnrealPrelaunchHook(PreLaunchHook): f"project [ {unreal_project_name} ]" )) + import openpype.hosts.unreal.lib as ue_lib + path = ue_lib.get_path_to_cmdlet_project(engine_version) + q_thread = QtCore.QThread() ue_project_worker = UEProjectGenerationWorker() ue_project_worker.setup( diff --git a/openpype/hosts/unreal/integration/README.md b/openpype/hosts/unreal/integration/README.md new file mode 100644 index 0000000000..961eea83e6 --- /dev/null +++ b/openpype/hosts/unreal/integration/README.md @@ -0,0 +1,10 @@ +# Building the plugin + +In order to successfully build the plugin, make sure that the path to the UnrealBuildTool.exe is specified correctly. +After the UBT path specify for which platform it will be compiled. in the -Project parameter, specify the path to the +CommandletProject.uproject file. Next the build type has to be specified (DebugGame, Development, Package, etc.) and then the -TargetType (Editor, Runtime, etc.) + +`BuildPlugin_[Ver].bat` runs the building process in the background. If you want to show the progress inside the +command prompt, use the `BuildPlugin_[Ver]_Window.bat` file. + + diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp index 316c4015af..e3989eb03c 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp @@ -3,7 +3,6 @@ #include "AyonAssetContainer.h" #include "AssetRegistryModule.h" #include "Misc/PackageName.h" -#include "Engine.h" #include "Containers/UnrealString.h" UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h index 9535ff4b13..d11af70058 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h @@ -2,8 +2,6 @@ #pragma once -#include "Engine.h" - class FAyonModule : public IModuleInterface { diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h index ed657a735c..da83b448fb 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h @@ -1,7 +1,6 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" #include "AyonLib.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h index 831ac022a5..3c429fd7d3 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h @@ -1,6 +1,5 @@ // Copyright 2023, Ayon, All rights reserved. #pragma once -#include "Engine.h" #include "AyonPythonBridge.generated.h" UCLASS(Blueprintable) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 2f3b6aa596..4a7a6a3a9f 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -3,7 +3,6 @@ // and will be removed in next versions of Ayon. #pragma once -#include "Engine.h" #include "OpenPypePublishInstance.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat new file mode 100644 index 0000000000..96cdb96f8a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat @@ -0,0 +1 @@ +D:\UE4\UE_4.27\Engine\Build\BatchFiles\RunUAT.bat BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_4.27\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\4.27" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat new file mode 100644 index 0000000000..1343843a82 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat @@ -0,0 +1 @@ +cmd /k "BuildPlugin_4-27.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 544cb6d915..9c0c4a69e5 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -3,7 +3,6 @@ // and will be removed in next versions of Ayon. #pragma once -#include "Engine.h" #include "OpenPypePublishInstance.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat new file mode 100644 index 0000000000..473c248cbe --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat @@ -0,0 +1 @@ +"C:\Program Files\Epic Games\UE_5.0\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.0\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.0" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat new file mode 100644 index 0000000000..b96de6d6c9 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat @@ -0,0 +1 @@ +cmd /k "BuildPlugin_5-0.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp index 7a65fd0c98..02a8ac800a 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp @@ -58,7 +58,7 @@ void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) if (!IsValid(Asset)) { UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); + *InAssetData.GetSoftObjectPath().ToString()); return; } diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h index 544cb6d915..9c0c4a69e5 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h @@ -3,7 +3,6 @@ // and will be removed in next versions of Ayon. #pragma once -#include "Engine.h" #include "OpenPypePublishInstance.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat new file mode 100644 index 0000000000..3cc82d54af --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat @@ -0,0 +1 @@ +"D:\UE_5.1\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.1\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.1" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat new file mode 100644 index 0000000000..e10f2c7add --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat @@ -0,0 +1 @@ +cmd /k "BuildPlugin_5-1.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index aa5b09fda8..840f79f3c8 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -323,12 +323,11 @@ def get_path_to_uat(engine_path: Path) -> Path: def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project = Path(os.path.dirname( - os.path.abspath(os.getenv("OPENPYPE_ROOT")))) + cmd_project = Path(os.getenv("OPENPYPE_ROOT")) # For now, only tested on Windows (For Linux and Mac # it has to be implemented) - cmd_project /= f"hosts/unreal/integration/UE_{ue_version}" + cmd_project /= f"openpype/hosts/unreal/integration/UE_{ue_version}" return cmd_project / "CommandletProject/CommandletProject.uproject" From 557cbb72cefa955f75fa6c3f1df7fe38665e1c99 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Apr 2023 11:26:57 +0200 Subject: [PATCH 378/918] :recycle: escape rootless path --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index f80bd40133..6ee100ddb4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -275,7 +275,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): args = [ "--headless", 'publish', - rootless_metadata_path, + '"{}"'.format(rootless_metadata_path), "--targets", "deadline", "--targets", "farm" ] From 4274300874dc2a3bdf6d91c79ccebd480a0ddd7e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 20:55:40 +0800 Subject: [PATCH 379/918] oscar's comments --- .../plugins/create/create_arnold_rop.py | 7 +++--- .../plugins/create/create_karma_rop.py | 25 +++++++++---------- .../plugins/create/create_mantra_rop.py | 7 +++--- .../houdini/plugins/create/create_vray_rop.py | 13 +++++++--- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 2ae6727ce4..9634bf1bd9 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -41,9 +41,10 @@ class CreateArnoldRop(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/renders/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) parms = { # Render frame range diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index e2fe7f40be..891a6c9794 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -32,18 +32,19 @@ class CreateKarmaROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/render/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) - checkpoint = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.checkpoint".format(subset_name) + checkpoint = "{cp_dir}{subset_name}.$F4.checkpoint".format( + cp_dir= hou.text.expandString("$HIP/pyblish/"), + subset_name= subset_name ) - usd_directory = "{}{}".format( - hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), - "{}_$RENDERID".format(subset_name, subset_name, ext) + usd_directory = "{usd_dir}{subset_name}_$RENDERID".format( + usd_dir = hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), + subset_name=subset_name ) parms = { @@ -66,12 +67,10 @@ class CreateKarmaROP(plugin.HoudiniCreator): camera = None for node in self.selected_nodes: if node.type().name() == "cam": - camera = node.path() - camera_node = hou.node(camera) has_camera = pre_create_data.get("cam_res") if has_camera: - res_x = camera_node.evalParm("resx") - res_y = camera_node.evalParm("resy") + res_x = node.evalParm("resx") + res_y = node.evalParm("resy") if not camera: self.log.warning("No render camera found in selection") diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 83332ec775..5ca53e96de 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -32,9 +32,10 @@ class CreateMantraROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/render/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) parms = { diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index e4875d5b0d..d9cbea2e53 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -69,6 +69,12 @@ class CreateVrayROP(plugin.HoudiniCreator): instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output + filepath = "{renders_dir}{subset_name}/{subset_name}.${aov}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + aov = "AOV", + ext=ext, + ) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/renders/"), "{}/{}.${}.$F4.{}".format(subset_name, @@ -90,9 +96,10 @@ class CreateVrayROP(plugin.HoudiniCreator): }) else: - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/renders/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) parms.update({ "use_render_channels": 0, From 685d5285922eb82540b20442b76e33b18b6983bf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:04:43 +0800 Subject: [PATCH 380/918] hound fix --- .../hosts/houdini/plugins/create/create_karma_rop.py | 7 ++++--- .../hosts/houdini/plugins/create/create_vray_rop.py | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 891a6c9794..acf6d25b3e 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -38,12 +38,13 @@ class CreateKarmaROP(plugin.HoudiniCreator): ext=ext, ) checkpoint = "{cp_dir}{subset_name}.$F4.checkpoint".format( - cp_dir= hou.text.expandString("$HIP/pyblish/"), - subset_name= subset_name + cp_dir=hou.text.expandString("$HIP/pyblish/"), + subset_name=subset_name ) usd_directory = "{usd_dir}{subset_name}_$RENDERID".format( - usd_dir = hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), + usd_dir=hou.text.expandString( + "$HIP/pyblish/renders/usd_renders/"), subset_name=subset_name ) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index d9cbea2e53..dcad2ca6b2 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -69,11 +69,11 @@ class CreateVrayROP(plugin.HoudiniCreator): instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output - filepath = "{renders_dir}{subset_name}/{subset_name}.${aov}.$F4.{ext}".format( + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - aov = "AOV", - ext=ext, + fmt="${aov}.$F4.{ext}".format(aov = "AOV", + ext=ext,) ) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/renders/"), @@ -96,10 +96,10 @@ class CreateVrayROP(plugin.HoudiniCreator): }) else: - filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - ext=ext, + fmt="$F4.{ext}".format(ext=ext) ) parms.update({ "use_render_channels": 0, From a376db5e5845a5b4384c40cc7b608176afef20b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:08:05 +0800 Subject: [PATCH 381/918] hound fix --- openpype/hosts/houdini/plugins/create/create_karma_rop.py | 3 +-- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index acf6d25b3e..edfb992e1a 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -43,8 +43,7 @@ class CreateKarmaROP(plugin.HoudiniCreator): ) usd_directory = "{usd_dir}{subset_name}_$RENDERID".format( - usd_dir=hou.text.expandString( - "$HIP/pyblish/renders/usd_renders/"), + usd_dir=hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), # noqa subset_name=subset_name ) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index dcad2ca6b2..0a74d93c99 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -72,8 +72,8 @@ class CreateVrayROP(plugin.HoudiniCreator): filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov = "AOV", - ext=ext,) + fmt="${aov}.$F4.{ext}".format(aov="AOV", + ext=ext) ) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/renders/"), From 6ec63c9e6013a0b39f93f370fded8aaaf2be2341 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:10:25 +0800 Subject: [PATCH 382/918] add to get ocio color management preference before rendering --- openpype/hosts/houdini/api/lib.py | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 2e58f3dd98..df5c95578b 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import sys import os +import re import uuid import logging from contextlib import contextmanager @@ -581,3 +582,73 @@ def splitext(name, allowed_multidot_extensions): return name[:-len(ext)], ext return os.path.splitext(name) + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +def get_color_management_preferences(): + """Get default OCIO preferences""" + data = { + "config": hou.Color.ocio_configPath() + + } + + # Get default display and view from OCIO + display = hou.Color.ocio_defaultDisplay() + disp_regex = re.compile(r"^(?P.+-)(?P.+)$") + disp_match = disp_regex.match(display) + + view = hou.Color.ocio_defaultView() + view_regex = re.compile(r"^(?P.+- )(?P.+)$") + view_match = view_regex.match(view) + data.update({ + "display": disp_match.group("display"), + "view": view_match.group("view") + + }) + + return data From c1d4ddf0d6b509e5cc8b2210b702d1eb8f067344 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:11:44 +0800 Subject: [PATCH 383/918] hound fix --- openpype/hosts/houdini/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index df5c95578b..a33ba7aad2 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -583,6 +583,7 @@ def splitext(name, allowed_multidot_extensions): return os.path.splitext(name) + def get_top_referenced_parm(parm): processed = set() # disallow infinite loop @@ -633,7 +634,7 @@ def evalParmNoFrame(node, parm, pad_character="#"): def get_color_management_preferences(): """Get default OCIO preferences""" data = { - "config": hou.Color.ocio_configPath() + "config": hou.Color.ocio_configPath() } From cc4a91b01a26ea3836ab101c62d0907cea56ce27 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:22:38 +0800 Subject: [PATCH 384/918] add colorspace parameters for publish job submission --- openpype/hosts/houdini/api/colorspace.py | 55 ++++++++++++++ .../plugins/publish/collect_arnold_rop.py | 67 +++++------------ .../plugins/publish/collect_karma_rop.py | 70 +++++------------ .../plugins/publish/collect_mantra_rop.py | 75 ++++++------------- .../plugins/publish/collect_redshift_rop.py | 72 ++++++------------ .../plugins/publish/collect_vray_rop.py | 72 +++++------------- 6 files changed, 155 insertions(+), 256 deletions(-) create mode 100644 openpype/hosts/houdini/api/colorspace.py diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py new file mode 100644 index 0000000000..b615bc199c --- /dev/null +++ b/openpype/hosts/houdini/api/colorspace.py @@ -0,0 +1,55 @@ +import attr +import hou +from openpype.hosts.houdini.api.lib import get_color_management_preferences + + +@attr.s +class LayerMetadata(object): + """Data class for Render Layer metadata.""" + frameStart = attr.ib() + frameEnd = attr.ib() + + +@attr.s +class RenderProduct(object): + """Getting Colorspace as + Specific Render Product Parameter for submitting + publish job. + + """ + colorspace = attr.ib() # colorspace + view = attr.ib() + productName = attr.ib(default=None) + +class ARenderProduct(object): + + def __init__(self): + """Constructor.""" + # Initialize + self.layer_data = self._get_layer_data() + self.layer_data.products = self.get_colorspace_data() + + def _get_layer_data(self): + return LayerMetadata( + frameStart=int(hou.playbar.frameRange()[0]), + frameEnd=int(hou.playbar.frameRange()[1]), + ) + + def get_colorspace_data(self): + """To be implemented by renderer class. + + This should return a list of RenderProducts. + + Returns: + list: List of RenderProduct + + """ + data = get_color_management_preferences() + colorspace_data = [ + RenderProduct( + colorspace=data["display"], + view=data["view"], + productName="" + ) + ] + return colorspace_data diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 4d82b74aa2..7ec4ead968 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): @@ -114,6 +75,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): filenames = list(render_products) instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -123,6 +85,12 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -157,9 +125,10 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index 10c97269fc..11747a4100 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): @@ -94,6 +55,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): filenames = list(render_products) instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() for product in render_products: self.log.debug("Found render product: %s" % product) @@ -102,13 +64,18 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): + product_name = prefix if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = prefix_base + "." + suffix + ext - else: - product_name = prefix return product_name @@ -119,9 +86,10 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 8f06eb12cf..9c9a15f8f7 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): @@ -113,8 +74,10 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): for product in render_products: self.log.debug("Found render product: %s" % product) - filenames = list(render_products) - instance.data["files"] = filenames + + filenames = list(render_products) + instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -124,13 +87,18 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): + product_name = prefix if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = prefix_base + "." + suffix + ext - else: - product_name = prefix return product_name @@ -141,9 +109,10 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 353a3756db..ab00e3c339 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): @@ -112,8 +73,10 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): for product in render_products: self.log.debug("Found render product: %s" % product) - filenames = list(render_products) - instance.data["files"] = filenames + + filenames = list(render_products) + instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -123,6 +86,12 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -154,9 +123,10 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 6ec9e0b37e..aa05cf6736 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): @@ -97,9 +58,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): for product in render_products: self.log.debug("Found render product: %s" % product) - filenames = list(render_products) - instance.data["files"] = filenames - self.log.debug("files:{}".format(render_products)) + filenames = list(render_products) + instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -110,6 +71,12 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"].append(files_by_aov) self.log.debug("expectedFiles:{}".format(files_by_aov)) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_beauty_render_product(self, prefix, suffix=""): """Return the beauty output filename if render element enabled """ @@ -144,9 +111,10 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path From f1aff5bcf2cd5ed167651d8946825b6be4c734a8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:24:38 +0800 Subject: [PATCH 385/918] hound fix --- openpype/hosts/houdini/api/colorspace.py | 1 + openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index b615bc199c..7047644225 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -21,6 +21,7 @@ class RenderProduct(object): view = attr.ib() productName = attr.ib(default=None) + class ARenderProduct(object): def __init__(self): diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 7ec4ead968..2fd419ef9b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index 11747a4100..b87bb06767 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 9c9a15f8f7..c4460f5350 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index ab00e3c339..dbb15ab88f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index aa05cf6736..d4fe37f993 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) From 61c37ebb2263af58666b314186652636186f3896 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Tue, 25 Apr 2023 15:54:25 +0200 Subject: [PATCH 386/918] add display handle setting for maya load references --- openpype/hosts/maya/plugins/load/load_reference.py | 9 ++++++--- openpype/settings/defaults/project_settings/maya.json | 3 ++- .../projects_schema/schemas/schema_maya_load.json | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 0dbdb03bb7..3309d7c207 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -162,9 +162,12 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with parent_nodes(roots, parent=None): cmds.xform(group_name, zeroTransformPivots=True) - cmds.setAttr("{}.displayHandle".format(group_name), 1) - settings = get_project_settings(os.environ['AVALON_PROJECT']) + + display_handle = settings['maya']['load'].get('reference_loader', {}).get( + 'display_handle', True) + cmds.setAttr("{}.displayHandle".format(group_name), display_handle) + colors = settings['maya']['load']['colors'] c = colors.get(family) if c is not None: @@ -174,7 +177,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): (float(c[1]) / 255), (float(c[2]) / 255)) - cmds.setAttr("{}.displayHandle".format(group_name), 1) + cmds.setAttr("{}.displayHandle".format(group_name), display_handle) # get bounding box bbox = cmds.exactWorldBoundingBox(group_name) # get pivot position on world space diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 12223216cd..72b330ce7a 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1460,7 +1460,8 @@ }, "reference_loader": { "namespace": "{asset_name}_{subset}_##_", - "group_name": "_GRP" + "group_name": "_GRP", + "display_handle": true } }, "workfile_build": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json index c1895c4824..4b6b97ab4e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_load.json @@ -111,6 +111,14 @@ { "type": "label", "label": "Here's a link to the doc where you can find explanations about customing the naming of referenced assets: https://openpype.io/docs/admin_hosts_maya#load-plugins" + }, + { + "type": "separator" + }, + { + "type": "boolean", + "key": "display_handle", + "label": "Display Handle On Load References" } ] } From 0d4fb1d8162f5647f53abc6d66419bd5f7cce5ba Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Wed, 26 Apr 2023 10:06:00 +0200 Subject: [PATCH 387/918] linting clean up --- openpype/hosts/maya/plugins/load/load_reference.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 3309d7c207..86c2a92a07 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -164,9 +164,10 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): settings = get_project_settings(os.environ['AVALON_PROJECT']) - display_handle = settings['maya']['load'].get('reference_loader', {}).get( - 'display_handle', True) - cmds.setAttr("{}.displayHandle".format(group_name), display_handle) + display_handle = settings['maya']['load'].get( + 'reference_loader', {}).get('display_handle', True) + cmds.setAttr( + "{}.displayHandle".format(group_name), display_handle) colors = settings['maya']['load']['colors'] c = colors.get(family) @@ -177,7 +178,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): (float(c[1]) / 255), (float(c[2]) / 255)) - cmds.setAttr("{}.displayHandle".format(group_name), display_handle) + cmds.setAttr( + "{}.displayHandle".format(group_name), display_handle) # get bounding box bbox = cmds.exactWorldBoundingBox(group_name) # get pivot position on world space From 37ea36b811d427f1c31563967789837c26b96cd6 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Wed, 26 Apr 2023 10:38:00 +0200 Subject: [PATCH 388/918] cosmetiques --- openpype/hosts/maya/plugins/load/load_reference.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 86c2a92a07..7d717dcd44 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -165,9 +165,11 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): settings = get_project_settings(os.environ['AVALON_PROJECT']) display_handle = settings['maya']['load'].get( - 'reference_loader', {}).get('display_handle', True) + 'reference_loader', {} + ).get('display_handle', True) cmds.setAttr( - "{}.displayHandle".format(group_name), display_handle) + "{}.displayHandle".format(group_name), display_handle + ) colors = settings['maya']['load']['colors'] c = colors.get(family) @@ -179,7 +181,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): (float(c[2]) / 255)) cmds.setAttr( - "{}.displayHandle".format(group_name), display_handle) + "{}.displayHandle".format(group_name), display_handle + ) # get bounding box bbox = cmds.exactWorldBoundingBox(group_name) # get pivot position on world space From 289c0ffa060f4df41895c76055bfbac2930da1c4 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Thu, 6 Apr 2023 18:35:09 +0200 Subject: [PATCH 389/918] remove defautl cameras from renderable cameras --- openpype/hosts/maya/api/workfile_template_builder.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index d65e4c74d2..e8d5fc4bfd 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -45,6 +45,13 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): cmds.sets(name=PLACEHOLDER_SET, empty=True) new_nodes = cmds.file(path, i=True, returnNewNodes=True) + # make default cameras non-renderable + default_cameras = [u'perspShape'] + for cam in default_cameras: + if not cmds.objExists("{}.renderable".format(cam)): + continue + cmds.setAttr("{}.renderable".format(cam), 0) + cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True) imported_sets = cmds.ls(new_nodes, set=True) From 6f57d567e45cbd3f3ca12c7b09c036f5b66d6123 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 7 Apr 2023 12:36:56 +0200 Subject: [PATCH 390/918] get default cameras from maya --- openpype/hosts/maya/api/workfile_template_builder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index e8d5fc4bfd..d91fb1e83a 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -46,7 +46,8 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): new_nodes = cmds.file(path, i=True, returnNewNodes=True) # make default cameras non-renderable - default_cameras = [u'perspShape'] + default_cameras = [cam for cam in cmds.ls(cameras=True) + if cmds.camera(cam, query=True, startupCamera=True)] for cam in default_cameras: if not cmds.objExists("{}.renderable".format(cam)): continue From 9cbcef4fd9d3883a13f62c0e24cb28463af499e4 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 7 Apr 2023 14:58:03 +0200 Subject: [PATCH 391/918] apply suggetion. use attribute query --- openpype/hosts/maya/api/workfile_template_builder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index d91fb1e83a..81fc54fe6f 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -49,7 +49,10 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): default_cameras = [cam for cam in cmds.ls(cameras=True) if cmds.camera(cam, query=True, startupCamera=True)] for cam in default_cameras: - if not cmds.objExists("{}.renderable".format(cam)): + if not cmds.attributeQuery("renderable", node=cam, exists=True): + self.log.debug( + "Camera {} has no attribute 'renderable'".format(cam) + ) continue cmds.setAttr("{}.renderable".format(cam), 0) From 4107874eb999a6a5dfb7bf00c209b365b71bd796 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:41:00 +0200 Subject: [PATCH 392/918] Project packager: Backup and restore can store only database (#4879) * added helper functions to client mongo api * pack and unpack project functions can work without project files * added flag argument to pack project command to zip only project files * unpack project has also only project argument * Fix extractions --- openpype/cli.py | 15 +- openpype/client/mongo.py | 223 +++++++++++++++++++++++++- openpype/lib/project_backpack.py | 267 +++++++++++++++++++------------ openpype/pype_commands.py | 8 +- 4 files changed, 394 insertions(+), 119 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index a650a9fdcc..54af42920d 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -415,11 +415,12 @@ def repack_version(directory): @main.command() @click.option("--project", help="Project name") @click.option( - "--dirpath", help="Directory where package is stored", default=None -) -def pack_project(project, dirpath): + "--dirpath", help="Directory where package is stored", default=None) +@click.option( + "--dbonly", help="Store only Database data", default=False, is_flag=True) +def pack_project(project, dirpath, dbonly): """Create a package of project with all files and database dump.""" - PypeCommands().pack_project(project, dirpath) + PypeCommands().pack_project(project, dirpath, dbonly) @main.command() @@ -427,9 +428,11 @@ def pack_project(project, dirpath): @click.option( "--root", help="Replace root which was stored in project", default=None ) -def unpack_project(zipfile, root): +@click.option( + "--dbonly", help="Store only Database data", default=False, is_flag=True) +def unpack_project(zipfile, root, dbonly): """Create a package of project with all files and database dump.""" - PypeCommands().unpack_project(zipfile, root) + PypeCommands().unpack_project(zipfile, root, dbonly) @main.command() diff --git a/openpype/client/mongo.py b/openpype/client/mongo.py index 72acbc5476..251041c028 100644 --- a/openpype/client/mongo.py +++ b/openpype/client/mongo.py @@ -5,6 +5,12 @@ import logging import pymongo import certifi +from bson.json_util import ( + loads, + dumps, + CANONICAL_JSON_OPTIONS +) + if sys.version_info[0] == 2: from urlparse import urlparse, parse_qs else: @@ -15,6 +21,49 @@ class MongoEnvNotSet(Exception): pass +def documents_to_json(docs): + """Convert documents to json string. + + Args: + Union[list[dict[str, Any]], dict[str, Any]]: Document/s to convert to + json string. + + Returns: + str: Json string with mongo documents. + """ + + return dumps(docs, json_options=CANONICAL_JSON_OPTIONS) + + +def load_json_file(filepath): + """Load mongo documents from a json file. + + Args: + filepath (str): Path to a json file. + + Returns: + Union[dict[str, Any], list[dict[str, Any]]]: Loaded content from a + json file. + """ + + if not os.path.exists(filepath): + raise ValueError("Path {} was not found".format(filepath)) + + with open(filepath, "r") as stream: + content = stream.read() + return loads("".join(content)) + + +def get_project_database_name(): + """Name of database name where projects are available. + + Returns: + str: Name of database name where projects are. + """ + + return os.environ.get("AVALON_DB") or "avalon" + + def _decompose_url(url): """Decompose mongo url to basic components. @@ -210,12 +259,102 @@ class OpenPypeMongoConnection: return mongo_client -def get_project_database(): - db_name = os.environ.get("AVALON_DB") or "avalon" - return OpenPypeMongoConnection.get_mongo_client()[db_name] +# ------ Helper Mongo functions ------ +# Functions can be helpful with custom tools to backup/restore mongo state. +# Not meant as API functionality that should be used in production codebase! +def get_collection_documents(database_name, collection_name, as_json=False): + """Query all documents from a collection. + + Args: + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection where to look for collection. + as_json (Optional[bool]): Output should be a json string. + Default: 'False' + + Returns: + Union[list[dict[str, Any]], str]: Queried documents. + """ + + client = OpenPypeMongoConnection.get_mongo_client() + output = list(client[database_name][collection_name].find({})) + if as_json: + output = documents_to_json(output) + return output -def get_project_connection(project_name): +def store_collection(filepath, database_name, collection_name): + """Store collection documents to a json file. + + Args: + filepath (str): Path to a json file where documents will be stored. + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection to store. + """ + + # Make sure directory for output file exists + dirpath = os.path.dirname(filepath) + if not os.path.isdir(dirpath): + os.makedirs(dirpath) + + content = get_collection_documents(database_name, collection_name, True) + with open(filepath, "w") as stream: + stream.write(content) + + +def replace_collection_documents(docs, database_name, collection_name): + """Replace all documents in a collection with passed documents. + + Warnings: + All existing documents in collection will be removed if there are any. + + Args: + docs (list[dict[str, Any]]): New documents. + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection where new documents are + uploaded. + """ + + client = OpenPypeMongoConnection.get_mongo_client() + database = client[database_name] + if collection_name in database.list_collection_names(): + database.drop_collection(collection_name) + col = database[collection_name] + col.insert_many(docs) + + +def restore_collection(filepath, database_name, collection_name): + """Restore/replace collection from a json filepath. + + Warnings: + All existing documents in collection will be removed if there are any. + + Args: + filepath (str): Path to a json with documents. + database_name (str): Name of database where to look for collection. + collection_name (str): Name of collection where new documents are + uploaded. + """ + + docs = load_json_file(filepath) + replace_collection_documents(docs, database_name, collection_name) + + +def get_project_database(database_name=None): + """Database object where project collections are. + + Args: + database_name (Optional[str]): Custom name of database. + + Returns: + pymongo.database.Database: Collection related to passed project. + """ + + if not database_name: + database_name = get_project_database_name() + return OpenPypeMongoConnection.get_mongo_client()[database_name] + + +def get_project_connection(project_name, database_name=None): """Direct access to mongo collection. We're trying to avoid using direct access to mongo. This should be used @@ -223,13 +362,83 @@ def get_project_connection(project_name): api calls for that. Args: - project_name(str): Project name for which collection should be + project_name (str): Project name for which collection should be returned. + database_name (Optional[str]): Custom name of database. Returns: - pymongo.Collection: Collection realated to passed project. + pymongo.collection.Collection: Collection related to passed project. """ if not project_name: raise ValueError("Invalid project name {}".format(str(project_name))) - return get_project_database()[project_name] + return get_project_database(database_name)[project_name] + + +def get_project_documents(project_name, database_name=None): + """Query all documents from project collection. + + Args: + project_name (str): Name of project. + database_name (Optional[str]): Name of mongo database where to look for + project. + + Returns: + list[dict[str, Any]]: Documents in project collection. + """ + + if not database_name: + database_name = get_project_database_name() + return get_collection_documents(database_name, project_name) + + +def store_project_documents(project_name, filepath, database_name=None): + """Store project documents to a file as json string. + + Args: + project_name (str): Name of project to store. + filepath (str): Path to a json file where output will be stored. + database_name (Optional[str]): Name of mongo database where to look for + project. + """ + + if not database_name: + database_name = get_project_database_name() + + store_collection(filepath, database_name, project_name) + + +def replace_project_documents(project_name, docs, database_name=None): + """Replace documents in mongo with passed documents. + + Warnings: + Existing project collection is removed if exists in mongo. + + Args: + project_name (str): Name of project. + docs (list[dict[str, Any]]): Documents to restore. + database_name (Optional[str]): Name of mongo database where project + collection will be created. + """ + + if not database_name: + database_name = get_project_database_name() + replace_collection_documents(docs, database_name, project_name) + + +def restore_project_documents(project_name, filepath, database_name=None): + """Replace documents in mongo with passed documents. + + Warnings: + Existing project collection is removed if exists in mongo. + + Args: + project_name (str): Name of project. + filepath (str): File to json file with project documents. + database_name (Optional[str]): Name of mongo database where project + collection will be created. + """ + + if not database_name: + database_name = get_project_database_name() + restore_collection(filepath, database_name, project_name) diff --git a/openpype/lib/project_backpack.py b/openpype/lib/project_backpack.py index ff2f1d4b88..07107ec011 100644 --- a/openpype/lib/project_backpack.py +++ b/openpype/lib/project_backpack.py @@ -1,16 +1,19 @@ -"""These lib functions are primarily for development purposes. +"""These lib functions are for development purposes. -WARNING: This is not meant for production data. +WARNING: + This is not meant for production data. Please don't write code which is + dependent on functionality here. -Goal is to be able create package of current state of project with related -documents from mongo and files from disk to zip file and then be able recreate -the project based on the zip. +Goal is to be able to create package of current state of project with related +documents from mongo and files from disk to zip file and then be able +to recreate the project based on the zip. This gives ability to create project where a changes and tests can be done. -Keep in mind that to be able create a package of project has few requirements. -Possible requirement should be listed in 'pack_project' function. +Keep in mind that to be able to create a package of project has few +requirements. Possible requirement should be listed in 'pack_project' function. """ + import os import json import platform @@ -19,16 +22,12 @@ import shutil import datetime import zipfile -from bson.json_util import ( - loads, - dumps, - CANONICAL_JSON_OPTIONS +from openpype.client.mongo import ( + load_json_file, + get_project_connection, + replace_project_documents, + store_project_documents, ) -from openpype.client import ( - get_project, - get_whole_project, -) -from openpype.pipeline import AvalonMongoDB DOCUMENTS_FILE_NAME = "database" METADATA_FILE_NAME = "metadata" @@ -43,7 +42,52 @@ def add_timestamp(filepath): return new_base + ext -def pack_project(project_name, destination_dir=None): +def get_project_document(project_name, database_name=None): + """Query project document. + + Function 'get_project' from client api cannot be used as it does not allow + to change which 'database_name' is used. + + Args: + project_name (str): Name of project. + database_name (Optional[str]): Name of mongo database where to look for + project. + + Returns: + Union[dict[str, Any], None]: Project document or None. + """ + + col = get_project_connection(project_name, database_name) + return col.find_one({"type": "project"}) + + +def _pack_files_to_zip(zip_stream, source_path, root_path): + """Pack files to a zip stream. + + Args: + zip_stream (zipfile.ZipFile): Stream to a zipfile. + source_path (str): Path to a directory where files are. + root_path (str): Path to a directory which is used for calculation + of relative path. + """ + + for root, _, filenames in os.walk(source_path): + for filename in filenames: + filepath = os.path.join(root, filename) + # TODO add one more folder + archive_name = os.path.join( + PROJECT_FILES_DIR, + os.path.relpath(filepath, root_path) + ) + zip_stream.write(filepath, archive_name) + + +def pack_project( + project_name, + destination_dir=None, + only_documents=False, + database_name=None +): """Make a package of a project with mongo documents and files. This function has few restrictions: @@ -52,13 +96,18 @@ def pack_project(project_name, destination_dir=None): "{root[...]}/{project[name]}" Args: - project_name(str): Project that should be packaged. - destination_dir(str): Optional path where zip will be stored. Project's - root is used if not passed. + project_name (str): Project that should be packaged. + destination_dir (Optional[str]): Optional path where zip will be + stored. Project's root is used if not passed. + only_documents (Optional[bool]): Pack only Mongo documents and skip + files. + database_name (Optional[str]): Custom database name from which is + project queried. """ + print("Creating package of project \"{}\"".format(project_name)) # Validate existence of project - project_doc = get_project(project_name) + project_doc = get_project_document(project_name, database_name) if not project_doc: raise ValueError("Project \"{}\" was not found in database".format( project_name @@ -119,12 +168,7 @@ def pack_project(project_name, destination_dir=None): temp_docs_json = s.name # Query all project documents and store them to temp json - docs = list(get_whole_project(project_name)) - data = dumps( - docs, json_options=CANONICAL_JSON_OPTIONS - ) - with open(temp_docs_json, "w") as stream: - stream.write(data) + store_project_documents(project_name, temp_docs_json, database_name) print("Packing files into zip") # Write all to zip file @@ -133,16 +177,10 @@ def pack_project(project_name, destination_dir=None): zip_stream.write(temp_metadata_json, METADATA_FILE_NAME + ".json") # Add database documents zip_stream.write(temp_docs_json, DOCUMENTS_FILE_NAME + ".json") + # Add project files to zip - for root, _, filenames in os.walk(project_source_path): - for filename in filenames: - filepath = os.path.join(root, filename) - # TODO add one more folder - archive_name = os.path.join( - PROJECT_FILES_DIR, - os.path.relpath(filepath, root_path) - ) - zip_stream.write(filepath, archive_name) + if not only_documents: + _pack_files_to_zip(zip_stream, project_source_path, root_path) print("Cleaning up") # Cleanup @@ -152,80 +190,30 @@ def pack_project(project_name, destination_dir=None): print("*** Packing finished ***") -def unpack_project(path_to_zip, new_root=None): - """Unpack project zip file to recreate project. +def _unpack_project_files(unzip_dir, root_path, project_name): + """Move project files from unarchived temp folder to new root. + + Unpack is skipped if source files are not available in the zip. That can + happen if nothing was published yet or only documents were stored to + package. Args: - path_to_zip(str): Path to zip which was created using 'pack_project' - function. - new_root(str): Optional way how to set different root path for unpacked - project. + unzip_dir (str): Location where zip was unzipped. + root_path (str): Path to new root. + project_name (str): Name of project. """ - print("Unpacking project from zip {}".format(path_to_zip)) - if not os.path.exists(path_to_zip): - print("Zip file does not exists: {}".format(path_to_zip)) + + src_project_files_dir = os.path.join( + unzip_dir, PROJECT_FILES_DIR, project_name + ) + # Skip if files are not in the zip + if not os.path.exists(src_project_files_dir): return - tmp_dir = tempfile.mkdtemp(prefix="unpack_") - print("Zip is extracted to temp: {}".format(tmp_dir)) - with zipfile.ZipFile(path_to_zip, "r") as zip_stream: - zip_stream.extractall(tmp_dir) - - metadata_json_path = os.path.join(tmp_dir, METADATA_FILE_NAME + ".json") - with open(metadata_json_path, "r") as stream: - metadata = json.load(stream) - - docs_json_path = os.path.join(tmp_dir, DOCUMENTS_FILE_NAME + ".json") - with open(docs_json_path, "r") as stream: - content = stream.readlines() - docs = loads("".join(content)) - - low_platform = platform.system().lower() - project_name = metadata["project_name"] - source_root = metadata["root"] - root_path = source_root[low_platform] - - # Drop existing collection - dbcon = AvalonMongoDB() - database = dbcon.database - if project_name in database.list_collection_names(): - database.drop_collection(project_name) - print("Removed existing project collection") - - print("Creating project documents ({})".format(len(docs))) - # Create new collection with loaded docs - collection = database[project_name] - collection.insert_many(docs) - - # Skip change of root if is the same as the one stored in metadata - if ( - new_root - and (os.path.normpath(new_root) == os.path.normpath(root_path)) - ): - new_root = None - - if new_root: - print("Using different root path {}".format(new_root)) - root_path = new_root - - project_doc = get_project(project_name) - roots = project_doc["config"]["roots"] - key = tuple(roots.keys())[0] - update_key = "config.roots.{}.{}".format(key, low_platform) - collection.update_one( - {"_id": project_doc["_id"]}, - {"$set": { - update_key: new_root - }} - ) - # Make sure root path exists if not os.path.exists(root_path): os.makedirs(root_path) - src_project_files_dir = os.path.join( - tmp_dir, PROJECT_FILES_DIR, project_name - ) dst_project_files_dir = os.path.normpath( os.path.join(root_path, project_name) ) @@ -241,8 +229,83 @@ def unpack_project(path_to_zip, new_root=None): )) shutil.move(src_project_files_dir, dst_project_files_dir) + +def unpack_project( + path_to_zip, new_root=None, database_only=None, database_name=None +): + """Unpack project zip file to recreate project. + + Args: + path_to_zip (str): Path to zip which was created using 'pack_project' + function. + new_root (str): Optional way how to set different root path for + unpacked project. + database_only (Optional[bool]): Unpack only database from zip. + database_name (str): Name of database where project will be recreated. + """ + + if database_only is None: + database_only = False + + print("Unpacking project from zip {}".format(path_to_zip)) + if not os.path.exists(path_to_zip): + print("Zip file does not exists: {}".format(path_to_zip)) + return + + tmp_dir = tempfile.mkdtemp(prefix="unpack_") + print("Zip is extracted to temp: {}".format(tmp_dir)) + with zipfile.ZipFile(path_to_zip, "r") as zip_stream: + if database_only: + for filename in ( + "{}.json".format(METADATA_FILE_NAME), + "{}.json".format(DOCUMENTS_FILE_NAME), + ): + zip_stream.extract(filename, tmp_dir) + else: + zip_stream.extractall(tmp_dir) + + metadata_json_path = os.path.join(tmp_dir, METADATA_FILE_NAME + ".json") + with open(metadata_json_path, "r") as stream: + metadata = json.load(stream) + + docs_json_path = os.path.join(tmp_dir, DOCUMENTS_FILE_NAME + ".json") + docs = load_json_file(docs_json_path) + + low_platform = platform.system().lower() + project_name = metadata["project_name"] + source_root = metadata["root"] + root_path = source_root[low_platform] + + # Drop existing collection + replace_project_documents(project_name, docs, database_name) + print("Creating project documents ({})".format(len(docs))) + + # Skip change of root if is the same as the one stored in metadata + if ( + new_root + and (os.path.normpath(new_root) == os.path.normpath(root_path)) + ): + new_root = None + + if new_root: + print("Using different root path {}".format(new_root)) + root_path = new_root + + project_doc = get_project_document(project_name) + roots = project_doc["config"]["roots"] + key = tuple(roots.keys())[0] + update_key = "config.roots.{}.{}".format(key, low_platform) + collection = get_project_connection(project_name, database_name) + collection.update_one( + {"_id": project_doc["_id"]}, + {"$set": { + update_key: new_root + }} + ) + + _unpack_project_files(tmp_dir, root_path, project_name) + # CLeanup print("Cleaning up") shutil.rmtree(tmp_dir) - dbcon.uninstall() print("*** Unpack finished ***") diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index dc5b3d63c3..6a24cb0ebc 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -353,12 +353,12 @@ class PypeCommands: version_packer = VersionRepacker(directory) version_packer.process() - def pack_project(self, project_name, dirpath): + def pack_project(self, project_name, dirpath, database_only): from openpype.lib.project_backpack import pack_project - pack_project(project_name, dirpath) + pack_project(project_name, dirpath, database_only) - def unpack_project(self, zip_filepath, new_root): + def unpack_project(self, zip_filepath, new_root, database_only): from openpype.lib.project_backpack import unpack_project - unpack_project(zip_filepath, new_root) + unpack_project(zip_filepath, new_root, database_only) From a1eff27bbfcb5e54e05061b644245c4eae9deaeb Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 09:17:09 +0200 Subject: [PATCH 393/918] Added OptionalPyblishPluginMixin and is_active check --- .../plugins/publish/increment_current_file.py | 19 ++++++++++++++----- .../publish/validate_background_depth.py | 15 ++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index 42891446f7..4facd61893 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -1,7 +1,10 @@ import pyblish.api +from openpype.pipeline import OptionalPyblishPluginMixin -class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): +class FusionIncrementCurrentFile( + pyblish.api.ContextPlugin, OptionalPyblishPluginMixin +): """Increment the current file. Saves the current file with an increased version number. @@ -15,15 +18,21 @@ class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): optional = True def process(self, context): + if not self.is_active(context.data): + return from openpype.lib import version_up from openpype.pipeline.publish import get_errored_plugins_from_context errored_plugins = get_errored_plugins_from_context(context) - if any(plugin.__name__ == "FusionSubmitDeadline" - for plugin in errored_plugins): - raise RuntimeError("Skipping incrementing current file because " - "submission to render farm failed.") + if any( + plugin.__name__ == "FusionSubmitDeadline" + for plugin in errored_plugins + ): + raise RuntimeError( + "Skipping incrementing current file because " + "submission to render farm failed." + ) comp = context.data.get("currentComp") assert comp, "Must have comp" diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index db2c4f0dd9..384f6c2979 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -1,12 +1,14 @@ import pyblish.api -from openpype.pipeline.publish import RepairAction +from openpype.pipeline import publish, OptionalPyblishPluginMixin from openpype.pipeline import PublishValidationError from openpype.hosts.fusion.api.action import SelectInvalidAction -class ValidateBackgroundDepth(pyblish.api.InstancePlugin): +class ValidateBackgroundDepth( + pyblish.api.InstancePlugin, OptionalPyblishPluginMixin +): """Validate if all Background tool are set to float32 bit""" order = pyblish.api.ValidatorOrder @@ -15,11 +17,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): families = ["render"] optional = True - actions = [SelectInvalidAction, RepairAction] + actions = [SelectInvalidAction, publish.RepairAction] @classmethod def get_invalid(cls, instance): - context = instance.context comp = context.data.get("currentComp") assert comp, "Must have Comp object" @@ -31,12 +32,16 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): return [i for i in backgrounds if i.GetInput("Depth") != 4.0] def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( "Found {} Backgrounds tools which" " are not set to float32".format(len(invalid)), - title=self.label) + title=self.label, + ) @classmethod def repair(cls, instance): From 1cf5a9e6a726d68f995da56933edba7ebea00dea Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 09:24:17 +0200 Subject: [PATCH 394/918] Cleaned up imports --- .../hosts/fusion/plugins/publish/increment_current_file.py | 1 + .../fusion/plugins/publish/validate_background_depth.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index 4facd61893..938a0ed698 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -1,4 +1,5 @@ import pyblish.api + from openpype.pipeline import OptionalPyblishPluginMixin diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index 384f6c2979..6908889eb4 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -1,7 +1,10 @@ import pyblish.api -from openpype.pipeline import publish, OptionalPyblishPluginMixin -from openpype.pipeline import PublishValidationError +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin, + PublishValidationError, +) from openpype.hosts.fusion.api.action import SelectInvalidAction From b230da486112d08698a5437ddb2f70c88ece773d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 28 Apr 2023 10:56:32 +0200 Subject: [PATCH 395/918] :recycle: change container to custom attrib modifier --- openpype/hosts/max/api/plugin.py | 93 +++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index b54568b360..213d6c04e0 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """3dsmax specific Avalon/Pyblish plugin definitions.""" from pymxs import runtime as rt +from typing import Union import six from abc import ABCMeta from openpype.pipeline import ( @@ -12,6 +13,59 @@ from openpype.lib import BoolDef from .lib import imprint, read, lsattr +MS_CUSTOM_ATTRIB = """attributes "openPypeData" +( + parameters main rollout:OPparams + ( + all_handles type:#maxObjectTab tabSize:0 tabSizeVariable:on + ) + + rollout OPparams "OP Parameters" + ( + listbox list_node "Node References" items:#() + button button_add "Add Selection" + + fn node_to_name the_node = + ( + handle = the_node.handle + obj_name = the_node.name + handle_name = obj_name + "<" + handle as string + ">" + return handle_name + ) + + on button_add pressed do + ( + current_selection = selectByName title:"Select Objects To Add To Container" buttontext:"Add" + temp_arr = #() + i_node_arr = #() + for c in current_selection do + ( + handle_name = node_to_name c + node_ref = NodeTransformMonitor node:c + append temp_arr handle_name + append i_node_arr node_ref + ) + all_handles = i_node_arr + list_node.items = temp_arr + ) + + on OPparams open do + ( + if all_handles.count != 0 do + ( + temp_arr = #() + for x in all_handles do + ( + print(x.node) + handle_name = node_to_name x.node + append temp_arr handle_name + ) + list_node.items = temp_arr + ) + ) + ) +)""" + class OpenPypeCreatorError(CreatorError): pass @@ -33,15 +87,25 @@ class MaxCreatorBase(object): return shared_data @staticmethod - def create_instance_node(node_name: str, parent: str = ""): - parent_node = rt.getNodeByName(parent) if parent else rt.rootScene - if not parent_node: - raise OpenPypeCreatorError(f"Specified parent {parent} not found") + def create_instance_node(node): + """Create instance node. - container = rt.container(name=node_name) - container.Parent = parent_node + If the supplied node is existing node, it will be used to hold the + instance, otherwise new node of type Dummy will be created. - return container + Args: + node (rt.MXSWrapperBase, str): Node or node name to use. + + Returns: + instance + """ + if isinstance(node, str): + node = rt.dummy(name=node) + + attrs = rt.execute(MS_CUSTOM_ATTRIB) + rt.custAttributes.add(node.baseObject, attrs) + + return node @six.add_metaclass(ABCMeta) @@ -60,8 +124,11 @@ class MaxCreator(Creator, MaxCreatorBase): instance_data, self ) - for node in self.selected_nodes: - node.Parent = instance_node + if pre_create_data.get("use_selection"): + print("adding selection") + print(rt.array(*self.selected_nodes)) + instance_node.openPypeData.all_nodes = rt.array( + *self.selected_nodes) self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) @@ -98,11 +165,11 @@ class MaxCreator(Creator, MaxCreatorBase): """ for instance in instances: - instance_node = rt.getNodeByName( - instance.data.get("instance_node")) - if instance_node: + if instance_node := rt.getNodeByName( + instance.data.get("instance_node") + ): rt.select(instance_node) - rt.execute(f'for o in selection do for c in o.children do c.parent = undefined') # noqa + rt.execute("for o in selection do for c in o.children do c.parent = undefined") # noqa rt.delete(instance_node) self._remove_instance_from_context(instance) From d5b719b8c87ca2cee643b460436852e64bc5c912 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 11:00:51 +0200 Subject: [PATCH 396/918] Made name more clear when showing up under `Context` in publisher --- openpype/hosts/fusion/plugins/publish/increment_current_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index 938a0ed698..ed0fd0fbc8 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -12,7 +12,7 @@ class FusionIncrementCurrentFile( """ - label = "Increment current file" + label = "Increment workfile version" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["fusion"] families = ["workfile"] From 4f5a85aee0d48738cfae3fda8acdf453d3e2556f Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 28 Apr 2023 11:01:26 +0200 Subject: [PATCH 397/918] Removed familiy so it can version up even if workfile isn't published --- openpype/hosts/fusion/plugins/publish/increment_current_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index ed0fd0fbc8..de6f697073 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -15,7 +15,6 @@ class FusionIncrementCurrentFile( label = "Increment workfile version" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["fusion"] - families = ["workfile"] optional = True def process(self, context): From eb78b8359b31ded4a4f3339b7d983599df667aff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 17:47:09 +0800 Subject: [PATCH 398/918] add validators and change to take the frame range from the render setting --- openpype/hosts/max/api/lib.py | 8 ++- openpype/hosts/max/api/lib_renderproducts.py | 5 +- .../max/plugins/publish/collect_render.py | 4 +- .../plugins/publish/validate_frame_range.py | 61 +++++++++++++++++++ .../publish/validate_frame_range_type.py | 32 ++++++++++ 5 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_frame_range.py create mode 100644 openpype/hosts/max/plugins/publish/validate_frame_range_type.py diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ad9a450cad..7d629922fc 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -150,10 +150,10 @@ def set_framerange(start_frame, end_frame): Todo: Current type is hard-coded, there should be a custom setting for this. """ - rt.rendTimeType = 4 + rt.rendTimeType = 3 if start_frame is not None and end_frame is not None: - frame_range = "{0}-{1}".format(start_frame, end_frame) - rt.rendPickupFrames = frame_range + rt.rendStart = int(start_frame) + rt.rendEnd = int(end_frame) def get_multipass_setting(project_setting=None): @@ -243,6 +243,7 @@ def reset_frame_range(fps: bool = True): frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) frange_cmd = f"animationRange = interval {frame_start} {frame_end}" rt.execute(frange_cmd) + set_framerange(frame_start, frame_end) def set_context_setting(): @@ -259,6 +260,7 @@ def set_context_setting(): None """ reset_scene_resolution() + reset_frame_range() def get_max_version(): diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 350eb97661..8224d589ad 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -36,8 +36,9 @@ class RenderProducts(object): container) context = get_current_project_asset() - startFrame = context["data"].get("frameStart") - endFrame = context["data"].get("frameEnd") + 1 + # TODO: change the frame range follows the current render setting + startFrame = int(rt.rendStart) + endFrame = int(rt.rendEnd) + 1 img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa full_render_list = self.beauty_render_product(output_file, diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index b040467522..9d93a40021 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -59,8 +59,8 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", - "frameStart": context.data['frameStart'], - "frameEnd": context.data['frameEnd'], + "frameStart": int(rt.rendStart), + "frameEnd": int(rt.rendEnd), "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py new file mode 100644 index 0000000000..2e1d7c9177 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -0,0 +1,61 @@ +import pyblish.api + +from pymxs import runtime as rt +from openpype.pipeline import ( + OptionalPyblishPluginMixin +) +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError +) + + +class ValidateFrameRange(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates the frame ranges. + + This is an optional validator checking if the frame range on instance + matches the frame range specified for the asset. + + It also validates render frame ranges of render layers. + + Repair action will change everything to match the asset frame range. + + This can be turned off by the artist to allow custom ranges. + """ + + label = "Validate Frame Range" + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + self.log.info("Skipping validation...") + return + context = instance.context + + frame_start = int(context.data.get("frameStart")) + frame_end = int(context.data.get("frameEnd")) + + inst_frame_start = int(instance.data.get("frameStart")) + inst_frame_end = int(instance.data.get("frameEnd")) + + + if frame_start != inst_frame_start: + raise PublishValidationError( + "startFrame on instance does not match" + " with startFrame from the context data") + + if frame_end != inst_frame_end: + raise PublishValidationError( + "endFrame on instance does not match" + " with endFrame from the context data") + + @classmethod + def repair(cls, instance): + rt.rendStart = instance.context.data.get("frameStart") + rt.rendEnd = instance.context.data.get("frameEnd") diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py new file mode 100644 index 0000000000..2403595b3b --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -0,0 +1,32 @@ +import pyblish.api + +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError +) + + +class ValidateFrameRangeType(pyblish.api.InstancePlugin): + """ + Validates whether the User + specified Frame Range(Type 3) is used in render setting + + """ + + label = "Validate Render Frame Range Type" + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + actions = [RepairAction] + + def process(self, instance): + if rt.rendTimeType != 3: + raise PublishValidationError("Incorrect type of frame range" + " used in render setting..") + + @classmethod + def repair(cls, instance): + rt.renderTimeType = 3 + return instance From c54447372308ac7fda9d91aa16a0ced50480ad17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 17:59:36 +0800 Subject: [PATCH 399/918] set frame range validator to switch off by default --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 2e1d7c9177..8d5d99197e 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -29,7 +29,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, order = ValidateContentsOrder families = ["maxrender"] hosts = ["max"] - optional = True + optional = False actions = [RepairAction] def process(self, instance): From d4422d7ec57fc894bac76b4bf974945d1db3413d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 18:00:12 +0800 Subject: [PATCH 400/918] cosmetic fix --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 8d5d99197e..1fafacb8b0 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -44,7 +44,6 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, inst_frame_start = int(instance.data.get("frameStart")) inst_frame_end = int(instance.data.get("frameEnd")) - if frame_start != inst_frame_start: raise PublishValidationError( "startFrame on instance does not match" From 13264ea11c7985f8c5c117f4dcc84ecc925c009b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 18:41:05 +0800 Subject: [PATCH 401/918] roy's comment --- openpype/hosts/max/api/lib_rendersettings.py | 4 ++-- openpype/hosts/max/plugins/publish/validate_frame_range.py | 6 ++++-- .../hosts/max/plugins/publish/validate_frame_range_type.py | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 4940265a23..82a25dfa29 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -6,7 +6,7 @@ from openpype.pipeline import legacy_io from openpype.pipeline.context_tools import get_current_project_asset from openpype.hosts.max.api.lib import ( - set_framerange, + set_render_frame_range, get_current_renderer, get_default_render_folder ) @@ -68,7 +68,7 @@ class RenderSettings(object): # Set Frame Range frame_start = context["data"].get("frame_start") frame_end = context["data"].get("frame_end") - set_framerange(frame_start, frame_end) + set_render_frame_range(frame_start, frame_end) # get the production render renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 1fafacb8b0..fc2782ded6 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -47,12 +47,14 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if frame_start != inst_frame_start: raise PublishValidationError( "startFrame on instance does not match" - " with startFrame from the context data") + " with startFrame from the context data" + " You can use repair action to fix it") if frame_end != inst_frame_end: raise PublishValidationError( "endFrame on instance does not match" - " with endFrame from the context data") + " with endFrame from the context data" + " You can use repair action to fix it") @classmethod def repair(cls, instance): diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py index 2403595b3b..7bc23e5a70 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -24,7 +24,8 @@ class ValidateFrameRangeType(pyblish.api.InstancePlugin): def process(self, instance): if rt.rendTimeType != 3: raise PublishValidationError("Incorrect type of frame range" - " used in render setting..") + " used in render setting.." + "Repair action can help to fix it.") @classmethod def repair(cls, instance): From 63c7c1c2e9ed8002dff9f703852e2d9b53c08a6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 28 Apr 2023 12:43:34 +0200 Subject: [PATCH 402/918] :rotating_light: tabs to spaces --- openpype/hosts/max/api/plugin.py | 104 ++++++++++++++++--------------- 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 213d6c04e0..15fcc89c5f 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -12,60 +12,61 @@ from openpype.pipeline import ( from openpype.lib import BoolDef from .lib import imprint, read, lsattr - MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( - parameters main rollout:OPparams - ( - all_handles type:#maxObjectTab tabSize:0 tabSizeVariable:on - ) + parameters main rollout:OPparams + ( + all_handles type:#maxObjectTab tabSize:0 tabSizeVariable:on + ) - rollout OPparams "OP Parameters" - ( - listbox list_node "Node References" items:#() - button button_add "Add Selection" + rollout OPparams "OP Parameters" + ( + listbox list_node "Node References" items:#() + button button_add "Add Selection" - fn node_to_name the_node = - ( - handle = the_node.handle - obj_name = the_node.name - handle_name = obj_name + "<" + handle as string + ">" - return handle_name - ) + fn node_to_name the_node = + ( + handle = the_node.handle + obj_name = the_node.name + handle_name = obj_name + "<" + handle as string + ">" + return handle_name + ) - on button_add pressed do - ( - current_selection = selectByName title:"Select Objects To Add To Container" buttontext:"Add" - temp_arr = #() - i_node_arr = #() - for c in current_selection do - ( - handle_name = node_to_name c - node_ref = NodeTransformMonitor node:c - append temp_arr handle_name - append i_node_arr node_ref - ) - all_handles = i_node_arr - list_node.items = temp_arr - ) + on button_add pressed do + ( + current_selection = selectByName title:"Select Objects To Add To + Container" buttontext:"Add" + temp_arr = #() + i_node_arr = #() + for c in current_selection do + ( + handle_name = node_to_name c + node_ref = NodeTransformMonitor node:c + append temp_arr handle_name + append i_node_arr node_ref + ) + all_handles = i_node_arr + list_node.items = temp_arr + ) - on OPparams open do - ( - if all_handles.count != 0 do - ( - temp_arr = #() - for x in all_handles do - ( - print(x.node) - handle_name = node_to_name x.node - append temp_arr handle_name - ) - list_node.items = temp_arr - ) - ) - ) + on OPparams open do + ( + if all_handles.count != 0 do + ( + temp_arr = #() + for x in all_handles do + ( + print(x.node) + handle_name = node_to_name x.node + append temp_arr handle_name + ) + list_node.items = temp_arr + ) + ) + ) )""" + class OpenPypeCreatorError(CreatorError): pass @@ -83,7 +84,8 @@ class MaxCreatorBase(object): shared_data["max_cached_subsets"][creator_id] = [i.name] else: shared_data[ - "max_cached_subsets"][creator_id].append(i.name) # noqa + "max_cached_subsets"][creator_id].append( + i.name) # noqa return shared_data @staticmethod @@ -138,7 +140,7 @@ class MaxCreator(Creator, MaxCreatorBase): def collect_instances(self): self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ - "max_cached_subsets"].get(self.identifier, []): + "max_cached_subsets"].get(self.identifier, []): created_instance = CreatedInstance.from_existing( read(rt.getNodeByName(instance)), self ) @@ -166,10 +168,12 @@ class MaxCreator(Creator, MaxCreatorBase): """ for instance in instances: if instance_node := rt.getNodeByName( - instance.data.get("instance_node") + instance.data.get("instance_node") ): rt.select(instance_node) - rt.execute("for o in selection do for c in o.children do c.parent = undefined") # noqa + rt.execute( + "for o in selection do for c in o.children do c.parent = " + "undefined") # noqa rt.delete(instance_node) self._remove_instance_from_context(instance) From f3b1002f2641bcc122e24debee6b042cb05dee1e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 28 Apr 2023 19:02:17 +0800 Subject: [PATCH 403/918] style fix --- openpype/hosts/max/api/lib.py | 4 ++-- openpype/hosts/max/plugins/publish/validate_frame_range.py | 4 ++-- .../hosts/max/plugins/publish/validate_frame_range_type.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 7d629922fc..1673fc5ab8 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -138,7 +138,7 @@ def get_default_render_folder(project_setting=None): ["default_render_image_folder"]) -def set_framerange(start_frame, end_frame): +def set_render_frame_range(start_frame, end_frame): """ Note: Frame range can be specified in different types. Possible values are: @@ -243,7 +243,7 @@ def reset_frame_range(fps: bool = True): frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) frange_cmd = f"animationRange = interval {frame_start} {frame_end}" rt.execute(frange_cmd) - set_framerange(frame_start, frame_end) + set_render_frame_range(frame_start, frame_end) def set_context_setting(): diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index fc2782ded6..dc12eece39 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -47,13 +47,13 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if frame_start != inst_frame_start: raise PublishValidationError( "startFrame on instance does not match" - " with startFrame from the context data" + " with startFrame from the context data." " You can use repair action to fix it") if frame_end != inst_frame_end: raise PublishValidationError( "endFrame on instance does not match" - " with endFrame from the context data" + " with endFrame from the context data." " You can use repair action to fix it") @classmethod diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py index 7bc23e5a70..944780f6fa 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -24,7 +24,7 @@ class ValidateFrameRangeType(pyblish.api.InstancePlugin): def process(self, instance): if rt.rendTimeType != 3: raise PublishValidationError("Incorrect type of frame range" - " used in render setting.." + " used in render setting." "Repair action can help to fix it.") @classmethod From fdc94f3b88a1eaaa1509ec5e802d82e261623034 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:50:55 +0300 Subject: [PATCH 404/918] add vscode workspace to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 18e7cd7bf2..a565c57b54 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ openpype/premiere/ppro/js/debug.log .env dump.sql test_localsystem.txt +*.code-workspace # website ########## From 015f13bb908e48db077c528e0388ef4c31a39d7a Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:57:00 +0300 Subject: [PATCH 405/918] better variable naming for utils --- openpype/hosts/resolve/utils.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 5881f153ae..8e5dd9a188 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -8,30 +8,30 @@ RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) def setup(env): log = Logger.get_logger("ResolveSetup") scripts = {} - us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"] + util_scripts_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") + util_scripts_dir = env["RESOLVE_UTILITY_SCRIPTS_DIR"] - us_paths = [os.path.join( + util_scripts_paths = [os.path.join( RESOLVE_ROOT_DIR, "utility_scripts" )] # collect script dirs - if us_env: - log.info("Utility Scripts Env: `{}`".format(us_env)) - us_paths = us_env.split( - os.pathsep) + us_paths + if util_scripts_env: + log.info("Utility Scripts Env: `{}`".format(util_scripts_env)) + util_scripts_paths = util_scripts_env.split( + os.pathsep) + util_scripts_paths # collect scripts from dirs - for path in us_paths: + for path in util_scripts_paths: scripts.update({path: os.listdir(path)}) - log.info("Utility Scripts Dir: `{}`".format(us_paths)) + log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths)) log.info("Utility Scripts: `{}`".format(scripts)) # make sure no script file is in folder - for s in os.listdir(us_dir): - path = os.path.join(us_dir, s) + for script in os.listdir(util_scripts_dir): + path = os.path.join(util_scripts_dir, script) log.info("Removing `{}`...".format(path)) if os.path.isdir(path): shutil.rmtree(path, onerror=None) @@ -39,12 +39,10 @@ def setup(env): os.remove(path) # copy scripts into Resolve's utility scripts dir - for d, sl in scripts.items(): - # directory and scripts list - for s in sl: - # script in script list - src = os.path.join(d, s) - dst = os.path.join(us_dir, s) + for directory, scripts in scripts.items(): + for script in scripts: + src = os.path.join(directory, script) + dst = os.path.join(util_scripts_dir, script) log.info("Copying `{}` to `{}`...".format(src, dst)) if os.path.isdir(src): shutil.copytree( From 7cc08d026a1d812ee62c5f12ba67dbf6c2670c60 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:57:29 +0300 Subject: [PATCH 406/918] upse pathlib instead of os.path, some cleanup --- .../hosts/resolve/hooks/pre_resolve_setup.py | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 8574b3ad01..3144a60312 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import platform from openpype.lib import PreLaunchHook from openpype.hosts.resolve.utils import setup @@ -16,10 +17,10 @@ class ResolvePrelaunch(PreLaunchHook): def execute(self): current_platform = platform.system().lower() - PROGRAMDATA = self.launch_context.env.get("PROGRAMDATA", "") - RESOLVE_SCRIPT_API_ = { + programdata = self.launch_context.env.get("PROGRAMDATA", "") + resolve_script_api_locations = { "windows": ( - f"{PROGRAMDATA}/Blackmagic Design/" + f"{programdata}/Blackmagic Design/" "DaVinci Resolve/Support/Developer/Scripting" ), "darwin": ( @@ -28,11 +29,10 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/Developer/Scripting" } - RESOLVE_SCRIPT_API = os.path.normpath( - RESOLVE_SCRIPT_API_[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_API"] = RESOLVE_SCRIPT_API + resolve_script_api = Path(resolve_script_api_locations[current_platform]) + self.launch_context.env["RESOLVE_SCRIPT_API"] = resolve_script_api.as_posix() - RESOLVE_SCRIPT_LIB_ = { + resolve_script_lib_dirs = { "windows": ( "C:/Program Files/Blackmagic Design" "/DaVinci Resolve/fusionscript.dll" @@ -43,45 +43,39 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/libs/Fusion/fusionscript.so" } - RESOLVE_SCRIPT_LIB = os.path.normpath( - RESOLVE_SCRIPT_LIB_[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_LIB"] = RESOLVE_SCRIPT_LIB + resolve_script_lib = Path(resolve_script_lib_dirs[current_platform]) + self.launch_context.env["RESOLVE_SCRIPT_LIB"] = resolve_script_lib.as_posix() - # TODO: add OTIO installation from `openpype/requirements.py` + # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path - python3_home = os.path.normpath( - self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) + python3_home = Path(self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) - assert os.path.isdir(python3_home), ( + assert python3_home.is_dir(), ( "Python 3 is not installed at the provided folder path. Either " "make sure the `environments\resolve.json` is having correctly " "set `RESOLVE_PYTHON3_HOME` or make sure Python 3 is installed " f"in given path. \nRESOLVE_PYTHON3_HOME: `{python3_home}`" ) - self.launch_context.env["PYTHONHOME"] = python3_home - self.log.info(f"Path to Resolve Python folder: `{python3_home}`...") + python3_home_str = python3_home.as_posix() + self.launch_context.env["PYTHONHOME"] = python3_home_str + self.log.info(f"Path to Resolve Python folder: `{python3_home_str}`") - # add to the python path to path + # add to the python path to PATH env_path = self.launch_context.env["PATH"] - self.launch_context.env["PATH"] = os.pathsep.join([ - python3_home, - os.path.join(python3_home, "Scripts") - ] + env_path.split(os.pathsep)) + self.launch_context.env["PATH"] = f"{python3_home_str}{os.pathsep}{env_path}" self.log.debug(f"PATH: {self.launch_context.env['PATH']}") # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] - self.launch_context.env["PYTHONPATH"] = os.pathsep.join([ - os.path.join(python3_home, "Lib", "site-packages"), - os.path.join(RESOLVE_SCRIPT_API, "Modules"), - ] + env_pythonpath.split(os.pathsep)) + modules_path = Path(resolve_script_api, "Modules").as_posix() + self.launch_context.env["PYTHONPATH"] = f"{modules_path}{os.pathsep}{env_pythonpath}" self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") - RESOLVE_UTILITY_SCRIPTS_DIR_ = { + resolve_utility_scripts_dirs = { "windows": ( - f"{PROGRAMDATA}/Blackmagic Design" + f"{programdata}/Blackmagic Design" "/DaVinci Resolve/Fusion/Scripts/Comp" ), "darwin": ( @@ -90,12 +84,9 @@ class ResolvePrelaunch(PreLaunchHook): ), "linux": "/opt/resolve/Fusion/Scripts/Comp" } - RESOLVE_UTILITY_SCRIPTS_DIR = os.path.normpath( - RESOLVE_UTILITY_SCRIPTS_DIR_[current_platform] - ) + resolve_utility_scripts_dir = Path(resolve_utility_scripts_dirs[current_platform]) # setting utility scripts dir for scripts syncing - self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = ( - RESOLVE_UTILITY_SCRIPTS_DIR) + self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = resolve_utility_scripts_dir.as_posix() # remove terminal coloring tags self.launch_context.env["OPENPYPE_LOG_NO_COLORS"] = "True" From affc00c77c1bcf473a5aa13e40ef229eb2f30c0f Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 17:59:26 +0300 Subject: [PATCH 407/918] remove bin folder from default values --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index df5b5e07c6..2c38676126 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1069,8 +1069,8 @@ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_PYTHON3_HOME": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darwin": "~/Library/Python/3.6/bin", - "linux": "/opt/Python/3.6/bin" + "darwin": "~/Library/Python/3.9", + "linux": "/opt/Python/3.9" } }, "variants": { From 74c1e6f3bb860c5aea18a669cb285d5fb2f0ca43 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 18:12:26 +0300 Subject: [PATCH 408/918] defaults to py3.6, set actual macos python path --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 2c38676126..9aa30093cc 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1069,8 +1069,8 @@ "RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR": [], "RESOLVE_PYTHON3_HOME": { "windows": "{LOCALAPPDATA}/Programs/Python/Python36", - "darwin": "~/Library/Python/3.9", - "linux": "/opt/Python/3.9" + "darwin": "/Library/Frameworks/Python.framework/Versions/3.6", + "linux": "/opt/Python/3.6" } }, "variants": { From 4388da15df4b6a5f5a07e3514c54652e12b58691 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Fri, 28 Apr 2023 18:32:33 +0300 Subject: [PATCH 409/918] black formatting --- .../hosts/resolve/hooks/pre_resolve_setup.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 3144a60312..8c88478104 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -12,6 +12,7 @@ class ResolvePrelaunch(PreLaunchHook): path to the project by environment variable to Premiere launcher shell script. """ + app_groups = ["resolve"] def execute(self): @@ -27,10 +28,14 @@ class ResolvePrelaunch(PreLaunchHook): "/Library/Application Support/Blackmagic Design" "/DaVinci Resolve/Developer/Scripting" ), - "linux": "/opt/resolve/Developer/Scripting" + "linux": "/opt/resolve/Developer/Scripting", } - resolve_script_api = Path(resolve_script_api_locations[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_API"] = resolve_script_api.as_posix() + resolve_script_api = Path( + resolve_script_api_locations[current_platform] + ) + self.launch_context.env[ + "RESOLVE_SCRIPT_API" + ] = resolve_script_api.as_posix() resolve_script_lib_dirs = { "windows": ( @@ -41,14 +46,18 @@ class ResolvePrelaunch(PreLaunchHook): "/Applications/DaVinci Resolve/DaVinci Resolve.app" "/Contents/Libraries/Fusion/fusionscript.so" ), - "linux": "/opt/resolve/libs/Fusion/fusionscript.so" + "linux": "/opt/resolve/libs/Fusion/fusionscript.so", } resolve_script_lib = Path(resolve_script_lib_dirs[current_platform]) - self.launch_context.env["RESOLVE_SCRIPT_LIB"] = resolve_script_lib.as_posix() + self.launch_context.env[ + "RESOLVE_SCRIPT_LIB" + ] = resolve_script_lib.as_posix() # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path - python3_home = Path(self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "")) + python3_home = Path( + self.launch_context.env.get("RESOLVE_PYTHON3_HOME", "") + ) assert python3_home.is_dir(), ( "Python 3 is not installed at the provided folder path. Either " @@ -62,14 +71,18 @@ class ResolvePrelaunch(PreLaunchHook): # add to the python path to PATH env_path = self.launch_context.env["PATH"] - self.launch_context.env["PATH"] = f"{python3_home_str}{os.pathsep}{env_path}" + self.launch_context.env[ + "PATH" + ] = f"{python3_home_str}{os.pathsep}{env_path}" self.log.debug(f"PATH: {self.launch_context.env['PATH']}") # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] modules_path = Path(resolve_script_api, "Modules").as_posix() - self.launch_context.env["PYTHONPATH"] = f"{modules_path}{os.pathsep}{env_pythonpath}" + self.launch_context.env[ + "PYTHONPATH" + ] = f"{modules_path}{os.pathsep}{env_pythonpath}" self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") @@ -82,11 +95,15 @@ class ResolvePrelaunch(PreLaunchHook): "/Library/Application Support/Blackmagic Design" "/DaVinci Resolve/Fusion/Scripts/Comp" ), - "linux": "/opt/resolve/Fusion/Scripts/Comp" + "linux": "/opt/resolve/Fusion/Scripts/Comp", } - resolve_utility_scripts_dir = Path(resolve_utility_scripts_dirs[current_platform]) + resolve_utility_scripts_dir = Path( + resolve_utility_scripts_dirs[current_platform] + ) # setting utility scripts dir for scripts syncing - self.launch_context.env["RESOLVE_UTILITY_SCRIPTS_DIR"] = resolve_utility_scripts_dir.as_posix() + self.launch_context.env[ + "RESOLVE_UTILITY_SCRIPTS_DIR" + ] = resolve_utility_scripts_dir.as_posix() # remove terminal coloring tags self.launch_context.env["OPENPYPE_LOG_NO_COLORS"] = "True" From 37d7a87fd116b2f3351df6ac42500ea696b427e6 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 29 Apr 2023 03:25:06 +0000 Subject: [PATCH 410/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 080fd6eece..72297a4430 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.6-nightly.1" +__version__ = "3.15.6-nightly.2" From 1d2123dce8cf3d5fd70467cac32e1f9a484ee199 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Sat, 29 Apr 2023 10:42:15 +0300 Subject: [PATCH 411/918] Update .gitignore Co-authored-by: Roy Nieterau --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a565c57b54..18e7cd7bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -85,7 +85,6 @@ openpype/premiere/ppro/js/debug.log .env dump.sql test_localsystem.txt -*.code-workspace # website ########## From 3e2559c0c2797c8c3dba717ac9594cd22499b80b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Sat, 29 Apr 2023 16:32:52 +0100 Subject: [PATCH 412/918] Fix repair and validation --- openpype/hosts/maya/plugins/publish/validate_attributes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_attributes.py b/openpype/hosts/maya/plugins/publish/validate_attributes.py index 6ca9afb9a4..7ebd9d7d03 100644 --- a/openpype/hosts/maya/plugins/publish/validate_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_attributes.py @@ -6,7 +6,7 @@ import pyblish.api from openpype.hosts.maya.api.lib import set_attribute from openpype.pipeline.publish import ( - RepairContextAction, + RepairAction, ValidateContentsOrder, ) @@ -26,7 +26,7 @@ class ValidateAttributes(pyblish.api.InstancePlugin): order = ValidateContentsOrder label = "Attributes" hosts = ["maya"] - actions = [RepairContextAction] + actions = [RepairAction] optional = True attributes = None @@ -81,7 +81,7 @@ class ValidateAttributes(pyblish.api.InstancePlugin): if node_name not in attributes: continue - for attr_name, expected in attributes.items(): + for attr_name, expected in attributes[node_name].items(): # Skip if attribute does not exist if not cmds.attributeQuery(attr_name, node=node, exists=True): From 88e7b3386b70756ff0b3a81bfd6a33bcd062830a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 May 2023 11:22:10 +0800 Subject: [PATCH 413/918] cosmetic fix --- openpype/hosts/max/plugins/publish/validate_frame_range_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py index 944780f6fa..d77b1503e0 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py @@ -25,7 +25,7 @@ class ValidateFrameRangeType(pyblish.api.InstancePlugin): if rt.rendTimeType != 3: raise PublishValidationError("Incorrect type of frame range" " used in render setting." - "Repair action can help to fix it.") + " Repair action can help to fix it.") @classmethod def repair(cls, instance): From 84cef9d3cf6135fd26b490f57e9bc68d9da36ace Mon Sep 17 00:00:00 2001 From: Michael reda Date: Tue, 2 May 2023 11:12:18 +0200 Subject: [PATCH 414/918] handel long lines --- openpype/modules/kitsu/kitsu_module.py | 19 ++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 48 +++++++++---------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index f4e3dd5691..7c9d888aa7 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,10 +124,23 @@ def push_to_zou(login, password): @cli_main.command() -@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") -@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" + "-prjs", + "--projects", + envvar="SYNC_PROJECTS", + help="Sync specific kitsu projects" +) +@click.option( + "-l", + "--login", + envvar="KITSU_LOGIN", + help="Kitsu login" +) +@click.option( + "-p", + "--password", + envvar="KITSU_PWD", + help="Password for kitsu username" ) def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a397198a13..ad8ccd9f3f 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -359,7 +359,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None, specific_projects: list = None + login: str, password: str, ignore_projects: list = None, + specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -383,7 +384,6 @@ def sync_all_projects( dbcon.install() all_projects = gazu.project.all_projects() - project_to_sync = [] if specific_projects == ['*']: project_to_sync = all_projects @@ -435,8 +435,8 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Do not sync closed kitsu project that is not found in openpype if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + project['project_status_name'] == "Closed" + and not get_project(project['name']) ): return @@ -451,10 +451,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] @@ -526,12 +526,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From b8ce6e9e9c10383c7e7e0c36fba7bb603a5d9ee7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 May 2023 11:19:50 +0200 Subject: [PATCH 415/918] Photoshop: add autocreators for review and flat image (#4871) * OP-5656 - added auto creator for review in PS Review instance should be togglable. Review instance needs to be created for non publisher based workflows. * OP-5656 - refactored names * OP-5656 - refactored names * OP-5656 - new auto creator for flat image In old version flat image was created if no instances were created. Explicit auto creator added for clarity. Standardization of state of plugins * OP-5656 - updated according to auto image creator Subset template should be used from autocreator and not be separate. * OP-5656 - fix proper creator name * OP-5656 - fix log message * OP-5656 - fix use enable state * OP-5656 - fix formatting * OP-5656 - add review toggle to image instance For special cases where each image should have separate review. * OP-5656 - fix description * OP-5656 - fix not present asset and task in instance context * OP-5656 - refactor - both auto creators should use same class Provided separate description. * OP-5656 - fix - propagate review to families Image and auto image could have now review flag. Bottom logic is only for Webpublisher. * OP-5656 - fix - rename review files to avaid collision Image family produces jpg and png, jpg review would clash with name. It should be replaced by 'jpg_jpg'. * OP-5656 - fix - limit additional auto created only on WP In artist based publishing auto image would be created by auto creator (if enabled). Artist might want to disable image creation. * OP-5656 - added mark_for_review flag to Publish tab * OP-5656 - fixes for auto creator * OP-5656 - fixe - outputDef not needed outputDef should contain dict of output definition. In PS it doesn't make sense as it has separate extract_review without output definitions. * OP-5656 - added persistency of changes to auto creators Changes as enabling/disabling, changing review flag should persist. * OP-5656 - added documentation for admins * OP-5656 - added link to new documentation for admins * OP-5656 - Hound * OP-5656 - Hound * OP-5656 - fix shared families list * OP-5656 - added default variant for review and workfile creator For workfile Main was default variant, "" was for review. * OP-5656 - fix - use values from Settings * OP-5656 - fix - use original name of review for main review family outputName cannot be in repre or file would have ..._jpg.jpg * OP-5656 - refactor - standardized settings Active by default denotes if created instance is active (eg. publishable) when created. * OP-5656 - fixes for skipping collecting auto_image data["ids"] are necessary for extracting. Members are physical layers in image, ids are "virtual" items, won't get grouped into real image instance. * OP-5656 - reworked auto collectors This allows to use automatic test for proper testing. * OP-5656 - added automatic tests * OP-5656 - fixes for auto collectors * OP-5656 - removed unnecessary collector Logic moved to auto collectors. * OP-5656 - Hound --- .../create/workfile_creator.py => lib.py} | 23 +-- .../plugins/create/create_flatten_image.py | 120 ++++++++++++++ .../photoshop/plugins/create/create_image.py | 47 +++++- .../photoshop/plugins/create/create_review.py | 28 ++++ .../plugins/create/create_workfile.py | 28 ++++ .../plugins/publish/collect_auto_image.py | 101 ++++++++++++ .../plugins/publish/collect_auto_review.py | 92 +++++++++++ .../plugins/publish/collect_auto_workfile.py | 99 ++++++++++++ .../plugins/publish/collect_instances.py | 116 -------------- .../plugins/publish/collect_review.py | 32 +--- .../plugins/publish/collect_workfile.py | 57 ++----- .../plugins/publish/extract_review.py | 34 ++-- .../defaults/project_settings/photoshop.json | 29 +++- .../schema_project_photoshop.json | 151 +++++++++++++++--- .../test_publish_in_photoshop_auto_image.py | 93 +++++++++++ .../test_publish_in_photoshop_review.py | 111 +++++++++++++ website/docs/admin_hosts_photoshop.md | 127 +++++++++++++++ .../assets/admin_hosts_photoshop_settings.png | Bin 0 -> 14364 bytes website/sidebars.js | 1 + 19 files changed, 1044 insertions(+), 245 deletions(-) rename openpype/hosts/photoshop/{plugins/create/workfile_creator.py => lib.py} (83%) create mode 100644 openpype/hosts/photoshop/plugins/create/create_flatten_image.py create mode 100644 openpype/hosts/photoshop/plugins/create/create_review.py create mode 100644 openpype/hosts/photoshop/plugins/create/create_workfile.py create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_auto_image.py create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_auto_review.py create mode 100644 openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py delete mode 100644 openpype/hosts/photoshop/plugins/publish/collect_instances.py create mode 100644 tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py create mode 100644 tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py create mode 100644 website/docs/admin_hosts_photoshop.md create mode 100644 website/docs/assets/admin_hosts_photoshop_settings.png diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/lib.py similarity index 83% rename from openpype/hosts/photoshop/plugins/create/workfile_creator.py rename to openpype/hosts/photoshop/lib.py index f5d56adcbc..ae7a33b7b6 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/lib.py @@ -7,28 +7,26 @@ from openpype.pipeline import ( from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances -class PSWorkfileCreator(AutoCreator): - identifier = "workfile" - family = "workfile" - - default_variant = "Main" - +class PSAutoCreator(AutoCreator): + """Generic autocreator to extend.""" def get_instance_attr_defs(self): return [] def collect_instances(self): for instance_data in cache_and_get_instances(self): 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 + instance = CreatedInstance.from_existing( + instance_data, self ) self._add_instance_to_context(instance) def update_instances(self, update_list): - # nothing to change on workfiles - pass + self.log.debug("update_list:: {}".format(update_list)) + for created_inst, _changes in update_list: + api.stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) def create(self, options=None): existing_instance = None @@ -58,6 +56,9 @@ class PSWorkfileCreator(AutoCreator): project_name, host_name, None )) + if not self.active_on_create: + data["active"] = False + new_instance = CreatedInstance( self.family, subset_name, data, self ) diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py new file mode 100644 index 0000000000..3bc61c8184 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -0,0 +1,120 @@ +from openpype.pipeline import CreatedInstance + +from openpype.lib import BoolDef +import openpype.hosts.photoshop.api as api +from openpype.hosts.photoshop.lib import PSAutoCreator +from openpype.pipeline.create import get_subset_name +from openpype.client import get_asset_by_name + + +class AutoImageCreator(PSAutoCreator): + """Creates flatten image from all visible layers. + + Used in simplified publishing as auto created instance. + Must be enabled in Setting and template for subset name provided + """ + identifier = "auto_image" + family = "image" + + # Settings + default_variant = "" + # - Mark by default instance for review + mark_for_review = True + active_on_create = True + + def create(self, options=None): + existing_instance = None + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + existing_instance = instance + break + + context = self.create_context + project_name = context.get_current_project_name() + asset_name = context.get_current_asset_name() + task_name = context.get_current_task_name() + host_name = context.host_name + asset_doc = get_asset_by_name(project_name, asset_name) + + if existing_instance is None: + subset_name = get_subset_name( + self.family, self.default_variant, task_name, asset_doc, + project_name, host_name + ) + + publishable_ids = [layer.id for layer in api.stub().get_layers() + if layer.visible] + data = { + "asset": asset_name, + "task": task_name, + # ids are "virtual" layers, won't get grouped as 'members' do + # same difference in color coded layers in WP + "ids": publishable_ids + } + + if not self.active_on_create: + data["active"] = False + + creator_attributes = {"mark_for_review": self.mark_for_review} + data.update({"creator_attributes": creator_attributes}) + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(new_instance) + api.stub().imprint(new_instance.get("instance_id"), + new_instance.data_to_store()) + + elif ( # existing instance from different context + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + subset_name = get_subset_name( + self.family, self.default_variant, task_name, asset_doc, + project_name, host_name + ) + + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name + + api.stub().imprint(existing_instance.get("instance_id"), + existing_instance.data_to_store()) + + def get_pre_create_attr_defs(self): + return [ + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review + ) + ] + + def get_instance_attr_defs(self): + return [ + BoolDef( + "mark_for_review", + label="Review" + ) + ] + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["AutoImageCreator"] + ) + + self.active_on_create = plugin_settings["active_on_create"] + self.default_variant = plugin_settings["default_variant"] + self.mark_for_review = plugin_settings["mark_for_review"] + self.enabled = plugin_settings["enabled"] + + def get_detail_description(self): + return """Creator for flatten image. + + Studio might configure simple publishing workflow. In that case + `image` instance is automatically created which will publish flat + image from all visible layers. + + Artist might disable this instance from publishing or from creating + review for it though. + """ diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 3d82d6b6f0..f3165fca57 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -23,6 +23,11 @@ class ImageCreator(Creator): family = "image" description = "Image creator" + # Settings + default_variants = "" + mark_for_review = False + active_on_create = True + def create(self, subset_name_from_ui, data, pre_create_data): groups_to_create = [] top_layers_to_wrap = [] @@ -94,6 +99,12 @@ class ImageCreator(Creator): data.update({"layer_name": layer_name}) data.update({"long_name": "_".join(layer_names_in_hierarchy)}) + creator_attributes = {"mark_for_review": self.mark_for_review} + data.update({"creator_attributes": creator_attributes}) + + if not self.active_on_create: + data["active"] = False + new_instance = CreatedInstance(self.family, subset_name, data, self) @@ -134,11 +145,6 @@ class ImageCreator(Creator): self.host.remove_instance(instance) self._remove_instance_from_context(instance) - def get_default_variants(self): - return [ - "Main" - ] - def get_pre_create_attr_defs(self): output = [ BoolDef("use_selection", default=True, @@ -148,10 +154,34 @@ class ImageCreator(Creator): label="Create separate instance for each selected"), BoolDef("use_layer_name", default=False, - label="Use layer name in subset") + label="Use layer name in subset"), + BoolDef( + "mark_for_review", + label="Create separate review", + default=False + ) ] return output + def get_instance_attr_defs(self): + return [ + BoolDef( + "mark_for_review", + label="Review" + ) + ] + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["ImageCreator"] + ) + + self.active_on_create = plugin_settings["active_on_create"] + self.default_variants = plugin_settings["default_variants"] + self.mark_for_review = plugin_settings["mark_for_review"] + self.enabled = plugin_settings["enabled"] + + def get_detail_description(self): return """Creator for Image instances @@ -180,6 +210,11 @@ class ImageCreator(Creator): but layer name should be used (set explicitly in UI or implicitly if multiple images should be created), it is added in capitalized form as a suffix to subset name. + + Each image could have its separate review created if necessary via + `Create separate review` toggle. + But more use case is to use separate `review` instance to create review + from all published items. """ def _handle_legacy(self, instance_data): diff --git a/openpype/hosts/photoshop/plugins/create/create_review.py b/openpype/hosts/photoshop/plugins/create/create_review.py new file mode 100644 index 0000000000..064485d465 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/create_review.py @@ -0,0 +1,28 @@ +from openpype.hosts.photoshop.lib import PSAutoCreator + + +class ReviewCreator(PSAutoCreator): + """Creates review instance which might be disabled from publishing.""" + identifier = "review" + family = "review" + + default_variant = "Main" + + def get_detail_description(self): + return """Auto creator for review. + + Photoshop review is created from all published images or from all + visible layers if no `image` instances got created. + + Review might be disabled by an artist (instance shouldn't be deleted as + it will get recreated in next publish either way). + """ + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["ReviewCreator"] + ) + + self.default_variant = plugin_settings["default_variant"] + self.active_on_create = plugin_settings["active_on_create"] + self.enabled = plugin_settings["enabled"] diff --git a/openpype/hosts/photoshop/plugins/create/create_workfile.py b/openpype/hosts/photoshop/plugins/create/create_workfile.py new file mode 100644 index 0000000000..d498f0549c --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/create_workfile.py @@ -0,0 +1,28 @@ +from openpype.hosts.photoshop.lib import PSAutoCreator + + +class WorkfileCreator(PSAutoCreator): + identifier = "workfile" + family = "workfile" + + default_variant = "Main" + + def get_detail_description(self): + return """Auto creator for workfile. + + It is expected that each publish will also publish its source workfile + for safekeeping. This creator triggers automatically without need for + an artist to remember and trigger it explicitly. + + Workfile instance could be disabled if it is not required to publish + workfile. (Instance shouldn't be deleted though as it will be recreated + in next publish automatically). + """ + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["photoshop"]["create"]["WorkfileCreator"] + ) + + self.active_on_create = plugin_settings["active_on_create"] + self.enabled = plugin_settings["enabled"] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py new file mode 100644 index 0000000000..ce408f8d01 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -0,0 +1,101 @@ +import pyblish.api + +from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline.create import get_subset_name + + +class CollectAutoImage(pyblish.api.ContextPlugin): + """Creates auto image in non artist based publishes (Webpublisher). + + 'remotepublish' should be renamed to 'autopublish' or similar in the future + """ + + label = "Collect Auto Image" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.2 + + targets = ["remotepublish"] + + def process(self, context): + family = "image" + for instance in context: + creator_identifier = instance.data.get("creator_identifier") + if creator_identifier and creator_identifier == "auto_image": + self.log.debug("Auto image instance found, won't create new") + return + + project_name = context.data["anatomyData"]["project"]["name"] + proj_settings = context.data["project_settings"] + task_name = context.data["anatomyData"]["task"]["name"] + host_name = context.data["hostName"] + asset_doc = context.data["assetEntity"] + asset_name = asset_doc["name"] + + auto_creator = proj_settings.get( + "photoshop", {}).get( + "create", {}).get( + "AutoImageCreator", {}) + + if not auto_creator or not auto_creator["enabled"]: + self.log.debug("Auto image creator disabled, won't create new") + return + + stub = photoshop.stub() + stored_items = stub.get_layers_metadata() + for item in stored_items: + if item.get("creator_identifier") == "auto_image": + if not item.get("active"): + self.log.debug("Auto_image instance disabled") + return + + layer_items = stub.get_layers() + + publishable_ids = [layer.id for layer in layer_items + if layer.visible] + + # collect stored image instances + instance_names = [] + for layer_item in layer_items: + layer_meta_data = stub.read(layer_item, stored_items) + + # Skip layers without metadata. + if layer_meta_data is None: + continue + + # Skip containers. + if "container" in layer_meta_data["id"]: + continue + + # active might not be in legacy meta + if layer_meta_data.get("active", True) and layer_item.visible: + instance_names.append(layer_meta_data["subset"]) + + if len(instance_names) == 0: + variants = proj_settings.get( + "photoshop", {}).get( + "create", {}).get( + "CreateImage", {}).get( + "default_variants", ['']) + family = "image" + + variant = context.data.get("variant") or variants[0] + + subset_name = get_subset_name( + family, variant, task_name, asset_doc, + project_name, host_name + ) + + instance = context.create_instance(subset_name) + instance.data["family"] = family + instance.data["asset"] = asset_name + instance.data["subset"] = subset_name + instance.data["ids"] = publishable_ids + instance.data["publish"] = True + instance.data["creator_identifier"] = "auto_image" + + if auto_creator["mark_for_review"]: + instance.data["creator_attributes"] = {"mark_for_review": True} + instance.data["families"] = ["review"] + + self.log.info("auto image instance: {} ".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py new file mode 100644 index 0000000000..7de4adcaf4 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_review.py @@ -0,0 +1,92 @@ +""" +Requires: + None + +Provides: + instance -> family ("review") +""" +import pyblish.api + +from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline.create import get_subset_name + + +class CollectAutoReview(pyblish.api.ContextPlugin): + """Create review instance in non artist based workflow. + + Called only if PS is triggered in Webpublisher or in tests. + """ + + label = "Collect Auto Review" + hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.2 + targets = ["remotepublish"] + + publish = True + + def process(self, context): + family = "review" + has_review = False + for instance in context: + if instance.data["family"] == family: + self.log.debug("Review instance found, won't create new") + has_review = True + + creator_attributes = instance.data.get("creator_attributes", {}) + if (creator_attributes.get("mark_for_review") and + "review" not in instance.data["families"]): + instance.data["families"].append("review") + + if has_review: + return + + stub = photoshop.stub() + stored_items = stub.get_layers_metadata() + for item in stored_items: + if item.get("creator_identifier") == family: + if not item.get("active"): + self.log.debug("Review instance disabled") + return + + auto_creator = context.data["project_settings"].get( + "photoshop", {}).get( + "create", {}).get( + "ReviewCreator", {}) + + if not auto_creator or not auto_creator["enabled"]: + self.log.debug("Review creator disabled, won't create new") + return + + variant = (context.data.get("variant") or + auto_creator["default_variant"]) + + project_name = context.data["anatomyData"]["project"]["name"] + proj_settings = context.data["project_settings"] + task_name = context.data["anatomyData"]["task"]["name"] + host_name = context.data["hostName"] + asset_doc = context.data["assetEntity"] + asset_name = asset_doc["name"] + + subset_name = get_subset_name( + family, + variant, + task_name, + asset_doc, + project_name, + host_name=host_name, + project_settings=proj_settings + ) + + instance = context.create_instance(subset_name) + instance.data.update({ + "subset": subset_name, + "label": subset_name, + "name": subset_name, + "family": family, + "families": [], + "representations": [], + "asset": asset_name, + "publish": self.publish + }) + + self.log.debug("auto review created::{}".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py new file mode 100644 index 0000000000..d10cf62c67 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_auto_workfile.py @@ -0,0 +1,99 @@ +import os +import pyblish.api + +from openpype.hosts.photoshop import api as photoshop +from openpype.pipeline.create import get_subset_name + + +class CollectAutoWorkfile(pyblish.api.ContextPlugin): + """Collect current script for publish.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Workfile" + hosts = ["photoshop"] + + targets = ["remotepublish"] + + def process(self, context): + family = "workfile" + file_path = context.data["currentFile"] + _, ext = os.path.splitext(file_path) + staging_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path) + workfile_representation = { + "name": ext[1:], + "ext": ext[1:], + "files": base_name, + "stagingDir": staging_dir, + } + + for instance in context: + if instance.data["family"] == family: + self.log.debug("Workfile instance found, won't create new") + instance.data.update({ + "label": base_name, + "name": base_name, + "representations": [], + }) + + # creating representation + _, ext = os.path.splitext(file_path) + instance.data["representations"].append( + workfile_representation) + + return + + stub = photoshop.stub() + stored_items = stub.get_layers_metadata() + for item in stored_items: + if item.get("creator_identifier") == family: + if not item.get("active"): + self.log.debug("Workfile instance disabled") + return + + project_name = context.data["anatomyData"]["project"]["name"] + proj_settings = context.data["project_settings"] + auto_creator = proj_settings.get( + "photoshop", {}).get( + "create", {}).get( + "WorkfileCreator", {}) + + if not auto_creator or not auto_creator["enabled"]: + self.log.debug("Workfile creator disabled, won't create new") + return + + # context.data["variant"] might come only from collect_batch_data + variant = (context.data.get("variant") or + auto_creator["default_variant"]) + + task_name = context.data["anatomyData"]["task"]["name"] + host_name = context.data["hostName"] + asset_doc = context.data["assetEntity"] + asset_name = asset_doc["name"] + + subset_name = get_subset_name( + family, + variant, + task_name, + asset_doc, + project_name, + host_name=host_name, + project_settings=proj_settings + ) + + # Create instance + instance = context.create_instance(subset_name) + instance.data.update({ + "subset": subset_name, + "label": base_name, + "name": base_name, + "family": family, + "families": [], + "representations": [], + "asset": asset_name + }) + + # creating representation + instance.data["representations"].append(workfile_representation) + + self.log.debug("auto workfile review created:{}".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py deleted file mode 100644 index 5bf12379b1..0000000000 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ /dev/null @@ -1,116 +0,0 @@ -import pprint - -import pyblish.api - -from openpype.settings import get_project_settings -from openpype.hosts.photoshop import api as photoshop -from openpype.lib import prepare_template_data -from openpype.pipeline import legacy_io - - -class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by LayerSet and file metadata - - Collects publishable instances from file metadata or enhance - already collected by creator (family == "image"). - - If no image instances are explicitly created, it looks if there is value - in `flatten_subset_template` (configurable in Settings), in that case it - produces flatten image with all visible layers. - - Identifier: - id (str): "pyblish.avalon.instance" - """ - - label = "Collect Instances" - order = pyblish.api.CollectorOrder - hosts = ["photoshop"] - families_mapping = { - "image": [] - } - # configurable in Settings - flatten_subset_template = "" - - def process(self, context): - instance_by_layer_id = {} - for instance in context: - if ( - instance.data["family"] == "image" and - instance.data.get("members")): - layer_id = str(instance.data["members"][0]) - instance_by_layer_id[layer_id] = instance - - stub = photoshop.stub() - layer_items = stub.get_layers() - layers_meta = stub.get_layers_metadata() - instance_names = [] - - all_layer_ids = [] - for layer_item in layer_items: - layer_meta_data = stub.read(layer_item, layers_meta) - all_layer_ids.append(layer_item.id) - - # Skip layers without metadata. - if layer_meta_data is None: - continue - - # Skip containers. - if "container" in layer_meta_data["id"]: - continue - - # active might not be in legacy meta - if not layer_meta_data.get("active", True): - continue - - instance = instance_by_layer_id.get(str(layer_item.id)) - if instance is None: - instance = context.create_instance(layer_meta_data["subset"]) - - instance.data["layer"] = layer_item - instance.data.update(layer_meta_data) - instance.data["families"] = self.families_mapping[ - layer_meta_data["family"] - ] - instance.data["publish"] = layer_item.visible - instance_names.append(layer_meta_data["subset"]) - - # Produce diagnostic message for any graphical - # user interface interested in visualising it. - self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.info("instance: {} ".format( - pprint.pformat(instance.data, indent=4))) - - if len(instance_names) != len(set(instance_names)): - self.log.warning("Duplicate instances found. " + - "Remove unwanted via Publisher") - - if len(instance_names) == 0 and self.flatten_subset_template: - project_name = context.data["projectEntity"]["name"] - variants = get_project_settings(project_name).get( - "photoshop", {}).get( - "create", {}).get( - "CreateImage", {}).get( - "defaults", ['']) - family = "image" - task_name = legacy_io.Session["AVALON_TASK"] - asset_name = context.data["assetEntity"]["name"] - - variant = context.data.get("variant") or variants[0] - fill_pairs = { - "variant": variant, - "family": family, - "task": task_name - } - - subset = self.flatten_subset_template.format( - **prepare_template_data(fill_pairs)) - - instance = context.create_instance(subset) - instance.data["family"] = family - instance.data["asset"] = asset_name - instance.data["subset"] = subset - instance.data["ids"] = all_layer_ids - instance.data["families"] = self.families_mapping[family] - instance.data["publish"] = True - - self.log.info("flatten instance: {} ".format(instance.data)) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 7e598a8250..87ec4ee3f1 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -14,10 +14,7 @@ from openpype.pipeline.create import get_subset_name class CollectReview(pyblish.api.ContextPlugin): - """Gather the active document as review instance. - - Triggers once even if no 'image' is published as by defaults it creates - flatten image from a workfile. + """Adds review to families for instances marked to be reviewable. """ label = "Collect Review" @@ -28,25 +25,8 @@ class CollectReview(pyblish.api.ContextPlugin): publish = True def process(self, context): - family = "review" - subset = get_subset_name( - family, - context.data.get("variant", ''), - context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"], - context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"], - project_settings=context.data["project_settings"] - ) - - instance = context.create_instance(subset) - instance.data.update({ - "subset": subset, - "label": subset, - "name": subset, - "family": family, - "families": [], - "representations": [], - "asset": os.environ["AVALON_ASSET"], - "publish": self.publish - }) + for instance in context: + creator_attributes = instance.data["creator_attributes"] + if (creator_attributes.get("mark_for_review") and + "review" not in instance.data["families"]): + instance.data["families"].append("review") diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 9a5aad5569..9625464499 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -14,50 +14,19 @@ class CollectWorkfile(pyblish.api.ContextPlugin): default_variant = "Main" def process(self, context): - 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 + file_path = context.data["currentFile"] + _, ext = os.path.splitext(file_path) + staging_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path) - family = "workfile" - # context.data["variant"] might come only from collect_batch_data - variant = context.data.get("variant") or self.default_variant - subset = get_subset_name( - family, - variant, - context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"], - context.data["anatomyData"]["project"]["name"], - host_name=context.data["hostName"], - project_settings=context.data["project_settings"] - ) - - file_path = context.data["currentFile"] - staging_dir = os.path.dirname(file_path) - base_name = os.path.basename(file_path) - - # Create instance - if existing_instance is None: - instance = context.create_instance(subset) - instance.data.update({ - "subset": subset, - "label": base_name, - "name": base_name, - "family": family, - "families": [], - "representations": [], - "asset": os.environ["AVALON_ASSET"] - }) - else: - instance = existing_instance - - # creating representation - _, ext = os.path.splitext(file_path) - instance.data["representations"].append({ - "name": ext[1:], - "ext": ext[1:], - "files": base_name, - "stagingDir": staging_dir, - }) + # creating representation + _, ext = os.path.splitext(file_path) + instance.data["representations"].append({ + "name": ext[1:], + "ext": ext[1:], + "files": base_name, + "stagingDir": staging_dir, + }) + return diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 9d7eff0211..d5416a389d 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -47,32 +47,42 @@ class ExtractReview(publish.Extractor): layers = self._get_layers_from_image_instances(instance) self.log.info("Layers image instance found: {}".format(layers)) + repre_name = "jpg" + repre_skeleton = { + "name": repre_name, + "ext": "jpg", + "stagingDir": staging_dir, + "tags": self.jpg_options['tags'], + } + + if instance.data["family"] != "review": + # enable creation of review, without this jpg review would clash + # with jpg of the image family + output_name = repre_name + repre_name = "{}_{}".format(repre_name, output_name) + repre_skeleton.update({"name": repre_name, + "outputName": output_name}) + if self.make_image_sequence and len(layers) > 1: self.log.info("Extract layers to image sequence.") img_list = self._save_sequence_images(staging_dir, layers) - instance.data["representations"].append({ - "name": "jpg", - "ext": "jpg", - "files": img_list, + repre_skeleton.update({ "frameStart": 0, "frameEnd": len(img_list), "fps": fps, - "stagingDir": staging_dir, - "tags": self.jpg_options['tags'], + "files": img_list, }) + instance.data["representations"].append(repre_skeleton) processed_img_names = img_list else: self.log.info("Extract layers to flatten image.") img_list = self._save_flatten_image(staging_dir, layers) - instance.data["representations"].append({ - "name": "jpg", - "ext": "jpg", - "files": img_list, # cannot be [] for single frame - "stagingDir": staging_dir, - "tags": self.jpg_options['tags'] + repre_skeleton.update({ + "files": img_list, }) + instance.data["representations"].append(repre_skeleton) processed_img_names = [img_list] ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index bcf21f55dd..2454691958 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -10,23 +10,40 @@ } }, "create": { - "CreateImage": { - "defaults": [ + "ImageCreator": { + "enabled": true, + "active_on_create": true, + "mark_for_review": false, + "default_variants": [ "Main" ] + }, + "AutoImageCreator": { + "enabled": false, + "active_on_create": true, + "mark_for_review": false, + "default_variant": "" + }, + "ReviewCreator": { + "enabled": true, + "active_on_create": true, + "default_variant": "" + }, + "WorkfileCreator": { + "enabled": true, + "active_on_create": true, + "default_variant": "Main" } }, "publish": { "CollectColorCodedInstances": { + "enabled": true, "create_flatten_image": "no", "flatten_subset_template": "", "color_code_mapping": [] }, - "CollectInstances": { - "flatten_subset_template": "" - }, "CollectReview": { - "publish": true + "enabled": true }, "CollectVersion": { "enabled": false diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 0071e632af..f6c46aba8b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -31,16 +31,126 @@ { "type": "dict", "collapsible": true, - "key": "CreateImage", + "key": "ImageCreator", "label": "Create Image", + "checkbox_key": "enabled", "children": [ + { + "type": "label", + "label": "Manually create instance from layer or group of layers. \n Separate review could be created for this image to be sent to Asset Management System." + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review by default" + }, { "type": "list", - "key": "defaults", - "label": "Default Subsets", + "key": "default_variants", + "label": "Default Variants", "object_type": "text" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "AutoImageCreator", + "label": "Create Flatten Image", + "checkbox_key": "enabled", + "children": [ + { + "type": "label", + "label": "Auto create image for all visible layers, used for simplified processing. \n Separate review could be created for this image to be sent to Asset Management System." + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ReviewCreator", + "label": "Create Review", + "checkbox_key": "enabled", + "children": [ + { + "type": "label", + "label": "Auto create review instance containing all published image instances or visible layers if no image instance." + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled", + "default": true + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "WorkfileCreator", + "label": "Create Workfile", + "checkbox_key": "enabled", + "children": [ + { + "type": "label", + "label": "Auto create workfile instance" + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + } + ] } ] }, @@ -56,11 +166,18 @@ "is_group": true, "key": "CollectColorCodedInstances", "label": "Collect Color Coded Instances", + "checkbox_key": "enabled", "children": [ { "type": "label", "label": "Set color for publishable layers, set its resulting family and template for subset name. \nCan create flatten image from published instances.(Applicable only for remote publishing!)" }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled", + "default": true + }, { "key": "create_flatten_image", "label": "Create flatten image", @@ -131,40 +248,26 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "CollectInstances", - "label": "Collect Instances", - "children": [ - { - "type": "label", - "label": "Name for flatten image created if no image instance present" - }, - { - "type": "text", - "key": "flatten_subset_template", - "label": "Subset template for flatten image" - } - ] - }, { "type": "dict", "collapsible": true, "key": "CollectReview", "label": "Collect Review", + "checkbox_key": "enabled", "children": [ { "type": "boolean", - "key": "publish", - "label": "Active" - } - ] + "key": "enabled", + "label": "Enabled", + "default": true + } + ] }, { "type": "dict", "key": "CollectVersion", "label": "Collect Version", + "checkbox_key": "enabled", "children": [ { "type": "label", diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py new file mode 100644 index 0000000000..1594b36dec --- /dev/null +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop_auto_image.py @@ -0,0 +1,93 @@ +import logging + +from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.photoshop.lib import PhotoshopTestClass + +log = logging.getLogger("test_publish_in_photoshop") + + +class TestPublishInPhotoshopAutoImage(PhotoshopTestClass): + """Test for publish in Phohoshop with different review configuration. + + Workfile contains 3 layers, auto image and review instances created. + + Test contains updates to Settings!!! + + """ + PERSIST = True + + TEST_FILES = [ + ("1iLF6aNI31qlUCD1rGg9X9eMieZzxL-rc", + "test_photoshop_publish_auto_image.zip", "") + ] + + APP_GROUP = "photoshop" + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + + APP_NAME = "{}/{}".format(APP_GROUP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + failures = [] + + failures.append(DBAssert.count_of_types(dbcon, "version", 3)) + + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 0, + name="imageMainForeground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 0, + name="imageMainBackground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) + + failures.append( + DBAssert.count_of_types(dbcon, "representation", 5)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + # review from image + additional_args = {"context.subset": "imageBeautyMain", + "context.ext": "jpg", + "name": "jpg_jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageBeautyMain", + "context.ext": "jpg", + "name": "jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "review"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + assert not any(failures) + + +if __name__ == "__main__": + test_case = TestPublishInPhotoshopAutoImage() diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py new file mode 100644 index 0000000000..64b6868d7c --- /dev/null +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop_review.py @@ -0,0 +1,111 @@ +import logging + +from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.photoshop.lib import PhotoshopTestClass + +log = logging.getLogger("test_publish_in_photoshop") + + +class TestPublishInPhotoshopImageReviews(PhotoshopTestClass): + """Test for publish in Phohoshop with different review configuration. + + Workfile contains 2 image instance, one has review flag, second doesn't. + + Regular `review` family is disabled. + + Expected result is to `imageMainForeground` to have additional file with + review, `imageMainBackground` without. No separate `review` family. + + `test_project_test_asset_imageMainForeground_v001_jpg.jpg` is expected name + of imageForeground review, `_jpg` suffix is needed to differentiate between + image and review file. + + """ + PERSIST = True + + TEST_FILES = [ + ("12WGbNy9RJ3m9jlnk0Ib9-IZmONoxIz_p", + "test_photoshop_publish_review.zip", "") + ] + + APP_GROUP = "photoshop" + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + + APP_NAME = "{}/{}".format(APP_GROUP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + failures = [] + + failures.append(DBAssert.count_of_types(dbcon, "version", 3)) + + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="imageMainForeground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="imageMainBackground")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) + + failures.append( + DBAssert.count_of_types(dbcon, "representation", 6)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 2, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainForeground", + "context.ext": "jpg", + "context.representation": "jpg_jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "png"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + additional_args = {"context.subset": "imageMainBackground", + "context.ext": "jpg", + "context.representation": "jpg_jpg"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + additional_args = {"context.subset": "review"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 0, + additional_args=additional_args)) + + assert not any(failures) + + +if __name__ == "__main__": + test_case = TestPublishInPhotoshopImageReviews() diff --git a/website/docs/admin_hosts_photoshop.md b/website/docs/admin_hosts_photoshop.md new file mode 100644 index 0000000000..de684f01d2 --- /dev/null +++ b/website/docs/admin_hosts_photoshop.md @@ -0,0 +1,127 @@ +--- +id: admin_hosts_photoshop +title: Photoshop Settings +sidebar_label: Photoshop +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Photoshop settings + +There is a couple of settings that could configure publishing process for **Photoshop**. +All of them are Project based, eg. each project could have different configuration. + +Location: Settings > Project > Photoshop + +![AfterEffects Project Settings](assets/admin_hosts_photoshop_settings.png) + +## Color Management (ImageIO) + +Placeholder for Color Management. Currently not implemented yet. + +## Creator plugins + +Contains configurable items for creators used during publishing from Photoshop. + +### Create Image + +Provides list of [variants](artist_concepts.md#variant) that will be shown to an artist in Publisher. Default value `Main`. + +### Create Flatten Image + +Provides simplified publishing process. It will create single `image` instance for artist automatically. This instance will +produce flatten image from all visible layers in a workfile. + +- Subset template for flatten image - provide template for subset name for this instance (example `imageBeauty`) +- Review - should be separate review created for this instance + +### Create Review + +Creates single `review` instance automatically. This allows artists to disable it if needed. + +### Create Workfile + +Creates single `workfile` instance automatically. This allows artists to disable it if needed. + +## Publish plugins + +Contains configurable items for publish plugins used during publishing from Photoshop. + +### Collect Color Coded Instances + +Used only in remote publishing! + +Allows to create automatically `image` instances for configurable highlight color set on layer or group in the workfile. + +#### Create flatten image + - Flatten with images - produce additional `image` with all published `image` instances merged + - Flatten only - produce only merged `image` instance + - No - produce only separate `image` instances + +#### Subset template for flatten image + +Template used to create subset name automatically (example `image{layer}Main` - uses layer name in subset name) + +### Collect Review + +Disable if no review should be created + +### Collect Version + +If enabled it will push version from workfile name to all published items. Eg. if artist is publishing `test_asset_workfile_v005.psd` +produced `image` and `review` files will contain `v005` (even if some previous version were skipped for particular family). + +### Validate Containers + +Checks if all imported assets to the workfile through `Loader` are in latest version. Limits cases that older version of asset would be used. + +If enabled, artist might still decide to disable validation for each publish (for special use cases). +Limit this optionality by toggling `Optional`. +`Active` toggle denotes that by default artists sees that optional validation as enabled. + +### Validate naming of subsets and layers + +Subset cannot contain invalid characters or extract to file would fail + +#### Regex pattern of invalid characters + +Contains weird characters like `/`, `/`, these might cause an issue when file (which contains subset name) is created on OS disk. + +#### Replacement character + +Replace all offending characters with this one. `_` is default. + +### Extract Image + +Controls extension formats of published instances of `image` family. `png` and `jpg` are by default. + +### Extract Review + +Controls output definitions of extracted reviews to upload on Asset Management (AM). + +#### Makes an image sequence instead of flatten image + +If multiple `image` instances are produced, glue created images into image sequence (`mov`) to review all of them separetely. +Without it only flatten image would be produced. + +#### Maximum size of sources for review + +Set Byte limit for review file. Applicable if gigantic `image` instances are produced, full image size is unnecessary to upload to AM. + +#### Extract jpg Options + +Handles tags for produced `.jpg` representation. `Create review` and `Add review to Ftrack` are defaults. + +#### Extract mov Options + +Handles tags for produced `.mov` representation. `Create review` and `Add review to Ftrack` are defaults. + + +### Workfile Builder + +Allows to open prepared workfile for an artist when no workfile exists. Useful to share standards, additional helpful content in the workfile. + +Could be configured per `Task type`, eg. `composition` task type could use different `.psd` template file than `art` task. +Workfile template must be accessible for all artists. +(Currently not handled by [SiteSync](module_site_sync.md)) \ No newline at end of file diff --git a/website/docs/assets/admin_hosts_photoshop_settings.png b/website/docs/assets/admin_hosts_photoshop_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..aaa6ecbed7b353733f8424abe3c2df0cb903935a GIT binary patch literal 14364 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 Date: Tue, 2 May 2023 16:25:03 +0200 Subject: [PATCH 416/918] :art: soft-fail when pan/zoom locked on camera --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 825a8d38c7..3ceef6f3d3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -217,7 +217,11 @@ class ExtractPlayblast(publish.Extractor): instance.data["panel"], edit=True, **viewport_defaults ) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) + try: + cmds.setAttr( + "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) + except RuntimeError: + self.log.warning("Cannot restore Pan/Zoom settings.") collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] From 0d2a0f87230a78fedbf3e68e54868c3a5269ff06 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 14:36:18 +0200 Subject: [PATCH 417/918] Feature: Remove and load inv action --- openpype/plugins/inventory/remove_and_load.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 openpype/plugins/inventory/remove_and_load.py diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py new file mode 100644 index 0000000000..27ae1d4139 --- /dev/null +++ b/openpype/plugins/inventory/remove_and_load.py @@ -0,0 +1,44 @@ +from openpype.pipeline import InventoryAction +from openpype.pipeline.legacy_io import Session +from openpype.pipeline.load.plugins import discover_loader_plugins +from openpype.pipeline.load.utils import ( + get_loader_identifier, + remove_container, + load_container, +) +from openpype.client import get_representation_by_id + + +class RemoveAndLoad(InventoryAction): + """Delete inventory item and reload it.""" + + label = "Remove and load" + icon = "refresh" + + def process(self, containers): + for container in containers: + project_name = Session.get("AVALON_PROJECT") + + # Get loader + loader_name = container["loader"] + for plugin in discover_loader_plugins(project_name=project_name): + if get_loader_identifier(plugin) == loader_name: + loader = plugin + break + + assert ( + loader, + "Failed to get loader, can't remove and load container", + ) + + # Get representation + representation = get_representation_by_id( + project_name, container["representation"] + ) + assert representation, "Represenatation not found" + + # Remove container + remove_container(container) + + # Load container + load_container(loader, representation) From cffe72f0010f0128efd257bf2d6ec749f56958a8 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 16:46:01 +0200 Subject: [PATCH 418/918] register inventory actions --- openpype/pipeline/context_tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index dede2b8fce..ada78b989d 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -35,6 +35,7 @@ from . import ( register_inventory_action_path, register_creator_plugin_path, deregister_loader_plugin_path, + deregister_inventory_action_path, ) @@ -54,6 +55,7 @@ PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") # Global plugin paths PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def _get_modules_manager(): @@ -158,6 +160,7 @@ def install_openpype_plugins(project_name=None, host_name=None): pyblish.api.register_plugin_path(PUBLISH_PATH) pyblish.api.register_discovery_filter(filter_pyblish_plugins) register_loader_plugin_path(LOAD_PATH) + register_inventory_action_path(INVENTORY_PATH) if host_name is None: host_name = os.environ.get("AVALON_APP") @@ -223,6 +226,7 @@ def uninstall_host(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) deregister_loader_plugin_path(LOAD_PATH) + deregister_inventory_action_path(INVENTORY_PATH) log.info("Global plug-ins unregistred") deregister_host() From dc34bcc776c14aba094d7d2bde0f58cbf53002d5 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 17:08:39 +0200 Subject: [PATCH 419/918] pre rebase From 865ba2c97539396ac4a871733d1088c2e25661bb Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Tue, 2 May 2023 17:12:36 +0200 Subject: [PATCH 420/918] edited assertion --- openpype/plugins/inventory/remove_and_load.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 27ae1d4139..981722c065 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -27,9 +27,8 @@ class RemoveAndLoad(InventoryAction): break assert ( - loader, - "Failed to get loader, can't remove and load container", - ) + loader + ), "Failed to get loader, can't remove and load container" # Get representation representation = get_representation_by_id( From fec104de8e085d0ce0d70e9679c98924338ab3ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 May 2023 18:49:02 +0200 Subject: [PATCH 421/918] Fix: Locally copied version of last published workfile is not incremented (#4722) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: Locally copied version of last published workfile is not incremented * fix subset first match * correct anatomy name * Fix typo and linting * keep source filepath for further path conformation * fetch also input dependencies of workfile * required changes * lint * fix case only one subset * Enhancement: copy last workfile as reusable methods (#6) * Enhancement: copy last published workfile as reusable methods (WiP) * Added get_host_extensions method, added subset_id and las_version_doc access, added optional arguments to get_last_published_workfile * Plugged in the new methods + minor changes * Added docstrings, last workfile optional argument, and removed unused code * Using new implementation to get local workfile path. Warning: It adds an extra dot to the extension which I need to fix * Refactoring and fixed double dots * Added match subset_id and get representation method, plus clan up * Removed unused vars * Fixed some rebasing errors * delinted unchanged code and renamed get_representation into get_representation_with_task * This time it's really delinted, I hope... * Update openpype/modules/sync_server/sync_server.py reprenation isn't the right spelling (: Co-authored-by: Félix David * Changes based on reviews * Fixed non imperative docstring and missing space * Fixed another non imperative docstring * Update openpype/modules/sync_server/sync_server.py Fixed typo Co-authored-by: Félix David Co-authored-by: Hayley GUILLOT Co-authored-by: Félix David * Fix: syntax error * fix single subset case * Restore sync server enabled test in hook * Python2 syntax * renaming and missing key case handling * Fix local workfile overwritten on update in some cases (#7) * Fix: Local workfile overwrite when local version number is higher than published workfile version number (WiP) * Changed regex search, clean up * Readded mistakenly removed newline * lint * remove anticipated functions for cleaner PR * remove funcs from entities.py * change to get_last_workfile_with_version * clean * Update openpype/modules/sync_server/sync_server.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * removed get_last_published_workfile_path * moved hook to sync server module * fix lint * Refactor - download only if not present * Refactor - change to list instead of set * Refactor - removing unnecessary code last_published_workfile_path must exists or we wouldn't get there. Use version only from that. * Refactor - removing unnecessary imports * Added check for max fail tries * Refactor - cleaned up how to get last workfile * Updated docstrings * Remove unused imports Co-authored-by: Félix David * OP-5466 - run this on more DCC * Updated documentation * Fix - handle hero versions Skip hero versions, look only for versioned published to get max version id. * Hound * Refactor - simplified download_last_published_workfile Logic should be in pre hook * Skip if no profile found * Removed unwanted import * Use collected project_doc Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Use cached project_settings Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Félix David Co-authored-by: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Co-authored-by: Hayley GUILLOT Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: Jakub Ježek --- .../pre_copy_last_published_workfile.py | 151 +++++------ openpype/modules/sync_server/sync_server.py | 104 +++++++- .../modules/sync_server/sync_server_module.py | 35 ++- website/docs/module_site_sync.md | 237 ++++++++++++------ 4 files changed, 379 insertions(+), 148 deletions(-) rename openpype/{hooks => modules/sync_server/launch_hooks}/pre_copy_last_published_workfile.py (56%) diff --git a/openpype/hooks/pre_copy_last_published_workfile.py b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py similarity index 56% rename from openpype/hooks/pre_copy_last_published_workfile.py rename to openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py index 26b43c39cb..bbc220945c 100644 --- a/openpype/hooks/pre_copy_last_published_workfile.py +++ b/openpype/modules/sync_server/launch_hooks/pre_copy_last_published_workfile.py @@ -1,15 +1,20 @@ import os import shutil -from time import sleep + from openpype.client.entities import ( - get_last_version_by_subset_id, get_representations, - get_subsets, + get_project ) + from openpype.lib import PreLaunchHook -from openpype.lib.local_settings import get_local_site_id from openpype.lib.profiles_filtering import filter_profiles -from openpype.pipeline.load.utils import get_representation_path +from openpype.modules.sync_server.sync_server import ( + download_last_published_workfile, +) +from openpype.pipeline.template_data import get_template_data +from openpype.pipeline.workfile.path_resolving import ( + get_workfile_template_key, +) from openpype.settings.lib import get_project_settings @@ -22,7 +27,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): # Before `AddLastWorkfileToLaunchArgs` order = -1 - app_groups = ["blender", "photoshop", "tvpaint", "aftereffects"] + # any DCC could be used but TrayPublisher and other specials + app_groups = ["blender", "photoshop", "tvpaint", "aftereffects", + "nuke", "nukeassist", "nukex", "hiero", "nukestudio", + "maya", "harmony", "celaction", "flame", "fusion", + "houdini", "tvpaint"] def execute(self): """Check if local workfile doesn't exist, else copy it. @@ -31,11 +40,11 @@ class CopyLastPublishedWorkfile(PreLaunchHook): 2- Check if workfile in work area doesn't exist 3- Check if published workfile exists and is copied locally in publish 4- Substitute copied published workfile as first workfile + with incremented version by +1 Returns: None: This is a void method. """ - sync_server = self.modules_manager.get("sync_server") if not sync_server or not sync_server.enabled: self.log.debug("Sync server module is not enabled or available") @@ -53,6 +62,7 @@ class CopyLastPublishedWorkfile(PreLaunchHook): # Get data project_name = self.data["project_name"] + asset_name = self.data["asset_name"] task_name = self.data["task_name"] task_type = self.data["task_type"] host_name = self.application.host_name @@ -68,6 +78,8 @@ class CopyLastPublishedWorkfile(PreLaunchHook): "hosts": host_name, } last_workfile_settings = filter_profiles(profiles, filter_data) + if not last_workfile_settings: + return use_last_published_workfile = last_workfile_settings.get( "use_last_published_workfile" ) @@ -92,57 +104,27 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return + max_retries = int((sync_server.sync_project_settings[project_name] + ["config"] + ["retry_cnt"])) + self.log.info("Trying to fetch last published workfile...") - project_doc = self.data.get("project_doc") asset_doc = self.data.get("asset_doc") anatomy = self.data.get("anatomy") - # Check it can proceed - if not project_doc and not asset_doc: - return + context_filters = { + "asset": asset_name, + "family": "workfile", + "task": {"name": task_name, "type": task_type} + } - # Get subset id - subset_id = next( - ( - subset["_id"] - for subset in get_subsets( - project_name, - asset_ids=[asset_doc["_id"]], - fields=["_id", "data.family", "data.families"], - ) - if subset["data"].get("family") == "workfile" - # Legacy compatibility - or "workfile" in subset["data"].get("families", {}) - ), - None, - ) - if not subset_id: - self.log.debug( - 'No any workfile for asset "{}".'.format(asset_doc["name"]) - ) - return + workfile_representations = list(get_representations( + project_name, + context_filters=context_filters + )) - # Get workfile representation - last_version_doc = get_last_version_by_subset_id( - project_name, subset_id, fields=["_id"] - ) - if not last_version_doc: - self.log.debug("Subset does not have any versions") - return - - workfile_representation = next( - ( - representation - for representation in get_representations( - project_name, version_ids=[last_version_doc["_id"]] - ) - if representation["context"]["task"]["name"] == task_name - ), - None, - ) - - if not workfile_representation: + if not workfile_representations: self.log.debug( 'No published workfile for task "{}" and host "{}".'.format( task_name, host_name @@ -150,28 +132,55 @@ class CopyLastPublishedWorkfile(PreLaunchHook): ) return - local_site_id = get_local_site_id() - sync_server.add_site( - project_name, - workfile_representation["_id"], - local_site_id, - force=True, - priority=99, - reset_timer=True, + filtered_repres = filter( + lambda r: r["context"].get("version") is not None, + workfile_representations ) - - while not sync_server.is_representation_on_site( - project_name, workfile_representation["_id"], local_site_id - ): - sleep(5) - - # Get paths - published_workfile_path = get_representation_path( - workfile_representation, root=anatomy.roots + workfile_representation = max( + filtered_repres, key=lambda r: r["context"]["version"] ) - local_workfile_dir = os.path.dirname(last_workfile) # Copy file and substitute path - self.data["last_workfile_path"] = shutil.copy( - published_workfile_path, local_workfile_dir + last_published_workfile_path = download_last_published_workfile( + host_name, + project_name, + task_name, + workfile_representation, + max_retries, + anatomy=anatomy ) + if not last_published_workfile_path: + self.log.debug( + "Couldn't download {}".format(last_published_workfile_path) + ) + return + + project_doc = self.data["project_doc"] + + project_settings = self.data["project_settings"] + template_key = get_workfile_template_key( + task_name, host_name, project_name, project_settings + ) + + # Get workfile data + workfile_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + + extension = last_published_workfile_path.split(".")[-1] + workfile_data["version"] = ( + workfile_representation["context"]["version"] + 1) + workfile_data["ext"] = extension + + anatomy_result = anatomy.format(workfile_data) + local_workfile_path = anatomy_result[template_key]["path"] + + # Copy last published workfile to local workfile directory + shutil.copy( + last_published_workfile_path, + local_workfile_path, + ) + + self.data["last_workfile_path"] = local_workfile_path + # Keep source filepath for further path conformation + self.data["source_filepath"] = last_published_workfile_path diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index 5b873a37cf..d1d5c2863d 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -3,10 +3,15 @@ import os import asyncio import threading import concurrent.futures -from concurrent.futures._base import CancelledError +from time import sleep from .providers import lib +from openpype.client.entity_links import get_linked_representation_id from openpype.lib import Logger +from openpype.lib.local_settings import get_local_site_id +from openpype.modules.base import ModulesManager +from openpype.pipeline import Anatomy +from openpype.pipeline.load.utils import get_representation_path_with_anatomy from .utils import SyncStatus, ResumableError @@ -189,6 +194,98 @@ def _site_is_working(module, project_name, site_name, site_config): return handler.is_active() +def download_last_published_workfile( + host_name: str, + project_name: str, + task_name: str, + workfile_representation: dict, + max_retries: int, + anatomy: Anatomy = None, +) -> str: + """Download the last published workfile + + Args: + host_name (str): Host name. + project_name (str): Project name. + task_name (str): Task name. + workfile_representation (dict): Workfile representation. + max_retries (int): complete file failure only after so many attempts + anatomy (Anatomy, optional): Anatomy (Used for optimization). + Defaults to None. + + Returns: + str: last published workfile path localized + """ + + if not anatomy: + anatomy = Anatomy(project_name) + + # Get sync server module + sync_server = ModulesManager().modules_by_name.get("sync_server") + if not sync_server or not sync_server.enabled: + print("Sync server module is disabled or unavailable.") + return + + if not workfile_representation: + print( + "Not published workfile for task '{}' and host '{}'.".format( + task_name, host_name + ) + ) + return + + last_published_workfile_path = get_representation_path_with_anatomy( + workfile_representation, anatomy + ) + if (not last_published_workfile_path or + not os.path.exists(last_published_workfile_path)): + return + + # If representation isn't available on remote site, then return. + if not sync_server.is_representation_on_site( + project_name, + workfile_representation["_id"], + sync_server.get_remote_site(project_name), + ): + print( + "Representation for task '{}' and host '{}'".format( + task_name, host_name + ) + ) + return + + # Get local site + local_site_id = get_local_site_id() + + # Add workfile representation to local site + representation_ids = {workfile_representation["_id"]} + representation_ids.update( + get_linked_representation_id( + project_name, repre_id=workfile_representation["_id"] + ) + ) + for repre_id in representation_ids: + if not sync_server.is_representation_on_site(project_name, repre_id, + local_site_id): + sync_server.add_site( + project_name, + repre_id, + local_site_id, + force=True, + priority=99 + ) + sync_server.reset_timer() + print("Starting to download:{}".format(last_published_workfile_path)) + # While representation unavailable locally, wait. + while not sync_server.is_representation_on_site( + project_name, workfile_representation["_id"], local_site_id, + max_retries=max_retries + ): + sleep(5) + + return last_published_workfile_path + + class SyncServerThread(threading.Thread): """ Separate thread running synchronization server with asyncio loop. @@ -358,7 +455,6 @@ class SyncServerThread(threading.Thread): duration = time.time() - start_time self.log.debug("One loop took {:.2f}s".format(duration)) - delay = self.module.get_loop_delay(project_name) self.log.debug( "Waiting for {} seconds to new loop".format(delay) @@ -370,8 +466,8 @@ class SyncServerThread(threading.Thread): self.log.warning( "ConnectionResetError in sync loop, trying next loop", exc_info=True) - except CancelledError: - # just stopping server + except asyncio.exceptions.CancelledError: + # cancelling timer pass except ResumableError: self.log.warning( diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 5a4fa07e98..b85b045bd9 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -838,6 +838,18 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return ret_dict + def get_launch_hook_paths(self): + """Implementation for applications launch hooks. + + Returns: + (str): full absolut path to directory with hooks for the module + """ + + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "launch_hooks" + ) + # Needs to be refactored after Settings are updated # # Methods for Settings to get appriate values to fill forms # def get_configurable_items(self, scope=None): @@ -1045,9 +1057,23 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.sync_server_thread.reset_timer() def is_representation_on_site( - self, project_name, representation_id, site_name + self, project_name, representation_id, site_name, max_retries=None ): - """Checks if 'representation_id' has all files avail. on 'site_name'""" + """Checks if 'representation_id' has all files avail. on 'site_name' + + Args: + project_name (str) + representation_id (str) + site_name (str) + max_retries (int) (optional) - provide only if method used in while + loop to bail out + Returns: + (bool): True if 'representation_id' has all files correctly on the + 'site_name' + Raises: + (ValueError) Only If 'max_retries' provided if upload/download + failed too many times to limit infinite loop check. + """ representation = get_representation_by_id(project_name, representation_id, fields=["_id", "files"]) @@ -1060,6 +1086,11 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if site["name"] != site_name: continue + if max_retries: + tries = self._get_tries_count_from_rec(site) + if tries >= max_retries: + raise ValueError("Failed too many times") + if (site.get("progress") or site.get("error") or not site.get("created_dt")): return False diff --git a/website/docs/module_site_sync.md b/website/docs/module_site_sync.md index 3e5794579c..68f56cb548 100644 --- a/website/docs/module_site_sync.md +++ b/website/docs/module_site_sync.md @@ -7,80 +7,112 @@ sidebar_label: Site Sync import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +Site Sync allows users and studios to synchronize published assets between +multiple 'sites'. Site denotes a storage location, +which could be a physical disk, server, cloud storage. To be able to use site +sync, it first needs to be configured. -:::warning -**This feature is** currently **in a beta stage** and it is not recommended to rely on it fully for production. -::: - -Site Sync allows users and studios to synchronize published assets between multiple 'sites'. Site denotes a storage location, -which could be a physical disk, server, cloud storage. To be able to use site sync, it first needs to be configured. - -The general idea is that each user acts as an individual site and can download and upload any published project files when they are needed. that way, artist can have access to the whole project, but only every store files that are relevant to them on their home workstation. +The general idea is that each user acts as an individual site and can download +and upload any published project files when they are needed. that way, artist +can have access to the whole project, but only every store files that are +relevant to them on their home workstation. :::note -At the moment site sync is only able to deal with publishes files. No workfiles will be synchronized unless they are published. We are working on making workfile synchronization possible as well. +At the moment site sync is only able to deal with publishes files. No workfiles +will be synchronized unless they are published. We are working on making +workfile synchronization possible as well. ::: ## System Settings -To use synchronization, *Site Sync* needs to be enabled globally in **OpenPype Settings/System/Modules/Site Sync**. +To use synchronization, *Site Sync* needs to be enabled globally in **OpenPype +Settings/System/Modules/Site Sync**. ![Configure module](assets/site_sync_system.png) -### Sites +### Sites By default there are two sites created for each OpenPype installation: -- **studio** - default site - usually a centralized mounted disk accessible to all artists. Studio site is used if Site Sync is disabled. -- **local** - each workstation or server running OpenPype Tray receives its own with unique site name. Workstation refers to itself as "local"however all other sites will see it under it's unique ID. -Artists can explore their site ID by opening OpenPype Info tool by clicking on a version number in the tray app. +- **studio** - default site - usually a centralized mounted disk accessible to + all artists. Studio site is used if Site Sync is disabled. +- **local** - each workstation or server running OpenPype Tray receives its own + with unique site name. Workstation refers to itself as "local"however all + other sites will see it under it's unique ID. -Many different sites can be created and configured on the system level, and some or all can be assigned to each project. +Artists can explore their site ID by opening OpenPype Info tool by clicking on +a version number in the tray app. -Each OpenPype Tray app works with two sites at one time. (Sites can be the same, and no syncing is done in this setup). +Many different sites can be created and configured on the system level, and +some or all can be assigned to each project. -Sites could be configured differently per project basis. +Each OpenPype Tray app works with two sites at one time. (Sites can be the +same, and no syncing is done in this setup). -Each new site needs to be created first in `System Settings`. Most important feature of site is its Provider, select one from already prepared Providers. +Sites could be configured differently per project basis. -#### Alternative sites +Each new site needs to be created first in `System Settings`. Most important +feature of site is its Provider, select one from already prepared Providers. + +#### Alternative sites This attribute is meant for special use cases only. -One of the use cases is sftp site vendoring (exposing) same data as regular site (studio). Each site is accessible for different audience. 'studio' for artists in a studio via shared disk, 'sftp' for externals via sftp server with mounted 'studio' drive. +One of the use cases is sftp site vendoring (exposing) same data as regular +site (studio). Each site is accessible for different audience. 'studio' for +artists in a studio via shared disk, 'sftp' for externals via sftp server with +mounted 'studio' drive. -Change of file status on one site actually means same change on 'alternate' site occurred too. (eg. artists publish to 'studio', 'sftp' is using -same location >> file is accessible on 'sftp' site right away, no need to sync it anyhow.) +Change of file status on one site actually means same change on 'alternate' +site occurred too. (eg. artists publish to 'studio', 'sftp' is using +same location >> file is accessible on 'sftp' site right away, no need to sync +it anyhow.) ##### Example + ![Configure module](assets/site_sync_system_sites.png) -Admin created new `sftp` site which is handled by `SFTP` provider. Somewhere in the studio SFTP server is deployed on a machine that has access to `studio` drive. +Admin created new `sftp` site which is handled by `SFTP` provider. Somewhere in +the studio SFTP server is deployed on a machine that has access to `studio` +drive. Alternative sites work both way: + - everything published to `studio` is accessible on a `sftp` site too -- everything published to `sftp` (most probably via artist's local disk - artists publishes locally, representation is marked to be synced to `sftp`. Immediately after it is synced, it is marked to be available on `studio` too for artists in the studio to use.) +- everything published to `sftp` (most probably via artist's local disk - + artists publishes locally, representation is marked to be synced to `sftp`. + Immediately after it is synced, it is marked to be available on `studio` too + for artists in the studio to use.) ## Project Settings -Sites need to be made available for each project. Of course this is possible to do on the default project as well, in which case all other projects will inherit these settings until overridden explicitly. +Sites need to be made available for each project. Of course this is possible to +do on the default project as well, in which case all other projects will +inherit these settings until overridden explicitly. You'll find the setting in **Settings/Project/Global/Site Sync** -The attributes that can be configured will vary between sites and their providers. +The attributes that can be configured will vary between sites and their +providers. ## Local settings -Each user should configure root folder for their 'local' site via **Local Settings** in OpenPype Tray. This folder will be used for all files that the user publishes or downloads while working on a project. Artist has the option to set the folder as "default"in which case it is used for all the projects, or it can be set on a project level individually. +Each user should configure root folder for their 'local' site via **Local +Settings** in OpenPype Tray. This folder will be used for all files that the +user publishes or downloads while working on a project. Artist has the option +to set the folder as "default"in which case it is used for all the projects, or +it can be set on a project level individually. -Artists can also override which site they use as active and remote if need be. +Artists can also override which site they use as active and remote if need be. ![Local overrides](assets/site_sync_local_setting.png) - ## Providers -Each site implements a so called `provider` which handles most common operations (list files, copy files etc.) and provides interface with a particular type of storage. (disk, gdrive, aws, etc.) -Multiple configured sites could share the same provider with different settings (multiple mounted disks - each disk can be a separate site, while +Each site implements a so called `provider` which handles most common +operations (list files, copy files etc.) and provides interface with a +particular type of storage. (disk, gdrive, aws, etc.) +Multiple configured sites could share the same provider with different +settings (multiple mounted disks - each disk can be a separate site, while all share the same provider). **Currently implemented providers:** @@ -89,21 +121,30 @@ all share the same provider). Handles files stored on disk storage. -Local drive provider is the most basic one that is used for accessing all standard hard disk storage scenarios. It will work with any storage that can be mounted on your system in a standard way. This could correspond to a physical external hard drive, network mounted storage, internal drive or even VPN connected network drive. It doesn't care about how the drive is mounted, but you must be able to point to it with a simple directory path. +Local drive provider is the most basic one that is used for accessing all +standard hard disk storage scenarios. It will work with any storage that can be +mounted on your system in a standard way. This could correspond to a physical +external hard drive, network mounted storage, internal drive or even VPN +connected network drive. It doesn't care about how the drive is mounted, but +you must be able to point to it with a simple directory path. Default sites `local` and `studio` both use local drive provider. - ### Google Drive -Handles files on Google Drive (this). GDrive is provided as a production example for implementing other cloud providers +Handles files on Google Drive (this). GDrive is provided as a production +example for implementing other cloud providers -Let's imagine a small globally distributed studio which wants all published work for all their freelancers uploaded to Google Drive folder. +Let's imagine a small globally distributed studio which wants all published +work for all their freelancers uploaded to Google Drive folder. For this use case admin needs to configure: -- how many times it tries to synchronize file in case of some issue (network, permissions) + +- how many times it tries to synchronize file in case of some issue (network, + permissions) - how often should synchronization check for new assets -- sites for synchronization - 'local' and 'gdrive' (this can be overridden in local settings) +- sites for synchronization - 'local' and 'gdrive' (this can be overridden in + local settings) - user credentials - root folder location on Google Drive side @@ -111,30 +152,43 @@ Configuration would look like this: ![Configure project](assets/site_sync_project_settings.png) -*Site Sync* for Google Drive works using its API: https://developers.google.com/drive/api/v3/about-sdk +*Site Sync* for Google Drive works using its +API: https://developers.google.com/drive/api/v3/about-sdk -To configure Google Drive side you would need to have access to Google Cloud Platform project: https://console.cloud.google.com/ +To configure Google Drive side you would need to have access to Google Cloud +Platform project: https://console.cloud.google.com/ To get working connection to Google Drive there are some necessary steps: -- first you need to enable GDrive API: https://developers.google.com/drive/api/v3/enable-drive-api -- next you need to create user, choose **Service Account** (for basic configuration no roles for account are necessary) + +- first you need to enable GDrive + API: https://developers.google.com/drive/api/v3/enable-drive-api +- next you need to create user, choose **Service Account** (for basic + configuration no roles for account are necessary) - add new key for created account and download .json file with credentials -- share destination folder on the Google Drive with created account (directly in GDrive web application) -- add new site back in OpenPype Settings, name as you want, provider needs to be 'gdrive' +- share destination folder on the Google Drive with created account (directly + in GDrive web application) +- add new site back in OpenPype Settings, name as you want, provider needs to + be 'gdrive' - distribute credentials file via shared mounted disk location :::note -If you are using regular personal GDrive for testing don't forget adding `/My Drive` as the prefix in root configuration. Business accounts and share drives don't need this. +If you are using regular personal GDrive for testing don't forget +adding `/My Drive` as the prefix in root configuration. Business accounts and +share drives don't need this. ::: ### SFTP -SFTP provider is used to connect to SFTP server. Currently authentication with `user:password` or `user:ssh key` is implemented. -Please provide only one combination, don't forget to provide password for ssh key if ssh key was created with a passphrase. +SFTP provider is used to connect to SFTP server. Currently authentication +with `user:password` or `user:ssh key` is implemented. +Please provide only one combination, don't forget to provide password for ssh +key if ssh key was created with a passphrase. -(SFTP connection could be a bit finicky, use FileZilla or WinSCP for testing connection, it will be mush faster.) +(SFTP connection could be a bit finicky, use FileZilla or WinSCP for testing +connection, it will be mush faster.) -Beware that ssh key expects OpenSSH format (`.pem`) not a Putty format (`.ppk`)! +Beware that ssh key expects OpenSSH format (`.pem`) not a Putty +format (`.ppk`)! #### How to set SFTP site @@ -143,60 +197,101 @@ Beware that ssh key expects OpenSSH format (`.pem`) not a Putty format (`.ppk`)! ![Enable syncing and create site](assets/site_sync_sftp_system.png) -- In Projects setting enable Site Sync (on default project - all project will be synched, or on specific project) -- Configure SFTP connection and destination folder on a SFTP server (in screenshot `/upload`) +- In Projects setting enable Site Sync (on default project - all project will + be synched, or on specific project) +- Configure SFTP connection and destination folder on a SFTP server (in + screenshot `/upload`) ![SFTP connection](assets/site_sync_project_sftp_settings.png) - -- if you want to force syncing between local and sftp site for all users, use combination `active site: local`, `remote site: NAME_OF_SFTP_SITE` -- if you want to allow only specific users to use SFTP syncing (external users, not located in the office), use `active site: studio`, `remote site: studio`. + +- if you want to force syncing between local and sftp site for all users, use + combination `active site: local`, `remote site: NAME_OF_SFTP_SITE` +- if you want to allow only specific users to use SFTP syncing (external users, + not located in the office), use `active site: studio`, `remote site: studio`. ![Select active and remote site on a project](assets/site_sync_sftp_project_setting_not_forced.png) -- Each artist can decide and configure syncing from his/her local to SFTP via `Local Settings` +- Each artist can decide and configure syncing from his/her local to SFTP + via `Local Settings` ![Select active and remote site on a project](assets/site_sync_sftp_settings_local.png) - + ### Custom providers -If a studio needs to use other services for cloud storage, or want to implement totally different storage providers, they can do so by writing their own provider plugin. We're working on a developer documentation, however, for now we recommend looking at `abstract_provider.py`and `gdrive.py` inside `openpype/modules/sync_server/providers` and using it as a template. +If a studio needs to use other services for cloud storage, or want to implement +totally different storage providers, they can do so by writing their own +provider plugin. We're working on a developer documentation, however, for now +we recommend looking at `abstract_provider.py`and `gdrive.py` +inside `openpype/modules/sync_server/providers` and using it as a template. ### Running Site Sync in background -Site Sync server synchronizes new published files from artist machine into configured remote location by default. +Site Sync server synchronizes new published files from artist machine into +configured remote location by default. -There might be a use case where you need to synchronize between "non-artist" sites, for example between studio site and cloud. In this case -you need to run Site Sync as a background process from a command line (via service etc) 24/7. +There might be a use case where you need to synchronize between "non-artist" +sites, for example between studio site and cloud. In this case +you need to run Site Sync as a background process from a command line (via +service etc) 24/7. -To configure all sites where all published files should be synced eventually you need to configure `project_settings/global/sync_server/config/always_accessible_on` property in Settings (per project) first. +To configure all sites where all published files should be synced eventually +you need to +configure `project_settings/global/sync_server/config/always_accessible_on` +property in Settings (per project) first. ![Set another non artist remote site](assets/site_sync_always_on.png) This is an example of: + - Site Sync is enabled for a project -- default active and remote sites are set to `studio` - eg. standard process: everyone is working in a studio, publishing to shared location etc. -- (but this also allows any of the artists to work remotely, they would change their active site in their own Local Settings to `local` and configure local root. - This would result in everything artist publishes is saved first onto his local folder AND synchronized to `studio` site eventually.) +- default active and remote sites are set to `studio` - eg. standard process: + everyone is working in a studio, publishing to shared location etc. +- (but this also allows any of the artists to work remotely, they would change + their active site in their own Local Settings to `local` and configure local + root. + This would result in everything artist publishes is saved first onto his + local folder AND synchronized to `studio` site eventually.) - everything exported must also be eventually uploaded to `sftp` site -This eventual synchronization between `studio` and `sftp` sites must be physically handled by background process. +This eventual synchronization between `studio` and `sftp` sites must be +physically handled by background process. -As current implementation relies heavily on Settings and Local Settings, background process for a specific site ('studio' for example) must be configured via Tray first to `syncserver` command to work. +As current implementation relies heavily on Settings and Local Settings, +background process for a specific site ('studio' for example) must be +configured via Tray first to `syncserver` command to work. To do this: -- run OP `Tray` with environment variable OPENPYPE_LOCAL_ID set to name of active (source) site. In most use cases it would be studio (for cases of backups of everything published to studio site to different cloud site etc.) +- run OP `Tray` with environment variable OPENPYPE_LOCAL_ID set to name of + active (source) site. In most use cases it would be studio (for cases of + backups of everything published to studio site to different cloud site etc.) - start `Tray` -- check `Local ID` in information dialog after clicking on version number in the Tray +- check `Local ID` in information dialog after clicking on version number in + the Tray - open `Local Settings` in the `Tray` - configure for each project necessary active site and remote site - close `Tray` - run OP from a command line with `syncserver` and `--active_site` arguments - -This is an example how to trigger background syncing process where active (source) site is `studio`. -(It is expected that OP is installed on a machine, `openpype_console` is on PATH. If not, add full path to executable. +This is an example how to trigger background syncing process where active ( +source) site is `studio`. +(It is expected that OP is installed on a machine, `openpype_console` is on +PATH. If not, add full path to executable. ) + ```shell openpype_console syncserver --active_site studio -``` \ No newline at end of file +``` + +### Syncing of last published workfile + +Some DCC might have enabled +in `project_setting/global/tools/Workfiles/last_workfile_on_startup`, eg. open +DCC with last opened workfile. + +Flag `use_last_published_workfile` tells that last published workfile should be +used if no workfile is present locally. +This use case could happen if artists starts working on new task locally, +doesn't have any workfile present. In that case last published will be +synchronized locally and its version bumped by 1 (as workfile's version is +always +1 from published version). \ No newline at end of file From 8e456f3f03ed2c976d2e959613b572a1360cbed3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 May 2023 21:19:25 +0200 Subject: [PATCH 422/918] changing structure of colorspace schemas - removing `set_ocio_config` - removing (imageio) from category label - adding global switch to colorspace management - File rules label with ocio v1 compatibility --- .../schema_project_aftereffects.json | 9 ++------- .../projects_schema/schema_project_blender.json | 9 ++------- .../projects_schema/schema_project_celaction.json | 4 ++-- .../projects_schema/schema_project_flame.json | 6 +++--- .../projects_schema/schema_project_fusion.json | 9 ++------- .../projects_schema/schema_project_global.json | 14 ++++++++++++-- .../projects_schema/schema_project_harmony.json | 4 ++-- .../projects_schema/schema_project_hiero.json | 9 ++------- .../projects_schema/schema_project_houdini.json | 9 ++------- .../projects_schema/schema_project_max.json | 9 ++------- .../projects_schema/schema_project_maya.json | 9 ++------- .../projects_schema/schema_project_photoshop.json | 4 ++-- .../projects_schema/schema_project_resolve.json | 4 ++-- .../schema_project_traypublisher.json | 4 ++-- .../projects_schema/schema_project_tvpaint.json | 4 ++-- .../projects_schema/schema_project_unreal.json | 9 ++------- .../schema_project_webpublisher.json | 4 ++-- .../schemas/schema_imageio_file_rules.json | 4 ++-- .../schemas/schema_nuke_imageio.json | 9 ++------- 19 files changed, 49 insertions(+), 84 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 148c1840e5..d9007d6185 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -8,19 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index fe6ee94654..8997b750ec 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -8,19 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json index ab3acaf4a2..23268d0d9a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index 5b96a49679..f18da95065 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", @@ -368,7 +368,7 @@ }, { "key": "colorspace_out", - "label": "Output color (imageio)", + "label": "Output color", "type": "text", "default": "linear" }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index f97a3a3a40..b236925c1c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -8,19 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json index f200c1722f..80ea73267b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -8,9 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "is_group": true, "children": [ + { + "type": "boolean", + "key": "activate_global_color_management", + "label": "Enable Color Management" + }, { "key": "ocio_config", "type": "dict", @@ -29,9 +34,14 @@ { "key": "file_rules", "type": "dict", - "label": "File Rules", + "label": "File Rules (OCIO v1 only)", "collapsible": true, "children": [ + { + "type": "boolean", + "key": "activate_global_file_rules", + "label": "Enable File Rules" + }, { "key": "rules", "label": "Rules", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 71f8cb4db2..840f1fa4c0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index a46611dc8b..e7bd91dcef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -8,19 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index d254b92269..1fb23759cb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -8,19 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 1141cefb40..bc632d4a20 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -8,19 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index b5366bb0a7..c69ccb3f07 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -48,19 +48,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 4431e3d95f..c4bb81e7f8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index 16de175933..ef1880af67 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index d3faf54ae1..a07c262375 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index a0d94ad7dc..1c1d75c4e6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 2dedadc6dd..10f562508a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -8,19 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index f596c89686..bb4287d4b4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -8,14 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" + "label": "Enable Color Management" }, { "type": "schema", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json index e76c8a326f..62b72c2518 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json @@ -1,13 +1,13 @@ { "key": "file_rules", "type": "dict", - "label": "File Rules", + "label": "File Rules (OCIO v1 only)", "collapsible": true, "children": [ { "type": "boolean", "key": "override_global_rules", - "label": "Override global file rules" + "label": "Override global File Rules" }, { "key": "rules", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index c69a4c4f4b..9eed442f25 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -1,19 +1,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management", "collapsible": true, "is_group": true, "children": [ { "type": "boolean", "key": "activate_host_color_management", - "label": "Enable Color Management in host" - }, - { - "type": "boolean", - "key": "set_ocio_config", - "label": "Set OCIO config file in host" + "label": "Enable Color Management" }, { "type": "schema", From bec1dd775d7be9b0d0051b2654288106ce527d6f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 May 2023 21:20:39 +0200 Subject: [PATCH 423/918] adding default settings --- .../settings/defaults/project_settings/aftereffects.json | 1 - openpype/settings/defaults/project_settings/blender.json | 1 - openpype/settings/defaults/project_settings/fusion.json | 1 - openpype/settings/defaults/project_settings/global.json | 6 +++++- openpype/settings/defaults/project_settings/hiero.json | 1 - openpype/settings/defaults/project_settings/houdini.json | 1 - openpype/settings/defaults/project_settings/max.json | 5 ++--- openpype/settings/defaults/project_settings/maya.json | 1 - openpype/settings/defaults/project_settings/nuke.json | 5 ++--- openpype/settings/defaults/project_settings/unreal.json | 1 - 10 files changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index c30356335b..74bd519bbd 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -1,7 +1,6 @@ { "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 1969cd8346..8328ceeda3 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,7 +1,6 @@ { "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index ba2abd467f..a506f0c182 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -1,7 +1,6 @@ { "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 3b7b29fe8b..dcc0fdb6b2 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -1,5 +1,6 @@ { "imageio": { + "activate_global_color_management": false, "ocio_config": { "filepath": [ "{OPENPYPE_ROOT}/vendor/bin/ocioconfig/OpenColorIOConfigs/aces_1.2/config.ocio", @@ -7,6 +8,7 @@ ] }, "file_rules": { + "activate_global_file_rules": false, "rules": { "example": { "pattern": ".*(beauty).*", @@ -250,7 +252,9 @@ } }, { - "families": ["review"], + "families": [ + "review" + ], "hosts": [ "maya", "houdini" diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index e876d1727d..01eb15bfbc 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -1,7 +1,6 @@ { "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index dd3fc87b80..2b7192ff99 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,7 +1,6 @@ { "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 89ba7a702d..e69b64f6cf 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,7 +1,6 @@ { "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] @@ -17,8 +16,8 @@ "image_format": "exr", "multipass": true }, - "PointCloud":{ - "attribute":{ + "PointCloud": { + "attribute": { "Age": "age", "Radius": "radius", "Position": "position", diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index c1ec473654..a31dffe4c2 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -411,7 +411,6 @@ }, "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 119a240ad5..a1e2e5c15b 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -10,7 +10,6 @@ }, "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] @@ -355,12 +354,12 @@ "optional": true, "active": true }, - "ValidateGizmo": { + "ValidateBackdrop": { "enabled": true, "optional": true, "active": true }, - "ValidateBackdrop": { + "ValidateGizmo": { "enabled": true, "optional": true, "active": true diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index eace5a3542..72acf17b9e 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,7 +1,6 @@ { "imageio": { "activate_host_color_management": true, - "set_ocio_config": false, "ocio_config": { "override_global_config": false, "filepath": [] From c129a7885170ce395e2399e9ce598aae72b5ba12 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 May 2023 21:32:28 +0200 Subject: [PATCH 424/918] remove set_ocio_config from global hook also clear the checking function from colorspace.py --- openpype/hooks/pre_ocio_hook.py | 15 +-------------- openpype/pipeline/colorspace.py | 22 ---------------------- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index bcff31fc93..49a042caa8 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -1,8 +1,7 @@ from openpype.lib import PreLaunchHook from openpype.pipeline.colorspace import ( - get_imageio_config, - is_set_ocio_config_activated + get_imageio_config ) from openpype.pipeline.template_data import get_template_data_with_names @@ -42,18 +41,6 @@ class OCIOEnvHook(PreLaunchHook): ) if config_data: - set_config_path = is_set_ocio_config_activated( - project_name=self.data["project_name"], - host_name=self.host_name, - project_settings=self.data["project_settings"] - ) - if not set_config_path: - self.log.info( - "Setting of OCIO environment with " - "config path was not activated..." - ) - return - ocio_path = config_data["path"] self.log.info( diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a1714bc75e..627d93153c 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -393,28 +393,6 @@ def get_imageio_config( return config_data -def is_set_ocio_config_activated( - project_name, host_name, project_settings=None -): - """Check if host OCIO config path is activated - - Args: - project_name (str): project name - host_name (str): host name - - Returns: - bool: True if activated - """ - project_settings = project_settings or get_project_settings(project_name) - - # get colorspace settings - _, imageio_host = _get_imageio_settings( - project_settings, host_name) - - # check if host settings is having set_ocio_config - return imageio_host.get("set_ocio_config", False) - - def _get_config_data(path_list, anatomy_data): """Return first existing path in path list. From e290d70584c085c0b968ae27687044060e103e52 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 May 2023 21:33:48 +0200 Subject: [PATCH 425/918] recognize global colorspace management switch --- openpype/pipeline/colorspace.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 627d93153c..ec793cd48b 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -346,11 +346,26 @@ def get_imageio_config( formatting_data["platform"] = platform.system().lower() # get colorspace settings + # check if global settings group is having activate_global_color_management + # key at all. If it does't then default it to False + # this is for backward compatibility only + # TODO: in future rewrite this to be more explicit imageio_global, imageio_host = _get_imageio_settings( project_settings, host_name) + activate_color_management = imageio_global.get( + "activate_global_color_management", False) + + if not activate_color_management: + # if global settings are disabled return False because + # it is expected that no colorspace management is needed + log.info( + "Colorspace management is disabled." + ) + return {} + # check if host settings group is having activate_host_color_management - # it it does not have activation key then default it to True so it uses + # if it does not have activation key then default it to True so it uses # global settings # this is for backward compatibility # TODO: in future rewrite this to be more explicit From e7cf7c0fecb8ce73fcf586606af0a817a38ddbd8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 2 May 2023 21:37:41 +0200 Subject: [PATCH 426/918] recognize global file rules switch --- openpype/pipeline/colorspace.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index ec793cd48b..652304ef33 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -357,7 +357,7 @@ def get_imageio_config( "activate_global_color_management", False) if not activate_color_management: - # if global settings are disabled return False because + # if global settings are disabled return empty dict because # it is expected that no colorspace management is needed log.info( "Colorspace management is disabled." @@ -477,6 +477,15 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): # get file rules from global and host_name frules_global = imageio_global["file_rules"] + activate_global_rules = frules_global.get( + "activate_global_file_rules", False) + + if not activate_global_rules: + log.info( + "Global File Rules are disabled." + ) + return {} + # host is optional, some might not have any settings frules_host = imageio_host.get("file_rules", {}) From c542934da45f6dc50bb8ceabfb23f4ff822f016b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 3 May 2023 03:25:25 +0000 Subject: [PATCH 427/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 72297a4430..9832ff4747 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.6-nightly.2" +__version__ = "3.15.6-nightly.3" From 7c2a1542145ba75808d7927507bfda45f5166810 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 May 2023 15:32:22 +0800 Subject: [PATCH 428/918] add settings to switch on/off the frame range validator --- .../plugins/publish/validate_frame_range.py | 2 +- .../publish/validate_frame_range_type.py | 33 ------------------- .../defaults/project_settings/max.json | 7 ++++ .../projects_schema/schema_project_max.json | 4 +++ .../schemas/schema_max_publish.json | 33 +++++++++++++++++++ 5 files changed, 45 insertions(+), 34 deletions(-) delete mode 100644 openpype/hosts/max/plugins/publish/validate_frame_range_type.py create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index dc12eece39..e07c6390c1 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -29,7 +29,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, order = ValidateContentsOrder families = ["maxrender"] hosts = ["max"] - optional = False + optional = True actions = [RepairAction] def process(self, instance): diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py b/openpype/hosts/max/plugins/publish/validate_frame_range_type.py deleted file mode 100644 index d77b1503e0..0000000000 --- a/openpype/hosts/max/plugins/publish/validate_frame_range_type.py +++ /dev/null @@ -1,33 +0,0 @@ -import pyblish.api - -from pymxs import runtime as rt -from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, - PublishValidationError -) - - -class ValidateFrameRangeType(pyblish.api.InstancePlugin): - """ - Validates whether the User - specified Frame Range(Type 3) is used in render setting - - """ - - label = "Validate Render Frame Range Type" - order = ValidateContentsOrder - families = ["maxrender"] - hosts = ["max"] - actions = [RepairAction] - - def process(self, instance): - if rt.rendTimeType != 3: - raise PublishValidationError("Incorrect type of frame range" - " used in render setting." - " Repair action can help to fix it.") - - @classmethod - def repair(cls, instance): - rt.renderTimeType = 3 - return instance diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index d59cdf8c4a..a757e08ef5 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -19,5 +19,12 @@ "custFloats": "custFloats", "custVecs": "custVecs" } + }, + "publish": { + "ValidateFrameRange": { + "enabled": true, + "optional": true, + "active": true + } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 4fba9aff0a..42506559d0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -73,6 +73,10 @@ } } ] + }, + { + "type": "schema", + "name": "schema_max_publish" } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json new file mode 100644 index 0000000000..ea08c735a6 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_max_publish.json @@ -0,0 +1,33 @@ +{ + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ValidateFrameRange", + "label": "Validate Frame Range", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + } + ] + } From 273eb747915722c29e2c88d123cac43977b3b575 Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Wed, 3 May 2023 08:13:37 +0000 Subject: [PATCH 429/918] Update openpype/plugins/inventory/remove_and_load.py Fix typo Co-authored-by: Roy Nieterau --- openpype/plugins/inventory/remove_and_load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 981722c065..9befaf8729 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -34,7 +34,7 @@ class RemoveAndLoad(InventoryAction): representation = get_representation_by_id( project_name, container["representation"] ) - assert representation, "Represenatation not found" + assert representation, "Representation not found" # Remove container remove_container(container) From e2da9a0552356688f2fe2e091ca5de0ae6a5b84c Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Wed, 3 May 2023 10:20:50 +0200 Subject: [PATCH 430/918] Using for else to raise loader not found error --- openpype/plugins/inventory/remove_and_load.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 9befaf8729..998be119d5 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -25,10 +25,10 @@ class RemoveAndLoad(InventoryAction): if get_loader_identifier(plugin) == loader_name: loader = plugin break - - assert ( - loader - ), "Failed to get loader, can't remove and load container" + else: + raise RuntimeError( + "Failed to get loader, can't remove and load container" + ) # Get representation representation = get_representation_by_id( From 2a9a7d6fd4f0a96dc70b1f61525d5b8f1ac05eb5 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:18:43 +0300 Subject: [PATCH 431/918] handle reviews --- openpype/modules/kitsu/kitsu_module.py | 19 ++---- .../modules/kitsu/utils/update_op_with_zou.py | 62 +++++++++---------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 7c9d888aa7..2a3a0f3bff 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -125,9 +125,10 @@ def push_to_zou(login, password): @cli_main.command() @click.option( - "-prjs", - "--projects", - envvar="SYNC_PROJECTS", + "-prj", + "--project", + multiple=True, + default=[""] help="Sync specific kitsu projects" ) @click.option( @@ -142,26 +143,18 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, projects="^"): +def sync_service(login, password, project): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password projects (str): specific kitsu projects - - SYNC_PROJECTS: - *: all projects - ^: dont sync any project just listen - "project01 project02 ...": to choose custom projects - - """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = projects.strip() - projects = projects.split(' ') + projects = ' '.join(project) sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index ad8ccd9f3f..7983765e83 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,9 +358,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects( - login: str, password: str, ignore_projects: list = None, - specific_projects: list = None +def sync_all_projects(login: str, password: str, ignore_projects: list = None, + filter_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -368,7 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - specific_projects (list): List of synced project names + filter_projects (list): List of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,22 +384,21 @@ def sync_all_projects( all_projects = gazu.project.all_projects() project_to_sync = [] - if specific_projects == ['*']: + + if not filter_projects: + # listen only + return + + if filter_projects == ['*']: project_to_sync = all_projects - elif specific_projects == ['^']: - return - - elif isinstance(specific_projects, list): - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in specific_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exists in kitsu.' - f' Please make sure you write the project correctly.') - else: - return + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: @@ -451,10 +449,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] From 0a3d206680ed7b21cfb30f1800ae1625e284793d Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:23:17 +0300 Subject: [PATCH 432/918] solve syntex error --- openpype/modules/kitsu/kitsu_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 2a3a0f3bff..59ad2efd29 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -128,7 +128,7 @@ def push_to_zou(login, password): "-prj", "--project", multiple=True, - default=[""] + default=[""], help="Sync specific kitsu projects" ) @click.option( From 6e9b6f6bef0c290dd46b7b3669fad2d3b338c0e8 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:38:42 +0300 Subject: [PATCH 433/918] correct arg name in sync_all_projects --- openpype/modules/kitsu/kitsu_module.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 59ad2efd29..bd8ade62d8 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -149,12 +149,10 @@ def sync_service(login, password, project): Args: login (str): Kitsu user login password (str): Kitsu user password - projects (str): specific kitsu projects + project (str): specific kitsu projects """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = ' '.join(project) - - sync_all_projects(login, password, specific_projects=projects) + sync_all_projects(login, password, filter_projects=project) start_listeners(login, password) From ad4f5876ce18239b244d59332ed40b5469cc0433 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:40:32 +0300 Subject: [PATCH 434/918] update condition to sync specific projects --- .../modules/kitsu/utils/update_op_with_zou.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 7983765e83..40e4191508 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -389,16 +389,18 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, # listen only return - if filter_projects == ['*']: + if '*' in filter_projects: + # all projects project_to_sync = all_projects - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in filter_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + else: + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From d6e7cd638d403f0d3f8ad53e7fef444122b1dc39 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 13:02:08 +0300 Subject: [PATCH 435/918] update kitsu docs --- website/docs/module_kitsu.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index d79c78fecf..4b827b3802 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,10 +16,18 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. +The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. ```bash +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 + +// sync all projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * + +// start listen only openpype_console module kitsu sync-service -l me@domain.ext -p my_password ``` From bc92395a7eb4fd98deb33299adca314b6c5ebfa0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 May 2023 15:44:37 +0200 Subject: [PATCH 436/918] update bug report workflow --- .github/workflows/update_bug_report.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update_bug_report.yml b/.github/workflows/update_bug_report.yml index 7a1bfb7bfd..1e5da414bb 100644 --- a/.github/workflows/update_bug_report.yml +++ b/.github/workflows/update_bug_report.yml @@ -18,10 +18,16 @@ jobs: uses: ynput/gha-populate-form-version@main with: github_token: ${{ secrets.YNPUT_BOT_TOKEN }} - github_user: ${{ secrets.CI_USER }} - github_email: ${{ secrets.CI_EMAIL }} registry: github dropdown: _version limit_to: 100 form: .github/ISSUE_TEMPLATE/bug_report.yml commit_message: 'chore(): update bug report / version' + dry_run: no-push + + - name: Push to protected develop branch + uses: CasperWA/push-protected@v2.10.0 + with: + token: ${{ secrets.YNPUT_BOT_TOKEN }} + branch: develop + unprotect_reviews: true \ No newline at end of file From 3d870ef794c8fbdf7bf6ac17351a7aaaeaa1811a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 May 2023 13:45:37 +0000 Subject: [PATCH 437/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index fe86a8400b..8328a35cad 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,9 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.6-nightly.3 + - 3.15.6-nightly.2 + - 3.15.6-nightly.1 - 3.15.5 - 3.15.5-nightly.2 - 3.15.5-nightly.1 @@ -132,9 +135,6 @@ body: - 3.14.0 - 3.14.0-nightly.1 - 3.13.1-nightly.3 - - 3.13.1-nightly.2 - - 3.13.1-nightly.1 - - 3.13.0 validations: required: true - type: dropdown From 9d3e15378b0109f332c99a21c2468012ff7d60c4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 May 2023 22:31:43 +0800 Subject: [PATCH 438/918] repharse the actual msg for the artists --- .../max/plugins/publish/validate_resolution_setting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 9424b24380..94cd093569 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -29,15 +29,15 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, current_width = rt.renderwidth current_height = rt.renderHeight if current_width != width and current_height != height: - raise PublishValidationError("Resolution Setting" - " not aligned with DB") + raise PublishValidationError("Resolution Setting " + "not matching resolution set on asset or shot.") if current_width != width: raise PublishValidationError("Width in Resolution Setting " - "not aligned with DB") + "not matching resolution set on asset or shot.") if current_height != height: raise PublishValidationError("Height in Resolution Setting " - "not aligned with DB") + "not matching resolution set on asset or shot.") def get_db_resolution(self, instance): data = ["data.resolutionWidth", "data.resolutionHeight"] From d8e62093acaf92ca21df28240c420a8d7895f808 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 May 2023 22:33:13 +0800 Subject: [PATCH 439/918] hound fix --- .../max/plugins/publish/validate_resolution_setting.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py index 94cd093569..5fcb843b20 100644 --- a/openpype/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/openpype/hosts/max/plugins/publish/validate_resolution_setting.py @@ -30,14 +30,17 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, current_height = rt.renderHeight if current_width != width and current_height != height: raise PublishValidationError("Resolution Setting " - "not matching resolution set on asset or shot.") + "not matching resolution " + "set on asset or shot.") if current_width != width: raise PublishValidationError("Width in Resolution Setting " - "not matching resolution set on asset or shot.") + "not matching resolution set " + "on asset or shot.") if current_height != height: raise PublishValidationError("Height in Resolution Setting " - "not matching resolution set on asset or shot.") + "not matching resolution set " + "on asset or shot.") def get_db_resolution(self, instance): data = ["data.resolutionWidth", "data.resolutionHeight"] From 17d39cc3561bc418191bf454bcc5567355a3fbcf Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Wed, 5 Apr 2023 18:43:19 +0200 Subject: [PATCH 440/918] preserve all references when importing a maya template --- openpype/hosts/maya/api/workfile_template_builder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index d65e4c74d2..c91544be0a 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -43,7 +43,13 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): )) cmds.sets(name=PLACEHOLDER_SET, empty=True) - new_nodes = cmds.file(path, i=True, returnNewNodes=True) + new_nodes = cmds.file( + path, + i=True, + returnNewNodes=True, + preserveReferences=True, + loadReferenceDepth="all", + ) cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True) From 459a246948569451a16b00fecd4d1a2e8e9d5f6c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 May 2023 17:31:43 +0200 Subject: [PATCH 441/918] Adding labels to settings --- .../projects_schema/schema_project_aftereffects.json | 6 +++++- .../schemas/projects_schema/schema_project_blender.json | 6 +++++- .../schemas/projects_schema/schema_project_celaction.json | 6 +++++- .../schemas/projects_schema/schema_project_flame.json | 6 +++++- .../schemas/projects_schema/schema_project_fusion.json | 6 +++++- .../schemas/projects_schema/schema_project_global.json | 4 ++++ .../schemas/projects_schema/schema_project_harmony.json | 6 +++++- .../schemas/projects_schema/schema_project_hiero.json | 6 +++++- .../schemas/projects_schema/schema_project_houdini.json | 6 +++++- .../schemas/projects_schema/schema_project_max.json | 6 +++++- .../schemas/projects_schema/schema_project_maya.json | 6 +++++- .../schemas/projects_schema/schema_project_photoshop.json | 6 +++++- .../schemas/projects_schema/schema_project_resolve.json | 6 +++++- .../projects_schema/schema_project_traypublisher.json | 6 +++++- .../schemas/projects_schema/schema_project_tvpaint.json | 6 +++++- .../schemas/projects_schema/schema_project_unreal.json | 6 +++++- .../projects_schema/schema_project_webpublisher.json | 6 +++++- .../projects_schema/schemas/schema_nuke_imageio.json | 6 +++++- 18 files changed, 89 insertions(+), 17 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index d9007d6185..5da632a933 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 8997b750ec..b15b508661 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json index 23268d0d9a..5729f70e2f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (derived to OCIO)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index f18da95065..625780a650 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (remapped to OCIO)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index b236925c1c..1e26e7d701 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json index 80ea73267b..d1d7f336e1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -11,6 +11,10 @@ "label": "Color Management", "is_group": true, "children": [ + { + "type": "label", + "label": "It's important to note that once color management is activated on a project, all hosts will be color managed by default.
The OpenColorIO (OCIO) config file is used either from the global settings or from the host's overrides. It's worth
noting that the order of the defined configuration paths matters, with higher priority given to paths listed earlier in
the configuration list.

To avoid potential issues, ensure that the OCIO configuration path is not an absolute path and includes at least
the root token (Anatomy). This helps ensure that the configuration path remains valid across different environments and
avoids any hard-coding of paths that may be specific to one particular system." + }, { "type": "boolean", "key": "activate_global_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 840f1fa4c0..0357a79aea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index e7bd91dcef..8c6be5d6d8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 1fb23759cb..d50ebd948f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index bc632d4a20..10a12dbecc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index c69ccb3f07..49bd1002aa 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -48,10 +48,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index c4bb81e7f8..898c3374d7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (remapped to OCIO)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index ef1880af67..758cf2a196 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (remapped to OCIO)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index a07c262375..c234cd1b71 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (derived to OCIO)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 1c1d75c4e6..6d446b5550 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (derived to OCIO)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 10f562508a..2d0870f76a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index bb4287d4b4..e319182e3c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -8,10 +8,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (derived to OCIO)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + }, { "type": "boolean", "key": "activate_host_color_management", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index 9eed442f25..7aeb3d32db 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -1,10 +1,14 @@ { "key": "imageio", "type": "dict", - "label": "Color Management", + "label": "Color Management (OCIO managed)", "collapsible": true, "is_group": true, "children": [ + { + "type": "label", + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + }, { "type": "boolean", "key": "activate_host_color_management", From ca3b9f5e3042b4bf25bdcb0ec95815eba3e2a5fb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 3 May 2023 18:48:41 +0200 Subject: [PATCH 442/918] :art: set references on modifier --- openpype/hosts/max/api/plugin.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 15fcc89c5f..52da23dc0a 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -102,7 +102,7 @@ class MaxCreatorBase(object): instance """ if isinstance(node, str): - node = rt.dummy(name=node) + node = rt.container(name=node) attrs = rt.execute(MS_CUSTOM_ATTRIB) rt.custAttributes.add(node.baseObject, attrs) @@ -127,10 +127,15 @@ class MaxCreator(Creator, MaxCreatorBase): self ) if pre_create_data.get("use_selection"): - print("adding selection") - print(rt.array(*self.selected_nodes)) - instance_node.openPypeData.all_nodes = rt.array( - *self.selected_nodes) + + node_list = [] + for i in self.selected_nodes: + node_ref = rt.NodeTransformMonitor(node=i) + node_list.append(node_ref) + + # Setting the property + rt.setProperty( + instance_node.openPypeData, "all_handles", node_list) self._add_instance_to_context(instance) imprint(instance_node.name, instance.data_to_store()) @@ -171,9 +176,7 @@ class MaxCreator(Creator, MaxCreatorBase): instance.data.get("instance_node") ): rt.select(instance_node) - rt.execute( - "for o in selection do for c in o.children do c.parent = " - "undefined") # noqa + rt.custAttributes.add(instance_node.baseObject, "openPypeData") rt.delete(instance_node) self._remove_instance_from_context(instance) From 37ce7fbc09fc1546dbc3ecc5620e5ba31d17b161 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 3 May 2023 18:49:04 +0200 Subject: [PATCH 443/918] :art: add members collector --- .../max/plugins/publish/collect_members.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/collect_members.py diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py new file mode 100644 index 0000000000..0b50ba0d8f --- /dev/null +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Collect instance members""" +import pyblish.api +from pymxs import runtime as rt + + +class CollectMembers(pyblish.api.InstancePlugin): + """Collect Render for Deadline""" + + order = pyblish.api.CollectorOrder + 0.01 + label = "Collect Instance Members" + hosts = ['max'] + + def process(self, instance): + + if instance.data.get("instance_node"): + container = rt.GetNodeByName(instance.data["instance_node"]) + instance.data["members"] = [i.node for i in container.openPypeData.all_handles] From b52527b55f48babb3f677bb4534ecdeaf21fd9f6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 3 May 2023 18:49:43 +0200 Subject: [PATCH 444/918] :recycle: switch the use to collected instance members --- .../max/plugins/publish/extract_camera_abc.py | 15 +- .../max/plugins/publish/extract_camera_fbx.py | 14 +- .../plugins/publish/extract_max_scene_raw.py | 14 +- .../max/plugins/publish/extract_model.py | 15 +- .../max/plugins/publish/extract_model_fbx.py | 15 +- .../max/plugins/publish/extract_model_obj.py | 15 +- .../max/plugins/publish/extract_model_usd.py | 43 ++--- .../max/plugins/publish/extract_pointcache.py | 14 +- .../max/plugins/publish/extract_pointcloud.py | 167 +++++++++++------- .../publish/validate_camera_contents.py | 22 +-- .../publish/validate_model_contents.py | 22 ++- .../publish/validate_no_max_content.py | 3 +- .../plugins/publish/validate_pointcloud.py | 128 ++++++-------- .../plugins/publish/validate_usd_plugin.py | 13 +- 14 files changed, 231 insertions(+), 269 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 8c23ff9878..b4f294fbf9 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -29,8 +28,6 @@ class ExtractCameraAlembic(publish.Extractor, start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - container = instance.data["instance_node"] - self.log.info("Extracting Camera ...") stagingdir = self.staging_dir(instance) @@ -38,8 +35,7 @@ class ExtractCameraAlembic(publish.Extractor, path = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info(f"Writing alembic '{filename}' to '{stagingdir}'") export_cmd = ( f""" @@ -57,8 +53,8 @@ exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.Select(instance.data["members"]) + rt.Execute(export_cmd) self.log.info("Performing Extraction ...") if "representations" not in instance.data: @@ -71,5 +67,4 @@ exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - path)) + self.log.info(f"Extracted instance '{instance.name}' to: {path}") diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 7e92f355ed..ffbd281c67 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -26,15 +25,13 @@ class ExtractCameraFbx(publish.Extractor, def process(self, instance): if not self.is_active(instance.data): return - container = instance.data["instance_node"] self.log.info("Extracting Camera ...") stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing fbx file '%s' to '%s'" % (filename, - filepath)) + self.log.info(f"Writing fbx file '{filename}' to '{filepath}'") # Need to export: # Animation = True @@ -57,8 +54,8 @@ exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(fbx_export_cmd) + rt.Select(instance.data["members"]) + rt.Execute(fbx_export_cmd) self.log.info("Performing Extraction ...") if "representations" not in instance.data: @@ -71,5 +68,4 @@ exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info(f"Extracted instance '{instance.name}' to: {filepath}") diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index c14fcdbd0b..ed98922462 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -28,7 +27,6 @@ class ExtractMaxSceneRaw(publish.Extractor, def process(self, instance): if not self.is_active(instance.data): return - container = instance.data["instance_node"] # publish the raw scene for camera self.log.info("Extracting Raw Max Scene ...") @@ -37,8 +35,7 @@ class ExtractMaxSceneRaw(publish.Extractor, filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) - self.log.info("Writing max file '%s' to '%s'" % (filename, - max_path)) + self.log.info(f"Writing max file '{filename}' to '{max_path}'") if "representations" not in instance.data: instance.data["representations"] = [] @@ -46,8 +43,8 @@ class ExtractMaxSceneRaw(publish.Extractor, # saving max scene with maintained_selection(): # need to figure out how to select the camera - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'saveNodes selection "{max_path}" quiet:true') + rt.Select(instance.data["members"]) + rt.Execute(f'saveNodes selection "{max_path}" quiet:true') self.log.info("Performing Extraction ...") @@ -58,5 +55,4 @@ class ExtractMaxSceneRaw(publish.Extractor, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - max_path)) + self.log.info(f"Extracted instance '{instance.name}' to: {max_path}") diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 710ad5f97d..d0f7bb0410 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -27,8 +26,6 @@ class ExtractModel(publish.Extractor, if not self.is_active(instance.data): return - container = instance.data["instance_node"] - self.log.info("Extracting Geometry ...") stagingdir = self.staging_dir(instance) @@ -36,8 +33,7 @@ class ExtractModel(publish.Extractor, filepath = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info(f"Writing alembic '{filename}' to '{stagingdir}'") export_cmd = ( f""" @@ -56,8 +52,8 @@ exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.Select(instance.data["members"]) + rt.Execute(export_cmd) self.log.info("Performing Extraction ...") if "representations" not in instance.data: @@ -70,5 +66,4 @@ exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info(f"Extracted instance '{instance.name}' to: {filepath}") diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index ce58e8cc17..696974a703 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -27,16 +26,13 @@ class ExtractModelFbx(publish.Extractor, if not self.is_active(instance.data): return - container = instance.data["instance_node"] - self.log.info("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing FBX '%s' to '%s'" % (filepath, - stagingdir)) + self.log.info(f"Writing FBX '{filepath}' to '{stagingdir}'") export_fbx_cmd = ( f""" @@ -56,8 +52,8 @@ exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_fbx_cmd) + rt.Select(instance.data["members"]) + rt.Execute(export_fbx_cmd) self.log.info("Performing Extraction ...") if "representations" not in instance.data: @@ -70,5 +66,4 @@ exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info(f"Extracted instance '{instance.name}' to: {filepath}") diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 7bda237880..93896eea02 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -27,21 +26,18 @@ class ExtractModelObj(publish.Extractor, if not self.is_active(instance.data): return - container = instance.data["instance_node"] - self.log.info("Extracting Geometry ...") stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing OBJ '%s' to '%s'" % (filepath, - stagingdir)) + self.log.info(f"Writing OBJ '{filepath}' to '{stagingdir}'") with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa + rt.Select(instance.data["members"]) + rt.Execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa self.log.info("Performing Extraction ...") if "representations" not in instance.data: @@ -55,5 +51,4 @@ class ExtractModelObj(publish.Extractor, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info(f"Extracted instance '{instance.name}' to: {filepath}") diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 0bed2d855e..ae250cae5a 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -26,31 +26,27 @@ class ExtractModelUSD(publish.Extractor, if not self.is_active(instance.data): return - container = instance.data["instance_node"] - self.log.info("Extracting Geometry ...") stagingdir = self.staging_dir(instance) asset_filename = "{name}.usda".format(**instance.data) asset_filepath = os.path.join(stagingdir, asset_filename) - self.log.info("Writing USD '%s' to '%s'" % (asset_filepath, - stagingdir)) + self.log.info(f"Writing USD '{asset_filepath}' to '{stagingdir}'") log_filename = "{name}.txt".format(**instance.data) log_filepath = os.path.join(stagingdir, log_filename) - self.log.info("Writing log '%s' to '%s'" % (log_filepath, - stagingdir)) + self.log.info(f"Writing log '{log_filepath}' to '{stagingdir}'") # get the nodes which need to be exported export_options = self.get_export_options(log_filepath) with maintained_selection(): # select and export - node_list = self.get_node_list(container) + node_list = instance.data["members"] rt.USDExporter.ExportFile(asset_filepath, exportOptions=export_options, - contentSource=rt.name("selected"), + contentSource=rt.Name("selected"), nodeList=node_list) self.log.info("Performing Extraction ...") @@ -73,25 +69,10 @@ class ExtractModelUSD(publish.Extractor, } instance.data["representations"].append(log_representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - asset_filepath)) + self.log.info(f"Extracted instance '{instance.name}' to: {asset_filepath}") - def get_node_list(self, container): - """ - Get the target nodes which are - the children of the container - """ - node_list = [] - - container_node = rt.getNodeByName(container) - target_node = container_node.Children - rt.select(target_node) - for sel in rt.selection: - node_list.append(sel) - - return node_list - - def get_export_options(self, log_path): + @staticmethod + def get_export_options(log_path): """Set Export Options for USD Exporter""" export_options = rt.USDExporter.createOptions() @@ -101,13 +82,13 @@ class ExtractModelUSD(publish.Extractor, export_options.Lights = False export_options.Cameras = False export_options.Materials = False - export_options.MeshFormat = rt.name('fromScene') - export_options.FileFormat = rt.name('ascii') - export_options.UpAxis = rt.name('y') - export_options.LogLevel = rt.name('info') + export_options.MeshFormat = rt.Name('fromScene') + export_options.FileFormat = rt.Name('ascii') + export_options.UpAxis = rt.Name('y') + export_options.LogLevel = rt.Name('info') export_options.LogPath = log_path export_options.PreserveEdgeOrientation = True - export_options.TimeMode = rt.name('current') + export_options.TimeMode = rt.Name('current') rt.USDexporter.UIOptions = export_options diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 75d8a7972c..400d55d198 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -42,8 +42,7 @@ import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) @@ -57,17 +56,14 @@ class ExtractAlembic(publish.Extractor): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - container = instance.data["instance_node"] - self.log.info("Extracting pointcache ...") parent_dir = self.staging_dir(instance) file_name = "{name}.abc".format(**instance.data) path = os.path.join(parent_dir, file_name) - # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (file_name, - parent_dir)) + self.log.info( + f"Writing alembic '{file_name}' to '{parent_dir}'") abc_export_cmd = ( f""" @@ -85,8 +81,8 @@ exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(abc_export_cmd) + rt.Select(instance.data["members"]) + rt.Execute(abc_export_cmd) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index e8d58ab713..5a85915967 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -9,13 +9,6 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io -def get_setting(project_setting=None): - project_setting = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) - return (project_setting["max"]["PointCloud"]) - - class ExtractPointCloud(publish.Extractor): """ Extract PRT format with tyFlow operators @@ -24,19 +17,20 @@ class ExtractPointCloud(publish.Extractor): Currently only works for the default partition setting Args: - export_particle(): sets up all job arguments for attributes - to be exported in MAXscript + self.export_particle(): sets up all job arguments for attributes + to be exported in MAXscript - get_operators(): get the export_particle operator + self.get_operators(): get the export_particle operator - get_custom_attr(): get all custom channel attributes from Openpype - setting and sets it as job arguments before exporting + self.get_custom_attr(): get all custom channel attributes from Openpype + setting and sets it as job arguments before exporting - get_files(): get the files with tyFlow naming convention - before publishing + self.get_files(): get the files with tyFlow naming convention + before publishing - partition_output_name(): get the naming with partition settings. - get_partition(): get partition value + self.partition_output_name(): get the naming with partition settings. + + self.get_partition(): get partition value """ @@ -46,6 +40,7 @@ class ExtractPointCloud(publish.Extractor): families = ["pointcloud"] def process(self, instance): + self.settings = self.get_setting(instance) start = int(instance.context.data.get("frameStart")) end = int(instance.context.data.get("frameEnd")) container = instance.data["instance_node"] @@ -56,12 +51,12 @@ class ExtractPointCloud(publish.Extractor): path = os.path.join(stagingdir, filename) with maintained_selection(): - job_args = self.export_particle(container, + job_args = self.export_particle(instance.data["members"], start, end, path) for job in job_args: - rt.execute(job) + rt.Execute(job) self.log.info("Performing Extraction ...") if "representations" not in instance.data: @@ -69,7 +64,7 @@ class ExtractPointCloud(publish.Extractor): self.log.info("Writing PRT with TyFlow Plugin...") filenames = self.get_files(container, path, start, end) - self.log.debug("filenames: {0}".format(filenames)) + self.log.debug(f"filenames: {filenames}") partition = self.partition_output_name(container) @@ -81,67 +76,87 @@ class ExtractPointCloud(publish.Extractor): "outputName": partition # partition value } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - path)) + self.log.info(f"Extracted instance '{instance.name}' to: {path}") def export_particle(self, - container, + members, start, end, filepath): + """Sets up all job arguments for attributes. + + Those attributes are to be exported in MAX Script. + + Args: + members (list): Member nodes of the instance. + start (int): Start frame. + end (int): End frame. + filepath (str): Path to PRT file. + + Returns: + list of arguments for MAX Script. + + """ job_args = [] - opt_list = self.get_operators(container) + opt_list = self.get_operators(members) for operator in opt_list: - start_frame = "{0}.frameStart={1}".format(operator, - start) + start_frame = f"{operator}.frameStart={start}" job_args.append(start_frame) - end_frame = "{0}.frameEnd={1}".format(operator, - end) + end_frame = f"{operator}.frameEnd={end}" job_args.append(end_frame) filepath = filepath.replace("\\", "/") - prt_filename = '{0}.PRTFilename="{1}"'.format(operator, - filepath) + prt_filename = f"{operator}.PRTFilename={filepath}" job_args.append(prt_filename) # Partition - mode = "{0}.PRTPartitionsMode=2".format(operator) + mode = f"{operator}.PRTPartitionsMode=2" job_args.append(mode) additional_args = self.get_custom_attr(operator) - for args in additional_args: - job_args.append(args) - - prt_export = "{0}.exportPRT()".format(operator) + job_args.extend(iter(additional_args)) + prt_export = f"{operator}.exportPRT()" job_args.append(prt_export) return job_args - def get_operators(self, container): - """Get Export Particles Operator""" + @staticmethod + def get_operators(members): + """Get Export Particles Operator. + Args: + members (list): Instance members. + + Returns: + list of particle operators + + """ opt_list = [] - node = rt.getNodebyName(container) - selection_list = list(node.Children) - for sel in selection_list: - obj = sel.baseobject + for member in members: + obj = member.baseobject # TODO: to see if it can be used maxscript instead - anim_names = rt.getsubanimnames(obj) + anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: - sub_anim = rt.getsubanim(obj, anim_name) - boolean = rt.isProperty(sub_anim, "Export_Particles") - event_name = sub_anim.name + sub_anim = rt.GetSubAnim(obj, anim_name) + boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: - opt = "${0}.{1}.export_particles".format(sel.name, - event_name) + event_name = sub_anim.Name + opt = f"${member.Name}.{event_name}.export_particles" opt_list.append(opt) return opt_list + @staticmethod + def get_setting(instance): + project_setting = get_project_settings( + instance.context.data["projectName"] + ) + return project_setting["max"]["PointCloud"] + def get_custom_attr(self, operator): """Get Custom Attributes""" custom_attr_list = [] - attr_settings = get_setting()["attribute"] + attr_settings = self.settings["attribute"] for key, value in attr_settings.items(): custom_attr = "{0}.PRTChannels_{1}=True".format(operator, value) @@ -157,14 +172,25 @@ class ExtractPointCloud(publish.Extractor): path, start_frame, end_frame): - """ - Note: - Set the filenames accordingly to the tyFlow file - naming extension for the publishing purpose + """Get file names for tyFlow. - Actual File Output from tyFlow: + Set the filenames accordingly to the tyFlow file + naming extension for the publishing purpose + + Actual File Output from tyFlow:: __partof..prt + e.g. tyFlow_cloth_CCCS_blobbyFill_001__part1of1_00004.prt + + Args: + container: Instance node. + path (str): Output directory. + start_frame (int): Start frame. + end_frame (int): End frame. + + Returns: + list of filenames + """ filenames = [] filename = os.path.basename(path) @@ -181,27 +207,36 @@ class ExtractPointCloud(publish.Extractor): return filenames def partition_output_name(self, container): - """ - Notes: - Partition output name set for mapping - the published file output + """Get partition output name. + + Partition output name set for mapping + the published file output. + + Todo: + Customizes the setting for the output. + + Args: + container: Instance node. + + Returns: + str: Partition name. - todo: - Customizes the setting for the output """ partition_count, partition_start = self.get_partition(container) - partition = "_part{:03}of{}".format(partition_start, - partition_count) - - return partition + return f"_part{partition_start:03}of{partition_count}" def get_partition(self, container): - """ - Get Partition Value + """Get Partition value. + + Args: + container: Instance node. + """ opt_list = self.get_operators(container) + # TODO: This looks strange? Iterating over + # the opt_list but returning from inside? for operator in opt_list: - count = rt.execute(f'{operator}.PRTPartitionsCount') - start = rt.execute(f'{operator}.PRTPartitionsFrom') + count = rt.Execute(f'{operator}.PRTPartitionsCount') + start = rt.Execute(f'{operator}.PRTPartitionsFrom') return count, start diff --git a/openpype/hosts/max/plugins/publish/validate_camera_contents.py b/openpype/hosts/max/plugins/publish/validate_camera_contents.py index c81e28a61f..a858092abd 100644 --- a/openpype/hosts/max/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_camera_contents.py @@ -18,30 +18,24 @@ class ValidateCameraContent(pyblish.api.InstancePlugin): "$Physical_Camera", "$Target"] def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError("Camera instance must only include" - "camera (and camera target)") + if invalid := self.get_invalid(instance): + raise PublishValidationError(("Camera instance must only include" + "camera (and camera target). " + f"Invalid content {invalid}")) def get_invalid(self, instance): """ Get invalid nodes if the instance is not camera """ - invalid = list() + invalid = [] container = instance.data["instance_node"] - self.log.info("Validating look content for " - "{}".format(container)) + self.log.info(f"Validating camera content for {container}") - con = rt.getNodeByName(container) - selection_list = list(con.Children) + selection_list = instance.data["members"] for sel in selection_list: # to avoid Attribute Error from pymxs wrapper sel_tmp = str(sel) - found = False - for cam in self.camera_type: - if sel_tmp.startswith(cam): - found = True - break + found = any(sel_tmp.startswith(cam) for cam in self.camera_type) if not found: self.log.error("Camera not found") invalid.append(sel) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index dd782674ff..b21c0184b0 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -17,28 +17,26 @@ class ValidateModelContent(pyblish.api.InstancePlugin): label = "Model Contents" def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError("Model instance must only include" - "Geometry and Editable Mesh") + if invalid := self.get_invalid(instance): + raise PublishValidationError(("Model instance must only include" + "Geometry and Editable Mesh. " + f"Invalid types on: {invalid}")) def get_invalid(self, instance): """ Get invalid nodes if the instance is not camera """ - invalid = list() + invalid = [] container = instance.data["instance_node"] - self.log.info("Validating look content for " - "{}".format(container)) + self.log.info(f"Validating model content for {container}") - con = rt.getNodeByName(container) - selection_list = list(con.Children) or rt.getCurrentSelection() + selection_list = instance.data["members"] for sel in selection_list: - if rt.classOf(sel) in rt.Camera.classes: + if rt.ClassOf(sel) in rt.Camera.classes: invalid.append(sel) - if rt.classOf(sel) in rt.Light.classes: + if rt.ClassOf(sel) in rt.Light.classes: invalid.append(sel) - if rt.classOf(sel) in rt.Shape.classes: + if rt.ClassOf(sel) in rt.Shape.classes: invalid.append(sel) return invalid diff --git a/openpype/hosts/max/plugins/publish/validate_no_max_content.py b/openpype/hosts/max/plugins/publish/validate_no_max_content.py index c20a1968ed..ba4a6882c2 100644 --- a/openpype/hosts/max/plugins/publish/validate_no_max_content.py +++ b/openpype/hosts/max/plugins/publish/validate_no_max_content.py @@ -18,6 +18,5 @@ class ValidateMaxContents(pyblish.api.InstancePlugin): label = "Max Scene Contents" def process(self, instance): - container = rt.getNodeByName(instance.data["instance_node"]) - if not list(container.Children): + if not instance.data["members"]: raise PublishValidationError("No content found in the container") diff --git a/openpype/hosts/max/plugins/publish/validate_pointcloud.py b/openpype/hosts/max/plugins/publish/validate_pointcloud.py index f654058648..38d8226e41 100644 --- a/openpype/hosts/max/plugins/publish/validate_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/validate_pointcloud.py @@ -9,11 +9,11 @@ def get_setting(project_setting=None): project_setting = get_project_settings( legacy_io.Session["AVALON_PROJECT"] ) - return (project_setting["max"]["PointCloud"]) + return project_setting["max"]["PointCloud"] class ValidatePointCloud(pyblish.api.InstancePlugin): - """Validate that workfile was saved.""" + """Validate that work file was saved.""" order = pyblish.api.ValidatorOrder families = ["pointcloud"] @@ -34,39 +34,37 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): of export_particle operator """ - invalid = self.get_tyFlow_object(instance) - if invalid: - raise PublishValidationError("Non tyFlow object " - "found: {}".format(invalid)) - invalid = self.get_tyFlow_operator(instance) - if invalid: - raise PublishValidationError("tyFlow ExportParticle operator " - "not found: {}".format(invalid)) + report = [] + if invalid := self.get_tyflow_object(instance): + report.append(f"Non tyFlow object found: {invalid}") - invalid = self.validate_export_mode(instance) - if invalid: - raise PublishValidationError("The export mode is not at PRT") + if invalid := self.get_tyflow_operator(instance): + report.append( + f"tyFlow ExportParticle operator not found: {invalid}") - invalid = self.validate_partition_value(instance) - if invalid: - raise PublishValidationError("tyFlow Partition setting is " - "not at the default value") - invalid = self.validate_custom_attribute(instance) - if invalid: - raise PublishValidationError("Custom Attribute not found " - ":{}".format(invalid)) + if self.validate_export_mode(instance): + report.append("The export mode is not at PRT") - def get_tyFlow_object(self, instance): + if self.validate_partition_value(instance): + report.append(("tyFlow Partition setting is " + "not at the default value")) + + if invalid := self.validate_custom_attribute(instance): + report.append(("Custom Attribute not found " + f":{invalid}")) + + if report: + raise PublishValidationError + + def get_tyflow_object(self, instance): invalid = [] container = instance.data["instance_node"] - self.log.info("Validating tyFlow container " - "for {}".format(container)) + self.log.info(f"Validating tyFlow container for {container}") - con = rt.getNodeByName(container) - selection_list = list(con.Children) + selection_list = instance.data["members"] for sel in selection_list: sel_tmp = str(sel) - if rt.classOf(sel) in [rt.tyFlow, + if rt.ClassOf(sel) in [rt.tyFlow, rt.Editable_Mesh]: if "tyFlow" not in sel_tmp: invalid.append(sel) @@ -75,23 +73,20 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): return invalid - def get_tyFlow_operator(self, instance): + def get_tyflow_operator(self, instance): invalid = [] container = instance.data["instance_node"] - self.log.info("Validating tyFlow object " - "for {}".format(container)) - - con = rt.getNodeByName(container) - selection_list = list(con.Children) + self.log.info(f"Validating tyFlow object for {container}") + selection_list = instance.data["members"] bool_list = [] for sel in selection_list: obj = sel.baseobject - anim_names = rt.getsubanimnames(obj) + anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get all the names of the related tyFlow nodes - sub_anim = rt.getsubanim(obj, anim_name) + sub_anim = rt.GetSubAnim(obj, anim_name) # check if there is export particle operator - boolean = rt.isProperty(sub_anim, "Export_Particles") + boolean = rt.IsProperty(sub_anim, "Export_Particles") bool_list.append(str(boolean)) # if the export_particles property is not there # it means there is not a "Export Particle" operator @@ -104,21 +99,18 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): def validate_custom_attribute(self, instance): invalid = [] container = instance.data["instance_node"] - self.log.info("Validating tyFlow custom " - "attributes for {}".format(container)) + self.log.info( + f"Validating tyFlow custom attributes for {container}") - con = rt.getNodeByName(container) - selection_list = list(con.Children) + selection_list = instance.data["members"] for sel in selection_list: obj = sel.baseobject - anim_names = rt.getsubanimnames(obj) + anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get all the names of the related tyFlow nodes - sub_anim = rt.getsubanim(obj, anim_name) - # check if there is export particle operator - boolean = rt.isProperty(sub_anim, "Export_Particles") - event_name = sub_anim.name - if boolean: + sub_anim = rt.GetSubAnim(obj, anim_name) + if rt.IsProperty(sub_anim, "Export_Particles"): + event_name = sub_anim.name opt = "${0}.{1}.export_particles".format(sel.name, event_name) attributes = get_setting()["attribute"] @@ -126,39 +118,36 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): custom_attr = "{0}.PRTChannels_{1}".format(opt, value) try: - rt.execute(custom_attr) + rt.Execute(custom_attr) except RuntimeError: - invalid.add(key) + invalid.append(key) return invalid def validate_partition_value(self, instance): invalid = [] container = instance.data["instance_node"] - self.log.info("Validating tyFlow partition " - "value for {}".format(container)) + self.log.info( + f"Validating tyFlow partition value for {container}") - con = rt.getNodeByName(container) - selection_list = list(con.Children) + selection_list = instance.data["members"] for sel in selection_list: obj = sel.baseobject - anim_names = rt.getsubanimnames(obj) + anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get all the names of the related tyFlow nodes - sub_anim = rt.getsubanim(obj, anim_name) - # check if there is export particle operator - boolean = rt.isProperty(sub_anim, "Export_Particles") - event_name = sub_anim.name - if boolean: + sub_anim = rt.GetSubAnim(obj, anim_name) + if rt.IsProperty(sub_anim, "Export_Particles"): + event_name = sub_anim.name opt = "${0}.{1}.export_particles".format(sel.name, event_name) - count = rt.execute(f'{opt}.PRTPartitionsCount') + count = rt.Execute(f'{opt}.PRTPartitionsCount') if count != 100: invalid.append(count) - start = rt.execute(f'{opt}.PRTPartitionsFrom') + start = rt.Execute(f'{opt}.PRTPartitionsFrom') if start != 1: invalid.append(start) - end = rt.execute(f'{opt}.PRTPartitionsTo') + end = rt.Execute(f'{opt}.PRTPartitionsTo') if end != 1: invalid.append(end) @@ -167,24 +156,23 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): def validate_export_mode(self, instance): invalid = [] container = instance.data["instance_node"] - self.log.info("Validating tyFlow export " - "mode for {}".format(container)) + self.log.info( + f"Validating tyFlow export mode for {container}") - con = rt.getNodeByName(container) + con = rt.GetNodeByName(container) selection_list = list(con.Children) for sel in selection_list: obj = sel.baseobject - anim_names = rt.getsubanimnames(obj) + anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: # get all the names of the related tyFlow nodes - sub_anim = rt.getsubanim(obj, anim_name) + sub_anim = rt.GetSubAnim(obj, anim_name) # check if there is export particle operator - boolean = rt.isProperty(sub_anim, "Export_Particles") + boolean = rt.IsProperty(sub_anim, "Export_Particles") event_name = sub_anim.name if boolean: - opt = "${0}.{1}.export_particles".format(sel.name, - event_name) - export_mode = rt.execute(f'{opt}.exportMode') + opt = f"${sel.name}.{event_name}.export_particles" + export_mode = rt.Execute(f'{opt}.exportMode') if export_mode != 1: invalid.append(export_mode) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py index 747147020a..8f11d72567 100644 --- a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py @@ -14,21 +14,20 @@ class ValidateUSDPlugin(pyblish.api.InstancePlugin): label = "USD Plugin" def process(self, instance): - plugin_mgr = rt.pluginManager + plugin_mgr = rt.PluginManager plugin_count = plugin_mgr.pluginDllCount plugin_info = self.get_plugins(plugin_mgr, plugin_count) usd_import = "usdimport.dli" if usd_import not in plugin_info: - raise PublishValidationError("USD Plugin {}" - " not found".format(usd_import)) + raise PublishValidationError(f"USD Plugin {usd_import} not found") usd_export = "usdexport.dle" if usd_export not in plugin_info: - raise PublishValidationError("USD Plugin {}" - " not found".format(usd_export)) + raise PublishValidationError(f"USD Plugin {usd_export} not found") - def get_plugins(self, manager, count): - plugin_info_list = list() + @staticmethod + def get_plugins(manager, count): + plugin_info_list = [] for p in range(1, count + 1): plugin_info = manager.pluginDllName(p) plugin_info_list.append(plugin_info) From 7a0f18e27849b3a67db57f1af4905d58044488fe Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 3 May 2023 18:50:30 +0200 Subject: [PATCH 445/918] :rotating_light: Fix type case to match Python API --- openpype/hosts/max/api/lib.py | 38 +++++++++----------- openpype/hosts/max/api/lib_renderproducts.py | 4 +-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ad9a450cad..572d99c1cd 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -3,7 +3,7 @@ import json import six from pymxs import runtime as rt -from typing import Union +from typing import Union, Any, Dict import contextlib from openpype.pipeline.context_tools import ( @@ -16,15 +16,15 @@ JSON_PREFIX = "JSON::" def imprint(node_name: str, data: dict) -> bool: - node = rt.getNodeByName(node_name) + node = rt.GetNodeByName(node_name) if not node: return False for k, v in data.items(): if isinstance(v, (dict, list)): - rt.setUserProp(node, k, f'{JSON_PREFIX}{json.dumps(v)}') + rt.SetUserProp(node, k, f'{JSON_PREFIX}{json.dumps(v)}') else: - rt.setUserProp(node, k, v) + rt.SetUserProp(node, k, v) return True @@ -44,7 +44,7 @@ def lsattr( Returns: list of nodes. """ - root = rt.rootnode if root is None else rt.getNodeByName(root) + root = rt.RootNode if root is None else rt.GetNodeByName(root) def output_node(node, nodes): nodes.append(node) @@ -55,16 +55,16 @@ def lsattr( output_node(root, nodes) return [ n for n in nodes - if rt.getUserProp(n, attr) == value + if rt.GetUserProp(n, attr) == value ] if value else [ n for n in nodes - if rt.getUserProp(n, attr) + if rt.GetUserProp(n, attr) ] def read(container) -> dict: data = {} - props = rt.getUserPropBuffer(container) + props = rt.GetUserPropBuffer(container) # this shouldn't happen but let's guard against it anyway if not props: return data @@ -79,29 +79,25 @@ def read(container) -> dict: value = value.strip() if isinstance(value.strip(), six.string_types) and \ value.startswith(JSON_PREFIX): - try: + with contextlib.suppress(json.JSONDecodeError): value = json.loads(value[len(JSON_PREFIX):]) - except json.JSONDecodeError: - # not a json - pass - data[key.strip()] = value - data["instance_node"] = container.name + data["instance_node"] = container.Name return data @contextlib.contextmanager def maintained_selection(): - previous_selection = rt.getCurrentSelection() + previous_selection = rt.GetCurrentSelection() try: yield finally: if previous_selection: - rt.select(previous_selection) + rt.Select(previous_selection) else: - rt.select() + rt.Select() def get_all_children(parent, node_type=None): @@ -123,7 +119,7 @@ def get_all_children(parent, node_type=None): return children child_list = list_children(parent) - return ([x for x in child_list if rt.superClassOf(x) == node_type] + return ([x for x in child_list if rt.SuperClassOf(x) == node_type] if node_type else child_list) @@ -199,7 +195,7 @@ def reset_scene_resolution(): set_scene_resolution(width, height) -def get_frame_range() -> dict: +def get_frame_range() -> Union[Dict[str, Any], None]: """Get the current assets frame range and handles. Returns: @@ -242,7 +238,7 @@ def reset_frame_range(fps: bool = True): frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) frange_cmd = f"animationRange = interval {frame_start} {frame_end}" - rt.execute(frange_cmd) + rt.Execute(frange_cmd) def set_context_setting(): @@ -270,5 +266,5 @@ def get_max_version(): #(25000, 62, 0, 25, 0, 0, 997, 2023, "") max_info[7] = max version date """ - max_info = rt.maxversion() + max_info = rt.MaxVersion() return max_info[7] diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 350eb97661..e42686020a 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -91,7 +91,7 @@ class RenderProducts(object): """Get all the Arnold AOVs""" aovs = [] - amw = rt.MaxtoAOps.AOVsManagerWindow() + amw = rt.MaxToAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager # Check if there is any aov group set in AOV manager aov_group_num = len(aov_mgr.drivers) @@ -114,7 +114,7 @@ class RenderProducts(object): """Get all the render element output files. """ render_dirname = [] - render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem = rt.MaxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() # get render elements from the renders for i in range(render_elem_num): From 6d84969457f930e4020633ab6ecfbfcf47d22afc Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 3 May 2023 19:06:22 +0200 Subject: [PATCH 446/918] :rotating_light: fix Hound :dog: --- openpype/hosts/max/api/plugin.py | 13 +++++-------- .../hosts/max/plugins/publish/collect_members.py | 4 +++- .../hosts/max/plugins/publish/extract_model_usd.py | 3 ++- .../max/plugins/publish/validate_camera_contents.py | 2 +- .../max/plugins/publish/validate_model_contents.py | 2 +- .../max/plugins/publish/validate_pointcloud.py | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 52da23dc0a..39a17c29ef 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -144,10 +144,9 @@ class MaxCreator(Creator, MaxCreatorBase): def collect_instances(self): self.cache_subsets(self.collection_shared_data) - for instance in self.collection_shared_data[ - "max_cached_subsets"].get(self.identifier, []): + for instance in self.collection_shared_data["max_cached_subsets"].get(self.identifier, []): # noqa created_instance = CreatedInstance.from_existing( - read(rt.getNodeByName(instance)), self + read(rt.GetNodeByName(instance)), self ) self._add_instance_to_context(created_instance) @@ -172,12 +171,10 @@ class MaxCreator(Creator, MaxCreatorBase): """ for instance in instances: - if instance_node := rt.getNodeByName( - instance.data.get("instance_node") - ): - rt.select(instance_node) + if instance_node := rt.GetNodeByName(instance.data.get("instance_node")): # noqa + rt.Select(instance_node) rt.custAttributes.add(instance_node.baseObject, "openPypeData") - rt.delete(instance_node) + rt.Delete(instance_node) self._remove_instance_from_context(instance) diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py index 0b50ba0d8f..4ceb6cdadf 100644 --- a/openpype/hosts/max/plugins/publish/collect_members.py +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -15,4 +15,6 @@ class CollectMembers(pyblish.api.InstancePlugin): if instance.data.get("instance_node"): container = rt.GetNodeByName(instance.data["instance_node"]) - instance.data["members"] = [i.node for i in container.openPypeData.all_handles] + instance.data["members"] = [ + i.node for i in container.openPypeData.all_handles + ] diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index ae250cae5a..df1e7a4f02 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -69,7 +69,8 @@ class ExtractModelUSD(publish.Extractor, } instance.data["representations"].append(log_representation) - self.log.info(f"Extracted instance '{instance.name}' to: {asset_filepath}") + self.log.info( + f"Extracted instance '{instance.name}' to: {asset_filepath}") @staticmethod def get_export_options(log_path): diff --git a/openpype/hosts/max/plugins/publish/validate_camera_contents.py b/openpype/hosts/max/plugins/publish/validate_camera_contents.py index a858092abd..85be5d59fa 100644 --- a/openpype/hosts/max/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_camera_contents.py @@ -18,7 +18,7 @@ class ValidateCameraContent(pyblish.api.InstancePlugin): "$Physical_Camera", "$Target"] def process(self, instance): - if invalid := self.get_invalid(instance): + if invalid := self.get_invalid(instance): # noqa raise PublishValidationError(("Camera instance must only include" "camera (and camera target). " f"Invalid content {invalid}")) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index b21c0184b0..1d834292b8 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -17,7 +17,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): label = "Model Contents" def process(self, instance): - if invalid := self.get_invalid(instance): + if invalid := self.get_invalid(instance): # noqa raise PublishValidationError(("Model instance must only include" "Geometry and Editable Mesh. " f"Invalid types on: {invalid}")) diff --git a/openpype/hosts/max/plugins/publish/validate_pointcloud.py b/openpype/hosts/max/plugins/publish/validate_pointcloud.py index 38d8226e41..e3a6face07 100644 --- a/openpype/hosts/max/plugins/publish/validate_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/validate_pointcloud.py @@ -35,10 +35,10 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): """ report = [] - if invalid := self.get_tyflow_object(instance): + if invalid := self.get_tyflow_object(instance): # noqa report.append(f"Non tyFlow object found: {invalid}") - if invalid := self.get_tyflow_operator(instance): + if invalid := self.get_tyflow_operator(instance): # noqa report.append( f"tyFlow ExportParticle operator not found: {invalid}") @@ -49,7 +49,7 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): report.append(("tyFlow Partition setting is " "not at the default value")) - if invalid := self.validate_custom_attribute(instance): + if invalid := self.validate_custom_attribute(instance): # noqa report.append(("Custom Attribute not found " f":{invalid}")) From 925ea3fe9312a3e78df1604e370a019cb2adbe5c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:14:33 +0300 Subject: [PATCH 447/918] Update module_kitsu docs --- website/docs/module_kitsu.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 4b827b3802..970bfb275e 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,19 +16,22 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. -The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. +- `-prj, --project` This flag accepts multiple project name to sync specific projects, and the default to sync all projects. +- `-lo, --listen-only` This flag to run listen to Kitsu events only without any sync. + +Note: You must use one argument of `-pro` or `-lo`, because the listen only flag override syncing flag. ```bash -// sync specific projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 - // sync all projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * +openpype_console module kitsu sync-service -l me@domain.ext -p my_password + +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 // start listen only -openpype_console module kitsu sync-service -l me@domain.ext -p my_password +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` ### Events listening From c5dd4d21eed2b7d9b67ecdb980a291742c050e7c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:16:23 +0300 Subject: [PATCH 448/918] Add listen-only flag to sync --- openpype/modules/kitsu/kitsu_module.py | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index bd8ade62d8..319b5de16b 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,13 +124,6 @@ def push_to_zou(login, password): @cli_main.command() -@click.option( - "-prj", - "--project", - multiple=True, - default=[""], - help="Sync specific kitsu projects" -) @click.option( "-l", "--login", @@ -143,16 +136,32 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, project): +@click.option( + "-prj", + "--project", + multiple=True, + default=[], + help="Sync specific kitsu projects" +) +@click.option( + "-lo", + "--listen_only/--listen-only", + default=False, + help="Listen to events only without any syncing" +) +def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password project (str): specific kitsu projects + listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password, filter_projects=project) + if not listen_only: + sync_all_projects(login, password, filter_projects=project) + start_listeners(login, password) From 44e533ff3306aa218a25960a1a41062e3fa82b5a Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:20:43 +0300 Subject: [PATCH 449/918] Update sync_all_projects function with filtered projects --- .../modules/kitsu/utils/update_op_with_zou.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 40e4191508..bfb4bd58fa 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -367,7 +367,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - filter_projects (list): List of filter project names to sync with + filter_projects (tuple): Tuple of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,15 +385,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] - if not filter_projects: - # listen only - return - - if '*' in filter_projects: - # all projects - project_to_sync = all_projects - - else: + if filter_projects: all_kitsu_projects = {p['name']: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: @@ -401,6 +393,9 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, else: log.info(f'`{proj_name}` project does not exist in Kitsu.' f' Please make sure the project is spelled correctly.') + else: + # all project + project_to_sync = all_projects for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From 64eea1dc4a58444afaa8a58e32e2b50d834144c7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 3 May 2023 21:33:01 +0200 Subject: [PATCH 450/918] Updated raise error --- .../hosts/fusion/plugins/publish/increment_current_file.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py index de6f697073..08a65bf52d 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file.py @@ -1,6 +1,7 @@ import pyblish.api from openpype.pipeline import OptionalPyblishPluginMixin +from openpype.pipeline import KnownPublishError class FusionIncrementCurrentFile( @@ -29,7 +30,7 @@ class FusionIncrementCurrentFile( plugin.__name__ == "FusionSubmitDeadline" for plugin in errored_plugins ): - raise RuntimeError( + raise KnownPublishError( "Skipping incrementing current file because " "submission to render farm failed." ) From eb6f8c1abb68dc42d03635907e163b4a5da8833b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 3 May 2023 22:29:36 +0200 Subject: [PATCH 451/918] fixing typo --- .../schemas/projects_schema/schema_project_aftereffects.json | 2 +- .../schemas/projects_schema/schema_project_blender.json | 2 +- .../entities/schemas/projects_schema/schema_project_fusion.json | 2 +- .../schemas/projects_schema/schema_project_harmony.json | 2 +- .../entities/schemas/projects_schema/schema_project_hiero.json | 2 +- .../schemas/projects_schema/schema_project_houdini.json | 2 +- .../entities/schemas/projects_schema/schema_project_max.json | 2 +- .../entities/schemas/projects_schema/schema_project_maya.json | 2 +- .../entities/schemas/projects_schema/schema_project_unreal.json | 2 +- .../schemas/projects_schema/schemas/schema_nuke_imageio.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 5da632a933..ef09a71bda 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index b15b508661..c3eab6c3f0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 1e26e7d701..6189da0e19 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 0357a79aea..a56f62c6d6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index 8c6be5d6d8..2c82e1a9ac 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index d50ebd948f..588e209718 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 10a12dbecc..5dac8ee7e9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 49bd1002aa..55f231e235 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -54,7 +54,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 2d0870f76a..8c3ff71489 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index 7aeb3d32db..f691518255 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -7,7 +7,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) confg path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." }, { "type": "boolean", From 1ec0b2aeac3ab160b96d02706177ccab016c29ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 4 May 2023 11:29:28 +0200 Subject: [PATCH 452/918] Update openpype/hosts/max/plugins/publish/extract_pointcloud.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 5a85915967..1387d47563 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -132,7 +132,10 @@ class ExtractPointCloud(publish.Extractor): """ opt_list = [] for member in members: - obj = member.baseobject + node = rt.getNodeByName(member) + selection_list = list(node.Children) + for sel in selection_list: + obj = sel.baseobject # TODO: to see if it can be used maxscript instead anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: From 5ced9885b1a79ccb8ad068366b186681304dc781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 4 May 2023 11:29:36 +0200 Subject: [PATCH 453/918] Update openpype/hosts/max/plugins/publish/extract_pointcloud.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 1387d47563..11fcdaaa38 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -137,7 +137,7 @@ class ExtractPointCloud(publish.Extractor): for sel in selection_list: obj = sel.baseobject # TODO: to see if it can be used maxscript instead - anim_names = rt.GetSubAnimNames(obj) + anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") From 4193b3c277992d6686a3f4fc009465b684a8a2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 4 May 2023 11:29:51 +0200 Subject: [PATCH 454/918] Update openpype/hosts/max/plugins/publish/extract_pointcloud.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 11fcdaaa38..2063514455 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -138,7 +138,7 @@ class ExtractPointCloud(publish.Extractor): obj = sel.baseobject # TODO: to see if it can be used maxscript instead anim_names = rt.GetSubAnimNames(obj) - for anim_name in anim_names: + for anim_name in anim_names: sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: From 0065dc1056b8e5f548a18297b9f6858c06736d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 4 May 2023 11:29:58 +0200 Subject: [PATCH 455/918] Update openpype/hosts/max/plugins/publish/extract_pointcloud.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 2063514455..97f0b32eca 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -139,8 +139,8 @@ class ExtractPointCloud(publish.Extractor): # TODO: to see if it can be used maxscript instead anim_names = rt.GetSubAnimNames(obj) for anim_name in anim_names: - sub_anim = rt.GetSubAnim(obj, anim_name) - boolean = rt.IsProperty(sub_anim, "Export_Particles") + sub_anim = rt.GetSubAnim(obj, anim_name) + boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: event_name = sub_anim.Name opt = f"${member.Name}.{event_name}.export_particles" From 6923f692057511ef7de985aecce37156e72de50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 4 May 2023 11:30:08 +0200 Subject: [PATCH 456/918] Update openpype/hosts/max/plugins/publish/extract_pointcloud.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 97f0b32eca..c04eef6604 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -141,7 +141,7 @@ class ExtractPointCloud(publish.Extractor): for anim_name in anim_names: sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") - if boolean: + if boolean: event_name = sub_anim.Name opt = f"${member.Name}.{event_name}.export_particles" opt_list.append(opt) From 774672a0063e2ba3edd2f2e26492a6607169ce46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 4 May 2023 11:30:17 +0200 Subject: [PATCH 457/918] Update openpype/hosts/max/plugins/publish/extract_pointcloud.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index c04eef6604..74265da7fb 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -43,7 +43,6 @@ class ExtractPointCloud(publish.Extractor): self.settings = self.get_setting(instance) start = int(instance.context.data.get("frameStart")) end = int(instance.context.data.get("frameEnd")) - container = instance.data["instance_node"] self.log.info("Extracting PRT...") stagingdir = self.staging_dir(instance) From 8d78288ccd3f3783c228eed131936d845ee0e333 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 11:49:38 +0200 Subject: [PATCH 458/918] :heavy_plus_sign: bump hound flake8 version --- .hound.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.hound.yml b/.hound.yml index df9cdab64a..4e749ac2d8 100644 --- a/.hound.yml +++ b/.hound.yml @@ -1,3 +1,4 @@ flake8: + version: 6.0.0 enabled: true config_file: setup.cfg From fec92a6c34f9e25b01a8d0094b9f90f0080f8e9c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 11:49:38 +0200 Subject: [PATCH 459/918] Revert ":heavy_plus_sign: bump hound flake8 version" This reverts commit 8d78288ccd3f3783c228eed131936d845ee0e333. --- .hound.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.hound.yml b/.hound.yml index 4e749ac2d8..df9cdab64a 100644 --- a/.hound.yml +++ b/.hound.yml @@ -1,4 +1,3 @@ flake8: - version: 6.0.0 enabled: true config_file: setup.cfg From e7aa413038f186b4f523318762d438f33c2004a8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 4 May 2023 12:16:58 +0200 Subject: [PATCH 460/918] AfterEffects: add review flag to each instance (#4884) * OP-5657 - add artist control for review in AfterEffects Artist can disable review to be created for particular publish. * OP-5657 - add artist control for review in AfterEffects Removed configuration for Deadline, should be controlled by what is on instance. * OP-5657 - handle legacy instances Legacy instances wont't have mark_for_review in creator_attributes. Set to true as by default we always want review. * OP-5657 - remove explicit review for all AE Now handled directly on instance * OP-5657 - fix - cannot remove now Without this 'review' wont be added to tags on representation. Eventually this should be refactored. Control on whole instance, eg. disabling review, should be enough. * OP-5657 - fix - correct host name used * OP-5657 - fix - correct handling of review On local renders review should be added only from families, not from older approach through Settings. Farm instance cannot have review in families or extract_review would get triggered even locally. * OP-5657 - refactor - changed label * OP-5657 - Hound * OP-5657 - added explicitly skipping review Instance might have set 'review' to False, which should explicitly skip review (might come from Publisher where artist can disable/enable review on an instance). * OP-5657 - updated setting of review variable instance.data.review == False >> explicitly set to do not create review. Keep None to let logic decide. * OP-5657 - fix adding review flag * OP-5657 - updated test Removed review for second instance. * OP-5657 - refactor to context plugin * OP-5657 - tie thumbnail to review for local render Produce thumbnail only when review should be created to synchronize state with farm rendering. Move creation of thumnbail out of this plugin to general plugin to limit duplication of logic. --- .../plugins/create/create_render.py | 45 +++++++++++++------ .../plugins/publish/collect_render.py | 18 +++----- .../plugins/publish/collect_review.py | 25 +++++++++++ .../plugins/publish/extract_local_render.py | 28 +----------- .../plugins/publish/submit_publish_job.py | 23 +++++++--- .../publish/abstract_collect_render.py | 2 +- .../project_settings/aftereffects.json | 6 ++- .../schema_project_aftereffects.json | 23 +++++++++- ...ublish_in_aftereffects_multicomposition.py | 9 ++-- 9 files changed, 116 insertions(+), 63 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/collect_review.py diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index c20b0ec51b..171d7053ce 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -26,12 +26,9 @@ class RenderCreator(Creator): create_allow_context_change = True - def __init__(self, project_settings, *args, **kwargs): - super(RenderCreator, self).__init__(project_settings, *args, **kwargs) - self._default_variants = (project_settings["aftereffects"] - ["create"] - ["RenderCreator"] - ["defaults"]) + # Settings + default_variants = [] + mark_for_review = True def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up @@ -82,28 +79,40 @@ class RenderCreator(Creator): use_farm = pre_create_data["farm"] new_instance.creator_attributes["farm"] = use_farm + review = pre_create_data["mark_for_review"] + new_instance.creator_attributes["mark_for_review"] = review + api.get_stub().imprint(new_instance.id, new_instance.data_to_store()) self._add_instance_to_context(new_instance) stub.rename_item(comp.id, subset_name) - def get_default_variants(self): - return self._default_variants - - 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"), BoolDef("use_composition_name", label="Use composition name in subset"), UISeparatorDef(), - BoolDef("farm", label="Render on farm") + BoolDef("farm", label="Render on farm"), + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review + ) ] return output + def get_instance_attr_defs(self): + return [ + BoolDef("farm", label="Render on farm"), + BoolDef( + "mark_for_review", + label="Review", + default=False + ) + ] + def get_icon(self): return resources.get_openpype_splash_filepath() @@ -143,6 +152,13 @@ class RenderCreator(Creator): api.get_stub().rename_item(comp_id, new_comp_name) + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["aftereffects"]["create"]["RenderCreator"] + ) + + self.mark_for_review = plugin_settings["mark_for_review"] + def get_detail_description(self): return """Creator for Render instances @@ -201,4 +217,7 @@ class RenderCreator(Creator): instance_data["creator_attributes"] = {"farm": is_old_farm} instance_data["family"] = self.family + if instance_data["creator_attributes"].get("mark_for_review") is None: + instance_data["creator_attributes"]["mark_for_review"] = True + return instance_data diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 6153a426cf..b01b707246 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -88,10 +88,11 @@ class CollectAERender(publish.AbstractCollectRender): raise ValueError("No file extension set in Render Queue") render_item = render_q[0] + instance_families = inst.data.get("families", []) subset_name = inst.data["subset"] instance = AERenderInstance( family="render", - families=inst.data.get("families", []), + families=instance_families, version=version, time="", source=current_file, @@ -109,6 +110,7 @@ class CollectAERender(publish.AbstractCollectRender): tileRendering=False, tilesX=0, tilesY=0, + review="review" in instance_families, frameStart=frame_start, frameEnd=frame_end, frameStep=1, @@ -139,6 +141,9 @@ class CollectAERender(publish.AbstractCollectRender): instance.toBeRenderedOn = "deadline" instance.renderer = "aerender" instance.farm = True # to skip integrate + if "review" in instance.families: + # to skip ExtractReview locally + instance.families.remove("review") instances.append(instance) instances_to_remove.append(inst) @@ -218,15 +223,4 @@ class CollectAERender(publish.AbstractCollectRender): 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_review.py b/openpype/hosts/aftereffects/plugins/publish/collect_review.py new file mode 100644 index 0000000000..a933b9fed2 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/collect_review.py @@ -0,0 +1,25 @@ +""" +Requires: + None + +Provides: + instance -> family ("review") +""" +import pyblish.api + + +class CollectReview(pyblish.api.ContextPlugin): + """Add review to families if instance created with 'mark_for_review' flag + """ + label = "Collect Review" + hosts = ["aftereffects"] + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, context): + for instance in context: + creator_attributes = instance.data.get("creator_attributes") or {} + if ( + creator_attributes.get("mark_for_review") + and "review" not in instance.data["families"] + ): + instance.data["families"].append("review") diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index d535329eb4..c70aa41dbe 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -66,33 +66,9 @@ class ExtractLocalRender(publish.Extractor): first_repre = not representations if instance.data["review"] and first_repre: repre_data["tags"] = ["review"] + thumbnail_path = os.path.join(staging_dir, files[0]) + instance.data["thumbnailSource"] = thumbnail_path representations.append(repre_data) instance.data["representations"] = representations - - ffmpeg_path = get_ffmpeg_tool_path("ffmpeg") - # Generate thumbnail. - thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") - - args = [ - ffmpeg_path, "-y", - "-i", first_file_path, - "-vf", "scale=300:-1", - "-vframes", "1", - thumbnail_path - ] - self.log.debug("Thumbnail args:: {}".format(args)) - try: - output = run_subprocess(args) - except TypeError: - self.log.warning("Error in creating thumbnail") - six.reraise(*sys.exc_info()) - - instance.data["representations"].append({ - "name": "thumbnail", - "ext": "jpg", - "files": os.path.basename(thumbnail_path), - "stagingDir": staging_dir, - "tags": ["thumbnail"] - }) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index f80bd40133..eeb813cb62 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -438,7 +438,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "Finished copying %i files" % len(resource_files)) def _create_instances_for_aov( - self, instance_data, exp_files, additional_data + self, instance_data, exp_files, additional_data, do_not_add_review ): """Create instance for each AOV found. @@ -449,6 +449,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instance_data (pyblish.plugin.Instance): skeleton data for instance (those needed) later by collector exp_files (list): list of expected files divided by aovs + additional_data (dict): + do_not_add_review (bool): explicitly skip review Returns: list of instances @@ -514,8 +516,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): app = os.environ.get("AVALON_APP", "") - preview = False - if isinstance(col, list): render_file_name = os.path.basename(col[0]) else: @@ -532,6 +532,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): new_instance = deepcopy(instance_data) new_instance["subset"] = subset_name new_instance["subsetGroup"] = group_name + + preview = preview and not do_not_add_review if preview: new_instance["review"] = True @@ -591,7 +593,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.debug("instances:{}".format(instances)) return instances - def _get_representations(self, instance, exp_files): + def _get_representations(self, instance, exp_files, do_not_add_review): """Create representations for file sequences. This will return representations of expected files if they are not @@ -602,6 +604,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instance (dict): instance data for which we are setting representations exp_files (list): list of expected files + do_not_add_review (bool): explicitly skip review Returns: list of representations @@ -651,6 +654,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if instance.get("slate"): frame_start -= 1 + preview = preview and not do_not_add_review rep = { "name": ext, "ext": ext, @@ -705,6 +709,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = match_aov_pattern( host_name, self.aov_filter, remainder ) + preview = preview and not do_not_add_review if preview: rep.update({ "fps": instance.get("fps"), @@ -820,8 +825,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): families = [family] # pass review to families if marked as review + do_not_add_review = False if data.get("review"): families.append("review") + elif data.get("review") == False: + self.log.debug("Instance has review explicitly disabled.") + do_not_add_review = True instance_skeleton_data = { "family": family, @@ -977,7 +986,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): instances = self._create_instances_for_aov( instance_skeleton_data, data.get("expectedFiles"), - additional_data + additional_data, + do_not_add_review ) self.log.info("got {} instance{}".format( len(instances), @@ -986,7 +996,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: representations = self._get_representations( instance_skeleton_data, - data.get("expectedFiles") + data.get("expectedFiles"), + do_not_add_review ) if "representations" not in instance_skeleton_data.keys(): diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index ccb2415346..fd35ddb719 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -58,7 +58,7 @@ class RenderInstance(object): # With default values # metadata renderer = attr.ib(default="") # renderer - can be used in Deadline - review = attr.ib(default=False) # generate review from instance (bool) + review = attr.ib(default=None) # False - explicitly skip review priority = attr.ib(default=50) # job priority on farm family = attr.ib(default="renderlayer") diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 669e1db0b8..6128534344 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -13,10 +13,14 @@ "RenderCreator": { "defaults": [ "Main" - ] + ], + "mark_for_review": true } }, "publish": { + "CollectReview": { + "enabled": true + }, "ValidateSceneSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 8dc83f5506..313e0ce8ea 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -40,7 +40,13 @@ "label": "Default Variants", "object_type": "text", "docstring": "Fill default variant(s) (like 'Main' or 'Default') used in subset name creation." - } + }, + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review", + "default": true + } ] } ] @@ -51,6 +57,21 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectReview", + "label": "Collect Review", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled", + "default": true + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py b/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py index d372efcb9a..0e9cd3b00d 100644 --- a/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py +++ b/tests/integration/hosts/aftereffects/test_deadline_publish_in_aftereffects_multicomposition.py @@ -9,6 +9,9 @@ log = logging.getLogger("test_publish_in_aftereffects") class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestClass): # noqa """est case for DL publishing in AfterEffects with multiple compositions. + Workfile contains 2 prepared `render` instances. First has review set, + second doesn't. + Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. @@ -68,7 +71,7 @@ class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestCla name="renderTest_taskMain2")) failures.append( - DBAssert.count_of_types(dbcon, "representation", 7)) + DBAssert.count_of_types(dbcon, "representation", 5)) additional_args = {"context.subset": "workfileTest_task", "context.ext": "aep"} @@ -105,13 +108,13 @@ class TestDeadlinePublishInAfterEffectsMultiComposition(AEDeadlinePublishTestCla additional_args = {"context.subset": "renderTest_taskMain2", "name": "thumbnail"} failures.append( - DBAssert.count_of_types(dbcon, "representation", 1, + DBAssert.count_of_types(dbcon, "representation", 0, additional_args=additional_args)) additional_args = {"context.subset": "renderTest_taskMain2", "name": "png_exr"} failures.append( - DBAssert.count_of_types(dbcon, "representation", 1, + DBAssert.count_of_types(dbcon, "representation", 0, additional_args=additional_args)) assert not any(failures) From 49f9822b3fb0bda3dcbf8a7352b2133b361a6f41 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 12:19:49 +0200 Subject: [PATCH 461/918] :recycle: try another linter --- .github/workflows/linting_pr.yml | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/linting_pr.yml diff --git a/.github/workflows/linting_pr.yml b/.github/workflows/linting_pr.yml new file mode 100644 index 0000000000..8cc822df6e --- /dev/null +++ b/.github/workflows/linting_pr.yml @@ -0,0 +1,36 @@ +name: 📇 Linting PR + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - name: Black + uses: microsoft/action-python@0.6.3 + with: + black: false + + - name: Bandit + uses: microsoft/action-python@0.6.3 + with: + bandit: true + + - name: Pylint + uses: microsoft/action-python@0.6.3 + with: + pylint: true + + - name: Pyright + uses: microsoft/action-python@0.6.3 + with: + pyright: true + + - name: Flake8 + uses: microsoft/action-python@0.6.3 + with: + flake8: true From c1349c6e94296af7e141cfe8f4e26e0df708b279 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 12:19:49 +0200 Subject: [PATCH 462/918] Revert ":recycle: try another linter" This reverts commit 49f9822b3fb0bda3dcbf8a7352b2133b361a6f41. --- .github/workflows/linting_pr.yml | 36 -------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/linting_pr.yml diff --git a/.github/workflows/linting_pr.yml b/.github/workflows/linting_pr.yml deleted file mode 100644 index 8cc822df6e..0000000000 --- a/.github/workflows/linting_pr.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: 📇 Linting PR - -on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] - -jobs: - linting: - runs-on: ubuntu-latest - steps: - - name: Black - uses: microsoft/action-python@0.6.3 - with: - black: false - - - name: Bandit - uses: microsoft/action-python@0.6.3 - with: - bandit: true - - - name: Pylint - uses: microsoft/action-python@0.6.3 - with: - pylint: true - - - name: Pyright - uses: microsoft/action-python@0.6.3 - with: - pyright: true - - - name: Flake8 - uses: microsoft/action-python@0.6.3 - with: - flake8: true From 27216b247bda55b9089896fe7494a1e54439201a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 12:43:48 +0200 Subject: [PATCH 463/918] :heavy_plus_sign: Test another linter --- .github/workflows/pr_linting.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/pr_linting.yml diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml new file mode 100644 index 0000000000..418e5d9379 --- /dev/null +++ b/.github/workflows/pr_linting.yml @@ -0,0 +1,13 @@ +name: 📇 Code Linting + +on: + push: + branches: [ develop ] + +jobs: + - linting: + - name: wemake-python-styleguide + uses: wemake-services/wemake-python-styleguide@master + with: + reporter: 'github-pr-review' + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" From 0f7d552eb3a614ff799ace2aeb49427c7f84da27 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 12:45:00 +0200 Subject: [PATCH 464/918] :recycle: linter events --- .github/workflows/pr_linting.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 418e5d9379..3f53182be0 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -3,6 +3,8 @@ name: 📇 Code Linting on: push: branches: [ develop ] + pull_request: + branches: [ develop ] jobs: - linting: From 98a358b60ac47630b70f6c04e0a62c67fff92929 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 12:49:26 +0200 Subject: [PATCH 465/918] :art: configure linting --- .github/workflows/pr_linting.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 3f53182be0..94574779d1 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -7,9 +7,11 @@ on: branches: [ develop ] jobs: - - linting: - - name: wemake-python-styleguide - uses: wemake-services/wemake-python-styleguide@master + lint: + runs-on: ubuntu-latest + steps: + - name: Code Check + - uses: wemake-services/wemake-python-styleguide@master with: reporter: 'github-pr-review' GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" From 07dd4d0b0ade51aeff2e9f899daede0b0ed85e68 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 12:52:00 +0200 Subject: [PATCH 466/918] :recycle: another hit on GH action configuration --- .github/workflows/pr_linting.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 94574779d1..eb9afffcc2 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -7,11 +7,12 @@ on: branches: [ develop ] jobs: - lint: + linting: runs-on: ubuntu-latest steps: - name: Code Check - - uses: wemake-services/wemake-python-styleguide@master + uses: wemake-services/wemake-python-styleguide@master with: reporter: 'github-pr-review' + env: GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" From 3e495bc88131d4938230ea105a0a8f245e3ccf55 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 13:03:57 +0200 Subject: [PATCH 467/918] :art: set linting permissions and checkout --- .github/workflows/pr_linting.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index eb9afffcc2..1deeef4726 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -6,10 +6,15 @@ on: pull_request: branches: [ develop ] +permissions: + contents: read + pull-requests: write + jobs: linting: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 - name: Code Check uses: wemake-services/wemake-python-styleguide@master with: From 00a2db65a12ebc888d09df2d4d8f6ba81a1ea972 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 14:02:02 +0200 Subject: [PATCH 468/918] :recycle: fix container use in extractor --- .../hosts/max/plugins/publish/extract_pointcloud.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 74265da7fb..ee9e9693e9 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -6,7 +6,6 @@ from openpype.hosts.max.api import ( maintained_selection ) from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io class ExtractPointCloud(publish.Extractor): @@ -62,10 +61,12 @@ class ExtractPointCloud(publish.Extractor): instance.data["representations"] = [] self.log.info("Writing PRT with TyFlow Plugin...") - filenames = self.get_files(container, path, start, end) + filenames = self.get_files( + instance.data["members"][0], path, start, end) self.log.debug(f"filenames: {filenames}") - partition = self.partition_output_name(container) + partition = self.partition_output_name( + instance.data["members"][0]) representation = { 'name': 'prt', @@ -141,9 +142,9 @@ class ExtractPointCloud(publish.Extractor): sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: - event_name = sub_anim.Name - opt = f"${member.Name}.{event_name}.export_particles" - opt_list.append(opt) + event_name = sub_anim.Name + opt = f"${member.Name}.{event_name}.export_particles" + opt_list.append(opt) return opt_list From cb24f2a9ba8c08b27980fa06e2cb83a2599173a9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 14:07:19 +0200 Subject: [PATCH 469/918] :art: configure linter --- .github/workflows/pr_linting.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/pr_linting.yml diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml new file mode 100644 index 0000000000..1deeef4726 --- /dev/null +++ b/.github/workflows/pr_linting.yml @@ -0,0 +1,23 @@ +name: 📇 Code Linting + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +permissions: + contents: read + pull-requests: write + +jobs: + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Code Check + uses: wemake-services/wemake-python-styleguide@master + with: + reporter: 'github-pr-review' + env: + GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" From e608e7c808c22a09c118b4acad14c66543342b7e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 14:57:28 +0200 Subject: [PATCH 470/918] :art: limit to changed files path --- .github/workflows/pr_linting.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 1deeef4726..1f6be64d31 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -15,9 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - run: | + echo "_CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} -- '*.py' | tr -s '\n' ' ' )" >> ${GITHUB_ENV} - name: Code Check uses: wemake-services/wemake-python-styleguide@master with: reporter: 'github-pr-review' + path: "${{ env._CHANGED_FILES }}" env: GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" From 5829f647e548ab46be77a4caf7d704297419bbf8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 14:59:41 +0200 Subject: [PATCH 471/918] :art: limit --- .github/workflows/pr_linting.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 1f6be64d31..59ac48aeb8 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -6,6 +6,8 @@ on: pull_request: branches: [ develop ] + workflow_dispatch: + permissions: contents: read pull-requests: write @@ -19,6 +21,7 @@ jobs: echo "_CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} -- '*.py' | tr -s '\n' ' ' )" >> ${GITHUB_ENV} - name: Code Check uses: wemake-services/wemake-python-styleguide@master + if: ${{ env._CHANGED_FILES }} with: reporter: 'github-pr-review' path: "${{ env._CHANGED_FILES }}" From 6fb6762f34c0b972e5061b5bfcae1755a6bba895 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 15:14:17 +0200 Subject: [PATCH 472/918] :recycle: limit using diff_context --- .github/workflows/pr_linting.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 59ac48aeb8..64b6bc0da5 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -17,13 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - run: | - echo "_CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }} -- '*.py' | tr -s '\n' ' ' )" >> ${GITHUB_ENV} - name: Code Check uses: wemake-services/wemake-python-styleguide@master - if: ${{ env._CHANGED_FILES }} with: reporter: 'github-pr-review' - path: "${{ env._CHANGED_FILES }}" + filter: 'diff_context' env: GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" From da12ac16c56f4510309275ec764b708e1a62bdb0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 15:32:50 +0200 Subject: [PATCH 473/918] :art: solving changed list --- .github/workflows/pr_linting.yml | 38 +++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 64b6bc0da5..975c83e677 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -8,19 +8,55 @@ on: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number}} + cancel-in-progress: true + permissions: contents: read pull-requests: write jobs: + files_changed: + runs-on: ubuntu-latest + outputs: + changed_python: ${{ steps.changes.outputs.python }} + steps: + - uses: actions/checkout@master + if: github.event_name == 'push' + - uses: dorny/paths-filter@master + id: changes + with: + filters: | + cpp: + - ["lte/gateway/c/**", "orc8r/gateway/c/**", "lte/gateway/python/**"] + javascript: + - ["nms/**", "**/*.js"] + python: + - ["**/*.py"] + terraform: + - ["**/*.tf"] + linting: + needs: files_changed + if: ${{ needs.files_changed.outputs.changed_python == 'true' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 0 + - name: Get changed Python files + id: py-changes + run: | + echo "py_files_list=$(git diff --name-only --diff-filter=ACMRT \ + ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} \ + | grep .py$ | xargs)" >> $GITHUB_OUTPUT - name: Code Check uses: wemake-services/wemake-python-styleguide@master with: reporter: 'github-pr-review' - filter: 'diff_context' + path: ${{ steps.py-changes.outputs.py_files_list }} env: GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" From e1a2792a7a8d774abf1777fa079938d71559e57e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 4 May 2023 13:39:59 +0000 Subject: [PATCH 474/918] [Automated] Release --- CHANGELOG.md | 248 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 250 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16deaaa4fd..07c1e7d5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,254 @@ # Changelog +## [3.15.6](https://github.com/ynput/OpenPype/tree/3.15.6) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.5...3.15.6) + +### **🆕 New features** + + +
+Substance Painter Integration #4283 + +This implements a part of #4205 by implementing a Substance Painter integration + +Status: +- [x] Implement Host +- [x] start substance with last workfile using `AddLastWorkfileToLaunchArgs` prelaunch hook +- [x] Implement Qt tools +- [x] Implement loaders +- [x] Implemented a Set project mesh loader (this is relatively special case because a Project will always have exactly one mesh - a Substance Painter project cannot exist without a mesh). +- [x] Implement project open callback +- [x] On project open it notifies the user if the loaded model is outdated +- [x] Implement publishing logic +- [x] Workfile publishing +- [x] Export Texture Sets +- [x] Support OCIO using #4195 (draft brach is set up - see comment) +- [ ] Likely needs more testing on the OCIO front +- [x] Validate all outputs of the Export template are exported/generated +- [x] Allow validation to be optional **(issue: there's no API method to detect what maps will be exported without doing an actual export to disk)** +- [x] Support extracting/integration if not all outputs are generated +- [x] Support multiple materials/texture sets per instance +- [ ] Add validator that can enforce only a single texture set output if studio prefers that. +- [ ] Implement Export File Format (extensions) override in Creator +- [ ] Add settings so Admin can choose which extensions are available. + + +___ + +
+ + +
+Data Exchange: Geometry in 3dsMax #4555 + +Introduces and updates a creator, extractors and loaders for model family + +Introduces new creator, extractors and loaders for model family while adding model families into the existing max scene loader and extractor +- [x] creators +- [x] adding model family into max scene loader and extractor +- [x] fbx loader +- [x] fbx extractor +- [x] usd loader +- [x] usd extractor +- [x] validator for model family +- [x] obj loader(update function) +- [x] fix the update function of the loader as #4675 +- [x] Add documentation + + +___ + +
+ + +
+AfterEffects: add review flag to each instance #4884 + +Adds `mark_for_review` flag to the Creator to allow artists to disable review if necessary.Exposed this flag in Settings, by default set to True (eg. same behavior as previously). + + +___ + +
+ +### **🚀 Enhancements** + + +
+Houdini: Fix Validate Output Node (VDB) #4819 + +- Removes plug-in that was a duplicate of this plug-in. +- Optimize logging of many prims slightly +- Fix error reporting like https://github.com/ynput/OpenPype/pull/4818 did + + +___ + +
+ + +
+Houdini: Add null node as output indicator when using TAB search #4834 + + +___ + +
+ + +
+Houdini: Don't error in collect review if camera is not set correctly #4874 + +Do not raise an error in collector when invalid path is set as camera path. Allow camera path to not be set correctly in review instance until validation so it's nicely shown in a validation report. + + +___ + +
+ + +
+Project packager: Backup and restore can store only database #4879 + +Pack project functionality have option to zip only project database without project files. Unpack project can skip project copy if the folder is not found.Added helper functions to `openpype.client.mongo` that can be also used for tests as replacement of mongo dump. + + +___ + +
+ + +
+Houdini: ExtractOpenGL for Review instance not optional #4881 + +Don't make ExtractOpenGL optional for review instance optional. + + +___ + +
+ + +
+Publisher: Small style changes #4894 + +Small changes in styles and form of publisher UI. + + +___ + +
+ + +
+Houdini: Workfile icon in new publisher #4898 + +Fix icon for the workfile instance in new publisher + + +___ + +
+ + +
+Fusion: Simplify creator icons code #4899 + +Simplify code for setting the icons for the Fusion creators + + +___ + +
+ + +
+Enhancement: Fix PySide 6.5 support for loader #4900 + +Fixes PySide 6.5 support in Loader. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Validate Attributes #4917 + +This plugin was broken due to bad fetching of data and wrong repair action. + + +___ + +
+ + +
+Fix: Locally copied version of last published workfile is not incremented #4722 + +### Fix 1 +When copied, the local workfile version keeps the published version number, when it must be +1 to follow OP's naming convention. + +### Fix 2 +Local workfile version's name is built from anatomy. This avoids to get workfiles with their publish template naming. + +### Fix 3 +In the case a subset has at least two tasks with published workfiles, for example `Modeling` and `Rigging`, launching `Rigging` was getting the first one with the `next` and trying to find representations, therefore `workfileModeling` and trying to match the current `task_name` (`Rigging`) with the `representation["context"]["task"]["name"]` of a Modeling representation, which was ending up to a `workfile_representation` to `None`, and exiting the process. + +Trying to find the `task_name` in the `subset['name']` fixes it. + +### Fix 4 +Fetch input dependencies of workfile. + +Replacing https://github.com/ynput/OpenPype/pull/4102 for changes to bring this home. +___ + +
+ + +
+Maya: soft-fail when pan/zoom locked on camera when playblasting #4929 + +When pan/zoom enabled attribute on camera is locked, playblasting with pan/zoom fails because it is trying to restore it. This is fixing it by skipping over with warning. + + +___ + +
+ +### **Merged pull requests** + + +
+Maya Load References - Add Display Handle Setting #4904 + +When we load a reference in Maya using OpenPype loader, display handle is checked by default and prevent us to select easily the object in the viewport. I understand that some productions like to keep this option, so I propose to add display handle to the reference loader settings. + + +___ + +
+ + +
+Photoshop: add autocreators for review and flat image #4871 + +Review and flatten image (produced when no instance of `image` family was created) were created somehow magically. This PRintroduces two new auto creators which allow artists to disable review or flatten image.For all `image` instances `Review` flag was added to provide functionality to create separate review per `image` instance. Previously was possible only to have separate instance of `review` family.Review is not enabled on `image` family by default. (Eg. follows original behavior)Review auto creator is enabled by default as it was before.Flatten image creator must be set in Settings in `project_settings/photoshop/create/AutoImageCreator`. + + +___ + +
+ + + + ## [3.15.5](https://github.com/ynput/OpenPype/tree/3.15.5) diff --git a/openpype/version.py b/openpype/version.py index 9832ff4747..dc0a3a8c9f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.6-nightly.3" +__version__ = "3.15.6" diff --git a/pyproject.toml b/pyproject.toml index 2f40d58f56..003f6cf2d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.5" # OpenPype +version = "3.15.6" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 11343088b55eb9962a91919295ecbe865b03c11c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 4 May 2023 13:41:12 +0000 Subject: [PATCH 475/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 8328a35cad..5050d37c7a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.6 - 3.15.6-nightly.3 - 3.15.6-nightly.2 - 3.15.6-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.1 - 3.14.0 - 3.14.0-nightly.1 - - 3.13.1-nightly.3 validations: required: true - type: dropdown From a3a996d5a56787fa88cf0d92a2c39a6bf0f31fbd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 15:42:04 +0200 Subject: [PATCH 476/918] :bug: fix checkout action --- .github/workflows/pr_linting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 975c83e677..4f4f7c2585 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -22,7 +22,7 @@ jobs: outputs: changed_python: ${{ steps.changes.outputs.python }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v3 if: github.event_name == 'push' - uses: dorny/paths-filter@master id: changes From 65110ded4609f2b6d7a5ccf97bff097f91d370de Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 15:43:32 +0200 Subject: [PATCH 477/918] :bug: fix checkout action --- .github/workflows/pr_linting.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 975c83e677..4f4f7c2585 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -22,7 +22,7 @@ jobs: outputs: changed_python: ${{ steps.changes.outputs.python }} steps: - - uses: actions/checkout@master + - uses: actions/checkout@v3 if: github.event_name == 'push' - uses: dorny/paths-filter@master id: changes From 5ef4f4fb131f569da9aa0b5db50c66c0a99f86e8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 15:47:52 +0200 Subject: [PATCH 478/918] :bug: fix syntax --- .github/workflows/pr_linting.yml | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 4f4f7c2585..58f52bb313 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -21,21 +21,15 @@ jobs: runs-on: ubuntu-latest outputs: changed_python: ${{ steps.changes.outputs.python }} - steps: - - uses: actions/checkout@v3 - if: github.event_name == 'push' - - uses: dorny/paths-filter@master - id: changes - with: - filters: | - cpp: - - ["lte/gateway/c/**", "orc8r/gateway/c/**", "lte/gateway/python/**"] - javascript: - - ["nms/**", "**/*.js"] - python: - - ["**/*.py"] - terraform: - - ["**/*.tf"] + steps: + - uses: actions/checkout@v3 + if: github.event_name == 'push' + - uses: dorny/paths-filter@master + id: changes + with: + filters: | + python: + - ["**/*.py"] linting: needs: files_changed From 3497b03b7fc8cffadf597908e7f4bdde60f1ee0e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 15:50:49 +0200 Subject: [PATCH 479/918] :bug: fix syntax --- .github/workflows/pr_linting.yml | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 975c83e677..58f52bb313 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -21,21 +21,15 @@ jobs: runs-on: ubuntu-latest outputs: changed_python: ${{ steps.changes.outputs.python }} - steps: - - uses: actions/checkout@master - if: github.event_name == 'push' - - uses: dorny/paths-filter@master - id: changes - with: - filters: | - cpp: - - ["lte/gateway/c/**", "orc8r/gateway/c/**", "lte/gateway/python/**"] - javascript: - - ["nms/**", "**/*.js"] - python: - - ["**/*.py"] - terraform: - - ["**/*.tf"] + steps: + - uses: actions/checkout@v3 + if: github.event_name == 'push' + - uses: dorny/paths-filter@master + id: changes + with: + filters: | + python: + - ["**/*.py"] linting: needs: files_changed From 227075f1a918d3ddc7aea60a57f955558820171b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 16:07:32 +0200 Subject: [PATCH 480/918] :art: add WPS to pre-commit --- .pre-commit-config.yaml | 9 +++++++++ poetry.lock | 24 +++++++++++++++++++++--- pyproject.toml | 1 + 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eec388924e..fe4c7e3da3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,3 +10,12 @@ repos: - id: check-added-large-files - id: no-commit-to-branch args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-_]+)$).*' ] +- repo: local + hooks: + - id: flake8 + name: flake8 + description: WPS enforced flake8 + entry: flake8 + args: ["--config=setup.cfg"] + language: python + types: [python] diff --git a/poetry.lock b/poetry.lock index f71611cb6f..cbc4ac7b94 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "acre" @@ -1456,11 +1456,13 @@ python-versions = ">=3.6" files = [ {file = "lief-0.12.3-cp310-cp310-macosx_10_14_arm64.whl", hash = "sha256:66724f337e6a36cea1a9380f13b59923f276c49ca837becae2e7be93a2e245d9"}, {file = "lief-0.12.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d18aafa2028587c98f6d4387bec94346e92f2b5a8a5002f70b1cf35b1c045cc"}, + {file = "lief-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f69d125caaa8d5ddb574f29cc83101e165ebea1a9f18ad042eb3544081a797"}, {file = "lief-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c078d6230279ffd3bca717c79664fb8368666f610b577deb24b374607936e9c1"}, {file = "lief-0.12.3-cp310-cp310-win32.whl", hash = "sha256:e3a6af926532d0aac9e7501946134513d63217bacba666e6f7f5a0b7e15ba236"}, {file = "lief-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:0750b72e3aa161e1fb0e2e7f571121ae05d2428aafd742ff05a7656ad2288447"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_arm64.whl", hash = "sha256:b5c123cb99a7879d754c059e299198b34e7e30e3b64cf22e8962013db0099f47"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8bc58fa26a830df6178e36f112cb2bbdd65deff593f066d2d51434ff78386ba5"}, + {file = "lief-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74ac6143ac6ccd813c9b068d9c5f1f9d55c8813c8b407387eb57de01c3db2d74"}, {file = "lief-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04eb6b70d646fb5bd6183575928ee23715550f161f2832cbcd8c6ff2071fb408"}, {file = "lief-0.12.3-cp311-cp311-win32.whl", hash = "sha256:7e2d0a53c403769b04adcf8df92e83c5e25f9103a052aa7f17b0a9cf057735fb"}, {file = "lief-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:7f6395c12ee1bc4a5162f567cba96d0c72dfb660e7902e84d4f3029daf14fe33"}, @@ -1480,6 +1482,7 @@ files = [ {file = "lief-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:b00667257b43e93d94166c959055b6147d46d302598f3ee55c194b40414c89cc"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_arm64.whl", hash = "sha256:e6a1b5b389090d524621c2455795e1262f62dc9381bedd96f0cd72b878c4066d"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ae773196df814202c0c51056163a1478941b299512b09660a3c37be3c7fac81e"}, + {file = "lief-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:66ddf88917ec7b00752687c476bb2771dc8ec19bd7e4c0dcff1f8ef774cad4e9"}, {file = "lief-0.12.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4a47f410032c63ac3be051d963d0337d6b47f0e94bfe8e946ab4b6c428f4d0f8"}, {file = "lief-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbd11367c2259bd1131a6c8755dcde33314324de5ea029227bfbc7d3755871e6"}, {file = "lief-0.12.3-cp39-cp39-win32.whl", hash = "sha256:2ce53e311918c3e5b54c815ef420a747208d2a88200c41cd476f3dd1eb876bcf"}, @@ -2352,7 +2355,7 @@ files = [ cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] @@ -3245,6 +3248,21 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "wemake-python-styleguide" +version = "0.0.1" +description = "Opinionated styleguide that we use in wemake.services projects" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "wemake-python-styleguide-0.0.1.tar.gz", hash = "sha256:e1f47a2be6aa79ca8a1cfbbbffdd67bf4df32b76306f4c3dd2a620a2af78e671"}, + {file = "wemake_python_styleguide-0.0.1-py2.py3-none-any.whl", hash = "sha256:505a19d82f9c4f450c6f06bb8c74d86c99cabcc4d5e6d8ea70e90b13b049f34f"}, +] + +[package.dependencies] +flake8 = "*" + [[package]] name = "wheel" version = "0.38.4" @@ -3462,4 +3480,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "02daca205796a0f29a0d9f50707544e6804f32027eba493cd2aa7f175a00dcea" +content-hash = "c566a959134559e2fccf9b17fc820956207c9d36a58195f48990b986114d7108" diff --git a/pyproject.toml b/pyproject.toml index 2f40d58f56..99e03daea1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ wheel = "*" enlighten = "*" # cool terminal progress bars toml = "^0.10.2" # for parsing pyproject.toml pre-commit = "*" +wemake-python-styleguide = "*" [tool.poetry.urls] "Bug Tracker" = "https://github.com/pypeclub/openpype/issues" From a6f3bbe0f4f653048e11d7ecbb282d7c7583ef53 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 16:49:09 +0200 Subject: [PATCH 481/918] :wrench: update flake8 configuration --- setup.cfg | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 140 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 10cca3eb3f..838c6bd4c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,6 @@ [flake8] -# ignore = D203 -ignore = BLK100, W504, W503 max-line-length = 79 +strictness = short exclude = .git, __pycache__, @@ -10,8 +9,145 @@ exclude = website, openpype/vendor, *deadline/repository/custom/plugins - max-complexity = 30 +ignore = + # line break before binary operator + W503, + # line break occurred after a binary operator + W504, + # wemake-python-styleguide warnings + # See https://wemake-python-stylegui.de/en/latest/pages/usage/violations/index.html for doc + # Found incorrect module name pattern + WPS102, + # Found wrong variable name + WPS110, + # Found too short name + WPS111, + # Found upper-case constant in a class + WPS115, + # Found module with too many imports + WPS201, + # Found too many module members + WPS202, + # Found overused expression + WPS204, + # Found too many local variables + WPS210, + # Found too many arguments + WPS211, + # Found too many return statements + WPS212, + # Found too many expressions + WPS213, + # Found too many methods + WPS214, + # Found too many await expressions + WPS217, + # Found line with high Jones Complexity + WPS221, + # Found too many `elif` branches + WPS223, + # Found string constant over-use + WPS226, + # Found too long try body length + WPS229, + # Found too many public instance attributes + WPS230, + # Found function with too much cognitive complexity + WPS231, + # Found module cognitive complexity that is too high + WPS232, + # Found too many imported names from a module + WPS235, + # Found too many raises in a function + WPS238, + # Found too deep nesting + WPS220, + # Found `f` string + WPS305, + # Found incorrect multi-line parameters + WPS317, + # Found extra indentation + WPS318, + # Found bracket in wrong position + WPS319, + # Found percent string formatting + WPS323, + # Found implicit string concatenation + WPS326, + # Found variables that are only used for `return` + WPS331, + # Found explicit string concatenation + WPS336, + # Found multiline conditions + WPS337, + # Found incorrect order of methods in a class + WPS338, + # Found line starting with a dot + WPS348, + # Found multiline loop + WPS352, + # Found incorrect unpacking target + WPS414, + # Found wrong keyword + WPS420, + # Found wrong function + WPS421, + # Found statement that has no effect + WPS428, + # Found nested function + WPS430, + # Found magic number + WPS432, + # Found protected attribute usage + WPS437, + # Found block variables overlap + WPS440, + # Found an infinite while loop + WPS457, + # Found a getter without a return value + WPS463, + # Found negated condition + WPS504, + # flake8-quotes warnings + # Remove bad quotes + Q000, + # Remove bad quotes from multiline string + Q001, + # Darglint warnings + # Incorrect indentation + DAR003, + # Excess parameter(s) in Docstring + DAR102, + # Excess exception(s) in Raises section + DAR402, + # pydocstyle warnings + # Missing docstring in __init_ + D107, + # White space formatting for doc strings + D2, + # First line should end with a period + D400, + # Others + # function name + N802, + # Found backslash that is used for line breaking + N400, + E501, + S105, + RST + +[isort] +profile=wemake +src_paths=isort,test +# isort configuration: +# https://github.com/timothycrosley/isort/wiki/isort-Settings +include_trailing_comma = true +use_parentheses = true +# See https://github.com/timothycrosley/isort#multi-line-output-modes +multi_line_output = 3 +# Is the same as 80 in flake8: +line_length = 79 [pylint.'MESSAGES CONTROL'] disable = no-member @@ -28,4 +164,4 @@ omit = /tests directory = ./coverage [tool:pytest] -norecursedirs = repos/* openpype/modules/ftrack/* \ No newline at end of file +norecursedirs = repos/* openpype/modules/ftrack/* From b9055d61af83760430cce647f7f2226b23b91bcf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 4 May 2023 17:42:16 +0200 Subject: [PATCH 482/918] POC wip --- .../fusion/plugins/create/create_saver.py | 2 +- .../fusion/plugins/publish/collect_renders.py | 8 + .../plugins/publish/submit_fusion_deadline.py | 140 ++++++++++++++++-- 3 files changed, 133 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index cedc4029fa..a66d9b7e86 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -28,7 +28,7 @@ class CreateSaver(Creator): description = "Fusion Saver to generate image sequence" icon = "fa5.eye" - instance_attributes = ["reviewable"] + instance_attributes = ["reviewable", "farm_rendering"] def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py index 7f38e68447..b1c12c7393 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ b/openpype/hosts/fusion/plugins/publish/collect_renders.py @@ -23,3 +23,11 @@ class CollectFusionRenders(pyblish.api.InstancePlugin): instance.data["families"].append( "{}.{}".format(family, render_target) ) + if render_target == "farm": + if "review" in instance.data["families"]: + instance.data["families"].remove("review") + + # Farm rendering + instance.data["transfer"] = False + instance.data["farm"] = True + self.log.info("Farm rendering ON ...") diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 8570c759bc..2885d91d07 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -1,15 +1,26 @@ import os import json import getpass +from pprint import pformat import requests import pyblish.api from openpype.pipeline import legacy_io +from openpype.pipeline.publish import ( + OpenPypePyblishPluginMixin +) +from openpype.lib import ( + BoolDef, + NumberDef +) -class FusionSubmitDeadline(pyblish.api.InstancePlugin): +class FusionSubmitDeadline( + pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin +): """Submit current Comp to Deadline Renders are submitted to a Deadline Web Service as @@ -17,12 +28,76 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): """ - label = "Submit to Deadline" + label = "Submit Fusion to Deadline" order = pyblish.api.IntegratorOrder hosts = ["fusion"] - families = ["render.farm"] + families = ["render"] + targets = ["local"] + + # presets + priority = 50 + chunk_size = 1 + concurrent_tasks = 1 + group = "" + department = "" + limit_groups = {} + use_gpu = False + env_allowed_keys = [] + env_search_replace_values = {} + + @classmethod + def get_attribute_defs(cls): + return [ + NumberDef( + "priority", + label="Priority", + default=cls.priority, + decimals=0 + ), + NumberDef( + "chunk", + label="Frames Per Task", + default=cls.chunk_size, + decimals=0, + minimum=1, + maximum=1000 + ), + NumberDef( + "concurrency", + label="Concurrency", + default=cls.concurrent_tasks, + decimals=0, + minimum=1, + maximum=10 + ), + BoolDef( + "use_gpu", + default=cls.use_gpu, + label="Use GPU" + ), + BoolDef( + "suspend_publish", + default=False, + label="Suspend publish" + ) + ] def process(self, instance): + if not instance.data.get("farm"): + self.log.info("Skipping local instance.") + return + + attribute_values = self.get_attr_values_from_data( + instance.data) + + self.log.debug(pformat(attribute_values)) + + # add suspend_publish attributeValue to instance data + instance.data["suspend_publish"] = attribute_values[ + "suspend_publish"] + + instance.data["toBeRenderedOn"] = "deadline" + context = instance.context key = "__hasRun{}".format(self.__class__.__name__) @@ -33,24 +108,24 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): from openpype.hosts.fusion.api.lib import get_frame_path - deadline_url = ( - context.data["system_settings"] - ["modules"] - ["deadline"] - ["DEADLINE_REST_URL"] - ) - assert deadline_url, "Requires DEADLINE_REST_URL" + # get default deadline webservice url from deadline module + deadline_url = instance.context.data["defaultDeadline"] + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + deadline_url = instance.data.get("deadlineUrl") + assert deadline_url, "Requires Deadline Webservice URL" # Collect all saver instances in context that are to be rendered saver_instances = [] - for instance in context[:]: - if not self.families[0] in instance.data.get("families"): + for instance in context: + if instance.data["family"] != "render": # Allow only saver family instances continue if not instance.data.get("publish", True): # Skip inactive instances continue + self.log.debug(instance.data["name"]) saver_instances.append(instance) @@ -58,11 +133,31 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): raise RuntimeError("No instances found for Deadline submittion") fusion_version = int(context.data["fusionVersion"]) - filepath = context.data["currentFile"] - filename = os.path.basename(filepath) comment = context.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) + script_path = context.data["currentFile"] + + for item in context: + if "workfile" in item.data["families"]: + msg = "Workfile (scene) must be published along" + assert item.data["publish"] is True, msg + + template_data = item.data.get("anatomyData") + rep = item.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = context.data["anatomy"].format(template_data) + template_filled = anatomy_filled["publish"]["path"] + script_path = os.path.normpath(template_filled) + + self.log.info( + "Using published scene for render {}".format(script_path) + ) + + filename = os.path.basename(script_path) + # Documentation for keys available at: # https://docs.thinkboxsoftware.com # /products/deadline/8.0/1_User%20Manual/manual @@ -73,11 +168,20 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): "BatchName": filename, # Asset dependency to wait for at least the scene file to sync. - "AssetDependency0": filepath, + "AssetDependency0": script_path, # Job name, as seen in Monitor "Name": filename, + "Priority": attribute_values.get( + "priority", self.priority), + "ChunkSize": attribute_values.get( + "chunk", self.chunk_size), + "ConcurrentTasks": attribute_values.get( + "concurrency", + self.concurrent_tasks + ), + # User, as seen in Monitor "UserName": deadline_user, @@ -94,7 +198,7 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): }, "PluginInfo": { # Input - "FlowFile": filepath, + "FlowFile": script_path, # Mandatory for Deadline "Version": str(fusion_version), @@ -109,6 +213,10 @@ class FusionSubmitDeadline(pyblish.api.InstancePlugin): # Proxy: higher numbers smaller images for faster test renders # 1 = no proxy quality "Proxy": 1, + + # using GPU by default + "UseGpu": attribute_values.get( + "use_gpu", self.use_gpu) }, # Mandatory for Deadline, may be empty From e1384a40b1b94a0f456d6cd4e01a63dd1e8a23e5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 4 May 2023 17:46:31 +0200 Subject: [PATCH 483/918] :heavy_plus_sign: add isort and fix python black check --- poetry.lock | 2 +- pyproject.toml | 1 + setup.cfg | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index cbc4ac7b94..563f905fad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3480,4 +3480,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "c566a959134559e2fccf9b17fc820956207c9d36a58195f48990b986114d7108" +content-hash = "45e91b47f9e6697b0eb9fdbe76981f691d389ce74bc5a0e98d72e1109b39bc63" diff --git a/pyproject.toml b/pyproject.toml index 4bb80c060b..0d236aedc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ enlighten = "*" # cool terminal progress bars toml = "^0.10.2" # for parsing pyproject.toml pre-commit = "*" wemake-python-styleguide = "*" +isort="*" [tool.poetry.urls] "Bug Tracker" = "https://github.com/pypeclub/openpype/issues" diff --git a/setup.cfg b/setup.cfg index 838c6bd4c5..7863a74894 100644 --- a/setup.cfg +++ b/setup.cfg @@ -135,7 +135,9 @@ ignore = N400, E501, S105, - RST + RST, + # Black would make changes error + BLK100, [isort] profile=wemake From ecc1ba12f0187c9d9e46e9a59e1749e90e7a6ed1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 5 May 2023 13:22:23 +0800 Subject: [PATCH 484/918] bug fix the validator error --- .../hosts/max/plugins/create/create_camera.py | 8 +++-- .../max/plugins/create/create_maxScene.py | 8 +++-- .../max/plugins/create/create_pointcache.py | 11 ++++-- .../max/plugins/create/create_pointcloud.py | 8 +++-- .../hosts/max/plugins/create/create_render.py | 8 +++-- .../max/plugins/publish/extract_pointcloud.py | 34 +++++++++---------- .../plugins/publish/validate_pointcloud.py | 2 +- 7 files changed, 47 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_camera.py b/openpype/hosts/max/plugins/create/create_camera.py index 91d0d4d3dc..ab5578a201 100644 --- a/openpype/hosts/max/plugins/create/create_camera.py +++ b/openpype/hosts/max/plugins/create/create_camera.py @@ -12,7 +12,6 @@ class CreateCamera(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) instance = super(CreateCamera, self).create( subset_name, instance_data, @@ -20,7 +19,10 @@ class CreateCamera(plugin.MaxCreator): container = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - for obj in sel_obj: - obj.parent = container + sel_obj = None + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_maxScene.py b/openpype/hosts/max/plugins/create/create_maxScene.py index 7900336f32..2d6ea8c27b 100644 --- a/openpype/hosts/max/plugins/create/create_maxScene.py +++ b/openpype/hosts/max/plugins/create/create_maxScene.py @@ -12,7 +12,6 @@ class CreateMaxScene(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) instance = super(CreateMaxScene, self).create( subset_name, instance_data, @@ -20,7 +19,10 @@ class CreateMaxScene(plugin.MaxCreator): container = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - for obj in sel_obj: - obj.parent = container + sel_obj = None + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py index 32f0838471..42aa743794 100644 --- a/openpype/hosts/max/plugins/create/create_pointcache.py +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -13,10 +13,17 @@ class CreatePointCache(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): # from pymxs import runtime as rt - _ = super(CreatePointCache, self).create( + instance = super(CreatePointCache, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance - + container = rt.getNodeByName(instance.data.get("instance_node")) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + sel_obj = None + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_pointcloud.py b/openpype/hosts/max/plugins/create/create_pointcloud.py index c83acac3df..e46bf30d1c 100644 --- a/openpype/hosts/max/plugins/create/create_pointcloud.py +++ b/openpype/hosts/max/plugins/create/create_pointcloud.py @@ -12,7 +12,6 @@ class CreatePointCloud(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) instance = super(CreatePointCloud, self).create( subset_name, instance_data, @@ -20,7 +19,10 @@ class CreatePointCloud(plugin.MaxCreator): container = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - for obj in sel_obj: - obj.parent = container + sel_obj = None + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 269fff2e32..43d69fa320 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -13,7 +13,6 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) instance = super(CreateRender, self).create( subset_name, instance_data, @@ -22,8 +21,11 @@ class CreateRender(plugin.MaxCreator): container = rt.getNodeByName(container_name) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - for obj in sel_obj: - obj.parent = container + sel_obj = None + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index ee9e9693e9..f85255bbf5 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -53,7 +53,9 @@ class ExtractPointCloud(publish.Extractor): start, end, path) + for job in job_args: + self.log.debug("job:{}".format(job)) rt.Execute(job) self.log.info("Performing Extraction ...") @@ -62,11 +64,11 @@ class ExtractPointCloud(publish.Extractor): self.log.info("Writing PRT with TyFlow Plugin...") filenames = self.get_files( - instance.data["members"][0], path, start, end) + instance.data["members"], path, start, end) self.log.debug(f"filenames: {filenames}") partition = self.partition_output_name( - instance.data["members"][0]) + instance.data["members"]) representation = { 'name': 'prt', @@ -105,7 +107,7 @@ class ExtractPointCloud(publish.Extractor): end_frame = f"{operator}.frameEnd={end}" job_args.append(end_frame) filepath = filepath.replace("\\", "/") - prt_filename = f"{operator}.PRTFilename={filepath}" + prt_filename = f"{operator}.PRTFilename='{filepath}'" job_args.append(prt_filename) # Partition @@ -113,7 +115,8 @@ class ExtractPointCloud(publish.Extractor): job_args.append(mode) additional_args = self.get_custom_attr(operator) - job_args.extend(iter(additional_args)) + for args in additional_args: + job_args.append(args) prt_export = f"{operator}.exportPRT()" job_args.append(prt_export) @@ -132,19 +135,16 @@ class ExtractPointCloud(publish.Extractor): """ opt_list = [] for member in members: - node = rt.getNodeByName(member) - selection_list = list(node.Children) - for sel in selection_list: - obj = sel.baseobject - # TODO: to see if it can be used maxscript instead - anim_names = rt.GetSubAnimNames(obj) - for anim_name in anim_names: - sub_anim = rt.GetSubAnim(obj, anim_name) - boolean = rt.IsProperty(sub_anim, "Export_Particles") - if boolean: - event_name = sub_anim.Name - opt = f"${member.Name}.{event_name}.export_particles" - opt_list.append(opt) + obj = member.baseobject + # TODO: to see if it can be used maxscript instead + anim_names = rt.GetSubAnimNames(obj) + for anim_name in anim_names: + sub_anim = rt.GetSubAnim(obj, anim_name) + boolean = rt.IsProperty(sub_anim, "Export_Particles") + if boolean: + event_name = sub_anim.Name + opt = f"${member.Name}.{event_name}.export_particles" + opt_list.append(opt) return opt_list diff --git a/openpype/hosts/max/plugins/publish/validate_pointcloud.py b/openpype/hosts/max/plugins/publish/validate_pointcloud.py index e3a6face07..e1c2151c9d 100644 --- a/openpype/hosts/max/plugins/publish/validate_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/validate_pointcloud.py @@ -54,7 +54,7 @@ class ValidatePointCloud(pyblish.api.InstancePlugin): f":{invalid}")) if report: - raise PublishValidationError + raise PublishValidationError(f"{report}") def get_tyflow_object(self, instance): invalid = [] From cca37521e9de457e5b4b519f09aaa4d1851f4c3d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 5 May 2023 13:34:18 +0800 Subject: [PATCH 485/918] refractor to the point cloud extractor and all creators --- openpype/hosts/max/plugins/create/create_camera.py | 7 +------ openpype/hosts/max/plugins/create/create_maxScene.py | 7 +------ openpype/hosts/max/plugins/create/create_model.py | 7 +------ openpype/hosts/max/plugins/create/create_pointcache.py | 9 ++------- openpype/hosts/max/plugins/create/create_pointcloud.py | 7 +------ openpype/hosts/max/plugins/publish/extract_pointcloud.py | 4 +--- 6 files changed, 7 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_camera.py b/openpype/hosts/max/plugins/create/create_camera.py index ab5578a201..b949c91857 100644 --- a/openpype/hosts/max/plugins/create/create_camera.py +++ b/openpype/hosts/max/plugins/create/create_camera.py @@ -16,13 +16,8 @@ class CreateCamera(plugin.MaxCreator): subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) + _ = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - sel_obj = None - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_maxScene.py b/openpype/hosts/max/plugins/create/create_maxScene.py index 2d6ea8c27b..bf03ee8c8a 100644 --- a/openpype/hosts/max/plugins/create/create_maxScene.py +++ b/openpype/hosts/max/plugins/create/create_maxScene.py @@ -16,13 +16,8 @@ class CreateMaxScene(plugin.MaxCreator): subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) + _ = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - sel_obj = None - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py index e7ae3af9db..4f7fd491e4 100644 --- a/openpype/hosts/max/plugins/create/create_model.py +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -16,13 +16,8 @@ class CreateModel(plugin.MaxCreator): subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) + _ = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - sel_obj = None - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py index 42aa743794..aa22a92b25 100644 --- a/openpype/hosts/max/plugins/create/create_pointcache.py +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -11,19 +11,14 @@ class CreatePointCache(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - # from pymxs import runtime as rt + from pymxs import runtime as rt instance = super(CreatePointCache, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) + _ = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - sel_obj = None - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_pointcloud.py b/openpype/hosts/max/plugins/create/create_pointcloud.py index e46bf30d1c..a8564ee118 100644 --- a/openpype/hosts/max/plugins/create/create_pointcloud.py +++ b/openpype/hosts/max/plugins/create/create_pointcloud.py @@ -16,13 +16,8 @@ class CreatePointCloud(plugin.MaxCreator): subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) + _ = rt.getNodeByName(instance.data.get("instance_node")) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - sel_obj = None - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index f85255bbf5..c807885859 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -55,7 +55,6 @@ class ExtractPointCloud(publish.Extractor): path) for job in job_args: - self.log.debug("job:{}".format(job)) rt.Execute(job) self.log.info("Performing Extraction ...") @@ -107,8 +106,7 @@ class ExtractPointCloud(publish.Extractor): end_frame = f"{operator}.frameEnd={end}" job_args.append(end_frame) filepath = filepath.replace("\\", "/") - prt_filename = f"{operator}.PRTFilename='{filepath}'" - + prt_filename = f'{operator}.PRTFilename="{filepath}"' job_args.append(prt_filename) # Partition mode = f"{operator}.PRTPartitionsMode=2" From 932c807913e5f7bac4de5d4acffd8ede3c9a0c3d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 5 May 2023 13:53:54 +0800 Subject: [PATCH 486/918] fix the bug shown in removing instance after refractoring --- openpype/hosts/max/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 39a17c29ef..a464d54b33 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -173,7 +173,7 @@ class MaxCreator(Creator, MaxCreatorBase): for instance in instances: if instance_node := rt.GetNodeByName(instance.data.get("instance_node")): # noqa rt.Select(instance_node) - rt.custAttributes.add(instance_node.baseObject, "openPypeData") + rt.execute(f'for o in selection do for c in o.children do c.parent = undefined') # noqa rt.Delete(instance_node) self._remove_instance_from_context(instance) From 44a88c3f32a049c0eac33c11d6127c0e62fc95c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 5 May 2023 10:11:03 +0200 Subject: [PATCH 487/918] :bug: add missing pyblish.util import --- openpype/pipeline/publish/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 265a9c7822..8b6212b3ef 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -7,6 +7,7 @@ import tempfile import xml.etree.ElementTree import six +import pyblish.util import pyblish.plugin import pyblish.api From b103d6d8373e0785a5449bad8ecc14b65609cd77 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 5 May 2023 10:45:42 +0100 Subject: [PATCH 488/918] Fix missing 'object_oath' property --- openpype/hosts/unreal/plugins/load/load_animation.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_layout.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 1fe0bef462..f0c08680d3 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -156,7 +156,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{root}/{hierarchy[0]}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_editor_property('object_path') + master_level = levels[0].get_full_name() hierarchy_dir = root for h in hierarchy: @@ -168,7 +168,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{hierarchy_dir}/"], recursive_paths=True) levels = ar.get_assets(_filter) - level = levels[0].get_editor_property('object_path') + level = levels[0].get_full_name() unreal.EditorLevelLibrary.save_all_dirty_levels() unreal.EditorLevelLibrary.load_level(level) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 63d415a52b..f0663a8778 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -819,7 +819,7 @@ class LayoutLoader(plugin.Loader): recursive_paths=False) levels = ar.get_assets(filter) - layout_level = levels[0].get_editor_property('object_path') + layout_level = levels[0].get_full_name() EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(layout_level) @@ -919,7 +919,7 @@ class LayoutLoader(plugin.Loader): package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_editor_property('object_path') + master_level = levels[0].get_full_name() sequences = [master_sequence] From feaa01eff56d9fbae19adf97edb5ef3f620ac1f5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 May 2023 11:49:47 +0200 Subject: [PATCH 489/918] :fire: remove obsolete validator --- .../publish/validate_sequence_frames.py | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 openpype/plugins/publish/validate_sequence_frames.py diff --git a/openpype/plugins/publish/validate_sequence_frames.py b/openpype/plugins/publish/validate_sequence_frames.py deleted file mode 100644 index 239008ee21..0000000000 --- a/openpype/plugins/publish/validate_sequence_frames.py +++ /dev/null @@ -1,66 +0,0 @@ -import os -import re - -import clique -import pyblish.api - - -class ValidateSequenceFrames(pyblish.api.InstancePlugin): - """Ensure the sequence of frames is complete - - The files found in the folder are checked against the startFrame and - endFrame of the instance. If the first or last file is not - corresponding with the first or last frame it is flagged as invalid. - - Used regular expression pattern handles numbers in the file names - (eg "Main_beauty.v001.1001.exr", "Main_beauty_v001.1001.exr", - "Main_beauty.1001.1001.exr") but not numbers behind frames (eg. - "Main_beauty.1001.v001.exr") - """ - - order = pyblish.api.ValidatorOrder - label = "Validate Sequence Frames" - families = ["imagesequence", "render"] - hosts = ["shell", "unreal"] - - def process(self, instance): - representations = instance.data.get("representations") - if not representations: - return - for repr in representations: - repr_files = repr["files"] - if isinstance(repr_files, str): - continue - - ext = repr.get("ext") - if not ext: - _, ext = os.path.splitext(repr_files[0]) - elif not ext.startswith("."): - ext = ".{}".format(ext) - pattern = r"\D?(?P(?P0*)\d+){}$".format( - re.escape(ext)) - patterns = [pattern] - - collections, remainder = clique.assemble( - repr_files, minimum_items=1, patterns=patterns) - - assert not remainder, "Must not have remainder" - assert len(collections) == 1, "Must detect single collection" - collection = collections[0] - frames = list(collection.indexes) - - if instance.data.get("slate"): - # Slate is not part of the frame range - frames = frames[1:] - - current_range = (frames[0], frames[-1]) - - required_range = (instance.data["frameStart"], - instance.data["frameEnd"]) - - if current_range != required_range: - raise ValueError(f"Invalid frame range: {current_range} - " - f"expected: {required_range}") - - missing = collection.holes().indexes - assert not missing, "Missing frames: %s" % (missing,) From 8242e61ad893843f4539e6a1517e50aa4e785de2 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 13:54:24 +0200 Subject: [PATCH 490/918] update linting --- openpype/modules/kitsu/kitsu_module.py | 18 ++----- .../modules/kitsu/utils/update_op_with_zou.py | 54 ++++++++++--------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 319b5de16b..dec19989ea 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -94,7 +94,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): return { "publish": [os.path.join(current_dir, "plugins", "publish")], - "actions": [os.path.join(current_dir, "actions")] + "actions": [os.path.join(current_dir, "actions")], } def cli(self, click_group): @@ -124,30 +124,22 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-l", - "--login", - envvar="KITSU_LOGIN", - help="Kitsu login" -) -@click.option( - "-p", - "--password", - envvar="KITSU_PWD", - help="Password for kitsu username" + "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) @click.option( "-prj", "--project", multiple=True, default=[], - help="Sync specific kitsu projects" + help="Sync specific kitsu projects", ) @click.option( "-lo", "--listen_only/--listen-only", default=False, - help="Listen to events only without any syncing" + help="Listen to events only without any syncing", ) def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index bfb4bd58fa..4ad08bb739 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -94,9 +94,7 @@ def update_op_assets( if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) - item_doc = get_asset_by_id( - project_name, insert_result.inserted_id - ) + item_doc = get_asset_by_id(project_name, insert_result.inserted_id) # Update asset item_data = deepcopy(item_doc["data"]) @@ -210,10 +208,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -329,7 +327,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "code": project_code, "fps": float(project["fps"]), "zou_id": project["id"], - "active": project['project_status_name'] != "Closed", + "active": project["project_status_name"] != "Closed", } ) @@ -350,7 +348,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,8 +356,11 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects(login: str, password: str, ignore_projects: list = None, - filter_projects: list = None +def sync_all_projects( + login: str, + password: str, + ignore_projects: list = None, + filter_projects: list = None, ): """Update all OP projects in DB with Zou data. @@ -386,13 +387,15 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] if filter_projects: - all_kitsu_projects = {p['name']: p for p in all_projects} + all_kitsu_projects = {p["name"]: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: project_to_sync.append(all_kitsu_projects[proj_name]) else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + log.info( + f"`{proj_name}` project does not exist in Kitsu." + f" Please make sure the project is spelled correctly." + ) else: # all project project_to_sync = all_projects @@ -424,14 +427,13 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Get all statuses for projects from Kitsu all_status = gazu.project.all_project_status() for status in all_status: - if project['project_status_id'] == status['id']: - project['project_status_name'] = status['name'] + if project["project_status_id"] == status["id"]: + project["project_status_name"] = status["name"] break # Do not sync closed kitsu project that is not found in openpype - if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + if project["project_status_name"] == "Closed" and not get_project( + project["name"] ): return @@ -460,7 +462,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): log.info("Project created: {}".format(project_name)) bulk_writes.append(write_project_to_op(project, dbcon)) - if project['project_status_name'] == "Closed": + if project["project_status_name"] == "Closed": return # Try to find project document @@ -521,12 +523,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From c0499c46b852c3ffe9983d17f739cabeee596963 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 5 May 2023 14:02:40 +0200 Subject: [PATCH 491/918] Changes based on suggestions --- openpype/plugins/inventory/remove_and_load.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 998be119d5..be24220c56 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -1,5 +1,5 @@ from openpype.pipeline import InventoryAction -from openpype.pipeline.legacy_io import Session +from openpype.pipeline import get_current_project_name from openpype.pipeline.load.plugins import discover_loader_plugins from openpype.pipeline.load.utils import ( get_loader_identifier, @@ -16,12 +16,13 @@ class RemoveAndLoad(InventoryAction): icon = "refresh" def process(self, containers): + project_name = get_current_project_name() for container in containers: - project_name = Session.get("AVALON_PROJECT") # Get loader loader_name = container["loader"] - for plugin in discover_loader_plugins(project_name=project_name): + loaders = discover_loader_plugins(project_name=project_name) + for plugin in loader: if get_loader_identifier(plugin) == loader_name: loader = plugin break From 07cf84e6481d013e79493e3d87c0189ab9ecf2cd Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 14:14:12 +0200 Subject: [PATCH 492/918] add variable name to `@click.option` --- openpype/modules/kitsu/kitsu_module.py | 11 +++++++---- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index dec19989ea..8d2d5ccd60 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -131,29 +131,32 @@ def push_to_zou(login, password): @click.option( "-prj", "--project", + "projects", multiple=True, default=[], help="Sync specific kitsu projects", ) @click.option( "-lo", - "--listen_only/--listen-only", + "--listen-only", + "listen_only", + is_flag=True, default=False, help="Listen to events only without any syncing", ) -def sync_service(login, password, project, listen_only): +def sync_service(login, password, projects, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password - project (str): specific kitsu projects + projects (tuple): specific kitsu projects listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners if not listen_only: - sync_all_projects(login, password, filter_projects=project) + sync_all_projects(login, password, filter_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4ad08bb739..b495cd1bea 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -360,7 +360,7 @@ def sync_all_projects( login: str, password: str, ignore_projects: list = None, - filter_projects: list = None, + filter_projects: tuple = None, ): """Update all OP projects in DB with Zou data. From 43b71e4f1e2f8387cb9fba4b20e401a15b140e51 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 5 May 2023 14:24:46 +0200 Subject: [PATCH 493/918] Fixed wrong variable --- openpype/plugins/inventory/remove_and_load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index be24220c56..d465154187 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -22,7 +22,7 @@ class RemoveAndLoad(InventoryAction): # Get loader loader_name = container["loader"] loaders = discover_loader_plugins(project_name=project_name) - for plugin in loader: + for plugin in loaders: if get_loader_identifier(plugin) == loader_name: loader = plugin break From a5efddf96926cd2a3137dc9a5ef6e7fd272d3326 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Fri, 5 May 2023 15:47:39 +0300 Subject: [PATCH 494/918] Update docs to be more specific MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix David --- website/docs/module_kitsu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 970bfb275e..9695542723 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -30,7 +30,7 @@ openpype_console module kitsu sync-service -l me@domain.ext -p my_password // sync specific projects then run listen openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 -// start listen only +// start listen only for all projects openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` From 76352bdfea0002fcfc2bb73865f9db3a326c5931 Mon Sep 17 00:00:00 2001 From: kaamaurice Date: Fri, 5 May 2023 15:07:30 +0200 Subject: [PATCH 495/918] fix error dialog missing parent arg --- openpype/tools/sceneinventory/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 3279be6094..73d33392b9 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -791,7 +791,7 @@ class SceneInventoryView(QtWidgets.QTreeView): else: version_str = version - dialog = QtWidgets.QMessageBox() + dialog = QtWidgets.QMessageBox(self) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.setStyleSheet(style.load_stylesheet()) dialog.setWindowTitle("Update failed") From 2d26cc00ad2c9787b47ede9fc81a6289a6edc911 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 May 2023 15:52:47 +0200 Subject: [PATCH 496/918] :rotating_light: some style issues and refactoring --- .pre-commit-config.yaml | 12 ++++-- .../hosts/max/plugins/create/create_camera.py | 14 +------ .../max/plugins/create/create_maxScene.py | 14 +------ .../hosts/max/plugins/create/create_model.py | 14 +------ .../max/plugins/create/create_pointcache.py | 15 +------- .../max/plugins/create/create_pointcloud.py | 14 +------ .../hosts/max/plugins/create/create_render.py | 13 +++---- .../plugins/publish/validate_usd_plugin.py | 38 ++++++++++--------- setup.cfg | 2 + 9 files changed, 41 insertions(+), 95 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe4c7e3da3..9c03dc8ff0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,11 +10,15 @@ repos: - id: check-added-large-files - id: no-commit-to-branch args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-_]+)$).*' ] -- repo: local - hooks: - - id: flake8 + +- repo: local + hooks: + - id: flake8 name: flake8 - description: WPS enforced flake8 + additional_dependencies: + - wemake-python-styleguide + - flake8 + description: Python style guide enforcement entry: flake8 args: ["--config=setup.cfg"] language: python diff --git a/openpype/hosts/max/plugins/create/create_camera.py b/openpype/hosts/max/plugins/create/create_camera.py index b949c91857..804d629ec7 100644 --- a/openpype/hosts/max/plugins/create/create_camera.py +++ b/openpype/hosts/max/plugins/create/create_camera.py @@ -1,23 +1,11 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance class CreateCamera(plugin.MaxCreator): + """Creator plugin for Camera.""" identifier = "io.openpype.creators.max.camera" label = "Camera" family = "camera" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt - instance = super(CreateCamera, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance - _ = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_maxScene.py b/openpype/hosts/max/plugins/create/create_maxScene.py index bf03ee8c8a..851e26dda2 100644 --- a/openpype/hosts/max/plugins/create/create_maxScene.py +++ b/openpype/hosts/max/plugins/create/create_maxScene.py @@ -1,23 +1,11 @@ # -*- coding: utf-8 -*- """Creator plugin for creating raw max scene.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance class CreateMaxScene(plugin.MaxCreator): + """Creator plugin for 3ds max scenes.""" identifier = "io.openpype.creators.max.maxScene" label = "Max Scene" family = "maxScene" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt - instance = super(CreateMaxScene, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance - _ = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_model.py b/openpype/hosts/max/plugins/create/create_model.py index 4f7fd491e4..fc09d475ef 100644 --- a/openpype/hosts/max/plugins/create/create_model.py +++ b/openpype/hosts/max/plugins/create/create_model.py @@ -1,23 +1,11 @@ # -*- coding: utf-8 -*- """Creator plugin for model.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance class CreateModel(plugin.MaxCreator): + """Creator plugin for Model.""" identifier = "io.openpype.creators.max.model" label = "Model" family = "model" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt - instance = super(CreateModel, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance - _ = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_pointcache.py b/openpype/hosts/max/plugins/create/create_pointcache.py index aa22a92b25..c2d11f4c32 100644 --- a/openpype/hosts/max/plugins/create/create_pointcache.py +++ b/openpype/hosts/max/plugins/create/create_pointcache.py @@ -1,24 +1,11 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance class CreatePointCache(plugin.MaxCreator): + """Creator plugin for Point caches.""" identifier = "io.openpype.creators.max.pointcache" label = "Point Cache" family = "pointcache" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt - - instance = super(CreatePointCache, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance - _ = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_pointcloud.py b/openpype/hosts/max/plugins/create/create_pointcloud.py index a8564ee118..bc7706069d 100644 --- a/openpype/hosts/max/plugins/create/create_pointcloud.py +++ b/openpype/hosts/max/plugins/create/create_pointcloud.py @@ -1,23 +1,11 @@ # -*- coding: utf-8 -*- """Creator plugin for creating point cloud.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance class CreatePointCloud(plugin.MaxCreator): + """Creator plugin for Point Clouds.""" identifier = "io.openpype.creators.max.pointcloud" label = "Point Cloud" family = "pointcloud" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt - instance = super(CreatePointCloud, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance - _ = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 43d69fa320..e9952c3124 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,24 +1,25 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" from openpype.hosts.max.api import plugin -from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings class CreateRender(plugin.MaxCreator): + """Creator plugin for Renders.""" identifier = "io.openpype.creators.max.render" label = "Render" family = "maxrender" icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt - instance = super(CreateRender, self).create( + """Plugin entry point.""" + from pymxs import runtime as rt # noqa: WPS433,I001 + instance = super().create( subset_name, instance_data, - pre_create_data) # type: CreatedInstance + pre_create_data) container_name = instance.data.get("instance_node") - container = rt.getNodeByName(container_name) + container = rt.GetNodeByName(container_name) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container sel_obj = None @@ -26,8 +27,6 @@ class CreateRender(plugin.MaxCreator): sel_obj = list(self.selected_nodes) for obj in sel_obj: obj.parent = container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) # set viewport camera for rendering(mandatory for deadline) RenderSettings().set_render_camera(sel_obj) diff --git a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py index 8f11d72567..9957e62736 100644 --- a/openpype/hosts/max/plugins/publish/validate_usd_plugin.py +++ b/openpype/hosts/max/plugins/publish/validate_usd_plugin.py @@ -1,35 +1,37 @@ # -*- coding: utf-8 -*- -import pyblish.api +"""Validator for USD plugin.""" from openpype.pipeline import PublishValidationError +from pyblish.api import InstancePlugin, ValidatorOrder from pymxs import runtime as rt -class ValidateUSDPlugin(pyblish.api.InstancePlugin): - """Validates if USD plugin is installed or loaded in Max - """ +def get_plugins() -> list: + """Get plugin list from 3ds max.""" + manager = rt.PluginManager + count = manager.pluginDllCount + plugin_info_list = [] + for p in range(1, count + 1): + plugin_info = manager.pluginDllName(p) + plugin_info_list.append(plugin_info) - order = pyblish.api.ValidatorOrder - 0.01 + return plugin_info_list + + +class ValidateUSDPlugin(InstancePlugin): + """Validates if USD plugin is installed or loaded in 3ds max.""" + + order = ValidatorOrder - 0.01 families = ["model"] hosts = ["max"] label = "USD Plugin" def process(self, instance): - plugin_mgr = rt.PluginManager - plugin_count = plugin_mgr.pluginDllCount - plugin_info = self.get_plugins(plugin_mgr, - plugin_count) + """Plugin entry point.""" + + plugin_info = get_plugins() usd_import = "usdimport.dli" if usd_import not in plugin_info: raise PublishValidationError(f"USD Plugin {usd_import} not found") usd_export = "usdexport.dle" if usd_export not in plugin_info: raise PublishValidationError(f"USD Plugin {usd_export} not found") - - @staticmethod - def get_plugins(manager, count): - plugin_info_list = [] - for p in range(1, count + 1): - plugin_info = manager.pluginDllName(p) - plugin_info_list.append(plugin_info) - - return plugin_info_list diff --git a/setup.cfg b/setup.cfg index 7863a74894..1d57657a19 100644 --- a/setup.cfg +++ b/setup.cfg @@ -138,6 +138,8 @@ ignore = RST, # Black would make changes error BLK100, + # Imperative mood of the first line on docstrings + D401, [isort] profile=wemake From 6b1a801f4c14be4d4c601b7c7267dd01b659dad3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 May 2023 15:55:27 +0200 Subject: [PATCH 497/918] adding links to documentation into settings labels --- .../schemas/projects_schema/schema_project_aftereffects.json | 2 +- .../schemas/projects_schema/schema_project_blender.json | 2 +- .../schemas/projects_schema/schema_project_celaction.json | 2 +- .../entities/schemas/projects_schema/schema_project_flame.json | 2 +- .../entities/schemas/projects_schema/schema_project_fusion.json | 2 +- .../entities/schemas/projects_schema/schema_project_global.json | 2 +- .../schemas/projects_schema/schema_project_harmony.json | 2 +- .../entities/schemas/projects_schema/schema_project_hiero.json | 2 +- .../schemas/projects_schema/schema_project_houdini.json | 2 +- .../entities/schemas/projects_schema/schema_project_max.json | 2 +- .../entities/schemas/projects_schema/schema_project_maya.json | 2 +- .../schemas/projects_schema/schema_project_photoshop.json | 2 +- .../schemas/projects_schema/schema_project_resolve.json | 2 +- .../schemas/projects_schema/schema_project_traypublisher.json | 2 +- .../schemas/projects_schema/schema_project_tvpaint.json | 2 +- .../entities/schemas/projects_schema/schema_project_unreal.json | 2 +- .../schemas/projects_schema/schema_project_webpublisher.json | 2 +- .../schemas/projects_schema/schemas/schema_nuke_imageio.json | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index ef09a71bda..d164a8f2c3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index c3eab6c3f0..79eea3f192 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json index 5729f70e2f..915f199b6e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index 625780a650..16c9378194 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures." + "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 6189da0e19..65584264c9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json index d1d7f336e1..953361935c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -13,7 +13,7 @@ "children": [ { "type": "label", - "label": "It's important to note that once color management is activated on a project, all hosts will be color managed by default.
The OpenColorIO (OCIO) config file is used either from the global settings or from the host's overrides. It's worth
noting that the order of the defined configuration paths matters, with higher priority given to paths listed earlier in
the configuration list.

To avoid potential issues, ensure that the OCIO configuration path is not an absolute path and includes at least
the root token (Anatomy). This helps ensure that the configuration path remains valid across different environments and
avoids any hard-coding of paths that may be specific to one particular system." + "label": "It's important to note that once color management is activated on a project, all hosts will be color managed by default.
The OpenColorIO (OCIO) config file is used either from the global settings or from the host's overrides. It's worth
noting that the order of the defined configuration paths matters, with higher priority given to paths listed earlier in
the configuration list.

To avoid potential issues, ensure that the OCIO configuration path is not an absolute path and includes at least
the root token (Anatomy). This helps ensure that the configuration path remains valid across different environments and
avoids any hard-coding of paths that may be specific to one particular system.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index a56f62c6d6..276b321b24 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index 2c82e1a9ac..c2339d8200 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 588e209718..a7032775c1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 5dac8ee7e9..d1e8e333cc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 55f231e235..fe7c262603 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -54,7 +54,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 898c3374d7..7ddd575dde 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures." + "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index 758cf2a196..aea019b77b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.." + "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.

Related documentation.." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index c234cd1b71..6b55837f12 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 6d446b5550..ed8887f93e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index 8c3ff71489..aa2fe40b4a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index e319182e3c..7b65dddda6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -14,7 +14,7 @@ "children": [ { "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing." + "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." }, { "type": "boolean", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index f691518255..864e084bde 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -7,7 +7,7 @@ "children": [ { "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures." + "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." }, { "type": "boolean", From e08ff46bfa4edb8b526501b88ff2dcdc36538885 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 May 2023 17:22:14 +0200 Subject: [PATCH 498/918] adding settings for creators --- .../defaults/project_settings/fusion.json | 13 ++++++ .../schema_project_fusion.json | 44 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index f974eebaca..d76ed82942 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -21,5 +21,18 @@ "copy_path": "~/.openpype/hosts/fusion/profiles", "copy_status": false, "force_sync": false + }, + "create": { + "CreateSaver": { + "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}..{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ] + } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 464cf2c06d..7971c62300 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -68,6 +68,50 @@ "label": "Resync profile on each launch" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CreateSaver", + "label": "Create Saver", + "is_group": true, + "children": [ + { + "type": "text", + "key": "temp_rendering_path_template", + "label": "Temporary rendering path template" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + }, + { + "key": "instance_attributes", + "label": "Instance attributes", + "type": "enum", + "multiselection": true, + "enum_items": [ + { + "reviewable": "Reviewable" + }, + { + "farm_rendering": "Farm rendering" + } + ] + } + ] + } + ] } ] } From d637fed33de88d4048c40b56ecf3f5cb86aab654 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 May 2023 17:23:27 +0200 Subject: [PATCH 499/918] implementing settings also adding temp rendering path attribute with with support for tempates --- .../fusion/plugins/create/create_saver.py | 78 +++++++++++++++---- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index cedc4029fa..fb6767d2cd 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,4 +1,6 @@ +from copy import deepcopy import os +from pprint import pformat from openpype.hosts.fusion.api import ( get_current_comp, @@ -11,7 +13,7 @@ from openpype.lib import ( ) from openpype.pipeline import ( legacy_io, - Creator, + Creator as NewCreator, CreatedInstance, ) from openpype.client import ( @@ -19,7 +21,7 @@ from openpype.client import ( ) -class CreateSaver(Creator): +class CreateSaver(NewCreator): identifier = "io.openpype.creators.fusion.saver" label = "Render (saver)" name = "render" @@ -28,7 +30,15 @@ class CreateSaver(Creator): description = "Fusion Saver to generate image sequence" icon = "fa5.eye" - instance_attributes = ["reviewable"] + instance_attributes = [ + "reviewable" + ] + default_variants = [ + "Main", + "Mask" + ] + temp_rendering_path_template = ( + "{workdir}/renders/fusion/{subset}/{subset}..{ext}") def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? @@ -125,17 +135,34 @@ class CreateSaver(Creator): original_subset = tool.GetData("openpype.subset") subset = data["subset"] if original_subset != subset: - # Subset change detected - # Update output filepath - workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - filename = f"{subset}..exr" - filepath = os.path.join(workdir, "render", subset, filename) - tool["Clip"] = filepath + self._configure_saver_tool(data, tool, subset) - # Rename tool - if tool.Name != subset: - print(f"Renaming {tool.Name} -> {subset}") - tool.SetAttrs({"TOOLS_Name": subset}) + def _configure_saver_tool(self, data, tool, subset): + formatting_data = deepcopy(data) + self.log.warning(pformat(formatting_data)) + + # Subset change detected + workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) + formatting_data.update({ + "workdir": workdir, + "ext": "exr" + }) + + # build file path to render + filepath = self.temp_rendering_path_template.format( + **formatting_data) + + # create directory + if not os.path.isdir(os.path.dirname(filepath)): + self.log.warning("Path does not exist! I am creating it.") + os.makedirs(os.path.dirname(filepath)) + + tool["Clip"] = filepath + + # Rename tool + if tool.Name != subset: + print(f"Renaming {tool.Name} -> {subset}") + tool.SetAttrs({"TOOLS_Name": subset}) def _collect_unmanaged_saver(self, tool): # TODO: this should not be done this way - this should actually @@ -238,3 +265,28 @@ class CreateSaver(Creator): default=("reviewable" in self.instance_attributes), label="Review", ) + + def apply_settings( + self, + project_settings, + system_settings + ): + """Method called on initialization of plugin to apply settings.""" + + # plugin settings + plugin_settings = self._get_creator_settings(project_settings) + + # individual attributes + self.instance_attributes = plugin_settings.get( + "instance_attributes") or self.instance_attributes + self.default_variants = plugin_settings.get( + "default_variants") or self.default_variants + self.temp_rendering_path_template = ( + plugin_settings.get("temp_rendering_path_template") + or self.temp_rendering_path_template + ) + + def _get_creator_settings(self, project_settings, settings_key=None): + if not settings_key: + settings_key = self.__class__.__name__ + return project_settings["fusion"]["create"][settings_key] From ac154303f3aaf6130492442c1c42496877006213 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 5 May 2023 17:23:42 +0200 Subject: [PATCH 500/918] :heavy_minus_sign: revert WPS tests --- .github/workflows/pr_linting.yml | 56 -------------------------------- .pre-commit-config.yaml | 13 -------- pyproject.toml | 2 -- 3 files changed, 71 deletions(-) delete mode 100644 .github/workflows/pr_linting.yml diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml deleted file mode 100644 index 58f52bb313..0000000000 --- a/.github/workflows/pr_linting.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: 📇 Code Linting - -on: - push: - branches: [ develop ] - pull_request: - branches: [ develop ] - - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number}} - cancel-in-progress: true - -permissions: - contents: read - pull-requests: write - -jobs: - files_changed: - runs-on: ubuntu-latest - outputs: - changed_python: ${{ steps.changes.outputs.python }} - steps: - - uses: actions/checkout@v3 - if: github.event_name == 'push' - - uses: dorny/paths-filter@master - id: changes - with: - filters: | - python: - - ["**/*.py"] - - linting: - needs: files_changed - if: ${{ needs.files_changed.outputs.changed_python == 'true' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - fetch-depth: 0 - - name: Get changed Python files - id: py-changes - run: | - echo "py_files_list=$(git diff --name-only --diff-filter=ACMRT \ - ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} \ - | grep .py$ | xargs)" >> $GITHUB_OUTPUT - - name: Code Check - uses: wemake-services/wemake-python-styleguide@master - with: - reporter: 'github-pr-review' - path: ${{ steps.py-changes.outputs.py_files_list }} - env: - GITHUB_TOKEN: "${{ secrets.YNPUT_BOT_TOKEN }}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9c03dc8ff0..eec388924e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,16 +10,3 @@ repos: - id: check-added-large-files - id: no-commit-to-branch args: [ '--pattern', '^(?!((release|enhancement|feature|bugfix|documentation|tests|local|chore)\/[a-zA-Z0-9\-_]+)$).*' ] - -- repo: local - hooks: - - id: flake8 - name: flake8 - additional_dependencies: - - wemake-python-styleguide - - flake8 - description: Python style guide enforcement - entry: flake8 - args: ["--config=setup.cfg"] - language: python - types: [python] diff --git a/pyproject.toml b/pyproject.toml index 0d236aedc6..003f6cf2d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,8 +94,6 @@ wheel = "*" enlighten = "*" # cool terminal progress bars toml = "^0.10.2" # for parsing pyproject.toml pre-commit = "*" -wemake-python-styleguide = "*" -isort="*" [tool.poetry.urls] "Bug Tracker" = "https://github.com/pypeclub/openpype/issues" From c73772919a32f349ecb16229140338b58e466445 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 5 May 2023 17:42:38 +0200 Subject: [PATCH 501/918] fixing path slashes --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index fb6767d2cd..8bf364cf20 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -144,7 +144,7 @@ class CreateSaver(NewCreator): # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) formatting_data.update({ - "workdir": workdir, + "workdir": workdir.replace("\\", "/"), "ext": "exr" }) From 7c6bbe8306fc0c17289d17c399105b1dc2c2019d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 6 May 2023 03:25:00 +0000 Subject: [PATCH 502/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index dc0a3a8c9f..e02053ba76 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.6" +__version__ = "3.15.7-nightly.1" From d87f217fb9cf766dfce449426a6ffa9550a2e99d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 6 May 2023 03:25:44 +0000 Subject: [PATCH 503/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5050d37c7a..cae6a6486b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7-nightly.1 - 3.15.6 - 3.15.6-nightly.3 - 3.15.6-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.2 - 3.14.1-nightly.1 - 3.14.0 - - 3.14.0-nightly.1 validations: required: true - type: dropdown From 86fcab6f8d423fd6d719f5ba50f63e72e278e056 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 15:20:25 +0800 Subject: [PATCH 504/918] refractor the creator for custom modifiers --- .../hosts/max/plugins/create/create_redshift_proxy.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 1bddbdafae..8c71feb40f 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -12,13 +12,8 @@ class CreateRedshiftProxy(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) - instance = super(CreateRedshiftProxy, self).create( + + _ = super(CreateRedshiftProxy, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container From e45098c2841c2390c42d7b46f2d1688a5220fa0a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 15:21:27 +0800 Subject: [PATCH 505/918] hound fix --- openpype/hosts/max/plugins/create/create_redshift_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 8c71feb40f..698ea82b69 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -11,7 +11,6 @@ class CreateRedshiftProxy(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt _ = super(CreateRedshiftProxy, self).create( subset_name, From 3822b8e7f24bbe9754793c77ced8d152fb172a02 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 16:29:32 +0800 Subject: [PATCH 506/918] refractor the render creator --- openpype/hosts/max/plugins/create/create_render.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index e9952c3124..136ca028ac 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -22,13 +22,9 @@ class CreateRender(plugin.MaxCreator): container = rt.GetNodeByName(container_name) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - sel_obj = None - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container - - # set viewport camera for rendering(mandatory for deadline) - RenderSettings().set_render_camera(sel_obj) + sel_obj = self.selected_nodes + if sel_obj: + # set viewport camera for rendering(mandatory for deadline) + RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) From d485fdbecf432f4097a48c91d6e242b94c80a9a1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 16:31:15 +0800 Subject: [PATCH 507/918] hound fix --- openpype/hosts/max/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 136ca028ac..2ea993494b 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -19,7 +19,6 @@ class CreateRender(plugin.MaxCreator): instance_data, pre_create_data) container_name = instance.data.get("instance_node") - container = rt.GetNodeByName(container_name) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container sel_obj = self.selected_nodes From d6d022989f5c2e8897abd27c3d80b4b5a23b5e77 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 16:32:52 +0800 Subject: [PATCH 508/918] hound fix --- openpype/hosts/max/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 2ea993494b..9b677a615f 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -13,7 +13,6 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): """Plugin entry point.""" - from pymxs import runtime as rt # noqa: WPS433,I001 instance = super().create( subset_name, instance_data, From 5e9ae9153b9af411ff2102102d09356d87d8208c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:45:52 +0800 Subject: [PATCH 509/918] bug fix redshift creators --- .../plugins/create/create_redshift_rop.py | 63 +++++++++++++++---- .../houdini/plugins/create/create_vray_rop.py | 2 - .../plugins/publish/submit_publish_job.py | 3 +- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 1fb9ab2f67..4f9c00b3a3 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- """Creator plugin to create Redshift ROP.""" +import hou # noqa + from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +from openpype.lib import EnumDef class CreateRedshiftROP(plugin.HoudiniCreator): @@ -11,9 +14,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator): family = "redshift_rop" icon = "magic" defaults = ["master"] + ext = "exr" def create(self, subset_name, instance_data, pre_create_data): - import hou # noqa instance_data.pop("active", None) instance_data.update({"node_type": "Redshift_ROP"}) @@ -22,12 +25,6 @@ class CreateRedshiftROP(plugin.HoudiniCreator): # Submit for job publishing instance_data["farm"] = True - # Clear the family prefix from the subset - subset = subset_name - subset_no_prefix = subset[len(self.family):] - subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:] - subset_name = subset_no_prefix - instance = super(CreateRedshiftROP, self).create( subset_name, instance_data, @@ -36,11 +33,10 @@ class CreateRedshiftROP(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) basename = instance_node.name() - instance_node.setName(basename + "_ROP", unique_name=True) # Also create the linked Redshift IPR Rop try: - ipr_rop = self.parent.createNode( + ipr_rop = instance_node.parent().createNode( "Redshift_IPR", node_name=basename + "_IPR" ) except hou.OperationFailed: @@ -52,19 +48,60 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) # Set the linked rop to the Redshift ROP - ipr_rop.parm("linked_rop").set(ipr_rop.relativePathTo(instance)) + ipr_rop.parm("linked_rop").set(instance_node.path()) + + ext = pre_create_data.get("image_format") + filepath ="{renders_dir}{subset_name}/{subset_name}.{fmt}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + fmt="${aov}.$F4.{ext}".format(aov="AOV", + ext=ext) + ) - prefix = '${HIP}/render/${HIPNAME}/`chs("subset")`.${AOV}.$F4.exr' parms = { # Render frame range "trange": 1, # Redshift ROP settings - "RS_outputFileNamePrefix": prefix, - "RS_outputMultilayerMode": 0, # no multi-layered exr + "RS_outputFileNamePrefix": filepath, + "RS_outputMultilayerMode": "1", # no multi-layered exr "RS_outputBeautyAOVSuffix": "beauty", } + + if self.selected_nodes: + # set up the render camera from the selected node + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + parms.update({ + "RS_renderCamera": camera or "" + }) instance_node.setParms(parms) # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) + + def remove_instances(self, instances): + for instance in instances: + node = instance.data.get("instance_node") + + ipr_node = hou.node(f"{node}_IPR") + if ipr_node: + ipr_node.destroy() + + return super(CreateRedshiftROP, self).remove_instances(instances) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs() + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default=self.ext, + label="Image Format Options") + ] diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 0a74d93c99..1de9be4ed6 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -154,5 +154,3 @@ class CreateVrayROP(plugin.HoudiniCreator): "if enabled", default=False) ] - -# ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c1c9d8c062..afe5c59834 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -125,7 +125,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "renderlayer", "imagesequence", "vrayscene", "maxrender", "arnold_rop", "mantra_rop", - "karma_rop", "vray_rop"] + "karma_rop", "vray_rop", + "redshift_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE From 708c125433aad8c88b56e86b1a54965d433ebe3d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:49:20 +0800 Subject: [PATCH 510/918] hound fix --- .../houdini/plugins/create/create_redshift_rop.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 4f9c00b3a3..4ec88fc468 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -51,11 +51,10 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ipr_rop.parm("linked_rop").set(instance_node.path()) ext = pre_create_data.get("image_format") - filepath ="{renders_dir}{subset_name}/{subset_name}.{fmt}".format( - renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov="AOV", - ext=ext) + fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) parms = { @@ -74,8 +73,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): if node.type().name() == "cam": camera = node.path() parms.update({ - "RS_renderCamera": camera or "" - }) + "RS_renderCamera": camera or ""}) instance_node.setParms(parms) # Lock some Avalon attributes From 34b387a29395e4bc05d911d151236a77c230c387 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:51:31 +0800 Subject: [PATCH 511/918] hound fix --- openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 4ec88fc468..242915ed6d 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -53,8 +53,8 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), - subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) + subset_name=subset_name, + fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) parms = { From 8d729995791997e9717e1463c04f9e5305e9ecce Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:52:38 +0800 Subject: [PATCH 512/918] hound fix --- openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 242915ed6d..e14ff15bf8 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -55,7 +55,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) - ) + ) parms = { # Render frame range From 291432d49bcd92120904ad868a27e9981d010865 Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Mon, 8 May 2023 16:01:57 +0000 Subject: [PATCH 513/918] Update openpype/plugins/inventory/remove_and_load.py Changed representation assertion into a warning with more info Co-authored-by: Roy Nieterau --- openpype/plugins/inventory/remove_and_load.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index d465154187..015f80cd4e 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -35,7 +35,10 @@ class RemoveAndLoad(InventoryAction): representation = get_representation_by_id( project_name, container["representation"] ) - assert representation, "Representation not found" + if not representation: + self.log.warning( + "Skipping remove and load because representation id is not" + " found in database: '{}'".format(container["representation"]) # Remove container remove_container(container) From 9a2a25a957cf25ed7fc5f1bcf10c3d15dc6b505c Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Mon, 8 May 2023 18:05:18 +0200 Subject: [PATCH 514/918] Added continue statement + linting --- openpype/plugins/inventory/remove_and_load.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 015f80cd4e..062a44354b 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -18,7 +18,6 @@ class RemoveAndLoad(InventoryAction): def process(self, containers): project_name = get_current_project_name() for container in containers: - # Get loader loader_name = container["loader"] loaders = discover_loader_plugins(project_name=project_name) @@ -38,7 +37,11 @@ class RemoveAndLoad(InventoryAction): if not representation: self.log.warning( "Skipping remove and load because representation id is not" - " found in database: '{}'".format(container["representation"]) + " found in database: '{}'".format( + container["representation"] + ) + ) + continue # Remove container remove_container(container) From 8830e63fb010d4cea374bdbbf095d81078e08ac2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 9 May 2023 10:38:14 +0200 Subject: [PATCH 515/918] :recycle: add/delete action from container --- openpype/hosts/max/api/plugin.py | 37 ++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index a464d54b33..8df620b913 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -22,7 +22,8 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" rollout OPparams "OP Parameters" ( listbox list_node "Node References" items:#() - button button_add "Add Selection" + button button_add "Add to Container" + button button_del "Delete from Container" fn node_to_name the_node = ( @@ -34,8 +35,8 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" on button_add pressed do ( - current_selection = selectByName title:"Select Objects To Add To - Container" buttontext:"Add" + current_selection = selectByName title:"Select Objects to add to + the Container" buttontext:"Add" temp_arr = #() i_node_arr = #() for c in current_selection do @@ -45,8 +46,33 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" append temp_arr handle_name append i_node_arr node_ref ) - all_handles = i_node_arr - list_node.items = temp_arr + all_handles = join i_node_arr all_handles + list_node.items = join temp_arr list_node.items + ) + + on button_del pressed do + ( + current_selection = selectByName title:"Select Objects to remove + from the Container" buttontext:"Remove" + temp_arr = #() + i_node_arr = #() + for c in current_selection do + ( + node_ref = NodeTransformMonitor node:c + handle_name = node_to_name c + idx = finditem all_handles node_ref + if idx do + ( + DeleteItem all_nodes idx + ) + idx = finditem list_node.items handle_name + if idx do + ( + DeleteItem list_node.items idx + ) + ) + all_handles = join i_node_arr all_handles + list_node.items = join temp_arr list_node.items ) on OPparams open do @@ -56,7 +82,6 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" temp_arr = #() for x in all_handles do ( - print(x.node) handle_name = node_to_name x.node append temp_arr handle_name ) From 41bd47d9b53335ed5b806a548b0589d219b86415 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 May 2023 11:01:18 +0100 Subject: [PATCH 516/918] Refactor code to use AyonAssetContainer instead of AssetContainer --- openpype/hosts/unreal/plugins/load/load_layout.py | 6 +++--- openpype/hosts/unreal/plugins/load/load_uasset.py | 2 +- openpype/hosts/unreal/plugins/publish/extract_layout.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index d4910f91b6..e5f32c3412 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -50,7 +50,7 @@ class LayoutLoader(plugin.Loader): # Get all the asset containers for a in asset_content: obj = ar.get_asset_by_object_path(a) - if obj.get_asset().get_class().get_name() == 'AssetContainer': + if obj.get_asset().get_class().get_name() == 'AyonAssetContainer': asset_containers.append(obj) return asset_containers @@ -338,7 +338,7 @@ class LayoutLoader(plugin.Loader): ).replace('\\', '/') _filter = unreal.ARFilter( - class_names=["AssetContainer"], + class_names=["AyonAssetContainer"], package_paths=[anim_path], recursive_paths=False) containers = ar.get_assets(_filter) @@ -519,7 +519,7 @@ class LayoutLoader(plugin.Loader): for asset in assets: obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() == 'AssetContainer': + if obj.get_class().get_name() == 'AyonAssetContainer': container = obj if obj.get_class().get_name() == 'Skeleton': skeleton = obj diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index b1a4fc6971..7606bc14e4 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -107,7 +107,7 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'AssetContainer': + if not obj.get_class().get_name() == 'AyonAssetContainer': unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) diff --git a/openpype/hosts/unreal/plugins/publish/extract_layout.py b/openpype/hosts/unreal/plugins/publish/extract_layout.py index cac7991f00..57e7957575 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_layout.py +++ b/openpype/hosts/unreal/plugins/publish/extract_layout.py @@ -48,7 +48,7 @@ class ExtractLayout(publish.Extractor): # Search the reference to the Asset Container for the object path = unreal.Paths.get_path(mesh.get_path_name()) filter = unreal.ARFilter( - class_names=["AssetContainer"], package_paths=[path]) + class_names=["AyonAssetContainer"], package_paths=[path]) ar = unreal.AssetRegistryHelpers.get_asset_registry() try: asset_container = ar.get_assets(filter)[0].get_asset() From 564da974b19f0c16c88b540d0c1fd5cf7b5864cc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 9 May 2023 21:47:04 +0800 Subject: [PATCH 517/918] add up-versioning to the max loader --- .../hosts/max/plugins/load/load_camera_fbx.py | 31 ++++++++++++++++--- .../hosts/max/plugins/load/load_model_fbx.py | 8 +++++ .../hosts/max/plugins/load/load_model_obj.py | 3 ++ .../hosts/max/plugins/load/load_pointcache.py | 22 ++++++++++--- .../hosts/max/plugins/load/load_pointcloud.py | 14 ++++++--- 5 files changed, 64 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 3a6947798e..183a6f5d45 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -4,7 +4,7 @@ from openpype.pipeline import ( get_representation_path ) from openpype.hosts.max.api.pipeline import containerise -from openpype.hosts.max.api import lib +from openpype.hosts.max.api import lib, maintained_selection class FbxLoader(load.LoaderPlugin): @@ -36,7 +36,13 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) - container_name = f"{name}_CON" + # create "missing" container for obj import + container = rt.container() + container.name = f"{name}" + + # get current selection + for selection in rt.getCurrentSelection(): + selection.Parent = container asset = rt.getNodeByName(f"{name}") @@ -48,15 +54,30 @@ importFile @"{filepath}" #noPrompt using:FBXIMP path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) + rt.select(node.Children) + fbx_reimport_cmd = ( + f""" - fbx_objects = self.get_container_children(node) - for fbx_object in fbx_objects: - fbx_object.source = path +FBXImporterSetParam "Animation" true +FBXImporterSetParam "Cameras" true +FBXImporterSetParam "AxisConversionMethod" true +FbxExporterSetParam "UpAxis" "Y" +FbxExporterSetParam "Preserveinstances" true + +importFile @"{path}" #noPrompt using:FBXIMP + """) + rt.execute(fbx_reimport_cmd) + + with maintained_selection(): + rt.select(node) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 88b8f1ed89..b8485ca333 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -37,6 +37,14 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) + # create "missing" container for obj import + container = rt.container() + container.name = f"{name}" + + # get current selection + for selection in rt.getCurrentSelection(): + selection.Parent = container + asset = rt.getNodeByName(f"{name}") return containerise( diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index c55e462111..ae42e1f3d3 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -61,6 +61,9 @@ class ObjLoader(load.LoaderPlugin): "representation": str(representation["_id"]) }) + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index b3e12adc7b..2001b78aba 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -9,7 +9,7 @@ from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.max.api.pipeline import containerise -from openpype.hosts.max.api import lib +from openpype.hosts.max.api import lib, maintained_selection class AbcLoader(load.LoaderPlugin): @@ -65,14 +65,26 @@ importFile @"{file_path}" #noPrompt path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - alembic_objects = self.get_container_children(node, "AlembicObject") - for alembic_object in alembic_objects: - alembic_object.source = path - lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) + rt.select(node.Children) + + for alembic in rt.selection: + abc = rt.getNodeByName(alembic.name) + rt.select(abc.Children) + for abc_con in rt.selection: + container = rt.getNodeByName(abc_con.name) + container.source = path + rt.select(container.Children) + for abc_obj in rt.selection: + alembic_obj = rt.getNodeByName(abc_obj.name) + alembic_obj.source = path + + with maintained_selection(): + rt.select(node) + def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index 27bc88b4f3..d4ae721c8a 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -3,7 +3,7 @@ from openpype.pipeline import ( load, get_representation_path ) from openpype.hosts.max.api.pipeline import containerise -from openpype.hosts.max.api import lib +from openpype.hosts.max.api import lib, maintained_selection class PointCloudLoader(load.LoaderPlugin): @@ -34,15 +34,21 @@ class PointCloudLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) + rt.select(node.Children) + for prt in rt.selection: + prt_object = rt.getNodeByName(prt.name) + prt_object.filename = path - prt_objects = self.get_container_children(node) - for prt_object in prt_objects: - prt_object.source = path + with maintained_selection(): + rt.select(node) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): """remove the container""" from pymxs import runtime as rt From da56583d198002865bcd9530c7b209c5b56ad66e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 9 May 2023 21:48:33 +0800 Subject: [PATCH 518/918] select the member data before exporting usd in the usd extractor --- openpype/hosts/max/plugins/publish/extract_model_usd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index df1e7a4f02..2500e6c905 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -44,6 +44,7 @@ class ExtractModelUSD(publish.Extractor, with maintained_selection(): # select and export node_list = instance.data["members"] + rt.select(node_list) rt.USDExporter.ExportFile(asset_filepath, exportOptions=export_options, contentSource=rt.Name("selected"), From 5d7bf26c8c0d3476eda5a4f6762cbfdb32858b58 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 May 2023 16:17:47 +0100 Subject: [PATCH 519/918] Added AyonPublishInstance class and its Factory --- .../Ayon/Private/AyonPublishInstance.cpp | 203 +++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 23 ++ .../OpenPypePublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Public/AyonPublishInstance.h | 103 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h} | 6 +- .../Ayon/Private/AyonPublishInstance.cpp | 204 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 23 ++ .../OpenPypePublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h} | 6 +- .../Ayon/Private/AyonPublishInstance.cpp | 204 ++++++++++++++++++ .../Private/AyonPublishInstanceFactory.cpp | 23 ++ .../OpenPypePublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 +++++++++ .../Ayon/Public/AyonPublishInstanceFactory.h} | 6 +- 15 files changed, 1000 insertions(+), 78 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h rename openpype/hosts/unreal/integration/{UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h => UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h} (71%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h rename openpype/hosts/unreal/integration/{UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h => UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h} (71%) create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h rename openpype/hosts/unreal/integration/{UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h => UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h} (71%) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..d7550e2ed1 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,203 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistryModule.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Widgets/Notifications/SNotificationList.h" + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif + +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..f79c428a6d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp deleted file mode 100644 index 4b4492bd20..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "OpenPypePublishInstanceFactory.h" -#include "OpenPypePublishInstance.h" - -UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UOpenPypePublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..0a0628c3ec --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,103 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h similarity index 71% rename from openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h index 5a02a51d1c..3cef8e76b2 100644 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -5,18 +5,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "OpenPypePublishInstanceFactory.generated.h" +#include "AyonPublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UAyonPublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..8d34090a15 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,204 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "Framework/Notifications/NotificationManager.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Widgets/Notifications/SNotificationList.h" + + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.ObjectPath.ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..f79c428a6d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp deleted file mode 100644 index 4b4492bd20..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "OpenPypePublishInstanceFactory.h" -#include "OpenPypePublishInstance.h" - -UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UOpenPypePublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..c89388036f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,104 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h similarity index 71% rename from openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h index 5a02a51d1c..3cef8e76b2 100644 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -5,18 +5,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "OpenPypePublishInstanceFactory.generated.h" +#include "AyonPublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UAyonPublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp new file mode 100644 index 0000000000..d1b47a19d4 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp @@ -0,0 +1,204 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.h" +#include "AssetRegistry/AssetRegistryModule.h" +#include "AssetToolsModule.h" +#include "Framework/Notifications/NotificationManager.h" +#include "AyonLib.h" +#include "AyonSettings.h" +#include "Widgets/Notifications/SNotificationList.h" + + +//Moves all the invalid pointers to the end to prepare them for the shrinking +#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ + VAR.Shrink(); + +UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) + : UPrimaryDataAsset(ObjectInitializer) +{ + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< + FAssetRegistryModule>("AssetRegistry"); + + const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( + "PropertyEditor"); + + FString Left, Right; + GetPathName().Split("/" + GetName(), &Left, &Right); + + FARFilter Filter; + Filter.PackagePaths.Emplace(FName(Left)); + + TArray FoundAssets; + AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); + + for (const FAssetData& AssetData : FoundAssets) + OnAssetCreated(AssetData); + + REMOVE_INVALID_ENTRIES(AssetDataInternal) + REMOVE_INVALID_ENTRIES(AssetDataExternal) + + AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); + AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); + AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); + +#ifdef WITH_EDITOR + ColorAyonDirs(); +#endif +} + +void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) +{ + TArray split; + + UObject* Asset = InAssetData.GetAsset(); + + if (!IsValid(Asset)) + { + UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), + *InAssetData.GetSoftObjectPath().ToString()); + return; + } + + const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; + + if (result) + { + if (AssetDataInternal.Emplace(Asset).IsValidId()) + { + UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), + *this->GetName(), *Asset->GetName()); + } + } +} + +void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) +{ + if (Cast(InAssetData.GetAsset()) == nullptr) + { + if (AssetDataInternal.Contains(nullptr)) + { + AssetDataInternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataInternal) + } + else + { + AssetDataExternal.Remove(nullptr); + REMOVE_INVALID_ENTRIES(AssetDataExternal) + } + } +} + +void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) +{ + REMOVE_INVALID_ENTRIES(AssetDataInternal); + REMOVE_INVALID_ENTRIES(AssetDataExternal); +} + +bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const +{ + FString ThisLeft, ThisRight; + this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); + + return InAsset->GetPathName().StartsWith(ThisLeft); +} + +#ifdef WITH_EDITOR + +void UAyonPublishInstance::ColorAyonDirs() +{ + FString PathName = this->GetPathName(); + + //Check whether the path contains the defined Ayon folder + if (!PathName.Contains(TEXT("Ayon"))) return; + + //Get the base path for open pype + FString PathLeft, PathRight; + PathName.Split(FString("Ayon"), &PathLeft, &PathRight); + + if (PathLeft.IsEmpty() || PathRight.IsEmpty()) + { + UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) + return; + } + + PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); + + //Get the current settings + const UAyonSettings* Settings = GetMutableDefault(); + + //Color the base folder + UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); + + //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings + const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( + "AssetRegistry"); + + TArray PathList; + + AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); + + if (PathList.Num() > 0) + { + for (const FString& Path : PathList) + { + UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); + } + } +} + +void UAyonPublishInstance::SendNotification(const FString& Text) const +{ + FNotificationInfo Info{FText::FromString(Text)}; + + Info.bFireAndForget = true; + Info.bUseLargeFont = false; + Info.bUseThrobber = false; + Info.bUseSuccessFailIcons = false; + Info.ExpireDuration = 4.f; + Info.FadeOutDuration = 2.f; + + FSlateNotificationManager::Get().AddNotification(Info); + + UE_LOG(LogAssetData, Warning, + TEXT( + "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" + ), *GetName() + ) +} + + +void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) +{ + Super::PostEditChangeProperty(PropertyChangedEvent); + + if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && + PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( + UAyonPublishInstance, AssetDataExternal)) + { + // Check for duplicated assets + for (const auto& Asset : AssetDataInternal) + { + if (AssetDataExternal.Contains(Asset)) + { + AssetDataExternal.Remove(Asset); + return SendNotification( + "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); + } + } + + // Check if no UAyonPublishInstance type assets are included + for (const auto& Asset : AssetDataExternal) + { + if (Cast(Asset.Get()) != nullptr) + { + AssetDataExternal.Remove(Asset); + return SendNotification("You are not allowed to add publish instances!"); + } + } + } +} + +#endif diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp new file mode 100644 index 0000000000..f79c428a6d --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp @@ -0,0 +1,23 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#include "AyonPublishInstanceFactory.h" +#include "AyonPublishInstance.h" + +UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) + : UFactory(ObjectInitializer) +{ + SupportedClass = UAyonPublishInstance::StaticClass(); + bCreateNew = false; + bEditorImport = true; +} + +UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) +{ + check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); + return NewObject(InParent, InClass, InName, Flags); +} + +bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { + return false; +} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp deleted file mode 100644 index 4b4492bd20..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "OpenPypePublishInstanceFactory.h" -#include "OpenPypePublishInstance.h" - -UOpenPypePublishInstanceFactory::UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UOpenPypePublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UOpenPypePublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UOpenPypePublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UOpenPypePublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h new file mode 100644 index 0000000000..c89388036f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h @@ -0,0 +1,104 @@ +// Copyright 2023, Ayon, All rights reserved. +// Deprecation warning: this is left here just for backwards compatibility +// and will be removed in next versions of Ayon. +#pragma once + +#include "AyonPublishInstance.generated.h" + + +UCLASS(Blueprintable) +class AYON_API UAyonPublishInstance : public UPrimaryDataAsset +{ + GENERATED_UCLASS_BODY() + +public: + /** + /** + * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is + * placed in) + * + * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetInternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataInternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Retrieves all the assets which have been added manually by the Publish Instance + * + * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetExternalAssets() const + { + //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. + TSet ResultSet; + + for (const auto& Asset : AssetDataExternal) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + + /** + * Function for returning all the assets in the container combined. + * + * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are + * returning raw pointers. Seems like an issue in UE5 + * + * @attention If the bAddExternalAssets variable is false, external assets won't be included! + */ + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") + TSet GetAllAssets() const + { + const TSet>& IteratedSet = bAddExternalAssets + ? AssetDataInternal.Union(AssetDataExternal) + : AssetDataInternal; + + //Create a new TSet only with raw pointers. + TSet ResultSet; + + for (auto& Asset : IteratedSet) + ResultSet.Add(Asset.LoadSynchronous()); + + return ResultSet; + } + +private: + UPROPERTY(VisibleAnywhere, Category="Assets") + TSet> AssetDataInternal; + + /** + * This property allows exposing the array to include other assets from any other directory than what it's currently + * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! + */ + UPROPERTY(EditAnywhere, Category = "Assets") + bool bAddExternalAssets = false; + + UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") + TSet> AssetDataExternal; + + + void OnAssetCreated(const FAssetData& InAssetData); + void OnAssetRemoved(const FAssetData& InAssetData); + void OnAssetUpdated(const FAssetData& InAssetData); + + bool IsUnderSameDir(const UObject* InAsset) const; + +#ifdef WITH_EDITOR + + void ColorAyonDirs(); + + void SendNotification(const FString& Text) const; + virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; + +#endif +}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h similarity index 71% rename from openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h index 5a02a51d1c..3cef8e76b2 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h @@ -5,18 +5,18 @@ #include "CoreMinimal.h" #include "Factories/Factory.h" -#include "OpenPypePublishInstanceFactory.generated.h" +#include "AyonPublishInstanceFactory.generated.h" /** * */ UCLASS() -class AYON_API UOpenPypePublishInstanceFactory : public UFactory +class AYON_API UAyonPublishInstanceFactory : public UFactory { GENERATED_BODY() public: - UOpenPypePublishInstanceFactory(const FObjectInitializer& ObjectInitializer); + UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; virtual bool ShouldShowInNewMenu() const override; }; From 1e26b177261a0dfe9ce5baf43ba43ecec22eb735 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:20:36 +0200 Subject: [PATCH 520/918] Fusion: rewrite collecting renders --- .../publish/collect_expected_frames.py | 50 ----- .../plugins/publish/collect_fusion_version.py | 22 -- .../plugins/publish/collect_instances.py | 28 --- .../fusion/plugins/publish/collect_render.py | 206 ++++++++++++++++++ .../fusion/plugins/publish/collect_renders.py | 33 --- 5 files changed, 206 insertions(+), 133 deletions(-) delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_expected_frames.py delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_fusion_version.py create mode 100644 openpype/hosts/fusion/plugins/publish/collect_render.py delete mode 100644 openpype/hosts/fusion/plugins/publish/collect_renders.py diff --git a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py b/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py deleted file mode 100644 index 0ba777629f..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_expected_frames.py +++ /dev/null @@ -1,50 +0,0 @@ -import pyblish.api -from openpype.pipeline import publish -import os - - -class CollectFusionExpectedFrames( - pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin -): - """Collect all frames needed to publish expected frames""" - - order = pyblish.api.CollectorOrder + 0.5 - label = "Collect Expected Frames" - hosts = ["fusion"] - families = ["render"] - - def process(self, instance): - context = instance.context - - frame_start = context.data["frameStartHandle"] - frame_end = context.data["frameEndHandle"] - path = instance.data["path"] - output_dir = instance.data["outputDir"] - - basename = os.path.basename(path) - head, ext = os.path.splitext(basename) - files = [ - f"{head}{str(frame).zfill(4)}{ext}" - for frame in range(frame_start, frame_end + 1) - ] - repre = { - "name": ext[1:], - "ext": ext[1:], - "frameStart": f"%0{len(str(frame_end))}d" % frame_start, - "files": files, - "stagingDir": output_dir, - } - - self.set_representation_colorspace( - representation=repre, - context=context, - ) - - # review representation - if instance.data.get("review", False): - repre["tags"] = ["review"] - - # add the repre to the instance - if "representations" not in instance.data: - instance.data["representations"] = [] - instance.data["representations"].append(repre) diff --git a/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py b/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py deleted file mode 100644 index 65d8386f33..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_fusion_version.py +++ /dev/null @@ -1,22 +0,0 @@ -import pyblish.api - - -class CollectFusionVersion(pyblish.api.ContextPlugin): - """Collect current comp""" - - order = pyblish.api.CollectorOrder - label = "Collect Fusion Version" - hosts = ["fusion"] - - def process(self, context): - """Collect all image sequence tools""" - - comp = context.data.get("currentComp") - if not comp: - raise RuntimeError("No comp previously collected, unable to " - "retrieve Fusion version.") - - version = comp.GetApp().Version - context.data["fusionVersion"] = version - - self.log.info("Fusion version: %s" % version) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index af227f03db..4608f79420 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -49,31 +49,3 @@ class CollectInstanceData(pyblish.api.InstancePlugin): if instance.data.get("review", False): self.log.info("Adding review family..") instance.data["families"].append("review") - - if instance.data["family"] == "render": - # TODO: This should probably move into a collector of - # its own for the "render" family - from openpype.hosts.fusion.api.lib import get_frame_path - comp = context.data["currentComp"] - - # This is only the case for savers currently but not - # for workfile instances. So we assume saver here. - tool = instance.data["transientData"]["tool"] - path = tool["Clip"][comp.TIME_UNDEFINED] - - filename = os.path.basename(path) - head, padding, tail = get_frame_path(filename) - ext = os.path.splitext(path)[1] - assert tail == ext, ("Tail does not match %s" % ext) - - instance.data.update({ - "path": path, - "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant? - - # Backwards compatibility: embed tool in instance.data - "tool": tool - }) - - # Add tool itself as member - instance.append(tool) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py new file mode 100644 index 0000000000..87c1d952e8 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -0,0 +1,206 @@ +import os +from pprint import pformat +import attr +import pyblish.api + +from openpype.pipeline import publish +from openpype.pipeline.publish import RenderInstance +from openpype.hosts.fusion.api.lib import get_frame_path + + +@attr.s +class FusionRenderInstance(RenderInstance): + # extend generic, composition name is needed + fps = attr.ib(default=None) + projectEntity = attr.ib(default=None) + stagingDir = attr.ib(default=None) + app_version = attr.ib(default=None) + toolSaver = attr.ib(default=None) + workfileComp = attr.ib(default=None) + publish_attributes = attr.ib(default={}) + + +class CollectFusionRender( + publish.AbstractCollectRender, + publish.ColormanagedPyblishPluginMixin +): + + order = pyblish.api.CollectorOrder + 0.09 + label = "Collect Fusion Render" + hosts = ["fusion"] + + def get_instances(self, context): + + comp = context.data.get("currentComp") + comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") + aspect_x = comp_frame_format_prefs.get("AspectX") + aspect_y = comp_frame_format_prefs.get("AspectY") + + instances = [] + instances_to_remove = [] + + current_file = context.data["currentFile"] + version = context.data["version"] + + project_entity = context.data["projectEntity"] + + for inst in context: + if not inst.data.get("active", True): + continue + + family = inst.data["family"] + if family not in ["render"]: + continue + + task_name = inst.data.get("task") # legacy + tool = inst.data["transientData"]["tool"] + + instance_families = inst.data.get("families", []) + subset_name = inst.data["subset"] + instance = FusionRenderInstance( + family="render", + toolSaver=tool, + workfileComp=comp, + families=instance_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, + name=subset_name, + resolutionWidth=comp_frame_format_prefs.get("Width"), + resolutionHeight=comp_frame_format_prefs.get("Height"), + pixelAspect=aspect_x / aspect_y, + tileRendering=False, + tilesX=0, + tilesY=0, + review="review" in instance_families, + frameStart=context.data["frameStart"], + frameEnd=context.data["frameEnd"], + handleStart=context.data["handleStart"], + handleEnd=context.data["handleEnd"], + frameStep=1, + fps=comp_frame_format_prefs.get("Rate"), + app_version=comp.GetApp().Version, + publish_attributes=inst.data.get("publish_attributes", {}) + ) + + render_target = inst.data["creator_attributes"]["render_target"] + self.log.debug("render_target: '{}'".format(render_target)) + + if render_target == "local": + # for local renders + self._instance_data_local_update( + project_entity, instance, f"render.{render_target}") + + if render_target == "frames": + self._instance_data_local_update( + project_entity, instance, f"render.{render_target}") + + if render_target == "farm": + fam = "render.farm" + if fam not in instance.families: + instance.families.append(fam) + instance.toBeRenderedOn = "deadline" + instance.farm = True # to skip integrate + if "review" in instance.families: + # to skip ExtractReview locally + instance.families.remove("review") + + instances.append(instance) + instances_to_remove.append(inst) + + for instance in instances_to_remove: + context.remove(instance) + + return instances + + def post_collecting_action(self): + for instance in self._context: + if "render.frames" in instance.data.get("families", []): + self._update_for_frames(instance) + self.log.debug(pformat(instance.data)) + + def get_expected_files(self, render_instance): + """ + Returns list of rendered files that should be created by + Deadline. These are not published directly, they are source + for later 'submit_publish_job'. + + Args: + render_instance (RenderInstance): to pull anatomy and parts used + in url + + Returns: + (list) of absolute urls to rendered file + """ + start = render_instance.frameStart - render_instance.handleStart + end = render_instance.frameEnd + render_instance.handleEnd + + path = ( + render_instance.toolSaver["Clip"] + [render_instance.workfileComp.TIME_UNDEFINED] + ) + output_dir = os.path.dirname(path) + render_instance.outputDir = output_dir + + basename = os.path.basename(path) + + head, padding, ext = get_frame_path(basename) + + expected_files = [] + for frame in range(start, end + 1): + expected_files.append( + os.path.join( + output_dir, + f"{head}{str(frame).zfill(padding)}{ext}" + ) + ) + + return expected_files + + def _update_for_frames(self, instance): + """Update old saved instances to current publishing format""" + + expected_files = instance.data["expectedFiles"] + + start = instance.data["frameStart"] - instance.data["handleStart"] + + path = expected_files[0] + basename = os.path.basename(path) + staging_dir = os.path.dirname(path) + _, padding, ext = get_frame_path(basename) + + repre = { + "name": ext[1:], + "ext": ext[1:], + "frameStart": f"%0{padding}d" % start, + "files": [os.path.basename(f) for f in expected_files], + "stagingDir": staging_dir, + } + + self.set_representation_colorspace( + representation=repre, + context=instance.context, + ) + + # review representation + if instance.data.get("review", False): + repre["tags"] = ["review"] + + # add the repre to the instance + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(repre) + + return instance + + def _instance_data_local_update(self, project_entity, instance, family): + instance.projectEntity = project_entity + if family not in instance.families: + instance.families.append(family) diff --git a/openpype/hosts/fusion/plugins/publish/collect_renders.py b/openpype/hosts/fusion/plugins/publish/collect_renders.py deleted file mode 100644 index b1c12c7393..0000000000 --- a/openpype/hosts/fusion/plugins/publish/collect_renders.py +++ /dev/null @@ -1,33 +0,0 @@ -import pyblish.api - - -class CollectFusionRenders(pyblish.api.InstancePlugin): - """Collect current saver node's render Mode - - Options: - local (Render locally) - frames (Use existing frames) - - """ - - order = pyblish.api.CollectorOrder + 0.4 - label = "Collect Renders" - hosts = ["fusion"] - families = ["render"] - - def process(self, instance): - render_target = instance.data["render_target"] - family = instance.data["family"] - - # add targeted family to families - instance.data["families"].append( - "{}.{}".format(family, render_target) - ) - if render_target == "farm": - if "review" in instance.data["families"]: - instance.data["families"].remove("review") - - # Farm rendering - instance.data["transfer"] = False - instance.data["farm"] = True - self.log.info("Farm rendering ON ...") From 7013be47de335bcb9d118d60e67d234360839d8b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:21:16 +0200 Subject: [PATCH 521/918] Fusion: refactor validators to work with new collected data --- .../publish/validate_create_folder_checked.py | 2 +- .../validate_expected_frames_existence.py | 27 ++++++------------- .../validate_filename_has_extension.py | 4 +-- .../publish/validate_saver_has_input.py | 2 +- .../publish/validate_saver_passthrough.py | 2 +- .../publish/validate_unique_subsets.py | 2 +- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 8a91f23578..82d34b0b5d 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -21,7 +21,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - tool = instance[0] + tool = instance.data["toolSaver"] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: cls.log.error( diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index c208b8ef15..befaae13be 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -14,7 +14,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Expected Frames Exists" - families = ["render"] + families = ["render.frames"] hosts = ["fusion"] actions = [RepairAction, SelectInvalidAction] @@ -23,25 +23,15 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - if instance.data.get("render_target") == "frames": - tool = instance[0] + if "render.frames" in instance.data.get("families", []): + tool = instance.data["toolSaver"] - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - path = instance.data["path"] - output_dir = instance.data["outputDir"] + expected_files = instance.data["expectedFiles"] - basename = os.path.basename(path) - head, ext = os.path.splitext(basename) - files = [ - f"{head}{str(frame).zfill(4)}{ext}" - for frame in range(frame_start, frame_end + 1) - ] - - for file in files: - if not os.path.exists(os.path.join(output_dir, file)): + for file in expected_files: + if not os.path.exists(file): cls.log.error( - f"Missing file: {os.path.join(output_dir, file)}" + f"Missing file: {file}" ) non_existing_frames.append(file) @@ -67,8 +57,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) if invalid: - tool = invalid[0] - + tool = instance.data["toolSaver"] # Change render target to local to render locally tool.SetData("openpype.creator_attributes.render_target", "local") diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index bbba2dde6e..1bf603e5b1 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -30,11 +30,11 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - path = instance.data["path"] + path = instance.data["expectedFiles"][0] fname, ext = os.path.splitext(path) if not ext: - tool = instance[0] + tool = instance.data["toolSaver"] cls.log.error("%s has no extension specified" % tool.Name) return [tool] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index e02125f531..b409608ec3 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -20,7 +20,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - saver = instance[0] + saver = instance.data["toolSaver"] if not saver.Input.GetConnectedOutput(): return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 56f2e7e6b8..677861a654 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -37,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): def is_invalid(self, instance): - saver = instance[0] + saver = instance.data["toolSaver"] attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 5b6ceb2fdb..6a65182fae 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -43,7 +43,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): invalid.extend(instances) # Return tools for the invalid instances so they can be selected - invalid = [instance.data["tool"] for instance in invalid] + invalid = [instance.data["toolSaver"] for instance in invalid] return invalid From b8e8ce66606d643e82bccf3d8864063581754867 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:22:01 +0200 Subject: [PATCH 522/918] fusion: rewriting render local to work with new instance data also adding colorspace data to representation --- .../plugins/publish/extract_render_local.py | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 5a0140c525..c2e38884c7 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,8 +1,11 @@ +import os import logging import contextlib import pyblish.api -from openpype.hosts.fusion.api import comp_lock_and_undo_chunk +from openpype.pipeline import publish +from openpype.hosts.fusion.api import comp_lock_and_undo_chunk +from openpype.hosts.fusion.api.lib import get_frame_path log = logging.getLogger(__name__) @@ -38,7 +41,10 @@ def enabled_savers(comp, savers): saver.SetAttrs({"TOOLB_PassThrough": original_state}) -class FusionRenderLocal(pyblish.api.InstancePlugin): +class FusionRenderLocal( + pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin +): """Render the current Fusion composition locally.""" order = pyblish.api.ExtractorOrder - 0.2 @@ -52,6 +58,8 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): # Start render self.render_once(context) + self._add_representation(instance) + # Log render status self.log.info( "Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format( @@ -71,11 +79,11 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): savers_to_render = [ # Get the saver tool from the instance - instance[0] for instance in context if + instance.data["toolSaver"] for instance in context if # Only active instances instance.data.get("publish", True) and # Only render.local instances - "render.local" in instance.data["families"] + "render.local" in instance.data.get("families") ] if key not in context.data: @@ -107,3 +115,39 @@ class FusionRenderLocal(pyblish.api.InstancePlugin): if context.data[key] is False: raise RuntimeError("Comp render failed") + + def _add_representation(self, instance): + """Add representation to instance""" + + expected_files = instance.data["expectedFiles"] + + start = instance.data["frameStart"] - instance.data["handleStart"] + + path = expected_files[0] + _, padding, ext = get_frame_path(path) + + staging_dir = os.path.dirname(path) + + repre = { + "name": ext[1:], + "ext": ext[1:], + "frameStart": f"%0{padding}d" % start, + "files": [os.path.basename(f) for f in expected_files], + "stagingDir": staging_dir, + } + + self.set_representation_colorspace( + representation=repre, + context=instance.context, + ) + + # review representation + if instance.data.get("review", False): + repre["tags"] = ["review"] + + # add the repre to the instance + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(repre) + + return instance From aace680fa19dee09dcb19d9692fd75ec4e6859ab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 9 May 2023 17:22:31 +0200 Subject: [PATCH 523/918] fusion deadline, rewriting to new instance data --- .../plugins/publish/submit_fusion_deadline.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 2885d91d07..092c317ce3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -96,8 +96,6 @@ class FusionSubmitDeadline( instance.data["suspend_publish"] = attribute_values[ "suspend_publish"] - instance.data["toBeRenderedOn"] = "deadline" - context = instance.context key = "__hasRun{}".format(self.__class__.__name__) @@ -132,8 +130,7 @@ class FusionSubmitDeadline( if not saver_instances: raise RuntimeError("No instances found for Deadline submittion") - fusion_version = int(context.data["fusionVersion"]) - comment = context.data.get("comment", "") + comment = instance.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) script_path = context.data["currentFile"] @@ -201,7 +198,7 @@ class FusionSubmitDeadline( "FlowFile": script_path, # Mandatory for Deadline - "Version": str(fusion_version), + "Version": str(instance.data["app_version"]), # Render in high quality "HighQuality": True, @@ -225,7 +222,9 @@ class FusionSubmitDeadline( # Enable going to rendered frames from Deadline Monitor for index, instance in enumerate(saver_instances): - head, padding, tail = get_frame_path(instance.data["path"]) + head, padding, tail = get_frame_path( + instance.data["expectedFiles"][0] + ) path = "{}{}{}".format(head, "#" * padding, tail) folder, filename = os.path.split(path) payload["JobInfo"]["OutputDirectory%d" % index] = folder From 33335a3e89c0c4d96af93c8983c29160f59a6135 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 9 May 2023 23:25:37 +0800 Subject: [PATCH 524/918] fix the update and load function of the max scene loader --- .../hosts/max/plugins/load/load_max_scene.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 4b19cd671f..cffc4ae559 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -26,20 +26,16 @@ class MaxSceneLoader(load.LoaderPlugin): merge_before = { c for c in rt.rootNode.Children - if rt.classOf(c) == rt.Container } rt.mergeMaxFile(path) merge_after = { c for c in rt.rootNode.Children - if rt.classOf(c) == rt.Container } - max_containers = merge_after.difference(merge_before) - - if len(max_containers) != 1: - self.log.error("Something failed when loading.") - - max_container = max_containers.pop() + max_objects = merge_after.difference(merge_before) + max_container = rt.container(name=f"{name}") + for max_object in max_objects: + max_object.Parent = max_container return containerise( name, [max_container], context, loader=self.__class__.__name__) @@ -48,15 +44,30 @@ class MaxSceneLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) - max_objects = node.Children + node_name = container["instance_node"] + instance_name, _ = node_name.split("_") + merge_before = { + c for c in rt.rootNode.Children + } + rt.mergeMaxFile(path, + rt.Name("noRedraw"), + rt.Name("deleteOldDups"), + rt.Name("useSceneMtlDups")) + merge_after = { + c for c in rt.rootNode.Children + } + max_objects = merge_after.difference(merge_before) + container_node = rt.getNodeByName(instance_name) for max_object in max_objects: - max_object.source = path + max_object.Parent = container_node lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): from pymxs import runtime as rt From 11691f091c3beccf9d7035b87596efe4e3813e8b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 May 2023 17:52:26 +0100 Subject: [PATCH 525/918] Fix hound issues --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 3 --- openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 085f80209d..f01609d314 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -107,9 +107,6 @@ class UnrealPrelaunchHook(PreLaunchHook): f"project [ {unreal_project_name} ]" )) - import openpype.hosts.unreal.lib as ue_lib - path = ue_lib.get_path_to_cmdlet_project(engine_version) - q_thread = QtCore.QThread() ue_project_worker = UEProjectGenerationWorker() ue_project_worker.setup( diff --git a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py index 8b1b9d8f9e..3a292fdbd1 100644 --- a/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -22,7 +22,8 @@ class PointCacheAlembicLoader(plugin.Loader): color = "orange" def get_task( - self, filename, asset_dir, asset_name, replace, frame_start=None, frame_end=None + self, filename, asset_dir, asset_name, replace, + frame_start=None, frame_end=None ): task = unreal.AssetImportTask() options = unreal.AbcImportSettings() From bbe5dbb14b8c8cb50a88ac945516fc3be0df0d9f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 10 May 2023 03:25:27 +0000 Subject: [PATCH 526/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index e02053ba76..7df154fe1e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7-nightly.1" +__version__ = "3.15.7-nightly.2" From 28bf443a29b95e78d18d08e844b9ca2e0750829a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 May 2023 03:26:09 +0000 Subject: [PATCH 527/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index cae6a6486b..0d75b669d2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7-nightly.2 - 3.15.7-nightly.1 - 3.15.6 - 3.15.6-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.3 - 3.14.1-nightly.2 - 3.14.1-nightly.1 - - 3.14.0 validations: required: true - type: dropdown From 6252e4b6c5c57e2a0f9f60fd0ad53cdcb7d26c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 10 May 2023 10:07:12 +0200 Subject: [PATCH 528/918] skipping if python script doesn't exist --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 0f959b8f54..8015a15de8 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -24,8 +24,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): # Test script path exists python_script_path = Path(python_script_path) if not python_script_path.exists(): - raise ValueError( - f"Python script {python_script_path} doesn't exist." + raise self.log.warning( + f"Python script {python_script_path} doesn't exist. Skipped..." ) if "--" in self.launch_context.launch_args: From 52ad442a9cacdd7675bb49c9fbcd144a2400df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 10 May 2023 10:10:12 +0200 Subject: [PATCH 529/918] lint --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 8015a15de8..2d1b773c5f 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -25,7 +25,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): python_script_path = Path(python_script_path) if not python_script_path.exists(): raise self.log.warning( - f"Python script {python_script_path} doesn't exist. Skipped..." + f"Python script {python_script_path} doesn't exist. " + "Skipped..." ) if "--" in self.launch_context.launch_args: From b09efa96756ecb02cd0c1ae8b1183c692ff38147 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:18:30 +0200 Subject: [PATCH 530/918] fusion: storing asset frame attribute at comp openpype_instance data --- openpype/hosts/fusion/api/lib.py | 7 ++++ .../publish/collect_comp_frame_range.py | 38 ++++++++++++++----- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 40cc4d2963..8f7b29f0c4 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -51,6 +51,12 @@ def update_frame_range(start, end, comp=None, set_render_range=True, "COMPN_GlobalStart": start - handle_start, "COMPN_GlobalEnd": end + handle_end } + frame_data = { + "frameStart": start, + "frameEnd": end, + "handleStart": handle_start, + "handleEnd": handle_end + } # set frame range if set_render_range: @@ -61,6 +67,7 @@ def update_frame_range(start, end, comp=None, set_render_range=True, with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) + comp.SetData("openpype_instance", frame_data) def set_asset_framerange(): diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index fbd7606cd7..2db0002ee6 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -9,14 +9,20 @@ def get_comp_render_range(comp): global_start = comp_attrs["COMPN_GlobalStart"] global_end = comp_attrs["COMPN_GlobalEnd"] + frame_data = comp.GetData("openpype_instance") + handle_start = frame_data.get("handleStart", 0) + handle_end = frame_data.get("handleEnd", 0) + frame_start = frame_data.get("frameStart", 0) + frame_end = frame_data.get("frameEnd", 0) + # Whenever render ranges are undefined fall back # to the comp's global start and end if start == -1000000000: - start = global_start + start = frame_start if end == -1000000000: - end = global_end + end = frame_end - return start, end, global_start, global_end + return start, end, global_start, global_end, handle_start, handle_end class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): @@ -34,10 +40,22 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): comp = context.data["currentComp"] # Store comp render ranges - start, end, global_start, global_end = get_comp_render_range(comp) - context.data["frameStart"] = int(start) - context.data["frameEnd"] = int(end) - context.data["frameStartHandle"] = int(global_start) - context.data["frameEndHandle"] = int(global_end) - context.data["handleStart"] = int(start) - int(global_start) - context.data["handleEnd"] = int(global_end) - int(end) + ( + start, end, + global_start, + global_end, + handle_start, + handle_end + ) = get_comp_render_range(comp) + + data = {} + data["frameStart"] = int(start) + data["frameEnd"] = int(end) + data["frameStartHandle"] = int(global_start) + data["frameEndHandle"] = int(global_end) + data["handleStart"] = int(handle_start) + data["handleEnd"] = int(handle_end) + + self.log.debug("_ data: {}".format(data)) + + context.data.update(data) From 903d873dbe253e92b3b805f798b21c374af0a661 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:19:08 +0200 Subject: [PATCH 531/918] fusion: frame ranges taken from instance --- .../hosts/fusion/plugins/publish/collect_render.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 87c1d952e8..5adb8a13f0 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -49,7 +49,7 @@ class CollectFusionRender( continue family = inst.data["family"] - if family not in ["render"]: + if family != "render": continue task_name = inst.data.get("task") # legacy @@ -80,10 +80,11 @@ class CollectFusionRender( tilesX=0, tilesY=0, review="review" in instance_families, - frameStart=context.data["frameStart"], - frameEnd=context.data["frameEnd"], - handleStart=context.data["handleStart"], - handleEnd=context.data["handleEnd"], + frameStart=inst.data["frameStart"], + frameEnd=inst.data["frameEnd"], + handleStart=inst.data["handleStart"], + handleEnd=inst.data["handleEnd"], + ignoreFrameHandleCheck=True, frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 9cbbbe818ca7df153ea00f04a452705d9279efca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:19:40 +0200 Subject: [PATCH 532/918] deadline fusion: frame range taken from handles version --- .../deadline/plugins/publish/submit_fusion_deadline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 092c317ce3..891bfde6c5 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -128,7 +128,7 @@ class FusionSubmitDeadline( saver_instances.append(instance) if not saver_instances: - raise RuntimeError("No instances found for Deadline submittion") + raise RuntimeError("No instances found for Deadline submission") comment = instance.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) @@ -187,8 +187,8 @@ class FusionSubmitDeadline( "Plugin": "Fusion", "Frames": "{start}-{end}".format( - start=int(context.data["frameStart"]), - end=int(context.data["frameEnd"]) + start=int(instance.data["frameStartHandle"]), + end=int(instance.data["frameEndHandle"]) ), "Comment": comment, From 6ea1ab1c9f3da8d06bea10bb3b2b8a3305139120 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 11:20:09 +0200 Subject: [PATCH 533/918] deadline submitter settings for aov filter --- openpype/settings/defaults/project_settings/deadline.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index fdd70f1a44..3f114025f3 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -114,6 +114,9 @@ ], "max": [ ".*" + ], + "fusion": [ + ".*" ] } } From 55bebf86426cf467f6f4268882ec00c7b44234e2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 12:33:38 +0200 Subject: [PATCH 534/918] pr comments --- openpype/hosts/fusion/plugins/create/create_saver.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 8bf364cf20..7378dec2b4 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,6 +1,5 @@ from copy import deepcopy import os -from pprint import pformat from openpype.hosts.fusion.api import ( get_current_comp, @@ -139,7 +138,6 @@ class CreateSaver(NewCreator): def _configure_saver_tool(self, data, tool, subset): formatting_data = deepcopy(data) - self.log.warning(pformat(formatting_data)) # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) @@ -274,7 +272,9 @@ class CreateSaver(NewCreator): """Method called on initialization of plugin to apply settings.""" # plugin settings - plugin_settings = self._get_creator_settings(project_settings) + plugin_settings = ( + project_settings["fusion"]["create"][self.__class__.__name__] + ) # individual attributes self.instance_attributes = plugin_settings.get( @@ -285,8 +285,3 @@ class CreateSaver(NewCreator): plugin_settings.get("temp_rendering_path_template") or self.temp_rendering_path_template ) - - def _get_creator_settings(self, project_settings, settings_key=None): - if not settings_key: - settings_key = self.__class__.__name__ - return project_settings["fusion"]["create"][settings_key] From 98d27fa5a403143e5910d343ccc62af8b5e8cbb1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 12:54:30 +0200 Subject: [PATCH 535/918] fusion: frame padding from anatomy templates --- .../hosts/fusion/plugins/create/create_saver.py | 16 +++++++++++++--- .../defaults/project_settings/fusion.json | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 7378dec2b4..fb05d13597 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -5,6 +5,7 @@ from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, ) +from openpype.hosts.fusion.api.lib import get_frame_path from openpype.lib import ( BoolDef, @@ -14,6 +15,7 @@ from openpype.pipeline import ( legacy_io, Creator as NewCreator, CreatedInstance, + Anatomy ) from openpype.client import ( get_asset_by_name, @@ -37,7 +39,7 @@ class CreateSaver(NewCreator): "Mask" ] temp_rendering_path_template = ( - "{workdir}/renders/fusion/{subset}/{subset}..{ext}") + "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): # TODO: Add pre_create attributes to choose file format? @@ -139,10 +141,17 @@ class CreateSaver(NewCreator): def _configure_saver_tool(self, data, tool, subset): formatting_data = deepcopy(data) + # get frame padding from anatomy templates + anatomy = Anatomy() + frame_padding = int( + anatomy.templates["render"].get("frame_padding", 4) + ) + # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) formatting_data.update({ "workdir": workdir.replace("\\", "/"), + "frame": "0" * frame_padding, "ext": "exr" }) @@ -180,8 +189,9 @@ class CreateSaver(NewCreator): path = tool["Clip"][comp.TIME_UNDEFINED] fname = os.path.basename(path) - fname, _ext = os.path.splitext(fname) - variant = fname.rstrip(".") + head, _, _ = get_frame_path(fname) + + variant = head.rstrip(".") subset = self.get_subset_name( variant=variant, task_name=task, diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index d76ed82942..066fc3816a 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -24,7 +24,7 @@ }, "create": { "CreateSaver": { - "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}..{ext}", + "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}", "default_variants": [ "Main", "Mask" From a08f9176b0bc26d9a5f32968f8643966f59ad502 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 12:55:30 +0200 Subject: [PATCH 536/918] fusion: removing path making during creation --- openpype/hosts/fusion/plugins/create/create_saver.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index fb05d13597..64a99d07c1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -159,11 +159,6 @@ class CreateSaver(NewCreator): filepath = self.temp_rendering_path_template.format( **formatting_data) - # create directory - if not os.path.isdir(os.path.dirname(filepath)): - self.log.warning("Path does not exist! I am creating it.") - os.makedirs(os.path.dirname(filepath)) - tool["Clip"] = filepath # Rename tool From 95237c43c775f3d15b8475ffacac53fcc4d94002 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 13:00:53 +0200 Subject: [PATCH 537/918] adding todo for later renaming --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 64a99d07c1..bb4615db17 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -38,6 +38,8 @@ class CreateSaver(NewCreator): "Main", "Mask" ] + + # TODO: This should be renamed together with Nuke so it is aligned temp_rendering_path_template = ( "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") From da6b2b31335cefbce32289ac11fa5432666e96d8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 10 May 2023 13:22:38 +0200 Subject: [PATCH 538/918] :bug: fix use and detection of compatible integrations --- openpype/hosts/unreal/addon.py | 13 +++++- openpype/hosts/unreal/lib.py | 75 ++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 0c42755d37..4468ce036c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,5 +1,8 @@ import os -from openpype.modules import OpenPypeModule, IHostAddon +from pathlib import Path + +from openpype.modules import IHostAddon, OpenPypeModule +from .lib import get_compatible_integration UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -19,9 +22,15 @@ class UnrealAddon(OpenPypeModule, IHostAddon): unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) + if not Path(unreal_plugin_path).exists(): + if compatible_versions := get_compatible_integration( + ue_version, Path(UNREAL_ROOT_DIR) / "integration" + ): + unreal_plugin_path = compatible_versions[-1] / "Ayon" + unreal_plugin_path = unreal_plugin_path.as_posix() if not env.get("AYON_UNREAL_PLUGIN") or \ - env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: + env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 38976c3ef1..821b4daecc 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -321,6 +321,47 @@ def get_path_to_uat(engine_path: Path) -> Path: return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" +def get_compatible_integration( + ue_version: str, integration_root: Path) -> List[Path]: + """Get path to compatible version of integration plugin. + + This will try to get the closest compatible versions to the one + specified in sorted list. + + Args: + ue_version (str): version of the current Unreal Engine. + integration_root (Path): path to built-in integration plugins. + + Returns: + list of Path: Sorted list of paths closest to the specified + version. + + """ + major, minor = ue_version.split(".") + integration_paths = [p for p in integration_root.iterdir() + if p.is_dir()] + + compatible_versions = [] + for i in integration_paths: + # parse version from path + try: + i_major, i_minor = re.search( + r"(?P\d+).(?P\d+)$", i.name).groups() + except AttributeError: + # in case there is no match, just skip to next + continue + + # consider versions with different major so different that they + # are incompatible + if int(major) != int(i_major): + continue + + compatible_versions.append(i) + + sorted(set(compatible_versions)) + return compatible_versions + + def get_path_to_cmdlet_project(ue_version: str) -> Path: cmd_project = Path( os.path.abspath(os.getenv("OPENPYPE_ROOT"))) @@ -334,31 +375,15 @@ def get_path_to_cmdlet_project(ue_version: str) -> Path: if cmd_project.exists(): return cmd_project / "CommandletProject/CommandletProject.uproject" - major, minor = ue_version.split(".") - integration_paths = [p for p in cmd_project.parent.iterdir() - if p.is_dir()] - - compatible_versions = [cmd_project] - for i in integration_paths: - - # parse version from path - i_major, i_minor = re.search( - r"(?P\d+).(?P\d+)$", i.name).groups() - - # consider versions with different major so different that they - # are incompatible - if int(major) != int(i_major): - continue - - compatible_versions.append(i) - - sorted(set(compatible_versions)) - - - - - - return cmd_project / "CommandletProject/CommandletProject.uproject" + if compatible_versions := get_compatible_integration( + ue_version, cmd_project.parent + ): + return compatible_versions[-1] / "CommandletProject/CommandletProject.uproject" # noqa: E501 + else: + raise RuntimeError( + ("There are no compatible versions of Unreal " + "integration plugin compatible with running version " + f"of Unreal Engine {ue_version}")) def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: From ce6b02862ad9179a199588b4c56ec90c456cffa5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 10 May 2023 13:25:48 +0200 Subject: [PATCH 539/918] :rotating_light: fix fixable hound issue --- openpype/hosts/unreal/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 4468ce036c..db40d629bc 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -30,7 +30,7 @@ class UnrealAddon(OpenPypeModule, IHostAddon): unreal_plugin_path = unreal_plugin_path.as_posix() if not env.get("AYON_UNREAL_PLUGIN") or \ - env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: + env.get("AYON_UNREAL_PLUGIN") != unreal_plugin_path: env["AYON_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings From 1a48ddefe4a665997dc9e4bac066200721949d3c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 15:37:19 +0200 Subject: [PATCH 540/918] normalizing path --- openpype/hosts/fusion/plugins/create/create_saver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index bb4615db17..f924c30b0a 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -152,7 +152,7 @@ class CreateSaver(NewCreator): # Subset change detected workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) formatting_data.update({ - "workdir": workdir.replace("\\", "/"), + "workdir": workdir, "frame": "0" * frame_padding, "ext": "exr" }) @@ -161,7 +161,7 @@ class CreateSaver(NewCreator): filepath = self.temp_rendering_path_template.format( **formatting_data) - tool["Clip"] = filepath + tool["Clip"] = os.path.normpath(filepath) # Rename tool if tool.Name != subset: From cbde1425fae8466d3b3db3f201708c1f08a80e88 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 May 2023 14:37:33 +0100 Subject: [PATCH 541/918] Fix camera framerange when loading it in Unreal --- openpype/hosts/unreal/plugins/load/load_camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index c082562775..2303ed1ffc 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -268,8 +268,8 @@ class CameraLoader(plugin.Loader): data = get_asset_by_name(project_name, asset)["data"] cam_seq.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) - cam_seq.set_playback_start(0) - cam_seq.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) + cam_seq.set_playback_start(data.get('clipIn')) + cam_seq.set_playback_end(data.get('clipOut') + 1) self._set_sequence_hierarchy( sequences[-1], cam_seq, data.get('clipIn'), data.get('clipOut')) From a42fbf5a47f8fcbeca35f6f50ee0fd091d45fe36 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 May 2023 14:51:45 +0100 Subject: [PATCH 542/918] Fix missing parameter when updating alembic staticmesh --- openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py index c435b8843d..befc7b0ac9 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -135,7 +135,7 @@ class StaticMeshAlembicLoader(plugin.Loader): source_path = get_representation_path(representation) destination_path = container["namespace"] - task = self.get_task(source_path, destination_path, name, True) + task = self.get_task(source_path, destination_path, name, True, False) # do import fbx and replace existing data unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) From 27ac1b4590c2211548d94dfc5a26b902c3938c72 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 16:16:50 +0200 Subject: [PATCH 543/918] Removing unmanaged compatibility This fixes issue https://github.com/ynput/OpenPype/pull/4943#pullrequestreview-1420557288 --- .../fusion/plugins/create/create_saver.py | 48 ++++++------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index f924c30b0a..27394ae15b 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -91,7 +91,7 @@ class CreateSaver(NewCreator): for tool in tools: data = self.get_managed_tool_data(tool) if not data: - data = self._collect_unmanaged_saver(tool) + data = self._collect_saver(tool) # Add instance created_instance = CreatedInstance.from_existing(data, self) @@ -168,43 +168,23 @@ class CreateSaver(NewCreator): print(f"Renaming {tool.Name} -> {subset}") tool.SetAttrs({"TOOLS_Name": subset}) - def _collect_unmanaged_saver(self, tool): - # TODO: this should not be done this way - this should actually - # get the data as stored on the tool explicitly (however) - # that would disallow any 'regular saver' to be collected - # unless the instance data is stored on it to begin with - - print("Collecting unmanaged saver..") - comp = tool.Comp() - - # Allow regular non-managed savers to also be picked up - project = legacy_io.Session["AVALON_PROJECT"] - asset = legacy_io.Session["AVALON_ASSET"] - task = legacy_io.Session["AVALON_TASK"] - - asset_doc = get_asset_by_name(project_name=project, asset_name=asset) - - path = tool["Clip"][comp.TIME_UNDEFINED] - fname = os.path.basename(path) - head, _, _ = get_frame_path(fname) - - variant = head.rstrip(".") - subset = self.get_subset_name( - variant=variant, - task_name=task, - asset_doc=asset_doc, - project_name=project, - ) - + def _collect_saver(self, tool): + print("Collecting saver..") attrs = tool.GetAttrs() + + ctx_data = {} + keys = ["asset", "subset", "task", "variant"] + for key in keys: + ctx_data[key] = tool.GetData(f"openpype.{key}") + passthrough = attrs["TOOLB_PassThrough"] return { # Required data - "project": project, - "asset": asset, - "subset": subset, - "task": task, - "variant": variant, + "project": self.project_name, + "asset": ctx_data["asset"], + "subset": ctx_data["subset"], + "task": ctx_data["task"], + "variant": ctx_data["variant"], "active": not passthrough, "family": self.family, # Unique identifier for instance and this creator From dfea365474995dfbdffd85d296f3b8e61b3e4ffd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 16:38:17 +0200 Subject: [PATCH 544/918] moving instnance id so it is imprinted once created addressing issue form here https://github.com/ynput/OpenPype/pull/4943#issuecomment-1542241467 --- openpype/hosts/fusion/plugins/create/create_saver.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 27394ae15b..224ec0d48e 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -44,6 +44,11 @@ class CreateSaver(NewCreator): "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): + instance_data.update({ + "id": "pyblish.avalon.instance", + "subset": subset_name + }) + # TODO: Add pre_create attributes to choose file format? file_format = "OpenEXRFormat" @@ -52,7 +57,6 @@ class CreateSaver(NewCreator): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) - instance_data["subset"] = subset_name self._update_tool_with_data(saver, data=instance_data) saver["OutputFormat"] = file_format @@ -173,7 +177,7 @@ class CreateSaver(NewCreator): attrs = tool.GetAttrs() ctx_data = {} - keys = ["asset", "subset", "task", "variant"] + keys = ["id", "asset", "subset", "task", "variant"] for key in keys: ctx_data[key] = tool.GetData(f"openpype.{key}") @@ -188,7 +192,7 @@ class CreateSaver(NewCreator): "active": not passthrough, "family": self.family, # Unique identifier for instance and this creator - "id": "pyblish.avalon.instance", + "id": ctx_data["id"], "creator_identifier": self.identifier, } From f5215f323b3c3b8b76ef278de6771bfdf6b0dcca Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 10 May 2023 16:39:29 +0200 Subject: [PATCH 545/918] hound --- openpype/hosts/fusion/plugins/create/create_saver.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 224ec0d48e..ecdad30b4c 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -5,7 +5,6 @@ from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, ) -from openpype.hosts.fusion.api.lib import get_frame_path from openpype.lib import ( BoolDef, @@ -17,9 +16,6 @@ from openpype.pipeline import ( CreatedInstance, Anatomy ) -from openpype.client import ( - get_asset_by_name, -) class CreateSaver(NewCreator): @@ -173,14 +169,11 @@ class CreateSaver(NewCreator): tool.SetAttrs({"TOOLS_Name": subset}) def _collect_saver(self, tool): - print("Collecting saver..") + self.log.info("Collecting saver..") attrs = tool.GetAttrs() - ctx_data = {} keys = ["id", "asset", "subset", "task", "variant"] - for key in keys: - ctx_data[key] = tool.GetData(f"openpype.{key}") - + ctx_data = {key: tool.GetData(f"openpype.{key}") for key in keys} passthrough = attrs["TOOLB_PassThrough"] return { # Required data From 4095c7bd008923f288c8b16f5ce94e0c0aacca99 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 22:51:52 +0800 Subject: [PATCH 546/918] use rt.rendpickupframe and ondrej's comment --- openpype/hosts/max/api/lib.py | 6 ++--- .../max/plugins/publish/collect_render.py | 8 +++--- .../plugins/publish/validate_frame_range.py | 26 +++++++++++-------- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 1673fc5ab8..b21ce0f789 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -150,10 +150,10 @@ def set_render_frame_range(start_frame, end_frame): Todo: Current type is hard-coded, there should be a custom setting for this. """ - rt.rendTimeType = 3 + rt.rendTimeType = 4 if start_frame is not None and end_frame is not None: - rt.rendStart = int(start_frame) - rt.rendEnd = int(end_frame) + frame_range = "{0}-{1}".format(start_frame, end_frame) + rt.rendPickupFrames = frame_range def get_multipass_setting(project_setting=None): diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 9d93a40021..2742c36fc8 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Collect Render""" import os +import re import pyblish.api from pymxs import runtime as rt @@ -46,7 +47,8 @@ class CollectRender(pyblish.api.InstancePlugin): self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int - + pattern = r"^(?P-?[0-9]+)(?:(?:-)(?P-?[0-9]+))?$" + match = re.match(pattern, rt.rendPickupFrames) # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, @@ -59,8 +61,8 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", - "frameStart": int(rt.rendStart), - "frameEnd": int(rt.rendEnd), + "frameStart": int(match.group("start")), + "frameEnd": int(match.group("end")), "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index e07c6390c1..4cc9cb530c 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -44,19 +44,23 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, inst_frame_start = int(instance.data.get("frameStart")) inst_frame_end = int(instance.data.get("frameEnd")) + errors = [] if frame_start != inst_frame_start: - raise PublishValidationError( - "startFrame on instance does not match" - " with startFrame from the context data." - " You can use repair action to fix it") - + errors.append( + f"Start frame ({inst_frame_start}) on instance does not match " + f"with the start frame ({frame_start}) set on the asset data. ") if frame_end != inst_frame_end: - raise PublishValidationError( - "endFrame on instance does not match" - " with endFrame from the context data." - " You can use repair action to fix it") + errors.append( + f"End frame ({inst_frame_end}) on instance does not match " + f"with the end frame ({frame_start}) from the asset data. ") + + if errors: + errors.append("You can use repair action to fix it.") + raise PublishValidationError("\n".join(errors)) @classmethod def repair(cls, instance): - rt.rendStart = instance.context.data.get("frameStart") - rt.rendEnd = instance.context.data.get("frameEnd") + start = instance.context.data.get("frameStart") + end = instance.context.data.get("frameEnd") + frame_range = "{0}-{1}".format(start, end) + rt.rendPickupFrames = frame_range From da7d8ac091d45d6d73a2d34177f2f7ab3992f9b0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 22:53:02 +0800 Subject: [PATCH 547/918] hound fix --- openpype/hosts/max/plugins/publish/validate_frame_range.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 4cc9cb530c..761e7bf085 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -47,8 +47,8 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, errors = [] if frame_start != inst_frame_start: errors.append( - f"Start frame ({inst_frame_start}) on instance does not match " - f"with the start frame ({frame_start}) set on the asset data. ") + f"Start frame ({inst_frame_start}) on instance does not match " # noqa + f"with the start frame ({frame_start}) set on the asset data. ") # noqa if frame_end != inst_frame_end: errors.append( f"End frame ({inst_frame_end}) on instance does not match " From e69e1f76c8f86ab2cfc9ca7a9272b99d2ea98546 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 23:16:39 +0800 Subject: [PATCH 548/918] use ranges --- openpype/hosts/max/api/lib.py | 6 +++--- openpype/hosts/max/plugins/publish/collect_render.py | 6 ++---- openpype/hosts/max/plugins/publish/validate_frame_range.py | 6 ++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index b21ce0f789..1673fc5ab8 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -150,10 +150,10 @@ def set_render_frame_range(start_frame, end_frame): Todo: Current type is hard-coded, there should be a custom setting for this. """ - rt.rendTimeType = 4 + rt.rendTimeType = 3 if start_frame is not None and end_frame is not None: - frame_range = "{0}-{1}".format(start_frame, end_frame) - rt.rendPickupFrames = frame_range + rt.rendStart = int(start_frame) + rt.rendEnd = int(end_frame) def get_multipass_setting(project_setting=None): diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 2742c36fc8..31f1eba409 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -47,8 +47,6 @@ class CollectRender(pyblish.api.InstancePlugin): self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int - pattern = r"^(?P-?[0-9]+)(?:(?:-)(?P-?[0-9]+))?$" - match = re.match(pattern, rt.rendPickupFrames) # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, @@ -61,8 +59,8 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", - "frameStart": int(match.group("start")), - "frameEnd": int(match.group("end")), + "frameStart": int(rt.rendStart), + "frameEnd": int(rt.rendEnd), "version": version_int, "farm": True } diff --git a/openpype/hosts/max/plugins/publish/validate_frame_range.py b/openpype/hosts/max/plugins/publish/validate_frame_range.py index 761e7bf085..21e847405e 100644 --- a/openpype/hosts/max/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/max/plugins/publish/validate_frame_range.py @@ -60,7 +60,5 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - start = instance.context.data.get("frameStart") - end = instance.context.data.get("frameEnd") - frame_range = "{0}-{1}".format(start, end) - rt.rendPickupFrames = frame_range + rt.rendStart = instance.context.data.get("frameStart") + rt.rendEnd = instance.context.data.get("frameEnd") From 210ed4d41fdc8c28cb18e9a2d61b10b8848cbf26 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 10 May 2023 23:17:58 +0800 Subject: [PATCH 549/918] hound fix --- openpype/hosts/max/plugins/publish/collect_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 31f1eba409..00e00a8eb5 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Collect Render""" import os -import re import pyblish.api from pymxs import runtime as rt From 9a6ae240e2fbafe693701cb791656e44e3440d74 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:00:29 +0800 Subject: [PATCH 550/918] using currentfile for redshift renderer --- .../hosts/max/plugins/publish/collect_render.py | 6 ++++-- .../plugins/publish/submit_max_deadline.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index b040467522..0d4dbc4521 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -5,7 +5,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -38,7 +38,8 @@ class CollectRender(pyblish.api.InstancePlugin): version_doc = get_last_version_by_subset_name(project_name, instance.name, asset_id) - + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] self.log.debug("version_doc: {0}".format(version_doc)) version_int = 1 if version_doc: @@ -59,6 +60,7 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", + "renderer": renderer, "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "version": version_int, diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c728b6b9c7..0cf4990428 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -14,7 +14,6 @@ from openpype.pipeline import ( ) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( - get_current_renderer, get_multipass_setting ) from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -157,6 +156,12 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return plugin_payload + def from_published_scene(self, replace_in_path=True): + instance = self._instance + if instance.data["renderer"]== "Redshift_renderer": + file_path = self.scene_path + return file_path + def process_submission(self): instance = self._instance @@ -185,6 +190,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance job_info = copy.deepcopy(self.job_info) plugin_info = copy.deepcopy(self.plugin_info) + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") plugin_data = {} project_setting = get_project_settings( legacy_io.Session["AVALON_PROJECT"] @@ -202,7 +209,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(expected_files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.from_published_scene() + filepath = self.scene_path def _clean_name(path): return os.path.splitext(os.path.basename(path))[0] @@ -214,9 +221,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, output_beauty = output_beauty.replace("\\", "/") plugin_data["RenderOutput"] = output_beauty - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - if renderer in [ + if instance.data["renderer"] in [ "ART_Renderer", "Redshift_Renderer", "V_Ray_6_Hotfix_3", @@ -227,6 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): element = element.replace(orig_scene, new_scene) + element = element.replace("\\", "/") plugin_data["RenderElementOutputFilename%d" % i] = element # noqa self.log.debug("plugin data:{}".format(plugin_data)) From be386a36880a7868a4e46ba42fd49bfa08df6905 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:05:56 +0800 Subject: [PATCH 551/918] hound fix --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 0cf4990428..a66d4d630a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -158,7 +158,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - if instance.data["renderer"]== "Redshift_renderer": + if instance.data["renderer"] == "Redshift_renderer": file_path = self.scene_path return file_path From 9f1bdda8b35509907351cc12ac271299bb186c0b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 11 May 2023 12:18:36 +0200 Subject: [PATCH 552/918] fusion: removing obsolete code --- .../fusion/plugins/create/create_saver.py | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index ecdad30b4c..2af811ef5b 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -90,8 +90,6 @@ class CreateSaver(NewCreator): tools = comp.GetToolList(False, "Saver").values() for tool in tools: data = self.get_managed_tool_data(tool) - if not data: - data = self._collect_saver(tool) # Add instance created_instance = CreatedInstance.from_existing(data, self) @@ -168,27 +166,6 @@ class CreateSaver(NewCreator): print(f"Renaming {tool.Name} -> {subset}") tool.SetAttrs({"TOOLS_Name": subset}) - def _collect_saver(self, tool): - self.log.info("Collecting saver..") - attrs = tool.GetAttrs() - - keys = ["id", "asset", "subset", "task", "variant"] - ctx_data = {key: tool.GetData(f"openpype.{key}") for key in keys} - passthrough = attrs["TOOLB_PassThrough"] - return { - # Required data - "project": self.project_name, - "asset": ctx_data["asset"], - "subset": ctx_data["subset"], - "task": ctx_data["task"], - "variant": ctx_data["variant"], - "active": not passthrough, - "family": self.family, - # Unique identifier for instance and this creator - "id": ctx_data["id"], - "creator_identifier": self.identifier, - } - def get_managed_tool_data(self, tool): """Return data of the tool if it matches creator identifier""" data = tool.GetData("openpype") From bf2e02699a0b60af16b1a6057b4f82d9faa33373 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 11 May 2023 12:20:56 +0200 Subject: [PATCH 553/918] returning important condition --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 2af811ef5b..13836aa1a0 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -90,6 +90,8 @@ class CreateSaver(NewCreator): tools = comp.GetToolList(False, "Saver").values() for tool in tools: data = self.get_managed_tool_data(tool) + if not data: + continue # Add instance created_instance = CreatedInstance.from_existing(data, self) From 03d777503cc8da73cd46ad213d9b249c3b3f7aa3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 11 May 2023 16:14:16 +0100 Subject: [PATCH 554/918] Remove render extractor --- .../unreal/plugins/publish/extract_render.py | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/publish/extract_render.py diff --git a/openpype/hosts/unreal/plugins/publish/extract_render.py b/openpype/hosts/unreal/plugins/publish/extract_render.py deleted file mode 100644 index 8ff38fbee0..0000000000 --- a/openpype/hosts/unreal/plugins/publish/extract_render.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path - -import unreal - -from openpype.pipeline import publish - - -class ExtractRender(publish.Extractor): - """Extract render.""" - - label = "Extract Render" - hosts = ["unreal"] - families = ["render"] - optional = True - - def process(self, instance): - # Define extract output file path - stagingdir = self.staging_dir(instance) - - # Perform extraction - self.log.info("Performing extraction..") - - # Get the render output directory - project_dir = unreal.Paths.project_dir() - render_dir = (f"{project_dir}/Saved/MovieRenders/" - f"{instance.data['subset']}") - - assert unreal.Paths.directory_exists(render_dir), \ - "Render directory does not exist" - - render_path = Path(render_dir) - - frames = [] - - for x in render_path.iterdir(): - if x.is_file() and x.suffix == '.png': - frames.append(str(x)) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - render_representation = { - 'name': 'png', - 'ext': 'png', - 'files': frames, - "stagingDir": stagingdir, - } - instance.data["representations"].append(render_representation) From 9086511970927f662807e67db02e43628f1adce9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 11 May 2023 16:14:41 +0100 Subject: [PATCH 555/918] Fix start and end frames to be int --- .../hosts/unreal/plugins/publish/collect_render_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index 6697a6b90d..a352b2c3f3 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -73,8 +73,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): new_data["level"] = data.get("level") new_data["output"] = s.get('output') new_data["fps"] = seq.get_display_rate().numerator - new_data["frameStart"] = s.get('frame_range')[0] - new_data["frameEnd"] = s.get('frame_range')[1] + new_data["frameStart"] = int(s.get('frame_range')[0]) + new_data["frameEnd"] = int(s.get('frame_range')[1]) new_data["sequence"] = seq.get_path_name() new_data["master_sequence"] = data["master_sequence"] new_data["master_level"] = data["master_level"] From fbee0a8b3c295dc55cd64a3037f517f88de150fa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 11:07:50 +0200 Subject: [PATCH 556/918] pr comments --- .../fusion/plugins/publish/collect_render.py | 3 --- .../validate_expected_frames_existence.py | 23 +++++++++---------- .../plugins/publish/submit_fusion_deadline.py | 3 --- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 5adb8a13f0..4898226f03 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -1,5 +1,4 @@ import os -from pprint import pformat import attr import pyblish.api @@ -92,7 +91,6 @@ class CollectFusionRender( ) render_target = inst.data["creator_attributes"]["render_target"] - self.log.debug("render_target: '{}'".format(render_target)) if render_target == "local": # for local renders @@ -125,7 +123,6 @@ class CollectFusionRender( for instance in self._context: if "render.frames" in instance.data.get("families", []): self._update_for_frames(instance) - self.log.debug(pformat(instance.data)) def get_expected_files(self, render_instance): """ diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index befaae13be..aa89799867 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -23,21 +23,20 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - if "render.frames" in instance.data.get("families", []): - tool = instance.data["toolSaver"] + tool = instance.data["toolSaver"] - expected_files = instance.data["expectedFiles"] + expected_files = instance.data["expectedFiles"] - for file in expected_files: - if not os.path.exists(file): - cls.log.error( - f"Missing file: {file}" - ) - non_existing_frames.append(file) + for file in expected_files: + if not os.path.exists(file): + cls.log.error( + f"Missing file: {file}" + ) + non_existing_frames.append(file) - if len(non_existing_frames) > 0: - cls.log.error(f"Some of {tool.Name}'s files does not exist") - return [tool] + if len(non_existing_frames) > 0: + cls.log.error(f"Some of {tool.Name}'s files does not exist") + return [tool] def process(self, instance): non_existing_frames = [] diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 891bfde6c5..af4bd37302 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -1,7 +1,6 @@ import os import json import getpass -from pprint import pformat import requests @@ -90,8 +89,6 @@ class FusionSubmitDeadline( attribute_values = self.get_attr_values_from_data( instance.data) - self.log.debug(pformat(attribute_values)) - # add suspend_publish attributeValue to instance data instance.data["suspend_publish"] = attribute_values[ "suspend_publish"] From 801b904aea357ac0523fd4b75f0f2e7bd8ac5df9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 11:29:29 +0200 Subject: [PATCH 557/918] rewriting logic for frame ranges --- openpype/hosts/fusion/api/lib.py | 7 ---- .../fusion/plugins/create/create_saver.py | 3 ++ .../publish/collect_comp_frame_range.py | 26 ++++----------- .../plugins/publish/collect_instances.py | 32 +++++++++++++++---- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 8f7b29f0c4..40cc4d2963 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -51,12 +51,6 @@ def update_frame_range(start, end, comp=None, set_render_range=True, "COMPN_GlobalStart": start - handle_start, "COMPN_GlobalEnd": end + handle_end } - frame_data = { - "frameStart": start, - "frameEnd": end, - "handleStart": handle_start, - "handleEnd": handle_end - } # set frame range if set_render_range: @@ -67,7 +61,6 @@ def update_frame_range(start, end, comp=None, set_render_range=True, with comp_lock_and_undo_chunk(comp): comp.SetAttrs(attrs) - comp.SetData("openpype_instance", frame_data) def set_asset_framerange(): diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index ecdad30b4c..58ffbe928a 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -223,6 +223,9 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), + BoolDef( + "custom_range", label="Custom range", default=False, + ) ] return attr_defs diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 2db0002ee6..38d6577667 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -9,20 +9,14 @@ def get_comp_render_range(comp): global_start = comp_attrs["COMPN_GlobalStart"] global_end = comp_attrs["COMPN_GlobalEnd"] - frame_data = comp.GetData("openpype_instance") - handle_start = frame_data.get("handleStart", 0) - handle_end = frame_data.get("handleEnd", 0) - frame_start = frame_data.get("frameStart", 0) - frame_end = frame_data.get("frameEnd", 0) - # Whenever render ranges are undefined fall back # to the comp's global start and end if start == -1000000000: - start = frame_start + start = global_start if end == -1000000000: - end = frame_end + end = global_end - return start, end, global_start, global_end, handle_start, handle_end + return start, end, global_start, global_end class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): @@ -44,18 +38,12 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): start, end, global_start, global_end, - handle_start, - handle_end ) = get_comp_render_range(comp) data = {} - data["frameStart"] = int(start) - data["frameEnd"] = int(end) - data["frameStartHandle"] = int(global_start) - data["frameEndHandle"] = int(global_end) - data["handleStart"] = int(handle_start) - data["handleEnd"] = int(handle_end) - - self.log.debug("_ data: {}".format(data)) + data["compFrameStart"] = int(start) + data["compFrameEnd"] = int(end) + data["compFrameStartHandle"] = int(global_start) + data["compFrameEndHandle"] = int(global_end) context.data.update(data) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 4608f79420..9c27e3f027 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -1,4 +1,6 @@ +from math import e import os +from turtle import st import pyblish.api @@ -24,6 +26,23 @@ class CollectInstanceData(pyblish.api.InstancePlugin): creator_attributes = instance.data["creator_attributes"] instance.data.update(creator_attributes) + # get asset frame ranges + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_handle = start - handle_start + end_handle = end + handle_end + + if creator_attributes["custom_range"]: + # get comp frame ranges + start = context.data["compFrameStart"] + end = context.data["compFrameEnd"] + handle_start = 0 + handle_end = 0 + start_handle = context.data["compFrameStartHandle"] + end_handle = context.data["compFrameEndHandle"] + # Include start and end render frame in label subset = instance.data["subset"] start = context.data["frameStart"] @@ -31,16 +50,17 @@ class CollectInstanceData(pyblish.api.InstancePlugin): label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) + instance.data.update({ "label": label, # todo: Allow custom frame range per instance - "frameStart": context.data["frameStart"], - "frameEnd": context.data["frameEnd"], - "frameStartHandle": context.data["frameStartHandle"], - "frameEndHandle": context.data["frameStartHandle"], - "handleStart": context.data["handleStart"], - "handleEnd": context.data["handleEnd"], + "frameStart": start, + "frameEnd": end, + "frameStartHandle": start_handle, + "frameEndHandle": end_handle, + "handleStart": handle_start, + "handleEnd": handle_end, "fps": context.data["fps"], }) From c80842f5a95aa88a6eaec0dd19b8326c89db760a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:19:33 +0200 Subject: [PATCH 558/918] fusion: improving custom frame range --- .../fusion/plugins/create/create_saver.py | 21 +++++++++++++++++++ .../plugins/publish/collect_instances.py | 8 +++---- .../fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 58ffbe928a..860a873442 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -40,6 +40,11 @@ class CreateSaver(NewCreator): "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") def create(self, subset_name, instance_data, pre_create_data): + self.pass_pre_attributes_to_instance( + instance_data, + pre_create_data + ) + instance_data.update({ "id": "pyblish.avalon.instance", "subset": subset_name @@ -215,6 +220,9 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), + BoolDef( + "custom_range", label="Custom range", default=False, + ) ] return attr_defs @@ -229,6 +237,19 @@ class CreateSaver(NewCreator): ] return attr_defs + def pass_pre_attributes_to_instance( + self, + instance_data, + pre_create_data, + keys=None + ): + if not keys: + keys = pre_create_data.keys() + + creator_attrs = instance_data["creator_attributes"] = {} + for pass_key in keys: + creator_attrs[pass_key] = pre_create_data[pass_key] + # These functions below should be moved to another file # so it can be used by other plugins. plugin.py ? diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 9c27e3f027..997bd66e4a 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -34,19 +34,17 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes["custom_range"]: + if creator_attributes.get("custom_range"): # get comp frame ranges start = context.data["compFrameStart"] end = context.data["compFrameEnd"] handle_start = 0 handle_end = 0 - start_handle = context.data["compFrameStartHandle"] - end_handle = context.data["compFrameEndHandle"] + start_handle = start + end_handle = end # Include start and end render frame in label subset = instance.data["subset"] - start = context.data["frameStart"] - end = context.data["frameEnd"] label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 4898226f03..26355b24d3 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,7 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=True, + ignoreFrameHandleCheck=(not inst.data.get("custom_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 78b0e3daa1922402b4e8d2068208959cf22fa793 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:35:15 +0200 Subject: [PATCH 559/918] removing unusable attribute GPU --- .../plugins/publish/submit_fusion_deadline.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index af4bd37302..d51299506c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -40,7 +40,6 @@ class FusionSubmitDeadline( group = "" department = "" limit_groups = {} - use_gpu = False env_allowed_keys = [] env_search_replace_values = {} @@ -69,11 +68,6 @@ class FusionSubmitDeadline( minimum=1, maximum=10 ), - BoolDef( - "use_gpu", - default=cls.use_gpu, - label="Use GPU" - ), BoolDef( "suspend_publish", default=False, @@ -206,11 +200,7 @@ class FusionSubmitDeadline( # Proxy: higher numbers smaller images for faster test renders # 1 = no proxy quality - "Proxy": 1, - - # using GPU by default - "UseGpu": attribute_values.get( - "use_gpu", self.use_gpu) + "Proxy": 1 }, # Mandatory for Deadline, may be empty From 5896c080132f4d62b3bc337ee586b40d7b4d991d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 13:38:23 +0200 Subject: [PATCH 560/918] hound --- openpype/hosts/fusion/plugins/publish/collect_instances.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 997bd66e4a..5a6a918730 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -1,7 +1,3 @@ -from math import e -import os -from turtle import st - import pyblish.api From e56f4286c123149f3595920fcb1d550e0c1a8c38 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 12 May 2023 13:39:51 +0200 Subject: [PATCH 561/918] remove python syntax available since 3.8 from unreal addon --- openpype/hosts/unreal/addon.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index db40d629bc..9ded333d7d 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -23,9 +23,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon): UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" ) if not Path(unreal_plugin_path).exists(): - if compatible_versions := get_compatible_integration( + compatible_versions = get_compatible_integration( ue_version, Path(UNREAL_ROOT_DIR) / "integration" - ): + ) + if compatible_versions: unreal_plugin_path = compatible_versions[-1] / "Ayon" unreal_plugin_path = unreal_plugin_path.as_posix() From 233c7b34545f9c6cfec2e87eb41983099bc96849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 12 May 2023 14:12:40 +0200 Subject: [PATCH 562/918] Update openpype/hosts/fusion/plugins/create/create_saver.py Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/plugins/create/create_saver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 860a873442..4993c882de 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -174,7 +174,6 @@ class CreateSaver(NewCreator): tool.SetAttrs({"TOOLS_Name": subset}) def _collect_saver(self, tool): - self.log.info("Collecting saver..") attrs = tool.GetAttrs() keys = ["id", "asset", "subset", "task", "variant"] From 02279a51c8f97d0abf8e2d53d4d89acf1b13f1ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 14:19:13 +0200 Subject: [PATCH 563/918] pr comments https://github.com/ynput/OpenPype/pull/4955#discussion_r1192264433 https://github.com/ynput/OpenPype/pull/4955#discussion_r1192267231 --- .../hosts/fusion/plugins/create/create_saver.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 4993c882de..caffb8e4a1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -227,26 +227,15 @@ class CreateSaver(NewCreator): def get_instance_attr_defs(self): """Settings for publish page""" - attr_defs = [ - self._get_render_target_enum(), - self._get_reviewable_bool(), - BoolDef( - "custom_range", label="Custom range", default=False, - ) - ] - return attr_defs + return self.get_pre_create_attr_defs() def pass_pre_attributes_to_instance( self, instance_data, - pre_create_data, - keys=None + pre_create_data ): - if not keys: - keys = pre_create_data.keys() - creator_attrs = instance_data["creator_attributes"] = {} - for pass_key in keys: + for pass_key in pre_create_data.keys(): creator_attrs[pass_key] = pre_create_data[pass_key] # These functions below should be moved to another file From 425ddc7b2bb9399dd2d187761a4cc8755b000b79 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 12 May 2023 14:26:15 +0200 Subject: [PATCH 564/918] fusion: reversing toolSaver back to tool --- .../hosts/fusion/plugins/publish/collect_render.py | 14 ++++++++++---- .../fusion/plugins/publish/extract_render_local.py | 2 +- .../publish/validate_create_folder_checked.py | 2 +- .../publish/validate_expected_frames_existence.py | 4 ++-- .../publish/validate_filename_has_extension.py | 2 +- .../plugins/publish/validate_saver_has_input.py | 2 +- .../plugins/publish/validate_saver_passthrough.py | 2 +- .../plugins/publish/validate_unique_subsets.py | 2 +- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 26355b24d3..64d9aedc3b 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -14,7 +14,7 @@ class FusionRenderInstance(RenderInstance): projectEntity = attr.ib(default=None) stagingDir = attr.ib(default=None) app_version = attr.ib(default=None) - toolSaver = attr.ib(default=None) + tool = attr.ib(default=None) workfileComp = attr.ib(default=None) publish_attributes = attr.ib(default={}) @@ -58,7 +58,7 @@ class CollectFusionRender( subset_name = inst.data["subset"] instance = FusionRenderInstance( family="render", - toolSaver=tool, + tool=tool, workfileComp=comp, families=instance_families, version=version, @@ -111,6 +111,8 @@ class CollectFusionRender( # to skip ExtractReview locally instance.families.remove("review") + # add new instance to the list and remove the original + # instance since it is not needed anymore instances.append(instance) instances_to_remove.append(inst) @@ -141,7 +143,7 @@ class CollectFusionRender( end = render_instance.frameEnd + render_instance.handleEnd path = ( - render_instance.toolSaver["Clip"] + render_instance.tool["Clip"] [render_instance.workfileComp.TIME_UNDEFINED] ) output_dir = os.path.dirname(path) @@ -163,7 +165,11 @@ class CollectFusionRender( return expected_files def _update_for_frames(self, instance): - """Update old saved instances to current publishing format""" + """Updating instance for render.frames family + + Adding representation data to the instance. Also setting + colorspaceData to the representation based on file rules. + """ expected_files = instance.data["expectedFiles"] diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index c2e38884c7..f093f7793f 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -79,7 +79,7 @@ class FusionRenderLocal( savers_to_render = [ # Get the saver tool from the instance - instance.data["toolSaver"] for instance in context if + instance.data["tool"] for instance in context if # Only active instances instance.data.get("publish", True) and # Only render.local instances diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 82d34b0b5d..35c92163eb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -21,7 +21,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - tool = instance.data["toolSaver"] + tool = instance.data["tool"] create_dir = tool.GetInput("CreateDir") if create_dir == 0.0: cls.log.error( diff --git a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py index aa89799867..3f84f59678 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py +++ b/openpype/hosts/fusion/plugins/publish/validate_expected_frames_existence.py @@ -23,7 +23,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): if non_existing_frames is None: non_existing_frames = [] - tool = instance.data["toolSaver"] + tool = instance.data["tool"] expected_files = instance.data["expectedFiles"] @@ -56,7 +56,7 @@ class ValidateLocalFramesExistence(pyblish.api.InstancePlugin): def repair(cls, instance): invalid = cls.get_invalid(instance) if invalid: - tool = instance.data["toolSaver"] + tool = instance.data["tool"] # Change render target to local to render locally tool.SetData("openpype.creator_attributes.render_target", "local") diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 1bf603e5b1..537e43c875 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -34,7 +34,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): fname, ext = os.path.splitext(path) if not ext: - tool = instance.data["toolSaver"] + tool = instance.data["tool"] cls.log.error("%s has no extension specified" % tool.Name) return [tool] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index b409608ec3..faf2102a8b 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -20,7 +20,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - saver = instance.data["toolSaver"] + saver = instance.data["tool"] if not saver.Input.GetConnectedOutput(): return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 677861a654..9004976dc5 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -37,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): def is_invalid(self, instance): - saver = instance.data["toolSaver"] + saver = instance.data["tool"] attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 6a65182fae..5b6ceb2fdb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -43,7 +43,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): invalid.extend(instances) # Return tools for the invalid instances so they can be selected - invalid = [instance.data["toolSaver"] for instance in invalid] + invalid = [instance.data["tool"] for instance in invalid] return invalid From 6968c7c8ba0bed66e131c81ec4b0f53eaa99c3da Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Fri, 12 May 2023 14:57:17 +0200 Subject: [PATCH 565/918] add shortcut to action if in configuration (#4927) Co-authored-by: Thomas Fricard --- .../python/common/scriptsmenu/scriptsmenu.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py index 6f6d0b5715..8ab621f757 100644 --- a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py +++ b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py @@ -19,9 +19,9 @@ class ScriptsMenu(QtWidgets.QMenu): Args: title (str): the name of the root menu which will be created - + parent (QtWidgets.QObject) : the QObject to parent the menu to - + Returns: None @@ -94,7 +94,7 @@ class ScriptsMenu(QtWidgets.QMenu): parent(QtWidgets.QWidget): the object to parent the menu to title(str): the title of the menu - + Returns: QtWidget.QMenu instance """ @@ -111,7 +111,7 @@ class ScriptsMenu(QtWidgets.QMenu): return menu def add_script(self, parent, title, command, sourcetype, icon=None, - tags=None, label=None, tooltip=None): + tags=None, label=None, tooltip=None, shortcut=None): """Create an action item which runs a script when clicked Args: @@ -134,6 +134,8 @@ class ScriptsMenu(QtWidgets.QMenu): tooltip (str): A tip for the user about the usage fo the tool + shortcut (str): A shortcut to run the command + Returns: QtWidget.QAction instance @@ -166,6 +168,9 @@ class ScriptsMenu(QtWidgets.QMenu): raise RuntimeError("Script action can't be " "processed: {}".format(e)) + if shortcut: + script_action.setShortcut(shortcut) + if icon: iconfile = os.path.expandvars(icon) script_action.iconfile = iconfile @@ -253,7 +258,7 @@ class ScriptsMenu(QtWidgets.QMenu): def _update_search(self, search): """Hide all the samples which do not match the user's import - + Returns: None From d3428da3937bb0dbad2e1f86705e95d2bbe6b695 Mon Sep 17 00:00:00 2001 From: kaa Date: Fri, 12 May 2023 15:19:23 +0200 Subject: [PATCH 566/918] fix get_linked_assets project name arg (#4940) --- openpype/pipeline/workfile/build_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/build_workfile.py b/openpype/pipeline/workfile/build_workfile.py index 26b17fa151..8329487839 100644 --- a/openpype/pipeline/workfile/build_workfile.py +++ b/openpype/pipeline/workfile/build_workfile.py @@ -186,7 +186,7 @@ class BuildWorkfile: if link_context_profiles: # Find and append linked assets if preset has set linked mapping - link_assets = get_linked_assets(current_asset_entity) + link_assets = get_linked_assets(project_name, current_asset_entity) if link_assets: assets.extend(link_assets) From d8b569b160167d491ab29367d00d0f504f40c0ab Mon Sep 17 00:00:00 2001 From: Sharkitty <81646000+Sharkitty@users.noreply.github.com> Date: Fri, 12 May 2023 14:14:44 +0000 Subject: [PATCH 567/918] Update openpype/plugins/inventory/remove_and_load.py Discover plugins only once Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/inventory/remove_and_load.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index 062a44354b..d90cc9462c 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -17,10 +17,10 @@ class RemoveAndLoad(InventoryAction): def process(self, containers): project_name = get_current_project_name() + loaders = discover_loader_plugins(project_name=project_name) for container in containers: # Get loader loader_name = container["loader"] - loaders = discover_loader_plugins(project_name=project_name) for plugin in loaders: if get_loader_identifier(plugin) == loader_name: loader = plugin From 9d6cd8d2c83fe3d98aca92fea206c34b208f49a0 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 12 May 2023 16:21:58 +0200 Subject: [PATCH 568/918] add options --- openpype/hosts/maya/api/lib.py | 4 ++-- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index f814187cc1..d44f1754f9 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3937,7 +3937,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): return capture_preset or {} -def create_rig_animation_instance(nodes, context, namespace, log=None): +def create_rig_animation_instance(nodes, context, namespace, options, log=None): """Create an animation publish instance for loaded rigs. See the RecreateRigAnimationInstance inventory action on how to use this @@ -3982,6 +3982,6 @@ def create_rig_animation_instance(nodes, context, namespace, log=None): creator_plugin, name=namespace, asset=asset, - options={"useSelection": True}, + options=options.update({"useSelection": True}), data={"dependencies": dependency} ) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 7d717dcd44..31b6e9d624 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -223,7 +223,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def _post_process_rig(self, name, namespace, context, options): nodes = self[:] create_rig_animation_instance( - nodes, context, namespace, log=self.log + nodes, context, namespace, options, log=self.log ) def _lock_camera_transforms(self, nodes): From ce1e45f7082a108258b2fa1bbd3e5a37e25f52e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 May 2023 16:35:10 +0200 Subject: [PATCH 569/918] fix key assignment on instance data (#4966) --- .../ftrack/plugins/publish/integrate_hierarchy_ftrack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 9f35424d42..6daaea5f18 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -378,7 +378,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): existing_tasks.append(task_name_low) for instance in instances_by_task_name[task_name_low]: - instance["ftrackTask"] = child + instance.data["ftrackTask"] = child for task_name in tasks: task_type = tasks[task_name]["type"] From ab357eb03e316691057633b5762cdb094b35ef8d Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 12 May 2023 16:44:44 +0200 Subject: [PATCH 570/918] Addons directory (#4893) * Add addons directory * add addons dir to modules dirs automatically --------- Co-authored-by: iLLiCiTiT --- .gitignore | 6 ++++++ openpype/addons/README.md | 3 +++ openpype/modules/base.py | 6 +++++- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 openpype/addons/README.md diff --git a/.gitignore b/.gitignore index 18e7cd7bf2..50f52f65a3 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,9 @@ tools/run_eventserver.* tools/dev_* .github_changelog_generator + + +# Addons +######## +/openpype/addons/* +!/openpype/addons/README.md diff --git a/openpype/addons/README.md b/openpype/addons/README.md new file mode 100644 index 0000000000..92b8b8c07c --- /dev/null +++ b/openpype/addons/README.md @@ -0,0 +1,3 @@ +This directory is for storing external addons that needs to be included in the pipeline when distributed. + +The directory is ignored by Git, but included in the zip and installation files. diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ed1eeb04cd..732525b6eb 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -311,6 +311,7 @@ def _load_modules(): # Look for OpenPype modules in paths defined with `get_module_dirs` # - dynamically imported OpenPype modules and addons module_dirs = get_module_dirs() + # Add current directory at first place # - has small differences in import logic current_dir = os.path.abspath(os.path.dirname(__file__)) @@ -318,8 +319,11 @@ def _load_modules(): module_dirs.insert(0, hosts_dir) module_dirs.insert(0, current_dir) + addons_dir = os.path.join(os.path.dirname(current_dir), "addons") + module_dirs.append(addons_dir) + processed_paths = set() - for dirpath in module_dirs: + for dirpath in frozenset(module_dirs): # Skip already processed paths if dirpath in processed_paths: continue From 5ff9ab368b392e48dbc1c522a461d5b52c300b79 Mon Sep 17 00:00:00 2001 From: Sharkitty Date: Fri, 12 May 2023 17:07:52 +0200 Subject: [PATCH 571/918] pre cache loader by name, loader name in error --- openpype/plugins/inventory/remove_and_load.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/inventory/remove_and_load.py b/openpype/plugins/inventory/remove_and_load.py index d90cc9462c..ae66b95f6e 100644 --- a/openpype/plugins/inventory/remove_and_load.py +++ b/openpype/plugins/inventory/remove_and_load.py @@ -17,17 +17,18 @@ class RemoveAndLoad(InventoryAction): def process(self, containers): project_name = get_current_project_name() - loaders = discover_loader_plugins(project_name=project_name) + loaders_by_name = { + get_loader_identifier(plugin): plugin + for plugin in discover_loader_plugins(project_name=project_name) + } for container in containers: # Get loader loader_name = container["loader"] - for plugin in loaders: - if get_loader_identifier(plugin) == loader_name: - loader = plugin - break - else: + loader = loaders_by_name.get(loader_name, None) + if not loader: raise RuntimeError( - "Failed to get loader, can't remove and load container" + "Failed to get loader '{}', can't remove " + "and load container".format(loader_name) ) # Get representation From 78f8bbfd8066604d490aa89b56e0061a0d8d191b Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Fri, 12 May 2023 17:29:38 +0200 Subject: [PATCH 572/918] Kitsu - Add "image", "online" and "plate" to review families (#4923) * Add kitsu review to the default png review's tags * Add "image", "online" and "plate" as possible review families --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 7 ++++--- .../kitsu/plugins/publish/integrate_kitsu_review.py | 3 +-- openpype/settings/defaults/project_settings/global.json | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index f8e56377bb..6e5dd056f3 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -9,7 +9,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" - families = ["render", "kitsu"] + families = ["render", "image", "online", "plate", "kitsu"] # status settings set_status_note = False @@ -52,8 +52,9 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): for instance in context: # Check if instance is a review by checking its family # Allow a match to primary family or any of families - families = set([instance.data["family"]] + - instance.data.get("families", [])) + families = set( + [instance.data["family"]] + instance.data.get("families", []) + ) if "review" not in families: continue diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index e05ff05f50..bbed4a3024 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -8,11 +8,10 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.01 label = "Kitsu Review" - families = ["render", "kitsu"] + families = ["render", "image", "online", "plate", "kitsu"] optional = True def process(self, instance): - # Check comment has been created comment_id = instance.data.get("kitsu_comment", {}).get("id") if not comment_id: diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 50b62737d8..75f335f1de 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -82,7 +82,8 @@ "png": { "ext": "png", "tags": [ - "ftrackreview" + "ftrackreview", + "kitsureview" ], "burnins": [], "ffmpeg_args": { From 454abe459ed834e6d10f014708c83a7142d1840d Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Fri, 12 May 2023 17:53:05 +0200 Subject: [PATCH 573/918] add custom animation subset --- openpype/hosts/maya/api/lib.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index d44f1754f9..124e0e5b8a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3937,7 +3937,9 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): return capture_preset or {} -def create_rig_animation_instance(nodes, context, namespace, options, log=None): +def create_rig_animation_instance( + nodes, context, namespace, options, log=None +): """Create an animation publish instance for loaded rigs. See the RecreateRigAnimationInstance inventory action on how to use this @@ -3971,6 +3973,12 @@ def create_rig_animation_instance(nodes, context, namespace, options, log=None): asset = legacy_io.Session["AVALON_ASSET"] dependency = str(context["representation"]["_id"]) + custom_subset = options.get("animationSubsetName") + + if custom_subset: + rig_subset = context['subset']['name'] + namespace = namespace.replace(rig_subset, custom_subset) + if log: log.info("Creating subset: {}".format(namespace)) @@ -3982,6 +3990,6 @@ def create_rig_animation_instance(nodes, context, namespace, options, log=None): creator_plugin, name=namespace, asset=asset, - options=options.update({"useSelection": True}), + options={"useSelection": True}, data={"dependencies": dependency} ) From eaefd594b6f2f89a0e8a1accbb5912b1a744035a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 13 May 2023 03:25:18 +0000 Subject: [PATCH 574/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 7df154fe1e..319a58d384 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7-nightly.2" +__version__ = "3.15.7-nightly.3" From 927f92df87d3bae5d035c3ae4f857ac171e84dfa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 May 2023 03:26:01 +0000 Subject: [PATCH 575/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0d75b669d2..7d224aa73f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7-nightly.3 - 3.15.7-nightly.2 - 3.15.7-nightly.1 - 3.15.6 @@ -134,7 +135,6 @@ body: - 3.14.1-nightly.4 - 3.14.1-nightly.3 - 3.14.1-nightly.2 - - 3.14.1-nightly.1 validations: required: true - type: dropdown From 11b895624afb2d1dde099eefe09c46b7d3f65b24 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 15 May 2023 10:44:58 +0100 Subject: [PATCH 576/918] Fix transform when loading layout to match existing assets --- .../plugins/load/load_layout_existing.py | 81 +++++++------------ 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index 96ee8cfc25..f4a2d1e7cc 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -89,50 +89,26 @@ class ExistingLayoutLoader(plugin.Loader): raise NotImplementedError( f"Unreal version {ue_major} not supported") - def _get_transform(self, ext, import_data, lasset): - conversion = unreal.Matrix.IDENTITY.transform() - fbx_tuning = unreal.Matrix.IDENTITY.transform() + def _transform_from_basis(self, transform, basis): + """Transform a transform from a basis to a new basis.""" + # Get the basis matrix + basis_matrix = unreal.Matrix( + basis[0], + basis[1], + basis[2], + basis[3] + ) + transform_matrix = unreal.Matrix( + transform[0], + transform[1], + transform[2], + transform[3] + ) - basis = unreal.Matrix( - lasset.get('basis')[0], - lasset.get('basis')[1], - lasset.get('basis')[2], - lasset.get('basis')[3] - ).transform() - transform = unreal.Matrix( - lasset.get('transform_matrix')[0], - lasset.get('transform_matrix')[1], - lasset.get('transform_matrix')[2], - lasset.get('transform_matrix')[3] - ).transform() + new_transform = ( + basis_matrix.get_inverse() * transform_matrix * basis_matrix) - # Check for the conversion settings. We cannot access - # the alembic conversion settings, so we assume that - # the maya ones have been applied. - if ext == '.fbx': - loc = import_data.import_translation - rot = import_data.import_rotation.to_vector() - scale = import_data.import_uniform_scale - conversion = unreal.Transform( - location=[loc.x, loc.y, loc.z], - rotation=[rot.x, rot.y, rot.z], - scale=[-scale, scale, scale] - ) - fbx_tuning = unreal.Transform( - rotation=[180.0, 0.0, 90.0], - scale=[1.0, 1.0, 1.0] - ) - elif ext == '.abc': - # This is the standard conversion settings for - # alembic files from Maya. - conversion = unreal.Transform( - location=[0.0, 0.0, 0.0], - rotation=[0.0, 0.0, 0.0], - scale=[1.0, -1.0, 1.0] - ) - - new_transform = (basis.inverse() * transform * basis) - return fbx_tuning * conversion.inverse() * new_transform + return new_transform.transform() def _spawn_actor(self, obj, lasset): actor = EditorLevelLibrary.spawn_actor_from_object( @@ -140,16 +116,13 @@ class ExistingLayoutLoader(plugin.Loader): ) actor.set_actor_label(lasset.get('instance_name')) - smc = actor.get_editor_property('static_mesh_component') - mesh = smc.get_editor_property('static_mesh') - import_data = mesh.get_editor_property('asset_import_data') - filename = import_data.get_first_filename() - path = Path(filename) - transform = self._get_transform( - path.suffix, import_data, lasset) + transform = lasset.get('transform_matrix') + basis = lasset.get('basis') - actor.set_actor_transform(transform, False, True) + t = self._transform_from_basis(transform, basis) + + actor.set_actor_transform(t, False, True) @staticmethod def _get_fbx_loader(loaders, family): @@ -320,9 +293,11 @@ class ExistingLayoutLoader(plugin.Loader): containers.append(container) # Set the transform for the actor. - transform = self._get_transform( - path.suffix, import_data, lasset) - actor.set_actor_transform(transform, False, True) + transform = lasset.get('transform_matrix') + basis = lasset.get('basis') + + t = self._transform_from_basis(transform, basis) + actor.set_actor_transform(t, False, True) actors_matched.append(actor) found = True From 2369d50224cd4768c182cee7004df4da7d381f27 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 11:25:56 +0100 Subject: [PATCH 577/918] Skipping rendersetup for members. --- .../maya/plugins/publish/validate_instance_has_members.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index 4870f27bff..fcafc2be79 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -14,6 +14,11 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return + invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] From 5625c1f45b542286877bc7a8894c7562053f7a30 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 15 May 2023 12:09:25 +0100 Subject: [PATCH 578/918] Use proper variable names --- .../hosts/unreal/plugins/load/load_layout_existing.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout_existing.py b/openpype/hosts/unreal/plugins/load/load_layout_existing.py index f4a2d1e7cc..929a9a1399 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout_existing.py +++ b/openpype/hosts/unreal/plugins/load/load_layout_existing.py @@ -120,9 +120,9 @@ class ExistingLayoutLoader(plugin.Loader): transform = lasset.get('transform_matrix') basis = lasset.get('basis') - t = self._transform_from_basis(transform, basis) + computed_transform = self._transform_from_basis(transform, basis) - actor.set_actor_transform(t, False, True) + actor.set_actor_transform(computed_transform, False, True) @staticmethod def _get_fbx_loader(loaders, family): @@ -296,8 +296,9 @@ class ExistingLayoutLoader(plugin.Loader): transform = lasset.get('transform_matrix') basis = lasset.get('basis') - t = self._transform_from_basis(transform, basis) - actor.set_actor_transform(t, False, True) + computed_transform = self._transform_from_basis( + transform, basis) + actor.set_actor_transform(computed_transform, False, True) actors_matched.append(actor) found = True From b47d172944aa1e0039b3fb554093dbc7508b3033 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 May 2023 12:30:32 +0100 Subject: [PATCH 579/918] Move family check to process --- .../plugins/publish/validate_instance_has_members.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py index fcafc2be79..63849cfd12 100644 --- a/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py +++ b/openpype/hosts/maya/plugins/publish/validate_instance_has_members.py @@ -13,12 +13,6 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - - # Allow renderlayer and workfile to be empty - skip_families = ["workfile", "renderlayer", "rendersetup"] - if instance.data.get("family") in skip_families: - return - invalid = list() if not instance.data["setMembers"]: objectset_name = instance.data['name'] @@ -27,6 +21,10 @@ class ValidateInstanceHasMembers(pyblish.api.InstancePlugin): return invalid def process(self, instance): + # Allow renderlayer and workfile to be empty + skip_families = ["workfile", "renderlayer", "rendersetup"] + if instance.data.get("family") in skip_families: + return invalid = self.get_invalid(instance) if invalid: From 0845ba29dd4edd1872118a65fe72653b6e429531 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 May 2023 14:08:35 +0200 Subject: [PATCH 580/918] General: Project Anatomy on creators (#4962) * added project anatomy to create context * added project anatomy to create plugin --- openpype/pipeline/create/context.py | 19 ++++++++++++++++++- openpype/pipeline/create/creator_plugins.py | 16 +++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 382bbea05e..2fc0669732 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -23,7 +23,7 @@ from openpype.lib.attribute_definitions import ( get_default_values, ) from openpype.host import IPublishHost, IWorkfileHost -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, Anatomy from openpype.pipeline.plugin_discover import DiscoverResult from .creator_plugins import ( @@ -1383,6 +1383,8 @@ class CreateContext: self._current_task_name = None self._current_workfile_path = None + self._current_project_anatomy = None + self._host_is_valid = host_is_valid # Currently unused variable self.headless = headless @@ -1546,6 +1548,18 @@ class CreateContext: return self._current_workfile_path + def get_current_project_anatomy(self): + """Project anatomy for current project. + + Returns: + Anatomy: Anatomy object ready to be used. + """ + + if self._current_project_anatomy is None: + self._current_project_anatomy = Anatomy( + self._current_project_name) + return self._current_project_anatomy + @property def context_has_changed(self): """Host context has changed. @@ -1568,6 +1582,7 @@ class CreateContext: ) project_name = property(get_current_project_name) + project_anatomy = property(get_current_project_anatomy) @property def log(self): @@ -1680,6 +1695,8 @@ class CreateContext: self._current_task_name = task_name self._current_workfile_path = workfile_path + self._current_project_anatomy = None + def reset_plugins(self, discover_publish_plugins=True): """Reload plugins. diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index bd3fbaf78f..9e47e9cc12 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -231,10 +231,24 @@ class BaseCreator: @property def project_name(self): - """Family that plugin represents.""" + """Current project name. + + Returns: + str: Name of a project. + """ return self.create_context.project_name + @property + def project_anatomy(self): + """Current project anatomy. + + Returns: + Anatomy: Project anatomy object. + """ + + return self.create_context.project_anatomy + @property def host(self): return self.create_context.host From 40d4fea857202fcb4194376511f208d8db0f3655 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 15 May 2023 14:10:49 +0200 Subject: [PATCH 581/918] Maya: Validate shader name - OP-5903 (#4971) * Fix regex matching. * Add active setting * Update openpype/hosts/maya/plugins/publish/validate_shader_name.py --- .../plugins/publish/validate_shader_name.py | 20 +++++++++---------- .../defaults/project_settings/maya.json | 1 + .../schemas/schema_maya_publish.json | 5 +++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_shader_name.py b/openpype/hosts/maya/plugins/publish/validate_shader_name.py index b3e51f011d..034db471da 100644 --- a/openpype/hosts/maya/plugins/publish/validate_shader_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_shader_name.py @@ -50,7 +50,8 @@ class ValidateShaderName(pyblish.api.InstancePlugin): asset_name = instance.data.get("asset", None) # Check the number of connected shadingEngines per shape - r = re.compile(cls.regex) + regex_compile = re.compile(cls.regex) + error_message = "object {0} has invalid shader name {1}" for shape in shapes: shading_engines = cmds.listConnections(shape, destination=True, @@ -60,19 +61,18 @@ class ValidateShaderName(pyblish.api.InstancePlugin): ) for shader in shaders: - m = r.match(cls.regex, shader) + m = regex_compile.match(shader) if m is None: invalid.append(shape) - cls.log.error( - "object {0} has invalid shader name {1}".format(shape, - shader) - ) + cls.log.error(error_message.format(shape, shader)) else: - if 'asset' in r.groupindex: + if 'asset' in regex_compile.groupindex: if m.group('asset') != asset_name: invalid.append(shape) - cls.log.error(("object {0} has invalid " - "shader name {1}").format(shape, - shader)) + message = error_message + message += " with missing asset name \"{2}\"" + cls.log.error( + message.format(shape, shader, asset_name) + ) return invalid diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 72b330ce7a..a2a43eefb5 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -734,6 +734,7 @@ "ValidateShaderName": { "enabled": false, "optional": true, + "active": true, "regex": "(?P.*)_(.*)_SHD" }, "ValidateShadingEngine": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 346948c658..07c8d8715b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -126,6 +126,11 @@ "key": "optional", "label": "Optional" }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, { "type": "label", "label": "Shader name regex can use named capture group asset to validate against current asset name.

Example:
^.*(?P=<asset>.+)_SHD

" From 251470997783c56c0bf075fd1e206e37e1ccb9d3 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 15:46:20 +0200 Subject: [PATCH 582/918] use custom namespace for animation instance --- openpype/hosts/maya/api/lib.py | 55 +++++++++++++++++++++++++++++-- openpype/hosts/maya/api/plugin.py | 38 --------------------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 124e0e5b8a..b84056f5ce 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -190,6 +190,44 @@ def maintained_selection(): cmds.select(clear=True) +def get_custom_namespace(custom_namespace): + """Return unique namespace. + + The input namespace can contain a single group + of '#' number tokens to indicate where the namespace's + unique index should go. The amount of tokens defines + the zero padding of the number, e.g ### turns into 001. + + Warning: Note that a namespace will always be + prefixed with a _ if it starts with a digit + + Example: + >>> get_custom_namespace("myspace_##_") + # myspace_01_ + >>> get_custom_namespace("##_myspace") + # _01_myspace + >>> get_custom_namespace("myspace##") + # myspace01 + + """ + split = re.split("([#]+)", custom_namespace, 1) + + if len(split) == 3: + base, padding, suffix = split + padding = "%0{}d".format(len(padding)) + else: + base = split[0] + padding = "%02d" # default padding + suffix = "" + + return unique_namespace( + base, + format=padding, + prefix="_" if not base or base[0].isdigit() else "", + suffix=suffix + ) + + def unique_namespace(namespace, format="%02d", prefix="", suffix=""): """Return unique namespace @@ -3974,10 +4012,21 @@ def create_rig_animation_instance( dependency = str(context["representation"]["_id"]) custom_subset = options.get("animationSubsetName") - if custom_subset: - rig_subset = context['subset']['name'] - namespace = namespace.replace(rig_subset, custom_subset) + formatting_data = { + "asset_name": context['asset']['name'], + "asset_type": context['asset']['type'], + "subset": context['subset']['name'], + "family": ( + context['subset']['data'].get('family') or + context['subset']['data']['families'][0] + ) + } + namespace = get_custom_namespace( + custom_subset.format( + **formatting_data + ) + ) if log: log.info("Creating subset: {}".format(namespace)) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 714278ba6c..3fce92db28 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -84,44 +84,6 @@ def get_reference_node_parents(ref): return parents -def get_custom_namespace(custom_namespace): - """Return unique namespace. - - The input namespace can contain a single group - of '#' number tokens to indicate where the namespace's - unique index should go. The amount of tokens defines - the zero padding of the number, e.g ### turns into 001. - - Warning: Note that a namespace will always be - prefixed with a _ if it starts with a digit - - Example: - >>> get_custom_namespace("myspace_##_") - # myspace_01_ - >>> get_custom_namespace("##_myspace") - # _01_myspace - >>> get_custom_namespace("myspace##") - # myspace01 - - """ - split = re.split("([#]+)", custom_namespace, 1) - - if len(split) == 3: - base, padding, suffix = split - padding = "%0{}d".format(len(padding)) - else: - base = split[0] - padding = "%02d" # default padding - suffix = "" - - return lib.unique_namespace( - base, - format=padding, - prefix="_" if not base or base[0].isdigit() else "", - suffix=suffix - ) - - class Creator(LegacyCreator): defaults = ['Main'] From 8ffe7f55522b364c07e6fd65cc8ad9cf5a125ed9 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 16:27:03 +0200 Subject: [PATCH 583/918] fix function call --- openpype/hosts/maya/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3fce92db28..604ff101db 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -178,7 +178,7 @@ class ReferenceLoader(Loader): count = options.get("count") or 1 for c in range(0, count): - namespace = get_custom_namespace(custom_namespace) + namespace = lib.get_custom_namespace(custom_namespace) group_name = "{}:{}".format( namespace, custom_group_name From b192f8395d7cdd9c417e96e06a112e5761983b9d Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 16:34:37 +0200 Subject: [PATCH 584/918] update docstring --- openpype/hosts/maya/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b84056f5ce..56ba59186d 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3987,6 +3987,7 @@ def create_rig_animation_instance( nodes (list): Member nodes of the rig instance. context (dict): Representation context of the rig container namespace (str): Namespace of the rig container + options (dict): Additional loader data log (logging.Logger, optional): Logger to log to if provided Returns: From f8ad6966fb53248e4c33ea2d3166c076fb078a42 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 17:12:17 +0200 Subject: [PATCH 585/918] make options optional --- openpype/hosts/maya/api/lib.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 56ba59186d..7f160afd3e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3976,7 +3976,7 @@ def get_capture_preset(task_name, task_type, subset, project_settings, log): def create_rig_animation_instance( - nodes, context, namespace, options, log=None + nodes, context, namespace, options=None, log=None ): """Create an animation publish instance for loaded rigs. @@ -3987,13 +3987,16 @@ def create_rig_animation_instance( nodes (list): Member nodes of the rig instance. context (dict): Representation context of the rig container namespace (str): Namespace of the rig container - options (dict): Additional loader data + options (dict, optional): Additional loader data log (logging.Logger, optional): Logger to log to if provided Returns: None """ + if options is None: + options = {} + output = next((node for node in nodes if node.endswith("out_SET")), None) controls = next((node for node in nodes if From a051bb281a880c155c59083c153d9ed4e44e615f Mon Sep 17 00:00:00 2001 From: Mreza Hashemizadeh <68907585+mre7a@users.noreply.github.com> Date: Mon, 15 May 2023 17:22:40 +0200 Subject: [PATCH 586/918] Update openpype/hosts/maya/plugins/load/load_reference.py Co-authored-by: Toke Jepsen --- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 31b6e9d624..f4a4a44344 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -223,7 +223,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def _post_process_rig(self, name, namespace, context, options): nodes = self[:] create_rig_animation_instance( - nodes, context, namespace, options, log=self.log + nodes, context, namespace, options=options, log=self.log ) def _lock_camera_transforms(self, nodes): From 628ecbe5e2e6fb2c11df0185726e60a168eba013 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 15 May 2023 17:58:12 +0200 Subject: [PATCH 587/918] :truck: move test file --- .../unreal}/plugins/publish/test_validate_sequence_frames.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/openpype/{ => hosts/unreal}/plugins/publish/test_validate_sequence_frames.py (100%) diff --git a/tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py b/tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py similarity index 100% rename from tests/unit/openpype/plugins/publish/test_validate_sequence_frames.py rename to tests/unit/openpype/hosts/unreal/plugins/publish/test_validate_sequence_frames.py From 3df689787699f6f51557e058357273689a64cbf0 Mon Sep 17 00:00:00 2001 From: Seyedmohammadreza Hashemizadeh Date: Mon, 15 May 2023 18:01:52 +0200 Subject: [PATCH 588/918] Add some documentations --- website/docs/admin_hosts_maya.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index f0b8710246..700822843f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -247,15 +247,24 @@ Fill in the necessary fields (the optional fields are regex filters) ![new place holder](assets/maya-placeholder_new.png) - - Builder type: Whether the the placeholder should load current asset representations or linked assets representations + - ***Builder type***: Whether the the placeholder should load current asset representations or linked assets representations - - Representation: Representation that will be loaded (ex: ma, abc, png, etc...) + - ***Representation***: Representation that will be loaded (ex: ma, abc, png, etc...) - - Family: Family of the representation to load (main, look, image, etc ...) + - ***Family***: Family of the representation to load (main, look, image, etc ...) - - Loader: Placeholder loader name that will be used to load corresponding representations + - ***Loader***: Placeholder loader name that will be used to load corresponding representations + + - ***Order***: Priority for current placeholder loader (priority is lowest first, highest last) + + - ***Loader arguments***: Loader arguments dictionary can be used to pass optional data to loaders. + One use case is to define a custom Subset name for the animation instances created while loading Rig references.This follows the custom namespace system used by loaders. + + **Example** + ``` + {"animationSubsetName": "{asset_name}_animation_{subset}_##_"} + ``` - - Order: Priority for current placeholder loader (priority is lowest first, highet last) - **Save your template** From f51af7de278e77724c058196eed7dd487da07dfe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 21:54:01 +0200 Subject: [PATCH 589/918] fusion: renaming comp frame range related attributes --- .../fusion/plugins/publish/collect_comp_frame_range.py | 8 ++++---- .../hosts/fusion/plugins/publish/collect_instances.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 38d6577667..08bdad3120 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -41,9 +41,9 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): ) = get_comp_render_range(comp) data = {} - data["compFrameStart"] = int(start) - data["compFrameEnd"] = int(end) - data["compFrameStartHandle"] = int(global_start) - data["compFrameEndHandle"] = int(global_end) + data["renderFrameStart"] = int(start) + data["renderFrameEnd"] = int(end) + data["compFrameStart"] = int(global_start) + data["compFrameEnd"] = int(global_end) context.data.update(data) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 5a6a918730..c1c23ec570 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -32,8 +32,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): if creator_attributes.get("custom_range"): # get comp frame ranges - start = context.data["compFrameStart"] - end = context.data["compFrameEnd"] + start = context.data["renderFrameStart"] + end = context.data["renderFrameEnd"] handle_start = 0 handle_end = 0 start_handle = start From 478c85d7cc96188db55af4cbc8c1a2479ff8717e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 22:09:18 +0200 Subject: [PATCH 590/918] Fusion: renaming confusing attribute name and label --- openpype/hosts/fusion/plugins/create/create_saver.py | 4 +++- openpype/hosts/fusion/plugins/publish/collect_instances.py | 2 +- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 1a60526e42..67b1465ec7 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -200,7 +200,9 @@ class CreateSaver(NewCreator): self._get_render_target_enum(), self._get_reviewable_bool(), BoolDef( - "custom_range", label="Custom range", default=False, + "viewer_render_range", + label="Viewer render in/out", + default=False, ) ] return attr_defs diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index c1c23ec570..6887f4f4e9 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -30,7 +30,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes.get("custom_range"): + if creator_attributes.get("viewer_render_range"): # get comp frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 64d9aedc3b..c3ae9f381d 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,7 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=(not inst.data.get("custom_range")), + ignoreFrameHandleCheck=(not inst.data.get("viewer_render_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From e14e0f5a40c6a48ffc9302544c99e0c3f757b2b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 15 May 2023 22:11:35 +0200 Subject: [PATCH 591/918] hound --- openpype/hosts/fusion/plugins/publish/collect_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index c3ae9f381d..6956b566ad 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -83,7 +83,8 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=(not inst.data.get("viewer_render_range")), + ignoreFrameHandleCheck=( + not inst.data.get("viewer_render_range")), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 11d47eb9407be381a6a10731b683886f9f26f0da Mon Sep 17 00:00:00 2001 From: Petr Dvorak Date: Tue, 16 May 2023 10:28:24 +0200 Subject: [PATCH 592/918] Company name and URL changed --- inno_setup.iss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index 3adde52a8b..418bedbd4d 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -14,10 +14,10 @@ AppId={{B9E9DF6A-5BDA-42DD-9F35-C09D564C4D93} AppName={#MyAppName} AppVersion={#AppVer} AppVerName={#MyAppName} version {#AppVer} -AppPublisher=Orbi Tools s.r.o -AppPublisherURL=http://pype.club -AppSupportURL=http://pype.club -AppUpdatesURL=http://pype.club +AppPublisher=Ynput s.r.o +AppPublisherURL=https://ynput.io +AppSupportURL=https://ynput.io +AppUpdatesURL=https://ynput.io DefaultDirName={autopf}\{#MyAppName}\{#AppVer} UsePreviousAppDir=no DisableProgramGroupPage=yes From 380a9cba51448a90e3e7b1c67fd50a818837f20c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 16 May 2023 08:44:24 +0000 Subject: [PATCH 593/918] [Automated] Release --- CHANGELOG.md | 297 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 299 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c1e7d5fd..bba6b64bfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,303 @@ # Changelog +## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.6...3.15.7) + +### **🆕 New features** + + +
+Addons directory #4893 + +This adds a directory for Addons, for easier distribution of studio specific code. + + +___ + +
+ + +
+Kitsu - Add "image", "online" and "plate" to review families #4923 + +This PR adds "image", "online" and "plate" to the review families so they also can be uploaded to Kitsu.It also adds the `Add review to Kitsu` tag to the default png review. Without it the user would manually need to add it for single image uploads to Kitsu and might confuse users (it confused me first for a while as movies did work). + + +___ + +
+ + +
+Feature/remove and load inv action #4930 + +Added the ability to remove and load a container, as a way to reset it.This can be useful in cases where a container breaks in a way that can be fixed by removing it, then reloading it.Also added the ability to add `InventoryAction` plugins by placing them in `openpype/plugins/inventory`. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Load Rig References - Change Rig to Animation in Animation instance #4877 + +We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu. + + +___ + +
+ + +
+Maya template builder - preserve all references when importing a template #4797 + +When building a template with Maya template builder, we import the template and also the references inside the template file. This causes some problems: +- We cannot use the references to version assets imported by the template. +- When we import the file, the internal reference files are also imported. As a side effect, Maya complains about a reference that no longer exists.`// Error: file: /xxx/maya/2023.3/linux/scripts/AETemplates/AEtransformRelated.mel line 58: Reference node 'turntable_mayaSceneMain_01_RN' is not associated with a reference file.` + + +___ + +
+ + +
+Unreal: Renaming the integration plugin to Ayon. #4646 + +Renamed the .h, and .cpp files to Ayon. Also renamed the classes to with the Ayon keyword. + + +___ + +
+ + +
+3dsMax: render dialogue needs to be closed #4729 + +Make sure the render setup dialog is in a closed state for the update of resolution and other render settings + + +___ + +
+ + +
+Maya Template Builder - Remove default cameras from renderable cameras #4815 + +When we build an asset workfile with build workfile from template inside Maya, we load our turntable camera. But then we end up with 2 renderables camera : **persp** the one imported from the template.We need to remove the **persp** camera (or any other default camera) from renderable cameras when building the work file. + + +___ + +
+ + +
+Validators for Frame Range in Max #4914 + +Switch Render Frame Range Type to 3 for specific ranges (initial setup for the range type is 4)Reset Frame Range will also set the frame range for render settingsRender Collector won't take the frame range from context data but take the range directly from render settingAdd validators for render frame range type and frame range respectively with repair action + + +___ + +
+ + +
+Fusion: Saver creator settings #4943 + +Adding Saver creator settings and enhanced rendering path with template. + + +___ + +
+ + +
+General: Project Anatomy on creators #4962 + +Anatomy object of current project is available on `CreateContext` and create plugins. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Validate shader name - OP-5903 #4971 + +Running the plugin would error with: +``` +// TypeError: 'str' object cannot be interpreted as an integer +```Fixed and added setting `active`. + + +___ + +
+ + +
+Houdini: Fix slow Houdini launch due to shelves generation #4829 + +Shelf generation during Houdini startup would add an insane amount of delay for the Houdini UI to launch correctly. By deferring the shelf generation this takes away the 5+ minutes of delay for the Houdini UI to launch. + + +___ + +
+ + +
+Fusion - Fixed "optional validation" #4912 + +Added OptionalPyblishPluginMixin and is_active checks for all publish tools that should be optional + + +___ + +
+ + +
+Bug: add missing `pyblish.util` import #4937 + +remote publishing was missing import of `remote_publish`. This is adding it back. + + +___ + +
+ + +
+Unreal: Fix missing 'object_path' property #4938 + +Epic removed the `object_path` property from `AssetData`. This PR fixes usages of that property.Fixes #4936 + + +___ + +
+ + +
+Remove obsolete global validator #4939 + +Removing `Validate Sequence Frames` validator from global plugins as it wasn't handling correctly many things and was by mistake enabled, breaking functionality on Deadline. + + +___ + +
+ + +
+General: fix build_workfile get_linked_assets missing project_name arg #4940 + +Linked assets collection don't work within `build_workfile` because `get_linked_assets` function call has a missing `project_name`argument. +- Added the `project_name` arg to the `get_linked_assets` function call. + + +___ + +
+ + +
+General: fix Scene Inventory switch version error dialog missing parent arg on init #4941 + +QuickFix for the switch version error dialog to set inventory widget as parent. + + +___ + +
+ + +
+Unreal: Fix camera frame range #4956 + +Fix the frame range of the level sequence for the Camera in Unreal. + + +___ + +
+ + +
+Unreal: Fix missing parameter when updating Alembic StaticMesh #4957 + +Fix an error when updating an Alembic StaticMesh in Unreal, due to a missing parameter in a function call. + + +___ + +
+ + +
+Unreal: Fix render extraction #4963 + +Fix a problem with the extraction of renders in Unreal. + + +___ + +
+ + +
+Unreal: Remove Python 3.8 syntax from addon #4965 + +Removed Python 3.8 syntax from addon. + + +___ + +
+ + +
+Ftrack: Fix editorial task creation #4966 + +Fix key assignment on instance data during editorial publishing in ftrack hierarchy integration. + + +___ + +
+ +### **Merged pull requests** + + +
+Add "shortcut" to Scripts Menu Definition #4927 + +Add the possibility to associate a shorcut for an entry in the script menu definition with the key "shortcut" + + +___ + +
+ + + + ## [3.15.6](https://github.com/ynput/OpenPype/tree/3.15.6) diff --git a/openpype/version.py b/openpype/version.py index 319a58d384..3a0d05be0e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7-nightly.3" +__version__ = "3.15.7" diff --git a/pyproject.toml b/pyproject.toml index 003f6cf2d3..190ecb9329 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.6" # OpenPype +version = "3.15.7" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 28e0838d00e9b3edc8880c3bc59a753cb6727929 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 May 2023 08:45:43 +0000 Subject: [PATCH 594/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7d224aa73f..08d20c2058 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.7 - 3.15.7-nightly.3 - 3.15.7-nightly.2 - 3.15.7-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.1 - 3.14.1-nightly.4 - 3.14.1-nightly.3 - - 3.14.1-nightly.2 validations: required: true - type: dropdown From 10b953f8bf0a16c2f49f5d89d3f0df6030ea85c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 16 May 2023 11:34:54 +0200 Subject: [PATCH 595/918] fusion: converting frame range source to enum --- .../fusion/plugins/create/create_saver.py | 19 +++++++++++++------ .../plugins/publish/collect_instances.py | 5 +++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 67b1465ec7..80f60a0c6e 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -199,11 +199,7 @@ class CreateSaver(NewCreator): attr_defs = [ self._get_render_target_enum(), self._get_reviewable_bool(), - BoolDef( - "viewer_render_range", - label="Viewer render in/out", - default=False, - ) + self._get_frame_range_enum() ] return attr_defs @@ -222,7 +218,6 @@ class CreateSaver(NewCreator): # These functions below should be moved to another file # so it can be used by other plugins. plugin.py ? - def _get_render_target_enum(self): rendering_targets = { "local": "Local machine rendering", @@ -235,6 +230,18 @@ class CreateSaver(NewCreator): "render_target", items=rendering_targets, label="Render target" ) + def _get_frame_range_enum(self): + frame_range_options = { + "asset_db": "From asset database", + "viewer_render_range": "From viewer render in/out" + } + + return EnumDef( + "frame_range_source", + items=frame_range_options, + label="Frame range source" + ) + def _get_reviewable_bool(self): return BoolDef( "review", diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 6887f4f4e9..61ce10d32f 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -20,6 +20,7 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include creator attributes directly as instance data creator_attributes = instance.data["creator_attributes"] + frame_range_source = creator_attributes.get("frame_range_source") instance.data.update(creator_attributes) # get asset frame ranges @@ -30,8 +31,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start - handle_start end_handle = end + handle_end - if creator_attributes.get("viewer_render_range"): - # get comp frame ranges + if frame_range_source == "viewer_render_range": + # set comp render frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] handle_start = 0 From 2d6919297cdc370bc43a4cc76390021e2ed8564b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 16 May 2023 11:40:00 +0200 Subject: [PATCH 596/918] fusion: adding comp range option --- .../hosts/fusion/plugins/create/create_saver.py | 3 ++- .../fusion/plugins/publish/collect_instances.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 80f60a0c6e..d5e77730c8 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,8 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "From asset database", - "viewer_render_range": "From viewer render in/out" + "viewer_render_range": "From viewer render in/out", + "comp_range": "From composition timeline" } return EnumDef( diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 61ce10d32f..59ff52f5b2 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -40,6 +40,19 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_handle = start end_handle = end + if frame_range_source == "comp_range": + comp_start = context.data["compFrameStart"] + comp_end = context.data["compFrameEnd"] + render_start = context.data["renderFrameStart"] + render_end = context.data["renderFrameEnd"] + # set comp frame ranges + start = render_start + end = render_end + handle_start = render_start - comp_start + handle_end = comp_end - render_end + start_handle = comp_start + end_handle = comp_end + # Include start and end render frame in label subset = instance.data["subset"] label = "{subset} ({start}-{end})".format(subset=subset, From 16f24c253803026ab0f768ef5c3dca1320561ca4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 16 May 2023 22:48:02 +0800 Subject: [PATCH 597/918] fix the bug of fbx loaders --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 10 +++++++--- openpype/hosts/max/plugins/load/load_model_fbx.py | 9 +++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 3a6947798e..ce4dec32a0 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -36,12 +36,16 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) - container_name = f"{name}_CON" + container = rt.getNodeByName(f"{name}") + if not container: + container = rt.container() + container.name = f"{name}" - asset = rt.getNodeByName(f"{name}") + for selection in rt.getCurrentSelection(): + selection.Parent = container return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 88b8f1ed89..7532e3a8a0 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -36,11 +36,16 @@ importFile @"{filepath}" #noPrompt using:FBXIMP self.log.debug(f"Executing command: {fbx_import_cmd}") rt.execute(fbx_import_cmd) + container = rt.getNodeByName(f"{name}") + if not container: + container = rt.container() + container.name = f"{name}" - asset = rt.getNodeByName(f"{name}") + for selection in rt.getCurrentSelection(): + selection.Parent = container return containerise( - name, [asset], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__) def update(self, container, representation): from pymxs import runtime as rt From 255a72bfed1e50dcc30ad46cee1bc44b82d1f2bd Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 17 May 2023 03:25:43 +0000 Subject: [PATCH 598/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 3a0d05be0e..954cfa945b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.7" +__version__ = "3.15.8-nightly.1" From c85ebbfac2236aa3bf761817024290a2a5c1a015 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 May 2023 03:26:23 +0000 Subject: [PATCH 599/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 08d20c2058..0f58d61881 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.1 - 3.15.7 - 3.15.7-nightly.3 - 3.15.7-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.1 - 3.14.1 - 3.14.1-nightly.4 - - 3.14.1-nightly.3 validations: required: true - type: dropdown From c78a74248702d47cf33ff55e38bf8def1f86ec17 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:02:59 +0300 Subject: [PATCH 600/918] add logs, remove adding PYTHONHOME to PATH --- .../Support/logs/davinci_resolve.log | 443 ++++++++++++++++++ .../hosts/resolve/hooks/pre_resolve_setup.py | 14 +- 2 files changed, 449 insertions(+), 8 deletions(-) create mode 100644 igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log diff --git a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log new file mode 100644 index 0000000000..8f1a3b712e --- /dev/null +++ b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log @@ -0,0 +1,443 @@ +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,249 | -------------------------------------------------------------------------------- +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | Loaded log config from C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\log-conf.xml +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | -------------------------------------------------------------------------------- +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | Running DaVinci Resolve Studio v18.1.2.0006 (Windows/MSVC x86_64) +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_BUILD_UUID 3ff36663-26c9-45d9-8506-101676c881e0 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_GIT_COMMIT a3be29d0542aeafcb1b0933bb5ace426aa7d047d +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,259 | Starting GPUDetect 1.2_3-a1 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Done in 161 ms. +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected System: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - OS: Windows 10 Pro (Build 19045) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - Model: ASUSTeK ROG STRIX B550-F GAMING (WI-FI) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - System ID: 838d5045-58b0-44ed-854c-19be5b814d6f +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - CPU: AMD Ryzen 7 3700X, 16 threads, x86-64 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - RAM: 16.6 GiB used of 63.9 GiB +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - NVIDIA GPU Driver: 527.56, supports CUDA 12.0 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected 1 GPUs: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) <- Main Display GPU +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Discrete, 2.0 GiB used of 7.6 GiB VRAM, PCI:8:0 (x16) +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Matches: CUDA, DirectX, NVAPI, NVML, OpenCL, Win32 +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Detected 1 monitors: +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "Generic PnP Monitor" <- Main Monitor +[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | 3840x2160, connected to "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Selected compute API: CUDA +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Automatic GPU Selection: +[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,438 | RED InitializeSdk with library path at C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,642 | R3DAPI 8.3.1-52407 (20220725 Wx64S) R3DSDK 8.3.1-52407 (20220725 Wx64D C3B3) RED CUDA 8.3.1-52408 (20220725) [C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries\] init is successful +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,642 | 0 RED rocket cards available +[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:08,645 | Failed to create instance. +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Decklink model name: '', version: '' +[0x00004fe8] | DVIP | INFO | 2023-05-17 09:46:08,645 | DVIP release/18.1.2 build 2 (86acfdf407856b6cd8daf2517ab0b44f1efc332f). Release, version 18.1.2. +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Resolve Module Handle 00007FF6A4B50000 +[0x00001238] | IO | INFO | 2023-05-17 09:46:08,646 | Using DNxHR library v2.7.3.27r +[0x00002044] | Fusion | INFO | 2023-05-17 09:46:09,093 | Fusion Build: 008b13c7_0004 (Dec 7 2022 15:23:40) +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,114 | fusionsystem: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionsystem.dll" +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,117 | NVDEC is using upto (1023) MB +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,119 | NVDEC decodes H264, chroma 4:2:0, bitdepth 8, upto 4096 x 4096 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,121 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,123 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,127 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 12, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,128 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,130 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 10, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,131 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 12, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,133 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,134 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | FusionLibs: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\" +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | UserData: = "C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Support\Fusion" +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | Profiles: = "UserData:Profiles\" +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,148 | Nvidia GPU (0) is initialised as decoding and encoding device. +[0x00001238] | IO | INFO | 2023-05-17 09:46:09,182 | IO codec library load completed in 536 ms. +[0x00005cbc] | SyManager | ERROR | 2023-05-17 09:46:09,202 | BlackmagicIDHelper::GetProjectLibraries() - Access token is empty +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:09,268 | Loading dblist file: C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\dblist.conf +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,314 | Finished loading Application style sheet +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen message: Starting Up +[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:09,398 | Module Handle 0000023A5FC80000 fusionsystem +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,410 | Module Handle 0000023A6C160000 C:\Program Files\Blackmagic Design\DaVinci Resolve\fusiongraphics.dll +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,437 | Module Handle 0000023A6FB90000 fusionoperators.dll +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,462 | Module Handle 0000023A70490000 fusioncontrols.dll +[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,488 | Module Handle 0000023A70870000 3d.plugin +[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,510 | Module Handle 0000023A70B50000 dimension.plugin +[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,528 | Module Handle 0000023A70F70000 alembic.plugin +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,551 | Module Handle 0000023A71320000 fbx.plugin +[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,566 | Module Handle 0000023A464C0000 fuses.plugin +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,582 | Module Handle 0000023A71A70000 opencolorio.plugin +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,599 | Module Handle 0000023A6ADF0000 openfx.plugin +[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,615 | Module Handle 0000023A68DA0000 openvr.plugin +[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,632 | Module Handle 0000023A6AE60000 paint.plugin +[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,653 | Module Handle 0000023A6AED0000 particles.plugin +[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,673 | Module Handle 0000023A720D0000 text.plugin +[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,690 | Module Handle 0000023A6CB80000 utilities.plugin +[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,715 | Module Handle 00007FF89F590000 KrokodoveFu16.plugin +[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,717 | Module Handle 00007FF89F330000 KrokodoveFu17.plugin +[0x00004f28] | OpenFX | INFO | 2023-05-17 09:46:09,817 | No context is available for com.absoft.NeatVideo5 +[0x00007fc8] | Main | INFO | 2023-05-17 09:46:09,828 | Started listener socket at port 15000 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,308 | Show splash screen message: Checking Licenses +[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Memory config: reserved=12270M pinned=8000M log=0 +[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Using default pooled memory manager +[0x00003798] | LeManager | INFO | 2023-05-17 09:46:10,533 | 521, 29 +[0x00008180] | BtCommon | INFO | 2023-05-17 09:46:10,533 | BtResourceManager Process Thread Started +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,609 | WMF encoder cnt for SW (hvc1) is (0) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | WMF encoder cnt for HW (hvc1) is (1) +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | Setting codec capacity (0) +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Total: 20, NumDtThreads: 8, NumComms: 0, NumSites: 1 + +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Lookaheads -> playback = 20, record = 20, stop = 2 + +[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Using 8 generic IO threads +[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Total of 16 IO threads (including 8 generic and 8 Red decode threads) +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,627 | Show splash screen message: Loading Project Libraries +[0x00001a20] | DtManager | INFO | 2023-05-17 09:46:10,627 | Dt Worker Thread Started +[0x00007da0] | GsManager | INFO | 2023-05-17 09:46:10,628 | Gs Processor Thread ----- (32160) + +[0x00004474] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00002b7c] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00002d64] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00007bbc] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Data Handler Thread Started +[0x00002ad0] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started +[0x00004a48] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00005084] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00003050] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00003e88] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00006550] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007950] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x000059a8] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007de4] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00007a24] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started +[0x00001eec] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00001f5c] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00007a50] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x000009c4] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,633 | Show splash screen message: Initializing system components +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,635 | Let There Be CUDA Light! +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,639 | Show splash screen message: Loading video codecs +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,644 | Show splash screen message: Loading video plugins +[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,680 | Creating shared OpenGL context for this thread (1 total). +[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,708 | Initialized OpenGL 4.6 (requested 2.0) on device 'NVIDIA Corporation NVIDIA GeForce RTX 2060 SUPER/PCIe/SSE2' +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,708 | Show splash screen message: Loading Fairlight Engine +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,708 | Initializing CUDA board manager for Main Display GPU gpu:a24a3d50.83f1fdb7. +[0x00001968] | IO | INFO | 2023-05-17 09:46:10,788 | IO codec initialization completed in 143 ms. +[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:46:10,811 | Board manager thread for "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) is ready. +[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. +[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. +[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,811 | Enabled CUDA pinned memory. +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.122(003): WASAPI: Scanning Speakers (Steam Streaming Microphone) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Device: Speakers (Steam Streaming Microphone). Scan time 6. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Scanning Speakers (High Definition Audio Device) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Device: Speakers (High Definition Audio Device). Scan time 61. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Scanning Mi 27 NU (NVIDIA High Definition Audio) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Device: Mi 27 NU (NVIDIA High Definition Audio). Scan time 45. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Scanning Speakers (Steam Streaming Speakers) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.242(000): WASAPI: Device: Speakers (Steam Streaming Speakers). Scan time 6. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.243(001): WASAPI: Scanning Digital Audio (S/PDIF) (High Definition Audio Device) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.286(000): WASAPI: Device: Digital Audio (S/PDIF) (High Definition Audio Device). Scan time 42. Formats = 1 15 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.287(001): WASAPI: Scanning Microphone (Steam Streaming Microphone) for formats +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.294(000): WASAPI: Device: Microphone (Steam Streaming Microphone). Scan time 7. Formats = 0 16 formats scanned +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.390(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004f94] | DbCommon2 | ERROR | 2023-05-17 09:46:11,307 | Cannot connect to soundfx192.168.100.31 database: could not connect to server: Connection refused (0x0000274D/10061) + Is the server running on host "192.168.100.31" and accepting + TCP/IP connections on port 5432? +QPSQL: Unable to connect +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,339 | postgres project library homepc at 127.0.0.1 version 9.5.22 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Project library [homepc127.0.0.1] current version <18.1.0.004> updated on <2022-11-22T16:16:24.518>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Connect to postgres project library homepc127.0.0.1 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,396 | postgres project library barney at 192.168.100.30 version 9.5.25 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Project library [barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-17T08:47:18.322>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Connect to postgres project library barney192.168.100.30 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,475 | postgres project library shtv_barney at 192.168.100.30 version 9.5.25 +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Project library [shtv_barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-15T13:16:50.006>, remark: +[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Connect to postgres project library shtv_barney192.168.100.30 +[0x00005850] | Fusion | INFO | 2023-05-17 09:46:11,704 | 260 templates scanned in 0.12 secs +[0x00004fe8] | Fairlight | INFO | 2023-05-17 09:46:12,310 | 00.00.01.565(000): Running Fairlight (939d5c56aa8d66c1f392ff963f9b9c349ef4d9fb) +[0x00004fe8] | FairlightLoader | INFO | 2023-05-17 09:46:12,310 | Fairlight lib initialized in 1598 ms. +[0x00006a7c] | UI.GLContext | INFO | 2023-05-17 09:46:12,405 | Creating shared OpenGL context for this thread (2 total). +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:12,417 | 00.00.01.501(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00006a7c] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | Initialized MainPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | MainPlayer: OpenGL I/O setup done. +[0x000076bc] | UI.GLContext | INFO | 2023-05-17 09:46:12,455 | Creating shared OpenGL context for this thread (3 total). +[0x000076bc] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | Initialized AuxPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | AuxPlayer: OpenGL I/O setup done. +[0x00008190] | UI.GLContext | INFO | 2023-05-17 09:46:12,506 | Creating shared OpenGL context for this thread (4 total). +[0x00008190] | UI.Scopes | INFO | 2023-05-17 09:46:12,565 | Initialized GPU Scopes Manager on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' +[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceLayoutFusion_sub001Default]'s slot is not defined: workspaceLayoutFusion_sub001Default_triggered() +[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceWIPlugins_placeholder]'s slot is not defined: workspaceWIPlugins_placeholder_triggered() +[0x00006e14] | SyManager | WARN | 2023-05-17 09:46:12,656 | socket failed to connect to server, error: 10061 + +[0x00006e14] | SyManager | ERROR | 2023-05-17 09:46:12,656 | DRIVER: panel connection failed +[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:12,657 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed +[0x000052e4] | BtCommon | INFO | 2023-05-17 09:46:12,661 | Starting Daemon: C:/Program Files/Blackmagic Design/DaVinci Resolve/DaVinciPanelDaemon.exe +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:12,686 | Show splash screen message: Loading Project Settings +[0x00006e14] | SyManager | INFO | 2023-05-17 09:46:12,757 | Connection to the panel server has been re-established +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:12,899 | Failed to find value '0' in combo-box +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,055 | Show splash screen message: Loading Media Page +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a global action +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a valid global action +[0x00004fe8] | UI.FairlightInterface | WARN | 2023-05-17 09:46:19,269 | SetActionEnabled: Failed to find action [viewTimelineScrollingFixed]'s action connector for handler Id [1] +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,424 | Show splash screen message: Loading Cut Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,698 | Show splash screen message: Loading Edit Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,951 | Show splash screen message: Loading Fusion Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,038 | Show splash screen message: Loading Fairlight Page +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,227 | Show splash screen message: Loading Color Page +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:20,296 | Not creating special GL widget for screen 0 +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,003 | Failed to find value '8192' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,284 | Failed to find value '100' in combo-box +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,006 | Show splash screen message: Loading Waveform Monitor +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,433 | Show splash screen message: Loading Audio Plugins +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,528 | Collaboration IP 127.0.0.1 Port 0 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,674 | Show splash screen message: Loading Projects +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:22,739 | Current user pointer is changed +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - SecondaryScreenIdx not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - UseDisplayNameForClips not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultTransitionKey not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultAudioTransitionKey not read +[0x00004fe8] | IO | INFO | 2023-05-17 09:46:22,767 | RED rocket decode has been disabled in the config file +[0x00004fe8] | LeManager | ERROR | 2023-05-17 09:46:22,820 | 444, 139, 0 +[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:22,922 | Failed to create instance. +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-2) or Search Operation (0) not supported, and hence disabling the condition. +[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-1) or Search Operation (-1) not supported, and hence disabling the condition. +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,077 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,080 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,098 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,099 | Failed to find value '4' in combo-box +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Start purging still caches +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Finish purging still caches +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,149 | Media pool relink status changed to 0 +[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:23,167 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,179 | Launching project manager +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,182 | Main view page is changed to 12 +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,209 | Show splash screen message: Ready +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,214 | Gallery pointer is changed, refreshing color gallery +[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,223 | Gallery pointer is changed, refreshing gallery browser +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,312 | Close splash screen +[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,314 | Launching main loop +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:24,215 | Fusion templates changed +[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:24,758 | Started script server: 32648 + +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction is ongoing, user initiated action Sync Asset Map is postponed +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction completed, enqueueing 1 postponed actions +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:48:41,842 | Loading project (gazprom_screens_06_R_Home_Painter_Lookdev_v002) from project library (homepc127.0.0.1) took 291 ms +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:42,159 | Action [fairlightBusStructure] is not a global action +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:42,390 | 00.02.31.644(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,528 | Failed to find value '0' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,531 | Failed to find value '0' in combo-box +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Start purging still caches +[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Finish purging still caches +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,557 | Media pool relink status changed to 0 +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:43,596 | 00.02.32.735(002): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current project pointer (gazprom_screens_06_R_Home_Painter_Lookdev_v002) is changed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current timeline pointer () is changed +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,868 | Lock project 513e336f-59c3-4fa8-b9e9-814cf2b797a3 +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:43,868 | The buttonId [7] is not found for [0] in [0] +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,876 | UiGPU::UploadWipeData called before UiGPU has been initialized. Returning false +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,877 | Gallery pointer is changed, refreshing color gallery +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,891 | Gallery pointer is changed, refreshing gallery browser +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,921 | Main view page is changed to 1 +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,921 | Failed to get auto update information. +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,930 | Main view page is changed to 1 +[0x00004fe8] | UI | WARN | 2023-05-17 09:48:44,566 | UI Persistence - MediaPoolFloatingWindowGeometry not read +[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:44,583 | The buttonId [7] is not found for [0] in [0] +[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:44,602 | 00.02.33.795(001): WASAPI: switching to AUTO because exclusive mode not allowed +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:44,912 | Main view page is changed to 1 +[0x00004fe8] | UI | INFO | 2023-05-17 09:48:45,076 | Launching main window +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,094 | No reply received from file system, assume successfully deleted folder D:\SYNC\BACKUP\Resolve Project Backups\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,105 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\CacheClip\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,116 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\.gallery\64d8dbe8-256e-46b1-81da-671a6a8fc423. +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newFolder] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProjectInReadOnlyMode] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Close] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Rename] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProjectAs] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProjectPlus] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProjectWithStillsAndLuts] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProjectPlus] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_archiveProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_unlockProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editDelete] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCut] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCopy] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editPaste] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_loadProjectSettingsToCurrentProject] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectBackups] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectSettings] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_dynamicProjectSwitching] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_closeProjectsInMemory] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_updateThumbnails] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_otherProjectBackups] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_refresh] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_resetUiLayout] due to [Owner is invisible] +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:51,277 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | module 'sys' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | module 're' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | TypeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | split() missing 2 required positional arguments: 'pattern' and 'string' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | NameError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | name 're__path__' is not defined +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | module 're' has no attribute '__path__' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | module 'sys' has no attribute 'info' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | Traceback (most recent call last): +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | File "", line 1, in +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | AttributeError +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | : +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | module 'sys' has no attribute 'info' +[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,078 | >>> [ openpype.hosts.resolve installed ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,083 | >>> [ Registering DaVinci Resovle plug-ins.. ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,100 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,105 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,110 | - { timers_manager }: [ Installing task changed callback ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,816 | - { openpype.pipeline.anatomy }: [ Looking for matching root in path "D:/SYNC/OPENPYPE/gazprom_screens/06_R_Home_Painter/work/Lookdev". ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,821 | >>> [ Found match in root "work". ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,952 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 190, in showEvent +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 176, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | self._model.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\model.py", line 27, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | instances = list_instances() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 285, in list_instances +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_timeline_items = lib.get_current_timeline_items( +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | subsets_model.set_assets(asset_ids) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | all_timeline_items = lib.get_current_timeline_items(filter=False) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,731 | Traceback (most recent call last): +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | subsets_model.set_assets(asset_ids) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | self.refresh() +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | repre_ids = {con.get("representation") for con in containers} +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | all_timeline_items = lib.get_current_timeline_items(filter=False) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | selected_track_count = timeline.GetTrackCount(track_type) +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | AttributeError +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | : +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | 'NoneType' object has no attribute 'GetTrackCount' +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,736 | +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - MediaPoolFloatingWindowGeometry not read +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - ConformEdlEffectsLibrary not read +[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:53:56,766 | Flushing GPU memory... +[0x000065b4] | UI.GLContext | INFO | 2023-05-17 09:53:56,766 | Creating shared OpenGL context for this thread (5 total). +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:53:56,782 | Main view page is changed to 2 +[0x000065b4] | UI.GLTexPool | INFO | 2023-05-17 09:53:56,884 | Released 0 MiB in 0 unused textures. +[0x00004fe8] | UI | INFO | 2023-05-17 09:53:56,944 | PBO is initialized with size [1920x1080], bitDepth=[8], hasAlpha=[1]. +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '1' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box +[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box +[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:53:59,548 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:01,297 | Current timeline pointer (Timeline 1) is changed +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,495 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,526 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed +[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,550 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,592 | Saving project took 97 ms +[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,619 | Database transaction completed, enqueueing 2 postponed actions +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,357 | >>> [ openpype.hosts.resolve installed ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,363 | >>> [ Registering DaVinci Resovle plug-ins.. ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,375 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,380 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] +[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,386 | - { timers_manager }: [ Installing task changed callback ] +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,823 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,826 | Saving project took 2 ms +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,920 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,923 | Saving project took 3 ms +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,137 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,321 | Saving project took 183 ms +[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:40,326 | Main view page is changed to 2 +[0x00004fe8] | BtCommon | WARN | 2023-05-17 09:54:40,570 | Negative duration +[0x00004fe8] | UI | WARN | 2023-05-17 09:54:40,638 | Unable to submit frame to GPU scopes, legacy OpenGL uploads not supported (Player Model). +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,497 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 +[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,555 | Saving project took 59 ms diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 8c88478104..486d8121cf 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -33,6 +33,9 @@ class ResolvePrelaunch(PreLaunchHook): resolve_script_api = Path( resolve_script_api_locations[current_platform] ) + self.log.info( + f"setting RESOLVE_SCRIPT_API variable to {resolve_script_api}" + ) self.launch_context.env[ "RESOLVE_SCRIPT_API" ] = resolve_script_api.as_posix() @@ -52,6 +55,9 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env[ "RESOLVE_SCRIPT_LIB" ] = resolve_script_lib.as_posix() + self.log.info( + f"setting RESOLVE_SCRIPT_LIB variable to {resolve_script_lib}" + ) # TODO: add OTIO installation from `openpype/requirements.py` # making sure python <3.9.* is installed at provided path @@ -69,14 +75,6 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env["PYTHONHOME"] = python3_home_str self.log.info(f"Path to Resolve Python folder: `{python3_home_str}`") - # add to the python path to PATH - env_path = self.launch_context.env["PATH"] - self.launch_context.env[ - "PATH" - ] = f"{python3_home_str}{os.pathsep}{env_path}" - - self.log.debug(f"PATH: {self.launch_context.env['PATH']}") - # add to the PYTHONPATH env_pythonpath = self.launch_context.env["PYTHONPATH"] modules_path = Path(resolve_script_api, "Modules").as_posix() From cd7b1d92f8169ac655f69cd1ba69272e449fa19b Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:05:28 +0300 Subject: [PATCH 601/918] remove resolve logs --- .../Support/logs/davinci_resolve.log | 443 ------------------ 1 file changed, 443 deletions(-) delete mode 100644 igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log diff --git a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log b/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log deleted file mode 100644 index 8f1a3b712e..0000000000 --- a/igniter/#ProgramData#Blackmagic Design/DaVinci Resolve/Support/logs/davinci_resolve.log +++ /dev/null @@ -1,443 +0,0 @@ -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,249 | -------------------------------------------------------------------------------- -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | Loaded log config from C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\log-conf.xml -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:46:08,250 | -------------------------------------------------------------------------------- -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | Running DaVinci Resolve Studio v18.1.2.0006 (Windows/MSVC x86_64) -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_BUILD_UUID 3ff36663-26c9-45d9-8506-101676c881e0 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,251 | BMD_GIT_COMMIT a3be29d0542aeafcb1b0933bb5ace426aa7d047d -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,259 | Starting GPUDetect 1.2_3-a1 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Done in 161 ms. -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected System: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - OS: Windows 10 Pro (Build 19045) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - Model: ASUSTeK ROG STRIX B550-F GAMING (WI-FI) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - System ID: 838d5045-58b0-44ed-854c-19be5b814d6f -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - CPU: AMD Ryzen 7 3700X, 16 threads, x86-64 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - RAM: 16.6 GiB used of 63.9 GiB -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | - NVIDIA GPU Driver: 527.56, supports CUDA 12.0 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,421 | Detected 1 GPUs: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) <- Main Display GPU -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Discrete, 2.0 GiB used of 7.6 GiB VRAM, PCI:8:0 (x16) -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Matches: CUDA, DirectX, NVAPI, NVML, OpenCL, Win32 -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | Detected 1 monitors: -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | - "Generic PnP Monitor" <- Main Monitor -[0x00004fe8] | GPUDetect | INFO | 2023-05-17 09:46:08,423 | 3840x2160, connected to "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Selected compute API: CUDA -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | Automatic GPU Selection: -[0x00004fe8] | Main.GPUConfig | INFO | 2023-05-17 09:46:08,424 | - "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,438 | RED InitializeSdk with library path at C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:08,642 | R3DAPI 8.3.1-52407 (20220725 Wx64S) R3DSDK 8.3.1-52407 (20220725 Wx64D C3B3) RED CUDA 8.3.1-52408 (20220725) [C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Libraries\] init is successful -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,642 | 0 RED rocket cards available -[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:08,645 | Failed to create instance. -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Decklink model name: '', version: '' -[0x00004fe8] | DVIP | INFO | 2023-05-17 09:46:08,645 | DVIP release/18.1.2 build 2 (86acfdf407856b6cd8daf2517ab0b44f1efc332f). Release, version 18.1.2. -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:08,645 | Resolve Module Handle 00007FF6A4B50000 -[0x00001238] | IO | INFO | 2023-05-17 09:46:08,646 | Using DNxHR library v2.7.3.27r -[0x00002044] | Fusion | INFO | 2023-05-17 09:46:09,093 | Fusion Build: 008b13c7_0004 (Dec 7 2022 15:23:40) -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,114 | fusionsystem: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionsystem.dll" -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,117 | NVDEC is using upto (1023) MB -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,119 | NVDEC decodes H264, chroma 4:2:0, bitdepth 8, upto 4096 x 4096 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,121 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,123 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,127 | NVDEC decodes HEVC, chroma 4:2:0, bitdepth 12, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,128 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,130 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 10, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,131 | NVDEC decodes HEVC, chroma 4:4:4, bitdepth 12, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,133 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 8, upto 8192 x 8192 -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,134 | NVDEC decodes VP9, chroma 4:2:0, bitdepth 10, upto 8192 x 8192 -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | FusionLibs: = "C:\Program Files\Blackmagic Design\DaVinci Resolve\" -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | UserData: = "C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Support\Fusion" -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,148 | Profiles: = "UserData:Profiles\" -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,148 | Nvidia GPU (0) is initialised as decoding and encoding device. -[0x00001238] | IO | INFO | 2023-05-17 09:46:09,182 | IO codec library load completed in 536 ms. -[0x00005cbc] | SyManager | ERROR | 2023-05-17 09:46:09,202 | BlackmagicIDHelper::GetProjectLibraries() - Access token is empty -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:09,268 | Loading dblist file: C:\Users\videopro\AppData\Roaming\Blackmagic Design\DaVinci Resolve\Preferences\dblist.conf -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,314 | Finished loading Application style sheet -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:09,396 | Show splash screen message: Starting Up -[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:09,398 | Module Handle 0000023A5FC80000 fusionsystem -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,410 | Module Handle 0000023A6C160000 C:\Program Files\Blackmagic Design\DaVinci Resolve\fusiongraphics.dll -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,437 | Module Handle 0000023A6FB90000 fusionoperators.dll -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,462 | Module Handle 0000023A70490000 fusioncontrols.dll -[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,488 | Module Handle 0000023A70870000 3d.plugin -[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,510 | Module Handle 0000023A70B50000 dimension.plugin -[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,528 | Module Handle 0000023A70F70000 alembic.plugin -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,551 | Module Handle 0000023A71320000 fbx.plugin -[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,566 | Module Handle 0000023A464C0000 fuses.plugin -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,582 | Module Handle 0000023A71A70000 opencolorio.plugin -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,599 | Module Handle 0000023A6ADF0000 openfx.plugin -[0x00007b34] | Fusion | INFO | 2023-05-17 09:46:09,615 | Module Handle 0000023A68DA0000 openvr.plugin -[0x0000772c] | Fusion | INFO | 2023-05-17 09:46:09,632 | Module Handle 0000023A6AE60000 paint.plugin -[0x00003934] | Fusion | INFO | 2023-05-17 09:46:09,653 | Module Handle 0000023A6AED0000 particles.plugin -[0x0000204c] | Fusion | INFO | 2023-05-17 09:46:09,673 | Module Handle 0000023A720D0000 text.plugin -[0x000078cc] | Fusion | INFO | 2023-05-17 09:46:09,690 | Module Handle 0000023A6CB80000 utilities.plugin -[0x00004f28] | Fusion | INFO | 2023-05-17 09:46:09,715 | Module Handle 00007FF89F590000 KrokodoveFu16.plugin -[0x00005f70] | Fusion | INFO | 2023-05-17 09:46:09,717 | Module Handle 00007FF89F330000 KrokodoveFu17.plugin -[0x00004f28] | OpenFX | INFO | 2023-05-17 09:46:09,817 | No context is available for com.absoft.NeatVideo5 -[0x00007fc8] | Main | INFO | 2023-05-17 09:46:09,828 | Started listener socket at port 15000 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,308 | Show splash screen message: Checking Licenses -[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Memory config: reserved=12270M pinned=8000M log=0 -[0x00004fe8] | BtCommon | INFO | 2023-05-17 09:46:10,532 | Using default pooled memory manager -[0x00003798] | LeManager | INFO | 2023-05-17 09:46:10,533 | 521, 29 -[0x00008180] | BtCommon | INFO | 2023-05-17 09:46:10,533 | BtResourceManager Process Thread Started -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,609 | WMF encoder cnt for SW (hvc1) is (0) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | WMF encoder cnt for HW (hvc1) is (1) -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:10,624 | Setting codec capacity (0) -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Total: 20, NumDtThreads: 8, NumComms: 0, NumSites: 1 - -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:10,625 | Lookaheads -> playback = 20, record = 20, stop = 2 - -[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Using 8 generic IO threads -[0x00004fe8] | DtManager | INFO | 2023-05-17 09:46:10,626 | Total of 16 IO threads (including 8 generic and 8 Red decode threads) -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,627 | Show splash screen message: Loading Project Libraries -[0x00001a20] | DtManager | INFO | 2023-05-17 09:46:10,627 | Dt Worker Thread Started -[0x00007da0] | GsManager | INFO | 2023-05-17 09:46:10,628 | Gs Processor Thread ----- (32160) - -[0x00004474] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00002b7c] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00002d64] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00007bbc] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Data Handler Thread Started -[0x00002ad0] | DtManager | INFO | 2023-05-17 09:46:10,628 | Dt Worker Thread Started -[0x00004a48] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00005084] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00003050] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00003e88] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00006550] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007950] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x000059a8] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007de4] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00007a24] | DtManager | INFO | 2023-05-17 09:46:10,629 | Dt Worker Thread Started -[0x00001eec] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00001f5c] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00007a50] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x000009c4] | DtManager | INFO | 2023-05-17 09:46:10,630 | Dt Worker Thread Started -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,633 | Show splash screen message: Initializing system components -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,635 | Let There Be CUDA Light! -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,639 | Show splash screen message: Loading video codecs -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,644 | Show splash screen message: Loading video plugins -[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,680 | Creating shared OpenGL context for this thread (1 total). -[0x00004fe8] | UI.GLContext | INFO | 2023-05-17 09:46:10,708 | Initialized OpenGL 4.6 (requested 2.0) on device 'NVIDIA Corporation NVIDIA GeForce RTX 2060 SUPER/PCIe/SSE2' -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:10,708 | Show splash screen message: Loading Fairlight Engine -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,708 | Initializing CUDA board manager for Main Display GPU gpu:a24a3d50.83f1fdb7. -[0x00001968] | IO | INFO | 2023-05-17 09:46:10,788 | IO codec initialization completed in 143 ms. -[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:46:10,811 | Board manager thread for "NVIDIA GeForce RTX 2060 SUPER" (gpu:a24a3d50.83f1fdb7) is ready. -[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. -[0x00001190] | UI.GLInterop | INFO | 2023-05-17 09:46:10,811 | OpenGL interop was initialized. -[0x00001190] | GPU.MultiBoardMgr | INFO | 2023-05-17 09:46:10,811 | Enabled CUDA pinned memory. -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.122(003): WASAPI: Scanning Speakers (Steam Streaming Microphone) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Device: Speakers (Steam Streaming Microphone). Scan time 6. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.128(000): WASAPI: Scanning Speakers (High Definition Audio Device) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Device: Speakers (High Definition Audio Device). Scan time 61. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.190(000): WASAPI: Scanning Mi 27 NU (NVIDIA High Definition Audio) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Device: Mi 27 NU (NVIDIA High Definition Audio). Scan time 45. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.236(000): WASAPI: Scanning Speakers (Steam Streaming Speakers) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.242(000): WASAPI: Device: Speakers (Steam Streaming Speakers). Scan time 6. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,011 | 00.00.00.243(001): WASAPI: Scanning Digital Audio (S/PDIF) (High Definition Audio Device) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.286(000): WASAPI: Device: Digital Audio (S/PDIF) (High Definition Audio Device). Scan time 42. Formats = 1 15 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.287(001): WASAPI: Scanning Microphone (Steam Streaming Microphone) for formats -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.294(000): WASAPI: Device: Microphone (Steam Streaming Microphone). Scan time 7. Formats = 0 16 formats scanned -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:11,212 | 00.00.00.390(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004f94] | DbCommon2 | ERROR | 2023-05-17 09:46:11,307 | Cannot connect to soundfx192.168.100.31 database: could not connect to server: Connection refused (0x0000274D/10061) - Is the server running on host "192.168.100.31" and accepting - TCP/IP connections on port 5432? -QPSQL: Unable to connect -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,339 | postgres project library homepc at 127.0.0.1 version 9.5.22 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Project library [homepc127.0.0.1] current version <18.1.0.004> updated on <2022-11-22T16:16:24.518>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,347 | Connect to postgres project library homepc127.0.0.1 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,396 | postgres project library barney at 192.168.100.30 version 9.5.25 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Project library [barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-17T08:47:18.322>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,422 | Connect to postgres project library barney192.168.100.30 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,475 | postgres project library shtv_barney at 192.168.100.30 version 9.5.25 -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Project library [shtv_barney192.168.100.30] current version <18.1.0.004> updated on <2022-11-15T13:16:50.006>, remark: -[0x00004f94] | DbCommon2 | INFO | 2023-05-17 09:46:11,501 | Connect to postgres project library shtv_barney192.168.100.30 -[0x00005850] | Fusion | INFO | 2023-05-17 09:46:11,704 | 260 templates scanned in 0.12 secs -[0x00004fe8] | Fairlight | INFO | 2023-05-17 09:46:12,310 | 00.00.01.565(000): Running Fairlight (939d5c56aa8d66c1f392ff963f9b9c349ef4d9fb) -[0x00004fe8] | FairlightLoader | INFO | 2023-05-17 09:46:12,310 | Fairlight lib initialized in 1598 ms. -[0x00006a7c] | UI.GLContext | INFO | 2023-05-17 09:46:12,405 | Creating shared OpenGL context for this thread (2 total). -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:46:12,417 | 00.00.01.501(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00006a7c] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | Initialized MainPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,454 | MainPlayer: OpenGL I/O setup done. -[0x000076bc] | UI.GLContext | INFO | 2023-05-17 09:46:12,455 | Creating shared OpenGL context for this thread (3 total). -[0x000076bc] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | Initialized AuxPlayer OpenGL I/O on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.GLIO | INFO | 2023-05-17 09:46:12,505 | AuxPlayer: OpenGL I/O setup done. -[0x00008190] | UI.GLContext | INFO | 2023-05-17 09:46:12,506 | Creating shared OpenGL context for this thread (4 total). -[0x00008190] | UI.Scopes | INFO | 2023-05-17 09:46:12,565 | Initialized GPU Scopes Manager on CUDA device 'NVIDIA GeForce RTX 2060 SUPER' -[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceLayoutFusion_sub001Default]'s slot is not defined: workspaceLayoutFusion_sub001Default_triggered() -[0x00004fe8] | UI.MenuBar | WARN | 2023-05-17 09:46:12,649 | Main menu action [workspaceWIPlugins_placeholder]'s slot is not defined: workspaceWIPlugins_placeholder_triggered() -[0x00006e14] | SyManager | WARN | 2023-05-17 09:46:12,656 | socket failed to connect to server, error: 10061 - -[0x00006e14] | SyManager | ERROR | 2023-05-17 09:46:12,656 | DRIVER: panel connection failed -[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:12,657 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed -[0x000052e4] | BtCommon | INFO | 2023-05-17 09:46:12,661 | Starting Daemon: C:/Program Files/Blackmagic Design/DaVinci Resolve/DaVinciPanelDaemon.exe -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:12,686 | Show splash screen message: Loading Project Settings -[0x00006e14] | SyManager | INFO | 2023-05-17 09:46:12,757 | Connection to the panel server has been re-established -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:12,899 | Failed to find value '0' in combo-box -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,055 | Show splash screen message: Loading Media Page -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a global action -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:46:19,145 | Action [fairlightBusStructure] is not a valid global action -[0x00004fe8] | UI.FairlightInterface | WARN | 2023-05-17 09:46:19,269 | SetActionEnabled: Failed to find action [viewTimelineScrollingFixed]'s action connector for handler Id [1] -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,424 | Show splash screen message: Loading Cut Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,698 | Show splash screen message: Loading Edit Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:19,951 | Show splash screen message: Loading Fusion Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,038 | Show splash screen message: Loading Fairlight Page -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:20,227 | Show splash screen message: Loading Color Page -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:20,296 | Not creating special GL widget for screen 0 -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,003 | Failed to find value '8192' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:21,284 | Failed to find value '100' in combo-box -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,006 | Show splash screen message: Loading Waveform Monitor -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,433 | Show splash screen message: Loading Audio Plugins -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,528 | Collaboration IP 127.0.0.1 Port 0 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:22,674 | Show splash screen message: Loading Projects -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:22,739 | Current user pointer is changed -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - SecondaryScreenIdx not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - UseDisplayNameForClips not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultTransitionKey not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:22,755 | UI Persistence - DefaultAudioTransitionKey not read -[0x00004fe8] | IO | INFO | 2023-05-17 09:46:22,767 | RED rocket decode has been disabled in the config file -[0x00004fe8] | LeManager | ERROR | 2023-05-17 09:46:22,820 | 444, 139, 0 -[0x00004fe8] | SyManager.DeckLink | ERROR | 2023-05-17 09:46:22,922 | Failed to create instance. -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-2) or Search Operation (0) not supported, and hence disabling the condition. -[0x00004fe8] | SyManager | INFO | 2023-05-17 09:46:22,973 | Search Parameter (-1) or Search Operation (-1) not supported, and hence disabling the condition. -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,077 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,080 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,098 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:46:23,099 | Failed to find value '4' in combo-box -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Start purging still caches -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:46:23,119 | Finish purging still caches -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,149 | Media pool relink status changed to 0 -[0x00004fe8] | FusionUtils | WARN | 2023-05-17 09:46:23,167 | Fusion DoAction ACTION_SET_FUSION_HOTKEY ignored, init not completed -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,179 | Launching project manager -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:23,182 | Main view page is changed to 12 -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,209 | Show splash screen message: Ready -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,214 | Gallery pointer is changed, refreshing color gallery -[0x00004fe8] | UI | INFO | 2023-05-17 09:46:23,223 | Gallery pointer is changed, refreshing gallery browser -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,312 | Close splash screen -[0x00004fe8] | Main | INFO | 2023-05-17 09:46:23,314 | Launching main loop -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:46:24,215 | Fusion templates changed -[0x00004fe8] | Fusion | INFO | 2023-05-17 09:46:24,758 | Started script server: 32648 - -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction is ongoing, user initiated action Sync Asset Map is postponed -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:48:41,800 | Database transaction completed, enqueueing 1 postponed actions -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:48:41,842 | Loading project (gazprom_screens_06_R_Home_Painter_Lookdev_v002) from project library (homepc127.0.0.1) took 291 ms -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:42,159 | Action [fairlightBusStructure] is not a global action -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:42,390 | 00.02.31.644(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,528 | Failed to find value '0' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,531 | Failed to find value '0' in combo-box -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Start purging still caches -[0x00004fe8] | SyManager.Gallery | INFO | 2023-05-17 09:48:43,539 | Finish purging still caches -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,557 | Media pool relink status changed to 0 -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:43,596 | 00.02.32.735(002): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current project pointer (gazprom_screens_06_R_Home_Painter_Lookdev_v002) is changed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,862 | Current timeline pointer () is changed -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,868 | Lock project 513e336f-59c3-4fa8-b9e9-814cf2b797a3 -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:43,868 | The buttonId [7] is not found for [0] in [0] -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,876 | UiGPU::UploadWipeData called before UiGPU has been initialized. Returning false -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,877 | Gallery pointer is changed, refreshing color gallery -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:43,891 | Gallery pointer is changed, refreshing gallery browser -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,921 | Main view page is changed to 1 -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:43,921 | Failed to get auto update information. -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:43,930 | Main view page is changed to 1 -[0x00004fe8] | UI | WARN | 2023-05-17 09:48:44,566 | UI Persistence - MediaPoolFloatingWindowGeometry not read -[0x00004fe8] | Undefined | INFO | 2023-05-17 09:48:44,583 | The buttonId [7] is not found for [0] in [0] -[0x000038ec] | Fairlight | INFO | 2023-05-17 09:48:44,602 | 00.02.33.795(001): WASAPI: switching to AUTO because exclusive mode not allowed -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:48:44,912 | Main view page is changed to 1 -[0x00004fe8] | UI | INFO | 2023-05-17 09:48:45,076 | Launching main window -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,094 | No reply received from file system, assume successfully deleted folder D:\SYNC\BACKUP\Resolve Project Backups\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,105 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\CacheClip\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | SyManager | WARN | 2023-05-17 09:48:45,116 | No reply received from file system, assume successfully deleted folder C:\Users\videopro\Videos\.gallery\64d8dbe8-256e-46b1-81da-671a6a8fc423. -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_newFolder] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_openProjectInReadOnlyMode] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Close] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_Rename] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,122 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_saveProjectAs] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_importProjectPlus] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_exportProjectWithStillsAndLuts] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_restoreProjectPlus] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_archiveProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_unlockProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editDelete] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCut] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editCopy] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [editPaste] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_loadProjectSettingsToCurrentProject] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectBackups] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_projectSettings] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_dynamicProjectSwitching] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_closeProjectsInMemory] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_updateThumbnails] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_otherProjectBackups] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_refresh] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:45,123 | Action owner [ProjectManager] is not active when refreshing shortcut [Context_resetUiLayout] due to [Owner is invisible] -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:48:51,277 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,060 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | module 'sys' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:01,061 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,715 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | module 're' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:08,716 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,531 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | TypeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | split() missing 2 required positional arguments: 'pattern' and 'string' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:21,532 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,443 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | NameError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | name 're__path__' is not defined -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:29,444 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,563 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | module 're' has no attribute '__path__' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:33,564 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | module 'sys' has no attribute 'info' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:39,740 | -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | Traceback (most recent call last): -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | File "", line 1, in -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | AttributeError -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | : -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | module 'sys' has no attribute 'info' -[0x00000114] | Fusion | ERROR | 2023-05-17 09:49:41,372 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,078 | >>> [ openpype.hosts.resolve installed ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,083 | >>> [ Registering DaVinci Resovle plug-ins.. ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,100 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,105 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:50:52,110 | - { timers_manager }: [ Installing task changed callback ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,816 | - { openpype.pipeline.anatomy }: [ Looking for matching root in path "D:/SYNC/OPENPYPE/gazprom_screens/06_R_Home_Painter/work/Lookdev". ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:51:12,821 | >>> [ Found match in root "work". ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,952 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 190, in showEvent -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\window.py", line 176, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,953 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | self._model.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\subsetmanager\model.py", line 27, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | instances = list_instances() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,954 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 285, in list_instances -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_timeline_items = lib.get_current_timeline_items( -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,955 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:12,956 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,000 | subsets_model.set_assets(asset_ids) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,001 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,002 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | all_timeline_items = lib.get_current_timeline_items(filter=False) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,003 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:52:21,004 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,731 | Traceback (most recent call last): -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\libraryloader\app.py", line 376, in _assetschanged -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | subsets_model.set_assets(asset_ids) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 259, in set_assets -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,732 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | self.refresh() -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in refresh -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,733 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\tools\loader\model.py", line 577, in -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | repre_ids = {con.get("representation") for con in containers} -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\pipeline.py", line 138, in ls -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,734 | all_timeline_items = lib.get_current_timeline_items(filter=False) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | File "C:\Users\videopro\Documents\github\OpenPype\openpype\hosts\resolve\api\lib.py", line 319, in get_current_timeline_items -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | selected_track_count = timeline.GetTrackCount(track_type) -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | AttributeError -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | : -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,735 | 'NoneType' object has no attribute 'GetTrackCount' -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:53:21,736 | -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - MediaPoolFloatingWindowGeometry not read -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:56,329 | UI Persistence - ConformEdlEffectsLibrary not read -[0x000065b4] | GPU.SingleBoardMgr | INFO | 2023-05-17 09:53:56,766 | Flushing GPU memory... -[0x000065b4] | UI.GLContext | INFO | 2023-05-17 09:53:56,766 | Creating shared OpenGL context for this thread (5 total). -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:53:56,782 | Main view page is changed to 2 -[0x000065b4] | UI.GLTexPool | INFO | 2023-05-17 09:53:56,884 | Released 0 MiB in 0 unused textures. -[0x00004fe8] | UI | INFO | 2023-05-17 09:53:56,944 | PBO is initialized with size [1920x1080], bitDepth=[8], hasAlpha=[1]. -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '1' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box -[0x00004fe8] | UI | WARN | 2023-05-17 09:53:59,491 | Failed to find value '2' in combo-box -[0x00004fe8] | UI.ActionManager | WARN | 2023-05-17 09:53:59,548 | Action owner [MediaPool] is not active when refreshing shortcut [editDuplicateTimelineOrClips] due to [No focused widget is set yet] -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:01,297 | Current timeline pointer (Timeline 1) is changed -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | UI | ERROR | 2023-05-17 09:54:01,405 | No marker found at frame -90000 to delete. -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,495 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,526 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed -[0x000083dc] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,550 | Database transaction is ongoing, user initiated action SmActIOCPSigInvoker is postponed -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:01,592 | Saving project took 97 ms -[0x00004fe8] | SyManager.ActionExecutor | INFO | 2023-05-17 09:54:01,619 | Database transaction completed, enqueueing 2 postponed actions -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,357 | >>> [ openpype.hosts.resolve installed ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,363 | >>> [ Registering DaVinci Resovle plug-ins.. ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,375 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvr`: Resolve (0x00007FF6BB5340D0) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,380 | >>> [ Assigning resolve module to `pype.hosts.resolve.api.bmdvf`: FusionUI (0x0000023A629E7040) [App: 'Resolve' on 127.0.0.1, UUID: f0d29a26-23ed-495a-9eb5-f264f7187252] ] -[0x00004fe8] | Fusion | ERROR | 2023-05-17 09:54:23,386 | - { timers_manager }: [ Installing task changed callback ] -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,823 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,826 | Saving project took 2 ms -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,920 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:39,923 | Saving project took 3 ms -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,137 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:40,321 | Saving project took 183 ms -[0x00004fe8] | SyManager.Signals | INFO | 2023-05-17 09:54:40,326 | Main view page is changed to 2 -[0x00004fe8] | BtCommon | WARN | 2023-05-17 09:54:40,570 | Negative duration -[0x00004fe8] | UI | WARN | 2023-05-17 09:54:40,638 | Unable to submit frame to GPU scopes, legacy OpenGL uploads not supported (Player Model). -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,497 | Start saving project gazprom_screens_06_R_Home_Painter_Lookdev_v002 -[0x00004fe8] | SyManager.ProjectManager | INFO | 2023-05-17 09:54:42,555 | Saving project took 59 ms From 8b6feb38ab7a6d71b82b27dce0c9a807cff291ed Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 10:55:41 +0300 Subject: [PATCH 602/918] Bring the PATH setting back --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 486d8121cf..549777c34e 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -84,6 +84,15 @@ class ResolvePrelaunch(PreLaunchHook): self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") + # add to the python path to PATH + env_path = self.launch_context.env["PATH"] + self.log.info(f"Adding `{python3_home_str}` to the PATH variable") + self.launch_context.env[ + "PATH" + ] = f"{python3_home_str}{os.pathsep}{env_path}" + + self.log.debug(f"PATH: {self.launch_context.env['PATH']}") + resolve_utility_scripts_dirs = { "windows": ( f"{programdata}/Blackmagic Design" From 05a25bb30b8770aad253fe9ecbe3609205c09923 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 11:46:06 +0300 Subject: [PATCH 603/918] add docstrings --- .../hosts/resolve/hooks/pre_resolve_setup.py | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 549777c34e..2cdeb8c4ff 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -7,10 +7,27 @@ from openpype.hosts.resolve.utils import setup class ResolvePrelaunch(PreLaunchHook): """ - This hook will check if current workfile path has Resolve - project inside. IF not, it initialize it and finally it pass - path to the project by environment variable to Premiere launcher - shell script. + This hook will set up the Resolve scripting environment as described in + Resolve's documentation found with the installed application at + {resolve}/Support/Developer/Scripting/README.txt + + Prepares the following environment variables: + - `RESOLVE_SCRIPT_API` + - `RESOLVE_SCRIPT_LIB` + + It adds $RESOLVE_SCRIPT_API/Modules to PYTHONPATH. + + Additionally it sets up the Python home for Python 3 based on the + RESOLVE_PYTHON3_HOME in the environment (usually defined in OpenPype's + Application environment for Resolve by the admin). For this it sets + PYTHONHOME and PATH variables. + + It also defines: + - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype + Fusion scripts to be copied to for Resolve to pick them up. + - `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to + use logging with terminal colors as it fails in Resolve. + """ app_groups = ["resolve"] From a5fde9663805326f1fea5de5b278e4f429d85e1a Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Wed, 17 May 2023 11:47:08 +0300 Subject: [PATCH 604/918] formatting --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 2cdeb8c4ff..6747e773a3 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -26,7 +26,7 @@ class ResolvePrelaunch(PreLaunchHook): - `RESOLVE_UTILITY_SCRIPTS_DIR`: Destination directory for OpenPype Fusion scripts to be copied to for Resolve to pick them up. - `OPENPYPE_LOG_NO_COLORS` to True to ensure OP doesn't try to - use logging with terminal colors as it fails in Resolve. + use logging with terminal colors as it fails in Resolve. """ From 9112b87ba6ffc095fd43d5a769ce3501ef85e765 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 May 2023 12:20:43 +0200 Subject: [PATCH 605/918] :truck: move unreal plugin to separate repository --- openpype/hosts/unreal/integration/README.md | 10 - .../integration/UE_4.27/Ayon/.gitignore | 35 --- .../integration/UE_4.27/Ayon/Ayon.uplugin | 23 -- .../Ayon/Config/DefaultAyonSettings.ini | 2 - .../UE_4.27/Ayon/Config/FilterPlugin.ini | 8 - .../Ayon/Content/Python/init_unreal.py | 30 --- .../unreal/integration/UE_4.27/Ayon/README.md | 3 - .../UE_4.27/Ayon/Resources/ayon128.png | Bin 2358 -> 0 bytes .../UE_4.27/Ayon/Resources/ayon40.png | Bin 721 -> 0 bytes .../UE_4.27/Ayon/Resources/ayon512.png | Bin 16705 -> 0 bytes .../UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs | 61 ------ .../UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp | 156 -------------- .../Ayon/Private/AyonAssetContainer.cpp | 114 ---------- .../Private/AyonAssetContainerFactory.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonLib.cpp | 53 ----- .../Ayon/Private/AyonPublishInstance.cpp | 203 ----------------- .../Private/AyonPublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 -- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 70 ------ .../Private/Commandlets/AyonActionResult.cpp | 41 ---- .../AyonGenerateProjectCommandlet.cpp | 141 ------------ .../Ayon/Private/OpenPypePublishInstance.cpp | 203 ----------------- .../UE_4.27/Ayon/Source/Ayon/Public/Ayon.h | 20 -- .../Source/Ayon/Public/AyonAssetContainer.h | 39 ---- .../Ayon/Public/AyonAssetContainerFactory.h | 21 -- .../Ayon/Source/Ayon/Public/AyonConstants.h | 15 -- .../UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h | 19 -- .../Source/Ayon/Public/AyonPublishInstance.h | 103 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 22 -- .../Source/Ayon/Public/AyonPythonBridge.h | 20 -- .../Ayon/Source/Ayon/Public/AyonSettings.h | 31 --- .../Ayon/Source/Ayon/Public/AyonStyle.h | 23 -- .../Public/Commandlets/AyonActionResult.h | 83 ------- .../AyonGenerateProjectCommandlet.h | 60 ------ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 - .../Ayon/Public/OpenPypePublishInstance.h | 103 --------- .../integration/UE_4.27/BuildPlugin_4-27.bat | 1 - .../UE_4.27/BuildPlugin_4-27_Window.bat | 1 - .../UE_4.27/CommandletProject/.gitignore | 8 - .../CommandletProject.uproject | 12 -- .../unreal/integration/UE_5.0/Ayon/.gitignore | 35 --- .../integration/UE_5.0/Ayon/Ayon.uplugin | 24 --- .../Ayon/Config/DefaultAyonSettings.ini | 2 - .../UE_5.0/Ayon/Config/FilterPlugin.ini | 8 - .../UE_5.0/Ayon/Content/Python/init_unreal.py | 30 --- .../unreal/integration/UE_5.0/Ayon/README.md | 3 - .../UE_5.0/Ayon/Resources/ayon128.png | Bin 2358 -> 0 bytes .../UE_5.0/Ayon/Resources/ayon40.png | Bin 721 -> 0 bytes .../UE_5.0/Ayon/Resources/ayon512.png | Bin 16705 -> 0 bytes .../UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs | 65 ------ .../UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp | 139 ------------ .../Ayon/Private/AyonAssetContainer.cpp | 113 ---------- .../Private/AyonAssetContainerFactory.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonCommands.cpp | 13 -- .../Ayon/Source/Ayon/Private/AyonLib.cpp | 51 ----- .../Ayon/Private/AyonPublishInstance.cpp | 204 ------------------ .../Private/AyonPublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 -- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 21 -- .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 62 ------ .../Private/Commandlets/AyonActionResult.cpp | 40 ---- .../AyonGenerateProjectCommandlet.cpp | 140 ------------ .../Ayon/Private/OpenPypePublishInstance.cpp | 204 ------------------ .../UE_5.0/Ayon/Source/Ayon/Public/Ayon.h | 24 --- .../Source/Ayon/Public/AyonAssetContainer.h | 34 --- .../Ayon/Public/AyonAssetContainerFactory.h | 18 -- .../Ayon/Source/Ayon/Public/AyonCommands.h | 24 --- .../Ayon/Source/Ayon/Public/AyonConstants.h | 13 -- .../UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h | 19 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 22 -- .../Source/Ayon/Public/AyonPythonBridge.h | 20 -- .../Ayon/Source/Ayon/Public/AyonSettings.h | 32 --- .../Ayon/Source/Ayon/Public/AyonStyle.h | 19 -- .../Public/Commandlets/AyonActionResult.h | 83 ------- .../AyonGenerateProjectCommandlet.h | 61 ------ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 - .../Ayon/Public/OpenPypePublishInstance.h | 104 --------- .../integration/UE_5.0/BuildPlugin_5-0.bat | 1 - .../UE_5.0/BuildPlugin_5-0_Window.bat | 1 - .../UE_5.0/CommandletProject/.gitignore | 41 ---- .../CommandletProject.uproject | 20 -- .../unreal/integration/UE_5.1/Ayon/.gitignore | 35 --- .../integration/UE_5.1/Ayon/Ayon.uplugin | 24 --- .../Ayon/Config/DefaultAyonSettings.ini | 2 - .../UE_5.1/Ayon/Config/FilterPlugin.ini | 8 - .../UE_5.1/Ayon/Content/Python/init_unreal.py | 30 --- .../unreal/integration/UE_5.1/Ayon/README.md | 3 - .../UE_5.1/Ayon/Resources/ayon128.png | Bin 2358 -> 0 bytes .../UE_5.1/Ayon/Resources/ayon40.png | Bin 721 -> 0 bytes .../UE_5.1/Ayon/Resources/ayon512.png | Bin 16705 -> 0 bytes .../UE_5.1/Ayon/Source/Ayon/Ayon.Build.cs | 65 ------ .../UE_5.1/Ayon/Source/Ayon/Private/Ayon.cpp | 139 ------------ .../Ayon/Private/AyonAssetContainer.cpp | 113 ---------- .../Private/AyonAssetContainerFactory.cpp | 20 -- .../Ayon/Source/Ayon/Private/AyonCommands.cpp | 13 -- .../Ayon/Source/Ayon/Private/AyonLib.cpp | 51 ----- .../Ayon/Private/AyonPublishInstance.cpp | 204 ------------------ .../Private/AyonPublishInstanceFactory.cpp | 23 -- .../Source/Ayon/Private/AyonPythonBridge.cpp | 14 -- .../Ayon/Source/Ayon/Private/AyonSettings.cpp | 21 -- .../Ayon/Source/Ayon/Private/AyonStyle.cpp | 62 ------ .../Private/Commandlets/AyonActionResult.cpp | 40 ---- .../AyonGenerateProjectCommandlet.cpp | 140 ------------ .../Ayon/Private/OpenPypePublishInstance.cpp | 204 ------------------ .../UE_5.1/Ayon/Source/Ayon/Public/Ayon.h | 24 --- .../Source/Ayon/Public/AyonAssetContainer.h | 34 --- .../Ayon/Public/AyonAssetContainerFactory.h | 18 -- .../Ayon/Source/Ayon/Public/AyonCommands.h | 24 --- .../Ayon/Source/Ayon/Public/AyonConstants.h | 13 -- .../UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h | 19 -- .../Source/Ayon/Public/AyonPublishInstance.h | 104 --------- .../Ayon/Public/AyonPublishInstanceFactory.h | 22 -- .../Source/Ayon/Public/AyonPythonBridge.h | 20 -- .../Ayon/Source/Ayon/Public/AyonSettings.h | 32 --- .../Ayon/Source/Ayon/Public/AyonStyle.h | 19 -- .../Public/Commandlets/AyonActionResult.h | 83 ------- .../AyonGenerateProjectCommandlet.h | 61 ------ .../Source/Ayon/Public/Logging/Ayon_Log.h | 4 - .../Ayon/Public/OpenPypePublishInstance.h | 104 --------- .../integration/UE_5.1/BuildPlugin_5-1.bat | 1 - .../UE_5.1/BuildPlugin_5-1_Window.bat | 1 - .../UE_5.1/CommandletProject/.gitignore | 41 ---- .../CommandletProject.uproject | 20 -- 125 files changed, 5525 deletions(-) delete mode 100644 openpype/hosts/unreal/integration/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon40.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Ayon.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Ayon.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon40.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Ayon.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Ayon.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon40.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Ayon.Build.cs delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Ayon.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore delete mode 100644 openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject diff --git a/openpype/hosts/unreal/integration/README.md b/openpype/hosts/unreal/integration/README.md deleted file mode 100644 index 961eea83e6..0000000000 --- a/openpype/hosts/unreal/integration/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Building the plugin - -In order to successfully build the plugin, make sure that the path to the UnrealBuildTool.exe is specified correctly. -After the UBT path specify for which platform it will be compiled. in the -Project parameter, specify the path to the -CommandletProject.uproject file. Next the build type has to be specified (DebugGame, Development, Package, etc.) and then the -TargetType (Editor, Runtime, etc.) - -`BuildPlugin_[Ver].bat` runs the building process in the background. If you want to show the progress inside the -command prompt, use the `BuildPlugin_[Ver]_Window.bat` file. - - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore deleted file mode 100644 index b32a6f55e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -/Binaries -/Intermediate diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin deleted file mode 100644 index 0838da5577..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Ayon.uplugin +++ /dev/null @@ -1,23 +0,0 @@ -{ - "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", - "FriendlyName": "Ayon", - "Description": "Ayon Integration", - "Category": "Ayon.Integration", - "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://ayon.ynput.io", - "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", - "MarketplaceURL": "", - "SupportURL": "https://ynput.io/", - "EngineVersion": "4.27", - "CanContainContent": true, - "Installed": true, - "Modules": [ - { - "Name": "Ayon", - "Type": "Editor", - "LoadingPhase": "Default" - } - ] -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini deleted file mode 100644 index 9ad7f55201..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/DefaultAyonSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/Ayon.AyonSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini deleted file mode 100644 index ccebca2f32..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Config/FilterPlugin.ini +++ /dev/null @@ -1,8 +0,0 @@ -[FilterPlugin] -; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and -; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. -; -; Examples: -; /README.txt -; /Extras/... -; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py deleted file mode 100644 index 43d6b8b7cf..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Content/Python/init_unreal.py +++ /dev/null @@ -1,30 +0,0 @@ -import unreal - -ayon_detected = True -try: - from openpype.pipeline import install_host - from openpype.hosts.unreal.api import UnrealHost - - ayon_host = UnrealHost() -except ImportError as exc: - ayon_host = None - ayon_detected = False - unreal.log_error(f"OpenPype: cannot load Ayon [ {exc} ]") - -if ayon_detected: - install_host(ayon_host) - - -@unreal.uclass() -class AyonIntegration(unreal.AyonPythonBridge): - @unreal.ufunction(override=True) - def RunInPython_Popup(self): - unreal.log_warning("Ayon: showing tools popup") - if ayon_detected: - ayon_host.show_tools_popup() - - @unreal.ufunction(override=True) - def RunInPython_Dialog(self): - unreal.log_warning("Ayon: showing tools dialog") - if ayon_detected: - ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md b/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md deleted file mode 100644 index 77ae8c7e98..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ayon Unreal Integration plugin - UE 4.x - -This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon128.png deleted file mode 100644 index 799d849aa3163ecb16be39c641a6ac30324906b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2358 zcmZ{mi$Bx*1I9nwmzi60i5$5#noB4`C?i(Vjdg~IlUovE!pda~6-Bx%m)u$-_f{v$ zny}oHu$DuOnp|Sct&`i%j$gk&;JjYX^Su9q=k>nfcG6j1MqLH~An$Sncj^}@|1T2p zYum8??*KrGU2q2pSBiwi7qSS46uLI++H@Lg$CQE<-4+jX)Km%W5^_d#jKQMIWFfO1 zX%v7)UPoC>5Srsb@U*-19+6)!!Nr3-sfPoGU?3RR;Ui{$%&Wa~Q@vw|oGGBxF~r{~Gp zL3!7h=P1V<<0Cxdo3|Wv)zO2%JEsqIXg#~W8^41}uSUZiGXWIDSVLRwJVzEk}l;{zmdylE=)*mLG4A$L^&B-bAg$E~?ulendSYc@VJfe^TGTbeh?&cEyH_WaD$9_vvzoC3JlB-U3^_0 zg?d>XmQ>FA{$>G3E~)CwEr(u_5F`DhgcAff{~)kr-D(NLcE`~^Zhg>0w*PvvQ2iw@ zjIIE-!Sm0f`L|b8Z}Ez^HtY@1;w_lqk(9^f@aHqigb38|O=huK=SpMyDsba5g7amn zgnRQFy+ak;=0z{awc(~EV?S2R9zqT$PT2)G4b%(#nY3y|$c0{efr=CP%ZGf?zx9GK ztB+~>?#A29OhtncX}-ppR*#_FeP0pvma7^Pui_|EBdc2YuMJ((KFJx>%o=<7#A2j+ zPYu~ayR>8@VY}-5y1^#u=aI@O$xEiRe`1JX&06hLT}JyTRNL`~esapCnrotP*?B#8 z#fJ~r4HtbxjpO6GqCFMXhow&(L?Y+o;zB6@T#ncAY8h`Q^q!U{>D@*^9U?K5Pj8pG?ug|JlH=1Z+wv_q#r%W2pDWibh;>01wF$WH-3Aq&MdhM zADt3xT)5j7{{55xmS$5tPkGJQ9*rGOF&SNwvm&e{l!Dytl5=Fkqd99$@ywx_H zBHeYoV*Z|&mIH{#n` z0?fdNuZWG?!Dw%q;i?uo^byheFizG|=))Gz1*Mssy?%3NY2=JQlcdBU$j3n$(<)s% z7G-9oLwSUG&&wnC+JV{;oG5#I&NX8?T>evFM&*}f_E6mj?WKWeNYi-*yW&iT^Zx{W z$psnJ7BRhg^rq`jOWvf!pl=5$uP4w&hQd)FnsvevDjw}}!l4xKVGiVrBc`Q~>avCM zjW*Ei@Xt3tqfnIhxf+9S$8m%>jbgGz(gWTrU$a+77nE~FbvAvlJzt;K+2RcoNQzCX z7kYesa8q+2?>?u`{r#*c?2qG?3v11WO zqU^M13+I&Etr+`be9}evu=$)f`=&HjN|k+rDcm(-3D!T(sM8_}FDUL{{rb$)n6$`S zN2xM~9mE_MW-X~Z0EbT!SL<`hJ>a(Oy~3DU(cmFO<#_23`LS1%ZqL&YmFBvyolnbr z$JQU$WS_mau8ln|;l333kd)G-Fx(bPcJ~@$l&v2QyV|v~@U5%0oT0r&ls-MN&QFuF z;kKRsjcQ)M!|cP9*CDIDlz8EKI?|eGPvIsQ%reY}cVl+WIMR!caU*vTJm=*W-6Rn9 zre(4ohPR%n072&kkAU>j1HI3q+{&MTjQ99z#kS!ED>#1(*mmfBwAsNN^TNmvDqNtp zHJ!Uxx9?l(sB4OuJoq_#`42A_gQK}!2W6>XdRtDxnzSEdzS)@y+`8y;C#$>c0*~Bz z5_m0Ng3UzQ7)WOg&6K(T>s=LwA&)GzFfbx3i|Sca9+>3sO#RE{F$PAnA}5&!&PZ;L**8{jQnF>D!d|KC9ZS6a=jZT}U6k5K-wWQ2T&3O@6tW;O%6Z+_{NXAwPs9XXc#qoq61uOI1}>^`9$Z z;!6rsQ5@(3+JPAG80Z5gT?0iT1xST}AwJJks9{MvY%=EN2%F0$cossq7qCU|BS7_WcSp0n?>| zn!7mc6rVHU`o}*|H+q-)k=yj8-kAMY16RT%3NwOhff1lKZx~Ha(mc`+*)&9BKfj+g z9b@;>2Ge&N@Tw?K1xE0^AI{T6dI~brP?Lao0x~nC($;76Mb~7mfStez)7X+o(%IMw zvlB3rqAj_d_WE@;|ARn}OTvQHTtc-AHQ#A$<<#;G%qq*?L}RfiHI6ywE5M^5F6oBD zBPOr=lIs4{Nzx+efu!|5+Zss^1Ask|w8`h!AnBf@{gni~GMEVi+{(q)w;@A%#f4B>c^^f(d*4(&58>b8a$yZ#(QjpZzvn{u_u7m$z>~n95DEOTVj=uD+8}L! zM?(a!lnw_0OfDke3e#W%eEoM=ta@u2ZGhJ+kf_v~-a@);+HLni?}efnxCU&^?as`C zA%Drc0DkuU|C00hRKhQoV;BXxfq{^PRaI40|E7Q+*y$4iSeuWd00000NkvXXu0mjf D;gw7~ diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Resources/ayon512.png deleted file mode 100644 index 990d5917e232a0644820428fb2790943de5ffaa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16705 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"LevelEditor"); - - TSharedPtr MenuExtender = MakeShareable(new FExtender()); - TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); - - MenuExtender->AddMenuExtension( - "LevelEditor", - EExtensionHook::After, - NULL, - FMenuExtensionDelegate::CreateRaw(this, &FAyonModule::AddMenuEntry) - ); - ToolbarExtender->AddToolBarExtension( - "Settings", - EExtensionHook::After, - NULL, - FToolBarExtensionDelegate::CreateRaw(this, &FAyonModule::AddToobarEntry)); - - - LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); - LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); - - RegisterSettings(); - } -} - -void FAyonModule::ShutdownModule() -{ - FAyonStyle::Shutdown(); -} - - -void FAyonModule::AddMenuEntry(FMenuBuilder& MenuBuilder) -{ - // Create Section - MenuBuilder.BeginSection("Ayon", TAttribute(FText::FromString("Ayon"))); - { - // Create a Submenu inside of the Section - MenuBuilder.AddMenuEntry( - FText::FromString("Tools..."), - FText::FromString("Pipeline tools"), - FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup)) - ); - - MenuBuilder.AddMenuEntry( - FText::FromString("Tools dialog..."), - FText::FromString("Pipeline tools dialog"), - FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo"), - FUIAction(FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog)) - ); - } - MenuBuilder.EndSection(); -} - -void FAyonModule::AddToobarEntry(FToolBarBuilder& ToolbarBuilder) -{ - ToolbarBuilder.BeginSection(TEXT("Ayon")); - { - ToolbarBuilder.AddToolBarButton( - FUIAction( - FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), - NULL, - FIsActionChecked() - - ), - NAME_None, - LOCTEXT("Ayon_label", "Ayon"), - LOCTEXT("Ayon_tooltip", "Ayon Tools"), - FSlateIcon(FAyonStyle::GetStyleSetName(), "Ayon.Logo") - ); - } - ToolbarBuilder.EndSection(); -} - -void FAyonModule::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UAyonSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); - } -} - -bool FAyonModule::HandleSettingsSaved() -{ - UAyonSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - - -void FAyonModule::MenuPopup() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FAyonModule::MenuDialog() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp deleted file mode 100644 index e3989eb03c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ /dev/null @@ -1,114 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AyonAssetContainer.h" -#include "AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Containers/UnrealString.h" - -UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAyonAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); -} - -void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AyonAssetContainer") - { - assets.Add(assetPath); - assetsData.Add(AssetData); - UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); - } - } -} - -void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - FString path = UAyonAssetContainer::GetPathName(); - FString lpp = FPackageName::GetLongPackagePath(*path); - - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AyonAssetContainer") - { - // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); - assets.Remove(assetPath); - assetsData.Remove(AssetData); - } - } -} - -void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AyonAssetContainer") - { - - assets.Remove(str); - assets.Add(assetPath); - assetsData.Remove(AssetData); - // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); - } - } -} - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp deleted file mode 100644 index 086fc1036e..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AyonAssetContainerFactory.h" -#include "AyonAssetContainer.h" - -UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp deleted file mode 100644 index bff99caee3..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonLib.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonLib.h" - -#include "AssetViewUtils.h" -#include "Misc/Paths.h" -#include "Misc/ConfigCacheIni.h" -#include "UObject/UnrealType.h" - -/** - * Sets color on folder icon on given path - * @param InPath - path to folder - * @param InFolderColor - color of the folder - * @warning This color will appear only after Editor restart. Is there a better way? - */ - -bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all poperties on given object - * @param cls - class - * @return TArray of properties - */ -TArray UAyonLib::GetAllProperties(UClass* cls) -{ - TArray Ret; - if (cls != nullptr) - { - for (TFieldIterator It(cls); It; ++It) - { - FProperty* Property = *It; - if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) - { - Ret.Add(Property->GetName()); - } - } - } - return Ret; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index d7550e2ed1..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistryModule.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Framework/Notifications/NotificationManager.h" -#include "Widgets/Notifications/SNotificationList.h" - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif - -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index f79c428a6d..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp deleted file mode 100644 index 0ed4b2f704..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPythonBridge.h" - -UAyonPythonBridge* UAyonPythonBridge::Get() -{ - TArray AyonPythonBridgeClasses; - GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); - int32 NumClasses = AyonPythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp deleted file mode 100644 index 509b7268ba..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonSettings.h" - -#include "Interfaces/IPluginManager.h" - -/** - * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config - */ -UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp deleted file mode 100644 index b133225fd5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/AyonStyle.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonStyle.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyle.h" -#include "Styling/SlateStyleRegistry.h" - - -TUniquePtr< FSlateStyleSet > FAyonStyle::AyonStyleInstance = nullptr; - -void FAyonStyle::Initialize() -{ - if (!AyonStyleInstance.IsValid()) - { - AyonStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); - } -} - -void FAyonStyle::Shutdown() -{ - if (AyonStyleInstance.IsValid()) - { - FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); - AyonStyleInstance.Reset(); - } -} - -FName FAyonStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("AyonStyle")); - return StyleSetName; -} - -FName FAyonStyle::GetContextName() -{ - static FName ContextName(TEXT("Ayon")); - return ContextName; -} - -#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush( Style->RootToContentDir( RelativePath, TEXT(".png") ), __VA_ARGS__ ) - -const FVector2D Icon40x40(40.0f, 40.0f); - -TUniquePtr< FSlateStyleSet > FAyonStyle::Create() -{ - TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); - Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/Ayon/Resources")); - - return Style; -} - -void FAyonStyle::SetIcon(const FString& StyleName, const FString& ResourcePath) -{ - FSlateStyleSet* Style = AyonStyleInstance.Get(); - - FString Name(GetContextName().ToString()); - Name = Name + "." + StyleName; - Style->Set(*Name, new FSlateImageBrush(Style->RootToContentDir(ResourcePath, TEXT(".png")), Icon40x40)); - - - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); -} - -#undef IMAGE_BRUSH - -const ISlateStyle& FAyonStyle::Get() -{ - check(AyonStyleInstance); - return *AyonStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp deleted file mode 100644 index 49376e8648..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - - -#include "Commandlets/AyonActionResult.h" -#include "Logging/Ayon_Log.h" - -EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() -{ - return Status; -} - -FText& FAyon_ActionResult::GetReason() -{ - return Reason; -} - -FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) -{ - -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FAyon_ActionResult::IsProblem() const -{ - return Status != EAyon_ActionResult::Ok; -} - -void FAyon_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp deleted file mode 100644 index 0328d3b7e6..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" - -#include "Editor.h" -#include "GameProjectUtils.h" -#include "AyonConstants.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" - -int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_AYON_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_AYON_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_AYON_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for Ayon - return 0; -} - - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") -{ -} - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FAyonGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); - return FAyon_ActionResult(); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); -} - -void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor AyonPluginDescriptor; - AyonPluginDescriptor.bEnabled = true; - AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; - ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); -} - -FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FAyonGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp deleted file mode 100644 index 320285591e..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.h" -#include "AssetRegistryModule.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Framework/Notifications/NotificationManager.h" -#include "Widgets/Notifications/SNotificationList.h" - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorOpenPypeDirs(); -#endif - -} - -void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UOpenPypePublishInstance::ColorOpenPypeDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined OpenPype folder - if (!PathName.Contains(TEXT("OpenPype"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UOpenPypePublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UOpenPypePublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UOpenPypePublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h deleted file mode 100644 index d11af70058..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Ayon.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - - -class FAyonModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterSettings(); - bool HandleSettingsSaved(); - - void AddMenuEntry(FMenuBuilder& MenuBuilder); - void AddToobarEntry(FToolBarBuilder& ToolbarBuilder); - void MenuPopup(); - void MenuDialog(); -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h deleted file mode 100644 index cc17b3960a..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainer.h +++ /dev/null @@ -1,39 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/NoExportTypes.h" -#include "Engine/AssetUserData.h" -#include "AssetData.h" -#include "AyonAssetContainer.generated.h" - -/** - * - */ -UCLASS(Blueprintable) -class AYON_API UAyonAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAyonAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") - TArray assets; - - // There seems to be no reflection option to expose array of FAssetData - /* - UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) - TArray assetsData; - */ -private: - TArray assetsData; - void OnAssetAdded(const FAssetData& AssetData); - void OnAssetRemoved(const FAssetData& AssetData); - void OnAssetRenamed(const FAssetData& AssetData, const FString& str); -}; - - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h deleted file mode 100644 index 7c35897911..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h +++ /dev/null @@ -1,21 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonAssetContainerFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h deleted file mode 100644 index 6a02b5682f..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonConstants.h +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "CoreMinimal.h" - -namespace AyonConstants -{ - const FString Ayon_PluginName = "Ayon"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h deleted file mode 100644 index da83b448fb..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonLib.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonLib.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd); - - UFUNCTION(BlueprintCallable, Category = Python) - static TArray GetAllProperties(UClass* cls); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index 0a0628c3ec..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 3cef8e76b2..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h deleted file mode 100644 index 3c429fd7d3..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "AyonPythonBridge.generated.h" - -UCLASS(Blueprintable) -class UAyonPythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UAyonPythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; - -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h deleted file mode 100644 index 7a93f107c5..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonSettings.h +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonSettings.generated.h" - -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") - -UCLASS(Config=AyonSettings, DefaultConfig) -class AYON_API UAyonSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h deleted file mode 100644 index 188e4a510c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/AyonStyle.h +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "CoreMinimal.h" - -class FSlateStyleSet; -class ISlateStyle; - - -class FAyonStyle -{ -public: - static void Initialize(); - static void Shutdown(); - static const ISlateStyle& Get(); - static FName GetStyleSetName(); - static FName GetContextName(); - - static void SetIcon(const FString& StyleName, const FString& ResourcePath); - -private: - static TUniquePtr< FSlateStyleSet > Create(); - static TUniquePtr< FSlateStyleSet > AyonStyleInstance; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h deleted file mode 100644 index 4694055164..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FAyon_ActionResult structure - */ -#define EVALUATE_AYON_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... -*/ -UENUM() -namespace EAyon_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FAyon_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FAyon_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EAyon_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EAyon_ActionResult::Ok - */ - bool IsProblem() const; - EAyon_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h deleted file mode 100644 index cabd524b8c..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "GameProjectUtils.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "AyonGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FAyonGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FAyonGenerateProjectParams(); - FAyonGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UAyonGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FAyonGenerateProjectParams ParseParameters(const FString& Params) const; - FAyon_ActionResult TryCreateProject() const; - FAyon_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FAyon_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h deleted file mode 100644 index 21571afd02..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); diff --git a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h deleted file mode 100644 index 4a7a6a3a9f..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorOpenPypeDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat deleted file mode 100644 index 96cdb96f8a..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27.bat +++ /dev/null @@ -1 +0,0 @@ -D:\UE4\UE_4.27\Engine\Build\BatchFiles\RunUAT.bat BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_4.27\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\4.27" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat b/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat deleted file mode 100644 index 1343843a82..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/BuildPlugin_4-27_Window.bat +++ /dev/null @@ -1 +0,0 @@ -cmd /k "BuildPlugin_4-27.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore deleted file mode 100644 index e74e6886b7..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -/Saved -/DerivedDataCache -/Intermediate -/Content -/Config -/Binaries -/.idea -/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject deleted file mode 100644 index ea7bf21dc4..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.27/CommandletProject/CommandletProject.uproject +++ /dev/null @@ -1,12 +0,0 @@ -{ - "FileVersion": 3, - "EngineAssociation": "4.27", - "Category": "", - "Description": "", - "Plugins": [ - { - "Name": "Ayon", - "Enabled": true - } - ] -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore deleted file mode 100644 index b32a6f55e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -/Binaries -/Intermediate diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin deleted file mode 100644 index 70ed8f6b9a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Ayon.uplugin +++ /dev/null @@ -1,24 +0,0 @@ -{ - "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", - "FriendlyName": "Ayon", - "Description": "Ayon Integration", - "Category": "Ayon.Integration", - "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://ayon.ynput.io", - "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", - "MarketplaceURL": "", - "SupportURL": "https://ynput.io/", - "CanContainContent": true, - "EngineVersion": "5.0", - "IsExperimentalVersion": false, - "Installed": true, - "Modules": [ - { - "Name": "Ayon", - "Type": "Editor", - "LoadingPhase": "Default" - } - ] -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini deleted file mode 100644 index 9ad7f55201..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/DefaultAyonSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/Ayon.AyonSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini deleted file mode 100644 index ccebca2f32..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Config/FilterPlugin.ini +++ /dev/null @@ -1,8 +0,0 @@ -[FilterPlugin] -; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and -; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. -; -; Examples: -; /README.txt -; /Extras/... -; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py deleted file mode 100644 index c0b1d0ce5d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Content/Python/init_unreal.py +++ /dev/null @@ -1,30 +0,0 @@ -import unreal - -ayon_detected = True -try: - from openpype.pipeline import install_host - from openpype.hosts.unreal.api import UnrealHost - - ayon_host = UnrealHost() -except ImportError as exc: - ayon_host = None - ayon_detected = False - unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") - -if ayon_detected: - install_host(ayon_host) - - -@unreal.uclass() -class AyonIntegration(unreal.AyonPythonBridge): - @unreal.ufunction(override=True) - def RunInPython_Popup(self): - unreal.log_warning("Ayon: showing tools popup") - if ayon_detected: - ayon_host.show_tools_popup() - - @unreal.ufunction(override=True) - def RunInPython_Dialog(self): - unreal.log_warning("Ayon: showing tools dialog") - if ayon_detected: - ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md deleted file mode 100644 index 865c8cafea..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ayon Unreal Integration plugin - UE 5.0 - -This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon128.png deleted file mode 100644 index 799d849aa3163ecb16be39c641a6ac30324906b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2358 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 D;gw7~ diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Resources/ayon512.png deleted file mode 100644 index 990d5917e232a0644820428fb2790943de5ffaa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16705 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 - FAyonCommands::Get().AyonTools, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), - FCanExecuteAction()); - PluginCommands->MapAction( - FAyonCommands::Get().AyonToolsDialog, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), - FCanExecuteAction()); - - UToolMenus::RegisterStartupCallback( - FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); - - RegisterSettings(); -} - -void FAyonModule::ShutdownModule() -{ - UToolMenus::UnRegisterStartupCallback(this); - - UToolMenus::UnregisterOwner(this); - - FAyonStyle::Shutdown(); - - FAyonCommands::Unregister(); -} - - -void FAyonModule::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UAyonSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); - } -} - -bool FAyonModule::HandleSettingsSaved() -{ - UAyonSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - -void FAyonModule::RegisterMenus() -{ - // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner - FToolMenuOwnerScoped OwnerScoped(this); - - { - UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); - { - // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); - FToolMenuSection& Section = Menu->AddSection( - "Ayon", - TAttribute(FText::FromString("Ayon")), - FToolMenuInsert("Programming", EToolMenuInsertType::Before) - ); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); - } - UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); - { - FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); - { - FToolMenuEntry& Entry = Section.AddEntry( - FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); - Entry.SetCommandList(PluginCommands); - } - } - } -} - - -void FAyonModule::MenuPopup() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FAyonModule::MenuDialog() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp deleted file mode 100644 index 869aa45256..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ /dev/null @@ -1,113 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AyonAssetContainer.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Containers/UnrealString.h" - -UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAyonAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); -} - -void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); - UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - assets.Add(assetPath); - assetsData.Add(AssetData); - UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); - } - } -} - -void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - FString path = UAyonAssetContainer::GetPathName(); - FString lpp = FPackageName::GetLongPackagePath(*path); - - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); - assets.Remove(assetPath); - assetsData.Remove(AssetData); - } - } -} - -void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.ObjectPath.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - - assets.Remove(str); - assets.Add(assetPath); - assetsData.Remove(AssetData); - // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); - } - } -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp deleted file mode 100644 index 086fc1036e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AyonAssetContainerFactory.h" -#include "AyonAssetContainer.h" - -UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp deleted file mode 100644 index 566ee1dcd1..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonCommands.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonCommands.h" - -#define LOCTEXT_NAMESPACE "FAyonModule" - -void FAyonCommands::RegisterCommands() -{ - UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); -} - -#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp deleted file mode 100644 index 7cfa0c9c30..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonLib.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonLib.h" - -#include "AssetViewUtils.h" -#include "UObject/UnrealType.h" - -/** - * Sets color on folder icon on given path - * @param InPath - path to folder - * @param InFolderColor - color of the folder - * @warning This color will appear only after Editor restart. Is there a better way? - */ - -bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all poperties on given object - * @param cls - class - * @return TArray of properties - */ -TArray UAyonLib::GetAllProperties(UClass* cls) -{ - TArray Ret; - if (cls != nullptr) - { - for (TFieldIterator It(cls); It; ++It) - { - FProperty* Property = *It; - if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) - { - Ret.Add(Property->GetName()); - } - } - } - return Ret; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index 8d34090a15..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index f79c428a6d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp deleted file mode 100644 index 0ed4b2f704..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPythonBridge.h" - -UAyonPythonBridge* UAyonPythonBridge::Get() -{ - TArray AyonPythonBridgeClasses; - GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); - int32 NumClasses = AyonPythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp deleted file mode 100644 index da388fbc8f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonSettings.h" - -#include "Interfaces/IPluginManager.h" -#include "UObject/UObjectGlobals.h" - -/** - * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config - */ -UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp deleted file mode 100644 index d88df78735..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/AyonStyle.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonStyle.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyleRegistry.h" -#include "Slate/SlateGameResources.h" -#include "Interfaces/IPluginManager.h" -#include "Styling/SlateStyleMacros.h" - -#define RootToContentDir Style->RootToContentDir - -TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; - -void FAyonStyle::Initialize() -{ - if (!AyonStyleInstance.IsValid()) - { - AyonStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); - } -} - -void FAyonStyle::Shutdown() -{ - FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); - ensure(AyonStyleInstance.IsUnique()); - AyonStyleInstance.Reset(); -} - -FName FAyonStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("AyonStyle")); - return StyleSetName; -} - -const FVector2D Icon16x16(16.0f, 16.0f); -const FVector2D Icon20x20(20.0f, 20.0f); -const FVector2D Icon40x40(40.0f, 40.0f); - -TSharedRef< FSlateStyleSet > FAyonStyle::Create() -{ - TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); - - Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - - return Style; -} - -void FAyonStyle::ReloadTextures() -{ - if (FSlateApplication::IsInitialized()) - { - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); - } -} - -const ISlateStyle& FAyonStyle::Get() -{ - return *AyonStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp deleted file mode 100644 index 2a137e3ed7..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Commandlets/AyonActionResult.h" -#include "Logging/Ayon_Log.h" - -EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() -{ - return Status; -} - -FText& FAyon_ActionResult::GetReason() -{ - return Reason; -} - -FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) -{ - -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FAyon_ActionResult::IsProblem() const -{ - return Status != EAyon_ActionResult::Ok; -} - -void FAyon_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp deleted file mode 100644 index ed876c8128..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" - -#include "GameProjectUtils.h" -#include "AyonConstants.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" - -int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_Ayon_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for Ayon - return 0; -} - - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") -{ -} - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FAyonGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); - return FAyon_ActionResult(); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); -} - -void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor AyonPluginDescriptor; - AyonPluginDescriptor.bEnabled = true; - AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; - ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); -} - -FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FAyonGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp deleted file mode 100644 index 7a65fd0c98..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorOpenPypeDirs(); -#endif -} - -void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.ObjectPath.ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UOpenPypePublishInstance::ColorOpenPypeDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined OpenPype folder - if (!PathName.Contains(TEXT("OpenPype"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UOpenPypePublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UOpenPypePublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UOpenPypePublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h deleted file mode 100644 index bb25430411..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Ayon.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" - - -class FAyonModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterMenus(); - void RegisterSettings(); - bool HandleSettingsSaved(); - - void MenuPopup(); - void MenuDialog(); - -private: - TSharedPtr PluginCommands; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h deleted file mode 100644 index d40642b149..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainer.h +++ /dev/null @@ -1,34 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/NoExportTypes.h" -#include "Engine/AssetUserData.h" -#include "AssetRegistry/AssetData.h" -#include "AyonAssetContainer.generated.h" - -UCLASS(Blueprintable) -class AYON_API UAyonAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAyonAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") - TArray assets; - - // There seems to be no reflection option to expose array of FAssetData - /* - UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) - TArray assetsData; - */ -private: - TArray assetsData; - void OnAssetAdded(const FAssetData& AssetData); - void OnAssetRemoved(const FAssetData& AssetData); - void OnAssetRenamed(const FAssetData& AssetData, const FString& str); -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h deleted file mode 100644 index da424cde2e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h +++ /dev/null @@ -1,18 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonAssetContainerFactory.generated.h" - -UCLASS() -class AYON_API UAyonAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h deleted file mode 100644 index 9c40dc8241..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonCommands.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Framework/Commands/Commands.h" -#include "AyonStyle.h" - -class FAyonCommands : public TCommands -{ -public: - - FAyonCommands() - : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) - { - } - - // TCommands<> interface - virtual void RegisterCommands() override; - -public: - TSharedPtr< FUICommandInfo > AyonTools; - TSharedPtr< FUICommandInfo > AyonToolsDialog; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h deleted file mode 100644 index 5fe7c14360..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonConstants.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -namespace AyonConstants -{ - const FString Ayon_PluginName = "Ayon"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h deleted file mode 100644 index da83b448fb..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonLib.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonLib.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd); - - UFUNCTION(BlueprintCallable, Category = Python) - static TArray GetAllProperties(UClass* cls); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index c89388036f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 3cef8e76b2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h deleted file mode 100644 index 3c429fd7d3..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "AyonPythonBridge.generated.h" - -UCLASS(Blueprintable) -class UAyonPythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UAyonPythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; - -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h deleted file mode 100644 index 4f12d1a5f2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonSettings.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/Object.h" -#include "AyonSettings.generated.h" - -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") - -UCLASS(Config=AyonSettings, DefaultConfig) -class AYON_API UAyonSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h deleted file mode 100644 index 58f6af656e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/AyonStyle.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "CoreMinimal.h" -#include "Styling/SlateStyle.h" - -class FAyonStyle -{ -public: - static void Initialize(); - static void Shutdown(); - static void ReloadTextures(); - static const ISlateStyle& Get(); - static FName GetStyleSetName(); - - -private: - static TSharedRef< class FSlateStyleSet > Create(); - static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h deleted file mode 100644 index bb995ec452..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FAyon_ActionResult structure - */ -#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... -*/ -UENUM() -namespace EAyon_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FAyon_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FAyon_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EAyon_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EAyon_ActionResult::Ok - */ - bool IsProblem() const; - EAyon_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h deleted file mode 100644 index da8e9af661..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - - -#include "GameProjectUtils.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "AyonGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FAyonGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FAyonGenerateProjectParams(); - FAyonGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UAyonGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FAyonGenerateProjectParams ParseParameters(const FString& Params) const; - FAyon_ActionResult TryCreateProject() const; - FAyon_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FAyon_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h deleted file mode 100644 index 25b33a63e8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h deleted file mode 100644 index 9c0c4a69e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorOpenPypeDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat deleted file mode 100644 index 473c248cbe..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0.bat +++ /dev/null @@ -1 +0,0 @@ -"C:\Program Files\Epic Games\UE_5.0\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.0\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.0" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat b/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat deleted file mode 100644 index b96de6d6c9..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/BuildPlugin_5-0_Window.bat +++ /dev/null @@ -1 +0,0 @@ -cmd /k "BuildPlugin_5-0.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore deleted file mode 100644 index 80814ef0a6..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -/Saved -/DerivedDataCache -/Intermediate -/Binaries -/Content -/Config -/.idea -/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject deleted file mode 100644 index 9cf75ebaf2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject +++ /dev/null @@ -1,20 +0,0 @@ -{ - "FileVersion": 3, - "EngineAssociation": "5.0", - "Category": "", - "Description": "", - "Plugins": [ - { - "Name": "ModelingToolsEditorMode", - "Enabled": true, - "TargetAllowList": [ - "Editor" - ] - }, - { - "Name": "Ayon", - "Enabled": true, - "Type": "Editor" - } - ] -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore deleted file mode 100644 index b32a6f55e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -/Binaries -/Intermediate diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin deleted file mode 100644 index 70ed8f6b9a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Ayon.uplugin +++ /dev/null @@ -1,24 +0,0 @@ -{ - "FileVersion": 3, - "Version": 1, - "VersionName": "1.0", - "FriendlyName": "Ayon", - "Description": "Ayon Integration", - "Category": "Ayon.Integration", - "CreatedBy": "Ondrej Samohel", - "CreatedByURL": "https://ayon.ynput.io", - "DocsURL": "https://ayon.ynput.io/docs/artist_hosts_unreal", - "MarketplaceURL": "", - "SupportURL": "https://ynput.io/", - "CanContainContent": true, - "EngineVersion": "5.0", - "IsExperimentalVersion": false, - "Installed": true, - "Modules": [ - { - "Name": "Ayon", - "Type": "Editor", - "LoadingPhase": "Default" - } - ] -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini deleted file mode 100644 index 9ad7f55201..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/DefaultAyonSettings.ini +++ /dev/null @@ -1,2 +0,0 @@ -[/Script/Ayon.AyonSettings] -FolderColor=(R=91,G=197,B=220,A=255) \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini deleted file mode 100644 index ccebca2f32..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Config/FilterPlugin.ini +++ /dev/null @@ -1,8 +0,0 @@ -[FilterPlugin] -; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and -; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. -; -; Examples: -; /README.txt -; /Extras/... -; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py deleted file mode 100644 index c0b1d0ce5d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Content/Python/init_unreal.py +++ /dev/null @@ -1,30 +0,0 @@ -import unreal - -ayon_detected = True -try: - from openpype.pipeline import install_host - from openpype.hosts.unreal.api import UnrealHost - - ayon_host = UnrealHost() -except ImportError as exc: - ayon_host = None - ayon_detected = False - unreal.log_error(f"Ayon: cannot load Ayon integration [ {exc} ]") - -if ayon_detected: - install_host(ayon_host) - - -@unreal.uclass() -class AyonIntegration(unreal.AyonPythonBridge): - @unreal.ufunction(override=True) - def RunInPython_Popup(self): - unreal.log_warning("Ayon: showing tools popup") - if ayon_detected: - ayon_host.show_tools_popup() - - @unreal.ufunction(override=True) - def RunInPython_Dialog(self): - unreal.log_warning("Ayon: showing tools dialog") - if ayon_detected: - ayon_host.show_tools_dialog() diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md b/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md deleted file mode 100644 index 417d490548..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Ayon Unreal Integration plugin - UE 5.1 - -This is plugin for Unreal Editor, creating menu for [Ayon](https://github.com/ynput/OpenPype) tools to run. diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon128.png deleted file mode 100644 index 799d849aa3163ecb16be39c641a6ac30324906b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2358 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 D;gw7~ diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Resources/ayon512.png deleted file mode 100644 index 990d5917e232a0644820428fb2790943de5ffaa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16705 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 - FAyonCommands::Get().AyonTools, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuPopup), - FCanExecuteAction()); - PluginCommands->MapAction( - FAyonCommands::Get().AyonToolsDialog, - FExecuteAction::CreateRaw(this, &FAyonModule::MenuDialog), - FCanExecuteAction()); - - UToolMenus::RegisterStartupCallback( - FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FAyonModule::RegisterMenus)); - - RegisterSettings(); -} - -void FAyonModule::ShutdownModule() -{ - UToolMenus::UnRegisterStartupCallback(this); - - UToolMenus::UnregisterOwner(this); - - FAyonStyle::Shutdown(); - - FAyonCommands::Unregister(); -} - - -void FAyonModule::RegisterSettings() -{ - ISettingsModule& SettingsModule = FModuleManager::LoadModuleChecked("Settings"); - - // Create the new category - // TODO: After the movement of the plugin from the game to editor, it might be necessary to move this! - ISettingsContainerPtr SettingsContainer = SettingsModule.GetContainer("Project"); - - UAyonSettings* Settings = GetMutableDefault(); - - // Register the settings - ISettingsSectionPtr SettingsSection = SettingsModule.RegisterSettings("Project", "Ayon", "General", - LOCTEXT("RuntimeGeneralSettingsName", - "General"), - LOCTEXT("RuntimeGeneralSettingsDescription", - "Base configuration for Open Pype Module"), - Settings - ); - - // Register the save handler to your settings, you might want to use it to - // validate those or just act to settings changes. - if (SettingsSection.IsValid()) - { - SettingsSection->OnModified().BindRaw(this, &FAyonModule::HandleSettingsSaved); - } -} - -bool FAyonModule::HandleSettingsSaved() -{ - UAyonSettings* Settings = GetMutableDefault(); - bool ResaveSettings = false; - - // You can put any validation code in here and resave the settings in case an invalid - // value has been entered - - if (ResaveSettings) - { - Settings->SaveConfig(); - } - - return true; -} - -void FAyonModule::RegisterMenus() -{ - // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner - FToolMenuOwnerScoped OwnerScoped(this); - - { - UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); - { - // FToolMenuSection& Section = Menu->FindOrAddSection("Ayon"); - FToolMenuSection& Section = Menu->AddSection( - "Ayon", - TAttribute(FText::FromString("Ayon")), - FToolMenuInsert("Programming", EToolMenuInsertType::Before) - ); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonTools, PluginCommands); - Section.AddMenuEntryWithCommandList(FAyonCommands::Get().AyonToolsDialog, PluginCommands); - } - UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar"); - { - FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools"); - { - FToolMenuEntry& Entry = Section.AddEntry( - FToolMenuEntry::InitToolBarButton(FAyonCommands::Get().AyonTools)); - Entry.SetCommandList(PluginCommands); - } - } - } -} - - -void FAyonModule::MenuPopup() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Popup(); -} - -void FAyonModule::MenuDialog() -{ - UAyonPythonBridge* bridge = UAyonPythonBridge::Get(); - bridge->RunInPython_Dialog(); -} - -IMPLEMENT_MODULE(FAyonModule, Ayon) diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp deleted file mode 100644 index 3022757dc8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainer.cpp +++ /dev/null @@ -1,113 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AyonAssetContainer.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Containers/UnrealString.h" - -UAyonAssetContainer::UAyonAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAyonAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAyonAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAyonAssetContainer::OnAssetRenamed); -} - -void UAyonAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.GetObjectPathString(); - UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - assets.Add(assetPath); - assetsData.Add(AssetData); - UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); - } - } -} - -void UAyonAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.GetObjectPathString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - FString path = UAyonAssetContainer::GetPathName(); - FString lpp = FPackageName::GetLongPackagePath(*path); - - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); - assets.Remove(assetPath); - assetsData.Remove(AssetData); - } - } -} - -void UAyonAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAyonAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.GetObjectPathString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - - assets.Remove(str); - assets.Add(assetPath); - assetsData.Remove(AssetData); - // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); - } - } -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp deleted file mode 100644 index 086fc1036e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonAssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AyonAssetContainerFactory.h" -#include "AyonAssetContainer.h" - -UAyonAssetContainerFactory::UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAyonAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAyonAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp deleted file mode 100644 index 566ee1dcd1..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonCommands.cpp +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonCommands.h" - -#define LOCTEXT_NAMESPACE "FAyonModule" - -void FAyonCommands::RegisterCommands() -{ - UI_COMMAND(AyonTools, "Ayon Tools", "Pipeline tools", EUserInterfaceActionType::Button, FInputChord()); - UI_COMMAND(AyonToolsDialog, "Ayon Tools Dialog", "Pipeline tools dialog", EUserInterfaceActionType::Button, FInputChord()); -} - -#undef LOCTEXT_NAMESPACE diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp deleted file mode 100644 index 7cfa0c9c30..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonLib.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonLib.h" - -#include "AssetViewUtils.h" -#include "UObject/UnrealType.h" - -/** - * Sets color on folder icon on given path - * @param InPath - path to folder - * @param InFolderColor - color of the folder - * @warning This color will appear only after Editor restart. Is there a better way? - */ - -bool UAyonLib::SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor, const bool& bForceAdd) -{ - if (AssetViewUtils::DoesFolderExist(FolderPath)) - { - const TSharedPtr LinearColor = MakeShared(FolderColor); - - AssetViewUtils::SaveColor(FolderPath, LinearColor, true); - UE_LOG(LogAssetData, Display, TEXT("A color {%s} has been set to folder \"%s\""), *LinearColor->ToString(), - *FolderPath) - return true; - } - - UE_LOG(LogAssetData, Display, TEXT("Setting a color {%s} to folder \"%s\" has failed! Directory doesn't exist!"), - *FolderColor.ToString(), *FolderPath) - return false; -} - -/** - * Returns all poperties on given object - * @param cls - class - * @return TArray of properties - */ -TArray UAyonLib::GetAllProperties(UClass* cls) -{ - TArray Ret; - if (cls != nullptr) - { - for (TFieldIterator It(cls); It; ++It) - { - FProperty* Property = *It; - if (Property->HasAnyPropertyFlags(EPropertyFlags::CPF_Edit)) - { - Ret.Add(Property->GetName()); - } - } - } - return Ret; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp deleted file mode 100644 index d1b47a19d4..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UAyonPublishInstance::UAyonPublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAyonPublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAyonPublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UAyonPublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorAyonDirs(); -#endif -} - -void UAyonPublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.GetSoftObjectPath().ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UAyonPublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UAyonPublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UAyonPublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UAyonPublishInstance::ColorAyonDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined Ayon folder - if (!PathName.Contains(TEXT("Ayon"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("Ayon"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base Ayon directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UAyonSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UAyonPublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UAyonPublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UAyonPublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UAyonPublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp deleted file mode 100644 index f79c428a6d..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPublishInstanceFactory.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#include "AyonPublishInstanceFactory.h" -#include "AyonPublishInstance.h" - -UAyonPublishInstanceFactory::UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAyonPublishInstance::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAyonPublishInstanceFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - check(InClass->IsChildOf(UAyonPublishInstance::StaticClass())); - return NewObject(InParent, InClass, InName, Flags); -} - -bool UAyonPublishInstanceFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp deleted file mode 100644 index 0ed4b2f704..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonPythonBridge.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "AyonPythonBridge.h" - -UAyonPythonBridge* UAyonPythonBridge::Get() -{ - TArray AyonPythonBridgeClasses; - GetDerivedClasses(UAyonPythonBridge::StaticClass(), AyonPythonBridgeClasses); - int32 NumClasses = AyonPythonBridgeClasses.Num(); - if (NumClasses > 0) - { - return Cast(AyonPythonBridgeClasses[NumClasses - 1]->GetDefaultObject()); - } - return nullptr; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp deleted file mode 100644 index da388fbc8f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonSettings.cpp +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonSettings.h" - -#include "Interfaces/IPluginManager.h" -#include "UObject/UObjectGlobals.h" - -/** - * Mainly is used for initializing default values if the DefaultAyonSettings.ini file does not exist in the saved config - */ -UAyonSettings::UAyonSettings(const FObjectInitializer& ObjectInitializer) -{ - - const FString ConfigFilePath = AYON_SETTINGS_FILEPATH; - - // This has to be probably in the future set using the UE Reflection system - FColor Color; - GConfig->GetColor(TEXT("/Script/Ayon.AyonSettings"), TEXT("FolderColor"), Color, ConfigFilePath); - - FolderColor = Color; -} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp deleted file mode 100644 index d88df78735..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/AyonStyle.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "AyonStyle.h" -#include "Framework/Application/SlateApplication.h" -#include "Styling/SlateStyleRegistry.h" -#include "Slate/SlateGameResources.h" -#include "Interfaces/IPluginManager.h" -#include "Styling/SlateStyleMacros.h" - -#define RootToContentDir Style->RootToContentDir - -TSharedPtr FAyonStyle::AyonStyleInstance = nullptr; - -void FAyonStyle::Initialize() -{ - if (!AyonStyleInstance.IsValid()) - { - AyonStyleInstance = Create(); - FSlateStyleRegistry::RegisterSlateStyle(*AyonStyleInstance); - } -} - -void FAyonStyle::Shutdown() -{ - FSlateStyleRegistry::UnRegisterSlateStyle(*AyonStyleInstance); - ensure(AyonStyleInstance.IsUnique()); - AyonStyleInstance.Reset(); -} - -FName FAyonStyle::GetStyleSetName() -{ - static FName StyleSetName(TEXT("AyonStyle")); - return StyleSetName; -} - -const FVector2D Icon16x16(16.0f, 16.0f); -const FVector2D Icon20x20(20.0f, 20.0f); -const FVector2D Icon40x40(40.0f, 40.0f); - -TSharedRef< FSlateStyleSet > FAyonStyle::Create() -{ - TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("AyonStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Resources")); - - Style->Set("Ayon.AyonTools", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - Style->Set("Ayon.AyonToolsDialog", new IMAGE_BRUSH(TEXT("ayon40"), Icon40x40)); - - return Style; -} - -void FAyonStyle::ReloadTextures() -{ - if (FSlateApplication::IsInitialized()) - { - FSlateApplication::Get().GetRenderer()->ReloadTextureResources(); - } -} - -const ISlateStyle& FAyonStyle::Get() -{ - return *AyonStyleInstance; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp deleted file mode 100644 index 2a137e3ed7..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/AyonActionResult.cpp +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#include "Commandlets/AyonActionResult.h" -#include "Logging/Ayon_Log.h" - -EAyon_ActionResult::Type& FAyon_ActionResult::GetStatus() -{ - return Status; -} - -FText& FAyon_ActionResult::GetReason() -{ - return Reason; -} - -FAyon_ActionResult::FAyon_ActionResult():Status(EAyon_ActionResult::Type::Ok) -{ - -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum):Status(InEnum) -{ - TryLog(); -} - -FAyon_ActionResult::FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) -{ - TryLog(); -}; - -bool FAyon_ActionResult::IsProblem() const -{ - return Status != EAyon_ActionResult::Ok; -} - -void FAyon_ActionResult::TryLog() const -{ - if(IsProblem()) - UE_LOG(LogCommandletAyonGenerateProject, Error, TEXT("%s"), *Reason.ToString()); -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp deleted file mode 100644 index ed876c8128..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/Commandlets/Implementations/AyonGenerateProjectCommandlet.cpp +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#include "Commandlets/Implementations/AyonGenerateProjectCommandlet.h" - -#include "GameProjectUtils.h" -#include "AyonConstants.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" - -int32 UAyonGenerateProjectCommandlet::Main(const FString& CommandLineParams) -{ - //Parses command line parameters & creates structure FProjectInformation - const FAyonGenerateProjectParams ParsedParams = FAyonGenerateProjectParams(CommandLineParams); - ProjectInformation = ParsedParams.GenerateUEProjectInformation(); - - //Creates .uproject & other UE files - EVALUATE_Ayon_ACTION_RESULT(TryCreateProject()); - - //Loads created .uproject - EVALUATE_Ayon_ACTION_RESULT(TryLoadProjectDescriptor()); - - //Adds needed plugin to .uproject - AttachPluginsToProjectDescriptor(); - - //Saves .uproject - EVALUATE_Ayon_ACTION_RESULT(TrySave()); - - //When we are here, there should not be problems in generating Unreal Project for Ayon - return 0; -} - - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(): FAyonGenerateProjectParams("") -{ -} - -FAyonGenerateProjectParams::FAyonGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( - CommandLineParams) -{ - UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); -} - -FProjectInformation FAyonGenerateProjectParams::GenerateUEProjectInformation() const -{ - FProjectInformation ProjectInformation = FProjectInformation(); - ProjectInformation.ProjectFilename = GetProjectFileName(); - - ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); - - return ProjectInformation; -} - -FString FAyonGenerateProjectParams::TryGetToken(const int32 Index) const -{ - return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; -} - -FString FAyonGenerateProjectParams::GetProjectFileName() const -{ - return TryGetToken(0); -} - -bool FAyonGenerateProjectParams::IsSwitchPresent(const FString& Switch) const -{ - return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool - { - return Item.Equals(Switch); - } - ); -} - - -UAyonGenerateProjectCommandlet::UAyonGenerateProjectCommandlet() -{ - LogToConsole = true; -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryCreateProject() const -{ - FText FailReason; - FText FailLog; - TArray OutCreatedFiles; - - if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) - return FAyon_ActionResult(EAyon_ActionResult::ProjectNotCreated, FailReason); - return FAyon_ActionResult(); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TryLoadProjectDescriptor() -{ - FText FailReason; - const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bLoaded ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotLoaded, FailReason); -} - -void UAyonGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() -{ - FPluginReferenceDescriptor AyonPluginDescriptor; - AyonPluginDescriptor.bEnabled = true; - AyonPluginDescriptor.Name = AyonConstants::Ayon_PluginName; - ProjectDescriptor.Plugins.Add(AyonPluginDescriptor); - - FPluginReferenceDescriptor PythonPluginDescriptor; - PythonPluginDescriptor.bEnabled = true; - PythonPluginDescriptor.Name = AyonConstants::PythonScript_PluginName; - ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); - - FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; - SequencerScriptingPluginDescriptor.bEnabled = true; - SequencerScriptingPluginDescriptor.Name = AyonConstants::SequencerScripting_PluginName; - ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); - - FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; - MovieRenderPipelinePluginDescriptor.bEnabled = true; - MovieRenderPipelinePluginDescriptor.Name = AyonConstants::MovieRenderPipeline_PluginName; - ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); - - FPluginReferenceDescriptor EditorScriptingPluginDescriptor; - EditorScriptingPluginDescriptor.bEnabled = true; - EditorScriptingPluginDescriptor.Name = AyonConstants::EditorScriptingUtils_PluginName; - ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); -} - -FAyon_ActionResult UAyonGenerateProjectCommandlet::TrySave() -{ - FText FailReason; - const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); - - return FAyon_ActionResult(bSaved ? EAyon_ActionResult::Ok : EAyon_ActionResult::ProjectNotSaved, FailReason); -} - -FAyonGenerateProjectParams UAyonGenerateProjectCommandlet::ParseParameters(const FString& Params) const -{ - FAyonGenerateProjectParams ParamsResult; - - TArray Tokens, Switches; - ParseCommandLine(*Params, Tokens, Switches); - - return ParamsResult; -} diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp deleted file mode 100644 index 02a8ac800a..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Private/OpenPypePublishInstance.cpp +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "AssetToolsModule.h" -#include "Framework/Notifications/NotificationManager.h" -#include "AyonLib.h" -#include "AyonSettings.h" -#include "Widgets/Notifications/SNotificationList.h" - - -//Moves all the invalid pointers to the end to prepare them for the shrinking -#define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ - VAR.Shrink(); - -UOpenPypePublishInstance::UOpenPypePublishInstance(const FObjectInitializer& ObjectInitializer) - : UPrimaryDataAsset(ObjectInitializer) -{ - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked< - FAssetRegistryModule>("AssetRegistry"); - - const FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked( - "PropertyEditor"); - - FString Left, Right; - GetPathName().Split("/" + GetName(), &Left, &Right); - - FARFilter Filter; - Filter.PackagePaths.Emplace(FName(Left)); - - TArray FoundAssets; - AssetRegistryModule.GetRegistry().GetAssets(Filter, FoundAssets); - - for (const FAssetData& AssetData : FoundAssets) - OnAssetCreated(AssetData); - - REMOVE_INVALID_ENTRIES(AssetDataInternal) - REMOVE_INVALID_ENTRIES(AssetDataExternal) - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UOpenPypePublishInstance::OnAssetCreated); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UOpenPypePublishInstance::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetUpdated().AddUObject(this, &UOpenPypePublishInstance::OnAssetUpdated); - -#ifdef WITH_EDITOR - ColorOpenPypeDirs(); -#endif -} - -void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) -{ - TArray split; - - UObject* Asset = InAssetData.GetAsset(); - - if (!IsValid(Asset)) - { - UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.GetSoftObjectPath().ToString()); - return; - } - - const bool result = IsUnderSameDir(Asset) && Cast(Asset) == nullptr; - - if (result) - { - if (AssetDataInternal.Emplace(Asset).IsValidId()) - { - UE_LOG(LogTemp, Log, TEXT("Added an Asset to PublishInstance - Publish Instance: %s, Asset %s"), - *this->GetName(), *Asset->GetName()); - } - } -} - -void UOpenPypePublishInstance::OnAssetRemoved(const FAssetData& InAssetData) -{ - if (Cast(InAssetData.GetAsset()) == nullptr) - { - if (AssetDataInternal.Contains(nullptr)) - { - AssetDataInternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataInternal) - } - else - { - AssetDataExternal.Remove(nullptr); - REMOVE_INVALID_ENTRIES(AssetDataExternal) - } - } -} - -void UOpenPypePublishInstance::OnAssetUpdated(const FAssetData& InAssetData) -{ - REMOVE_INVALID_ENTRIES(AssetDataInternal); - REMOVE_INVALID_ENTRIES(AssetDataExternal); -} - -bool UOpenPypePublishInstance::IsUnderSameDir(const UObject* InAsset) const -{ - FString ThisLeft, ThisRight; - this->GetPathName().Split(this->GetName(), &ThisLeft, &ThisRight); - - return InAsset->GetPathName().StartsWith(ThisLeft); -} - -#ifdef WITH_EDITOR - -void UOpenPypePublishInstance::ColorOpenPypeDirs() -{ - FString PathName = this->GetPathName(); - - //Check whether the path contains the defined OpenPype folder - if (!PathName.Contains(TEXT("OpenPype"))) return; - - //Get the base path for open pype - FString PathLeft, PathRight; - PathName.Split(FString("OpenPype"), &PathLeft, &PathRight); - - if (PathLeft.IsEmpty() || PathRight.IsEmpty()) - { - UE_LOG(LogAssetData, Error, TEXT("Failed to retrieve the base OpenPype directory!")) - return; - } - - PathName.RemoveFromEnd(PathRight, ESearchCase::CaseSensitive); - - //Get the current settings - const UAyonSettings* Settings = GetMutableDefault(); - - //Color the base folder - UAyonLib::SetFolderColor(PathName, Settings->GetFolderFColor(), false); - - //Get Sub paths, iterate through them and color them according to the folder color in UOpenPypeSettings - const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked( - "AssetRegistry"); - - TArray PathList; - - AssetRegistryModule.Get().GetSubPaths(PathName, PathList, true); - - if (PathList.Num() > 0) - { - for (const FString& Path : PathList) - { - UAyonLib::SetFolderColor(Path, Settings->GetFolderFColor(), false); - } - } -} - -void UOpenPypePublishInstance::SendNotification(const FString& Text) const -{ - FNotificationInfo Info{FText::FromString(Text)}; - - Info.bFireAndForget = true; - Info.bUseLargeFont = false; - Info.bUseThrobber = false; - Info.bUseSuccessFailIcons = false; - Info.ExpireDuration = 4.f; - Info.FadeOutDuration = 2.f; - - FSlateNotificationManager::Get().AddNotification(Info); - - UE_LOG(LogAssetData, Warning, - TEXT( - "Removed duplicated asset from the AssetsDataExternal in Container \"%s\", Asset is already included in the AssetDataInternal!" - ), *GetName() - ) -} - - -void UOpenPypePublishInstance::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) -{ - Super::PostEditChangeProperty(PropertyChangedEvent); - - if (PropertyChangedEvent.ChangeType == EPropertyChangeType::ValueSet && - PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED( - UOpenPypePublishInstance, AssetDataExternal)) - { - // Check for duplicated assets - for (const auto& Asset : AssetDataInternal) - { - if (AssetDataExternal.Contains(Asset)) - { - AssetDataExternal.Remove(Asset); - return SendNotification( - "You are not allowed to add assets into AssetDataExternal which are already included in AssetDataInternal!"); - } - } - - // Check if no UOpenPypePublishInstance type assets are included - for (const auto& Asset : AssetDataExternal) - { - if (Cast(Asset.Get()) != nullptr) - { - AssetDataExternal.Remove(Asset); - return SendNotification("You are not allowed to add publish instances!"); - } - } - } -} - -#endif diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h deleted file mode 100644 index bb25430411..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Ayon.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" - - -class FAyonModule : public IModuleInterface -{ -public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; - -private: - void RegisterMenus(); - void RegisterSettings(); - bool HandleSettingsSaved(); - - void MenuPopup(); - void MenuDialog(); - -private: - TSharedPtr PluginCommands; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h deleted file mode 100644 index d40642b149..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainer.h +++ /dev/null @@ -1,34 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/NoExportTypes.h" -#include "Engine/AssetUserData.h" -#include "AssetRegistry/AssetData.h" -#include "AyonAssetContainer.generated.h" - -UCLASS(Blueprintable) -class AYON_API UAyonAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAyonAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAyonAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Assets") - TArray assets; - - // There seems to be no reflection option to expose array of FAssetData - /* - UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) - TArray assetsData; - */ -private: - TArray assetsData; - void OnAssetAdded(const FAssetData& AssetData); - void OnAssetRemoved(const FAssetData& AssetData); - void OnAssetRenamed(const FAssetData& AssetData, const FString& str); -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h deleted file mode 100644 index da424cde2e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonAssetContainerFactory.h +++ /dev/null @@ -1,18 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonAssetContainerFactory.generated.h" - -UCLASS() -class AYON_API UAyonAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h deleted file mode 100644 index 9c40dc8241..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonCommands.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "Framework/Commands/Commands.h" -#include "AyonStyle.h" - -class FAyonCommands : public TCommands -{ -public: - - FAyonCommands() - : TCommands(TEXT("Ayon"), NSLOCTEXT("Contexts", "Ayon", "Ayon Tools"), NAME_None, FAyonStyle::GetStyleSetName()) - { - } - - // TCommands<> interface - virtual void RegisterCommands() override; - -public: - TSharedPtr< FUICommandInfo > AyonTools; - TSharedPtr< FUICommandInfo > AyonToolsDialog; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h deleted file mode 100644 index 5fe7c14360..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonConstants.h +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -namespace AyonConstants -{ - const FString Ayon_PluginName = "Ayon"; - const FString PythonScript_PluginName = "PythonScriptPlugin"; - const FString SequencerScripting_PluginName = "SequencerScripting"; - const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; - const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; -} - - diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h deleted file mode 100644 index da83b448fb..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonLib.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -#include "AyonLib.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonLib : public UBlueprintFunctionLibrary -{ - - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static bool SetFolderColor(const FString& FolderPath, const FLinearColor& FolderColor,const bool& bForceAdd); - - UFUNCTION(BlueprintCallable, Category = Python) - static TArray GetAllProperties(UClass* cls); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h deleted file mode 100644 index c89388036f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "AyonPublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UAyonPublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorAyonDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h deleted file mode 100644 index 3cef8e76b2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPublishInstanceFactory.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AyonPublishInstanceFactory.generated.h" - -/** - * - */ -UCLASS() -class AYON_API UAyonPublishInstanceFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAyonPublishInstanceFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h deleted file mode 100644 index 3c429fd7d3..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonPythonBridge.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "AyonPythonBridge.generated.h" - -UCLASS(Blueprintable) -class UAyonPythonBridge : public UObject -{ - GENERATED_BODY() - -public: - UFUNCTION(BlueprintCallable, Category = Python) - static UAyonPythonBridge* Get(); - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Popup() const; - - UFUNCTION(BlueprintImplementableEvent, Category = Python) - void RunInPython_Dialog() const; - -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h deleted file mode 100644 index 4f12d1a5f2..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonSettings.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/Object.h" -#include "AyonSettings.generated.h" - -#define AYON_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("Ayon")->GetBaseDir() / TEXT("Config") / TEXT("DefaultAyonSettings.ini") - -UCLASS(Config=AyonSettings, DefaultConfig) -class AYON_API UAyonSettings : public UObject -{ - GENERATED_UCLASS_BODY() - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FColor GetFolderFColor() const - { - return FolderColor; - } - - UFUNCTION(BlueprintCallable, BlueprintPure, Category = Settings) - FLinearColor GetFolderFLinearColor() const - { - return FLinearColor(FolderColor); - } - -protected: - - UPROPERTY(config, EditAnywhere, Category = Folders) - FColor FolderColor = FColor(25,45,223); -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h deleted file mode 100644 index 58f6af656e..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/AyonStyle.h +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once -#include "CoreMinimal.h" -#include "Styling/SlateStyle.h" - -class FAyonStyle -{ -public: - static void Initialize(); - static void Shutdown(); - static void ReloadTextures(); - static const ISlateStyle& Get(); - static FName GetStyleSetName(); - - -private: - static TSharedRef< class FSlateStyleSet > Create(); - static TSharedPtr< class FSlateStyleSet > AyonStyleInstance; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h deleted file mode 100644 index bb995ec452..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/AyonActionResult.h +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. - -#pragma once - -#include "CoreMinimal.h" -#include "AyonActionResult.generated.h" - -/** - * @brief This macro returns error code when is problem or does nothing when there is no problem. - * @param ActionResult FAyon_ActionResult structure - */ -#define EVALUATE_Ayon_ACTION_RESULT(ActionResult) \ - if(ActionResult.IsProblem()) \ - return ActionResult.GetStatus(); - -/** -* @brief This enum values are humanly readable mapping of error codes. -* Here should be all error codes to be possible find what went wrong. -* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... -*/ -UENUM() -namespace EAyon_ActionResult -{ - enum Type - { - Ok, - ProjectNotCreated, - ProjectNotLoaded, - ProjectNotSaved, - //....Here insert another values - - //Do not remove! - //Usable for looping through enum values - __Last UMETA(Hidden) - }; -} - - -/** - * @brief This struct holds action result enum and optionally reason of fail - */ -USTRUCT() -struct FAyon_ActionResult -{ - GENERATED_BODY() - -public: - /** @brief Default constructor usable when there is no problem */ - FAyon_ActionResult(); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum); - - /** - * @brief This constructor initializes variables & attempts to log when is error - * @param InEnum Status - * @param InReason Reason of potential fail - */ - FAyon_ActionResult(const EAyon_ActionResult::Type& InEnum, const FText& InReason); - -private: - /** @brief Action status */ - EAyon_ActionResult::Type Status; - - /** @brief Optional reason of fail */ - FText Reason; - -public: - /** - * @brief Checks if there is problematic state - * @return true when status is not equal to EAyon_ActionResult::Ok - */ - bool IsProblem() const; - EAyon_ActionResult::Type& GetStatus(); - FText& GetReason(); - -private: - void TryLog() const; -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h deleted file mode 100644 index da8e9af661..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Commandlets/Implementations/AyonGenerateProjectCommandlet.h +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - - -#include "GameProjectUtils.h" -#include "Commandlets/AyonActionResult.h" -#include "ProjectDescriptor.h" -#include "Commandlets/Commandlet.h" -#include "AyonGenerateProjectCommandlet.generated.h" - -struct FProjectDescriptor; -struct FProjectInformation; - -/** -* @brief Structure which parses command line parameters and generates FProjectInformation -*/ -USTRUCT() -struct FAyonGenerateProjectParams -{ - GENERATED_BODY() - -private: - FString CommandLineParams; - TArray Tokens; - TArray Switches; - -public: - FAyonGenerateProjectParams(); - FAyonGenerateProjectParams(const FString& CommandLineParams); - - FProjectInformation GenerateUEProjectInformation() const; - -private: - FString TryGetToken(const int32 Index) const; - FString GetProjectFileName() const; - - bool IsSwitchPresent(const FString& Switch) const; -}; - -UCLASS() -class AYON_API UAyonGenerateProjectCommandlet : public UCommandlet -{ - GENERATED_BODY() - -private: - FProjectInformation ProjectInformation; - FProjectDescriptor ProjectDescriptor; - -public: - UAyonGenerateProjectCommandlet(); - - virtual int32 Main(const FString& CommandLineParams) override; - -private: - FAyonGenerateProjectParams ParseParameters(const FString& Params) const; - FAyon_ActionResult TryCreateProject() const; - FAyon_ActionResult TryLoadProjectDescriptor(); - void AttachPluginsToProjectDescriptor(); - FAyon_ActionResult TrySave(); -}; - diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h deleted file mode 100644 index 25b33a63e8..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/Logging/Ayon_Log.h +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -#pragma once - -DEFINE_LOG_CATEGORY_STATIC(LogCommandletAyonGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h deleted file mode 100644 index 9c0c4a69e5..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/Ayon/Source/Ayon/Public/OpenPypePublishInstance.h +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2023, Ayon, All rights reserved. -// Deprecation warning: this is left here just for backwards compatibility -// and will be removed in next versions of Ayon. -#pragma once - -#include "OpenPypePublishInstance.generated.h" - - -UCLASS(Blueprintable) -class AYON_API UOpenPypePublishInstance : public UPrimaryDataAsset -{ - GENERATED_UCLASS_BODY() - -public: - /** - /** - * Retrieves all the assets which are monitored by the Publish Instance (Monitors assets in the directory which is - * placed in) - * - * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetInternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataInternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Retrieves all the assets which have been added manually by the Publish Instance - * - * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetExternalAssets() const - { - //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. - TSet ResultSet; - - for (const auto& Asset : AssetDataExternal) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - - /** - * Function for returning all the assets in the container combined. - * - * @return Returns all the internal and externally added assets into one set (TSet of UObjects). Careful! They are - * returning raw pointers. Seems like an issue in UE5 - * - * @attention If the bAddExternalAssets variable is false, external assets won't be included! - */ - UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") - TSet GetAllAssets() const - { - const TSet>& IteratedSet = bAddExternalAssets - ? AssetDataInternal.Union(AssetDataExternal) - : AssetDataInternal; - - //Create a new TSet only with raw pointers. - TSet ResultSet; - - for (auto& Asset : IteratedSet) - ResultSet.Add(Asset.LoadSynchronous()); - - return ResultSet; - } - -private: - UPROPERTY(VisibleAnywhere, Category="Assets") - TSet> AssetDataInternal; - - /** - * This property allows exposing the array to include other assets from any other directory than what it's currently - * monitoring. NOTE: that these assets have to be added manually! They are not automatically registered or added! - */ - UPROPERTY(EditAnywhere, Category = "Assets") - bool bAddExternalAssets = false; - - UPROPERTY(EditAnywhere, meta=(EditCondition="bAddExternalAssets"), Category="Assets") - TSet> AssetDataExternal; - - - void OnAssetCreated(const FAssetData& InAssetData); - void OnAssetRemoved(const FAssetData& InAssetData); - void OnAssetUpdated(const FAssetData& InAssetData); - - bool IsUnderSameDir(const UObject* InAsset) const; - -#ifdef WITH_EDITOR - - void ColorOpenPypeDirs(); - - void SendNotification(const FString& Text) const; - virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; - -#endif -}; diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat deleted file mode 100644 index 3cc82d54af..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1.bat +++ /dev/null @@ -1 +0,0 @@ -"D:\UE_5.1\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -plugin="D:\OpenPype\openpype\hosts\unreal\integration\UE_5.1\Ayon\Ayon.uplugin" -Package="D:\BuiltPlugins\5.1" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat b/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat deleted file mode 100644 index e10f2c7add..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/BuildPlugin_5-1_Window.bat +++ /dev/null @@ -1 +0,0 @@ -cmd /k "BuildPlugin_5-1.bat" \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore deleted file mode 100644 index 80814ef0a6..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Prerequisites -*.d - -# Compiled Object files -*.slo -*.lo -*.o -*.obj - -# Precompiled Headers -*.gch -*.pch - -# Compiled Dynamic libraries -*.so -*.dylib -*.dll - -# Fortran module files -*.mod -*.smod - -# Compiled Static libraries -*.lai -*.la -*.a -*.lib - -# Executables -*.exe -*.out -*.app - -/Saved -/DerivedDataCache -/Intermediate -/Binaries -/Content -/Config -/.idea -/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject deleted file mode 100644 index fe83346624..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.1/CommandletProject/CommandletProject.uproject +++ /dev/null @@ -1,20 +0,0 @@ -{ - "FileVersion": 3, - "EngineAssociation": "5.1", - "Category": "", - "Description": "", - "Plugins": [ - { - "Name": "ModelingToolsEditorMode", - "Enabled": true, - "TargetAllowList": [ - "Editor" - ] - }, - { - "Name": "Ayon", - "Enabled": true, - "Type": "Editor" - } - ] -} \ No newline at end of file From bc1ce951035917705669b1a66188dfe0109469e9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 May 2023 12:22:21 +0200 Subject: [PATCH 606/918] =?UTF-8?q?=F0=9F=A7=B1=20add=20unreal=20plugin=20?= =?UTF-8?q?repo=20as=20submodule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 5 ++++- openpype/hosts/unreal/integration | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 160000 openpype/hosts/unreal/integration diff --git a/.gitmodules b/.gitmodules index fe93791c4e..4de92471f7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,4 +4,7 @@ [submodule "tools/modules/powershell/PSWriteColor"] path = tools/modules/powershell/PSWriteColor - url = https://github.com/EvotecIT/PSWriteColor.git \ No newline at end of file + url = https://github.com/EvotecIT/PSWriteColor.git +[submodule "openpype/hosts/unreal/integration"] + path = openpype/hosts/unreal/integration + url = https://github.com/ynput/ayon-unreal-plugin.git diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration new file mode 160000 index 0000000000..ff15c70077 --- /dev/null +++ b/openpype/hosts/unreal/integration @@ -0,0 +1 @@ +Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 From da29890d9be2634d92115c6e960ba066e4e48954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 17 May 2023 13:48:34 +0200 Subject: [PATCH 607/918] Update openpype/hosts/fusion/plugins/publish/extract_render_local.py Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index f093f7793f..f801f30577 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -83,7 +83,7 @@ class FusionRenderLocal( # Only active instances instance.data.get("publish", True) and # Only render.local instances - "render.local" in instance.data.get("families") + "render.local" in instance.data.get("families", []) ] if key not in context.data: From 6b09504eadb131dfcb78834c6b67ae90102fd8ca Mon Sep 17 00:00:00 2001 From: Zipodod <49460980+Zipodod@users.noreply.github.com> Date: Wed, 17 May 2023 08:14:26 -0400 Subject: [PATCH 608/918] Bugfix/frame variable fix (#4978) * Fix variable name on Max reset frame range * Fix variable name on Maya collect animation * Fix variable name on Nuke reset frame range * Fix lines over max width * Fix error on variable rename * Fix line over max width --------- Co-authored-by: jbeaulieu --- openpype/hosts/max/api/lib.py | 12 ++++++++---- openpype/hosts/maya/api/lib.py | 10 ++++++---- openpype/hosts/nuke/api/lib.py | 8 ++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 1310097f29..d9213863b1 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -245,11 +245,15 @@ def reset_frame_range(fps: bool = True): fps_number = float(data_fps["data"]["fps"]) rt.frameRate = fps_number frame_range = get_frame_range() - frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) - frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) - frange_cmd = f"animationRange = interval {frame_start} {frame_end}" + frame_start_handle = frame_range["frameStart"] - int( + frame_range["handleStart"] + ) + frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"]) + frange_cmd = ( + f"animationRange = interval {frame_start_handle} {frame_end_handle}" + ) rt.execute(frange_cmd) - set_render_frame_range(frame_start, frame_end) + set_render_frame_range(frame_start_handle, frame_end_handle) def set_context_setting(): diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index f814187cc1..0bcadbe76e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -316,11 +316,13 @@ def collect_animation_data(fps=False): # get scene values as defaults frame_start = cmds.playbackOptions(query=True, minTime=True) frame_end = cmds.playbackOptions(query=True, maxTime=True) - handle_start = cmds.playbackOptions(query=True, animationStartTime=True) - handle_end = cmds.playbackOptions(query=True, animationEndTime=True) + frame_start_handle = cmds.playbackOptions( + query=True, animationStartTime=True + ) + frame_end_handle = cmds.playbackOptions(query=True, animationEndTime=True) - handle_start = frame_start - handle_start - handle_end = handle_end - frame_end + handle_start = frame_start - frame_start_handle + handle_end = frame_end_handle - frame_end # build attributes data = OrderedDict() diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 64fa32a383..a439142051 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2239,13 +2239,13 @@ class WorkfileSettings(object): handle_end = data["handleEnd"] fps = float(data["fps"]) - frame_start = int(data["frameStart"]) - handle_start - frame_end = int(data["frameEnd"]) + handle_end + frame_start_handle = int(data["frameStart"]) - handle_start + frame_end_handle = int(data["frameEnd"]) + handle_end self._root_node["lock_range"].setValue(False) self._root_node["fps"].setValue(fps) - self._root_node["first_frame"].setValue(frame_start) - self._root_node["last_frame"].setValue(frame_end) + self._root_node["first_frame"].setValue(frame_start_handle) + self._root_node["last_frame"].setValue(frame_end_handle) self._root_node["lock_range"].setValue(True) # setting active viewers From 0f80ad01ec243e08e6d7824772861a2b655c64f6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:16:49 +0200 Subject: [PATCH 609/918] adding deadline settings including Pools --- .../plugins/publish/submit_fusion_deadline.py | 9 ++-- .../defaults/project_settings/deadline.json | 9 ++++ .../schema_project_deadline.json | 44 +++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index d51299506c..717391100d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -38,10 +38,6 @@ class FusionSubmitDeadline( chunk_size = 1 concurrent_tasks = 1 group = "" - department = "" - limit_groups = {} - env_allowed_keys = [] - env_search_replace_values = {} @classmethod def get_attribute_defs(cls): @@ -173,8 +169,9 @@ class FusionSubmitDeadline( # User, as seen in Monitor "UserName": deadline_user, - # Use a default submission pool for Fusion - "Pool": "fusion", + "Pool": instance.data.get("primaryPool"), + "SecondaryPool": instance.data.get("secondaryPool"), + "Group": self.group, "Plugin": "Fusion", "Frames": "{start}-{end}".format( diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 3f114025f3..1b8c8397d7 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -45,6 +45,15 @@ "chunk_size": 10, "group": "none" }, + "FusionSubmitDeadline": { + "enabled": true, + "optional": false, + "active": true, + "priority": 50, + "chunk_size": 10, + "concurrent_tasks": 1, + "group": "" + }, "NukeSubmitDeadline": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index d8b5e4dc1f..6d59b5a92b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -248,6 +248,50 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "FusionSubmitDeadline", + "label": "Fusion submit to Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "number", + "key": "priority", + "label": "Priority" + }, + { + "type": "number", + "key": "chunk_size", + "label": "Frame per Task" + }, + { + "type": "number", + "key": "concurrent_tasks", + "label": "Number of concurrent tasks" + }, + { + "type": "text", + "key": "group", + "label": "Group Name" + } + ] + }, { "type": "dict", "collapsible": true, From a6059afe869aed7996d44699f87ef30e338da4ae Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:37:37 +0200 Subject: [PATCH 610/918] pr comments also renamed start_handle as it is easily confusable with handles --- .../fusion/plugins/create/create_saver.py | 2 +- .../plugins/publish/collect_instances.py | 41 +++++++++++-------- .../fusion/plugins/publish/collect_render.py | 2 +- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index d5e77730c8..28917cb27d 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "From asset database", - "viewer_render_range": "From viewer render in/out", + "render_range": "From viewer render in/out", "comp_range": "From composition timeline" } diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 59ff52f5b2..98ea0a34e4 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -20,25 +20,28 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include creator attributes directly as instance data creator_attributes = instance.data["creator_attributes"] - frame_range_source = creator_attributes.get("frame_range_source") instance.data.update(creator_attributes) - # get asset frame ranges - start = context.data["frameStart"] - end = context.data["frameEnd"] - handle_start = context.data["handleStart"] - handle_end = context.data["handleEnd"] - start_handle = start - handle_start - end_handle = end + handle_end + frame_range_source = creator_attributes.get("frame_range_source") + instance.data["frame_range_source"] = frame_range_source - if frame_range_source == "viewer_render_range": + if frame_range_source == "asset_db": + # get asset frame ranges + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_with_handle = start - handle_start + end_with_handle = end + handle_end + + if frame_range_source == "render_range": # set comp render frame ranges start = context.data["renderFrameStart"] end = context.data["renderFrameEnd"] handle_start = 0 handle_end = 0 - start_handle = start - end_handle = end + start_with_handle = start + end_with_handle = end if frame_range_source == "comp_range": comp_start = context.data["compFrameStart"] @@ -50,14 +53,16 @@ class CollectInstanceData(pyblish.api.InstancePlugin): end = render_end handle_start = render_start - comp_start handle_end = comp_end - render_end - start_handle = comp_start - end_handle = comp_end + start_with_handle = comp_start + end_with_handle = comp_end # Include start and end render frame in label subset = instance.data["subset"] - label = "{subset} ({start}-{end})".format(subset=subset, - start=int(start), - end=int(end)) + label = "{subset} ({start}-{end})".format( + subset=subset, + start=int(start), + end=int(end) + ) instance.data.update({ "label": label, @@ -65,8 +70,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # todo: Allow custom frame range per instance "frameStart": start, "frameEnd": end, - "frameStartHandle": start_handle, - "frameEndHandle": end_handle, + "frameStartHandle": start_with_handle, + "frameEndHandle": end_with_handle, "handleStart": handle_start, "handleEnd": handle_end, "fps": context.data["fps"], diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 6956b566ad..dd6d9c2567 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -84,7 +84,7 @@ class CollectFusionRender( handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], ignoreFrameHandleCheck=( - not inst.data.get("viewer_render_range")), + inst.data["frame_range_source"] == "render_range"), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From 497a97b70d1eef294e0b7d2ea14365f5add380b4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:40:47 +0200 Subject: [PATCH 611/918] pr comment --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 28917cb27d..f1e7791972 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -232,7 +232,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { - "asset_db": "From asset database", + "asset_db": "Current asset context", "render_range": "From viewer render in/out", "comp_range": "From composition timeline" } From fb3c4b613ffd08ac5677a11c2a42f9d217770ca7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:47:32 +0200 Subject: [PATCH 612/918] improving label --- .../hosts/fusion/plugins/publish/collect_instances.py | 8 ++++++-- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 98ea0a34e4..458f00c7ed 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -58,10 +58,14 @@ class CollectInstanceData(pyblish.api.InstancePlugin): # Include start and end render frame in label subset = instance.data["subset"] - label = "{subset} ({start}-{end})".format( + label = ( + "{subset} ({start}-{end}) [{handle_start}-{handle_end}]" + ).format( subset=subset, start=int(start), - end=int(end) + end=int(end), + handle_start=int(handle_start), + handle_end=int(handle_end) ) instance.data.update({ diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index dd6d9c2567..c13d2a0c99 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -64,7 +64,7 @@ class CollectFusionRender( version=version, time="", source=current_file, - label="{} - {}".format(subset_name, family), + label=inst.data["label"], subset=subset_name, asset=inst.data["asset"], task=task_name, From 089fe88ee104c7f0c3d7d85f66d9d03f3aafbbbf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:53:09 +0200 Subject: [PATCH 613/918] task is on context --- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index c13d2a0c99..0a850a4982 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -51,7 +51,7 @@ class CollectFusionRender( if family != "render": continue - task_name = inst.data.get("task") # legacy + task_name = context.data["task"] tool = inst.data["transientData"]["tool"] instance_families = inst.data.get("families", []) From 163756d74c93ad74b5edf0095ee2279de66fabef Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 14:58:24 +0200 Subject: [PATCH 614/918] adding comment for ambiguous function call --- openpype/hosts/fusion/plugins/publish/collect_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 0a850a4982..cbb9fea76d 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -125,6 +125,7 @@ class CollectFusionRender( def post_collecting_action(self): for instance in self._context: if "render.frames" in instance.data.get("families", []): + # adding representation data to the instance self._update_for_frames(instance) def get_expected_files(self, render_instance): From 40426cd69c4c70723254d0b301d7060fc858a1f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 15:03:21 +0200 Subject: [PATCH 615/918] simplifying code --- .../fusion/plugins/publish/collect_render.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index cbb9fea76d..d0b7f1c4ff 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -32,8 +32,8 @@ class CollectFusionRender( comp = context.data.get("currentComp") comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") - aspect_x = comp_frame_format_prefs.get("AspectX") - aspect_y = comp_frame_format_prefs.get("AspectY") + aspect_x = comp_frame_format_prefs["AspectX"] + aspect_y = comp_frame_format_prefs["AspectY"] instances = [] instances_to_remove = [] @@ -93,14 +93,14 @@ class CollectFusionRender( render_target = inst.data["creator_attributes"]["render_target"] - if render_target == "local": - # for local renders - self._instance_data_local_update( - project_entity, instance, f"render.{render_target}") + # Add render target family + render_target_family = f"render.{render_target}" + if render_target_family not in instance.families: + instance.families.append(render_target_family) - if render_target == "frames": - self._instance_data_local_update( - project_entity, instance, f"render.{render_target}") + # Add render target specific data + if render_target in {"local", "frames"}: + instance.projectEntity = project_entity if render_target == "farm": fam = "render.farm" @@ -205,8 +205,3 @@ class CollectFusionRender( instance.data["representations"].append(repre) return instance - - def _instance_data_local_update(self, project_entity, instance, family): - instance.projectEntity = project_entity - if family not in instance.families: - instance.families.append(family) From 25832ed496f79063c4c2f0c3f9c87eb0e4b227c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 May 2023 15:07:10 +0200 Subject: [PATCH 616/918] collect frame range simplification --- .../publish/collect_comp_frame_range.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 08bdad3120..24a9a92337 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -34,16 +34,11 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): comp = context.data["currentComp"] # Store comp render ranges - ( - start, end, - global_start, - global_end, - ) = get_comp_render_range(comp) + start, end, global_start, global_end = get_comp_render_range(comp) - data = {} - data["renderFrameStart"] = int(start) - data["renderFrameEnd"] = int(end) - data["compFrameStart"] = int(global_start) - data["compFrameEnd"] = int(global_end) - - context.data.update(data) + context.data.update({ + "renderFrameStart": int(start), + "renderFrameEnd": int(end), + "compFrameStart": int(global_start), + "compFrameEnd": int(global_end) + }) From 0ea7b25c4675f84181cd4635ae5d74e2b18b39fd Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 14:50:12 +0100 Subject: [PATCH 617/918] refactor: rt.execute(saveNodes) replaces with pymxs function - changed the function to no longer use the selection and instead feed it the nodes directly from get_all_children function. - removed maintained_seclection() as we're no longer overriding the selection of the Max scene. - black also used to format. --- .../plugins/publish/extract_max_scene_raw.py | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index c14fcdbd0b..0f1f6f5b3b 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import get_all_children -class ExtractMaxSceneRaw(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Raw Max Scene with SaveSelected """ @@ -20,9 +13,7 @@ class ExtractMaxSceneRaw(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] - families = ["camera", - "maxScene", - "model"] + families = ["camera", "maxScene", "model"] optional = True def process(self, instance): @@ -37,26 +28,21 @@ class ExtractMaxSceneRaw(publish.Extractor, filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) - self.log.info("Writing max file '%s' to '%s'" % (filename, - max_path)) + self.log.info("Writing max file '%s' to '%s'" % (filename, max_path)) if "representations" not in instance.data: instance.data["representations"] = [] - # saving max scene - with maintained_selection(): - # need to figure out how to select the camera - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'saveNodes selection "{max_path}" quiet:true') + nodes = get_all_children(rt.getNodeByName(container)) + rt.saveNodes(nodes, max_path, quiet=True) self.log.info("Performing Extraction ...") representation = { - 'name': 'max', - 'ext': 'max', - 'files': filename, + "name": "max", + "ext": "max", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - max_path)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, max_path)) From d39fe870923383c62ea8995b578c9b3b82d8995a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 14:58:10 +0100 Subject: [PATCH 618/918] Fix abc loading in Blender --- .../hosts/blender/plugins/load/load_abc.py | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 1b2e800769..c1d73eff02 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -65,37 +65,19 @@ class CacheModelLoader(plugin.AssetLoader): imported = lib.get_selection() - empties = [obj for obj in imported if obj.type == 'EMPTY'] - - container = None - - for empty in empties: - if not empty.parent: - container = empty - break - - assert container, "No asset group found" - # Children must be linked before parents, # otherwise the hierarchy will break objects = [] - nodes = list(container.children) - for obj in nodes: + for obj in imported: obj.parent = asset_group - bpy.data.objects.remove(container) - - for obj in nodes: + for obj in imported: objects.append(obj) - nodes.extend(list(obj.children)) + imported.extend(list(obj.children)) objects.reverse() - for obj in objects: - parent.objects.link(obj) - collection.objects.unlink(obj) - for obj in objects: name = obj.name obj.name = f"{group_name}:{name}" @@ -138,13 +120,14 @@ class CacheModelLoader(plugin.AssetLoader): group_name = plugin.asset_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" - avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) - if not avalon_container: - avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) - bpy.context.scene.collection.children.link(avalon_container) + avalon_containers = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_containers: + avalon_containers = bpy.data.collections.new( + name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_containers) asset_group = bpy.data.objects.new(group_name, object_data=None) - avalon_container.objects.link(asset_group) + avalon_containers.objects.link(asset_group) objects = self._process(libpath, asset_group, group_name) From 67cd145ce2cca0b0979eb017813713159eb413ed Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:05:00 +0100 Subject: [PATCH 619/918] refactor: replaced rt.export string with proper pymxs implementation - black used for formatting - moved the general flow around as each function call is now seperate instead of large string --- .../max/plugins/publish/extract_camera_abc.py | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 8c23ff9878..3ca72abd88 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraAlembic(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport """ @@ -38,38 +31,28 @@ class ExtractCameraAlembic(publish.Extractor, path = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} -AlembicExport.CustomAttributes = true - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile(path, selectedOnly=True, using="AlembicExport", noPrompt=True) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - path)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) From 8f5b14ad243153953e273a686453a2b50ee4a329 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:13:13 +0100 Subject: [PATCH 620/918] refactor: replaced rt.execute with pymxs implementation --- .../max/plugins/publish/extract_camera_fbx.py | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 7e92f355ed..c216e726dc 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with FbxExporter """ @@ -33,43 +26,28 @@ class ExtractCameraFbx(publish.Extractor, filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing fbx file '%s' to '%s'" % (filename, - filepath)) + self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) - # Need to export: - # Animation = True - # Cameras = True - # AxisConversionMethod - fbx_export_cmd = ( - f""" - -FBXExporterSetParam "Animation" true -FBXExporterSetParam "Cameras" true -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {fbx_export_cmd}") + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(fbx_export_cmd) + rt.exportFile(filepath, selectedOnly=True, using="FBXEXP", noPrompt=True) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From 63c463f618cbc8a94053117b02461cc4d67ae838 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:20:59 +0100 Subject: [PATCH 621/918] refactor: replaced rt.execute with proper pymxs --- .../max/plugins/publish/extract_model.py | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 710ad5f97d..23fe59954c 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModel(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in Alembic Format """ @@ -36,39 +29,31 @@ class ExtractModel(publish.Extractor, filepath = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.CustomAttributes = true -AlembicExport.UVs = true -AlembicExport.VertexColors = true -AlembicExport.PreserveInstances = true - -exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + filepath, selectedOnly=True, using="AlembicExport", noPrompt=True + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From 6d9c0e30802db386476f5541732526abfa77265e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 15:46:34 +0100 Subject: [PATCH 622/918] Added setting for base file unit scale --- openpype/settings/defaults/project_settings/blender.json | 1 + .../schemas/projects_schema/schema_project_blender.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 20eec0c09d..0b3f38a40f 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,4 +1,5 @@ { + "base_file_unit_scale": 0.01, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 725d9bfb08..00414b3210 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -5,6 +5,12 @@ "label": "Blender", "is_file": true, "children": [ + { + "key": "base_file_unit_scale", + "type": "number", + "label": "Base File Unit Scale", + "decimal": 2 + }, { "key": "imageio", "type": "dict", From 3350e0995f43094a93a2e2a54abba7b52a7916fe Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 May 2023 15:47:38 +0100 Subject: [PATCH 623/918] Set base unit scale when opening a file or creating a new one --- openpype/hosts/blender/api/pipeline.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index c2aee1e653..02b1560e56 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -26,6 +26,8 @@ from openpype.lib import ( emit_event ) import openpype.hosts.blender +from openpype.settings import get_project_settings + HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__)) PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") @@ -122,12 +124,23 @@ def set_start_end_frames(): scene.render.resolution_y = resolution_y +def set_base_file_unit_scale(): + project = os.environ.get("AVALON_PROJECT") + settings = get_project_settings(project) + + unit_scale = settings.get("blender").get("base_file_unit_scale") + + bpy.context.scene.unit_settings.scale_length = unit_scale + + def on_new(): set_start_end_frames() + set_base_file_unit_scale() def on_open(): set_start_end_frames() + set_base_file_unit_scale() @bpy.app.handlers.persistent From 28078c0508598f7413dd9851a35f3d56f3ce05a1 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:48:55 +0100 Subject: [PATCH 624/918] refactor: replaced rt.execute where possible --- .../max/plugins/publish/extract_model_fbx.py | 55 +++++++------------ 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index ce58e8cc17..e2bbac4ac2 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in FBX Format """ @@ -33,42 +26,32 @@ class ExtractModelFbx(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing FBX '%s' to '%s'" % (filepath, - stagingdir)) - - export_fbx_cmd = ( - f""" -FBXExporterSetParam "Animation" false -FBXExporterSetParam "Cameras" false -FBXExporterSetParam "Lights" false -FBXExporterSetParam "PointCache" false -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {export_fbx_cmd}") + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) with maintained_selection(): + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_fbx_cmd) + rt.execute( + f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP' + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From 1d7edf1fb2982353a2be3ea3405b20c1fb9479a4 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:00:17 +0100 Subject: [PATCH 625/918] fix: pymxs terrible argument handling - noPrompt doesn't seem to work unless you call rt.name and is also positional - using doesn't work as a string you need to feed it the actual rt object --- .../hosts/max/plugins/publish/extract_model.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 23fe59954c..56e791d2e7 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -31,18 +31,17 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.CustomAttributes = True - rt.AlembicExport.UVs = True - rt.AlembicExport.VertexColors = True - rt.AlembicExport.PreserveInstances = True - with maintained_selection(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, selectedOnly=True, using="AlembicExport", noPrompt=True + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport ) self.log.info("Performing Extraction ...") From b3b07cec7cc772e15bdf03510cb8c1ba2808a5e9 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:04:38 +0100 Subject: [PATCH 626/918] fix: exportFile to use correct arguments --- .../max/plugins/publish/extract_camera_abc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 3ca72abd88..db96470f17 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -33,16 +33,17 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - rt.AlembicExport.CustomAttributes = True - with maintained_selection(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.exportFile(path, selectedOnly=True, using="AlembicExport", noPrompt=True) + rt.exportFile( + path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: From 4f2951b6ec64bacdaec96e20693436aecbd89dad Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:07:12 +0100 Subject: [PATCH 627/918] fix: rt.exportfile args --- .../max/plugins/publish/extract_camera_fbx.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index c216e726dc..16dea0b41e 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -28,16 +28,17 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) - rt.FBXExporterSetParam("Animation", True) - rt.FBXExporterSetParam("Cameras", True) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) - with maintained_selection(): + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.exportFile(filepath, selectedOnly=True, using="FBXEXP", noPrompt=True) + rt.exportFile( + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: From 069bcba72f459245fecb025870f92537a4e6b1f7 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:16:57 +0100 Subject: [PATCH 628/918] fix: exportfile args --- .../max/plugins/publish/extract_model_obj.py | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 7bda237880..3d98f37263 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelObj(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in OBJ Format """ @@ -33,27 +26,26 @@ class ExtractModelObj(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing OBJ '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing OBJ '%s' to '%s'" % (filepath, stagingdir)) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa + rt.exportFile( + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.ObjExp + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'obj', - 'ext': 'obj', - 'files': filename, + "name": "obj", + "ext": "obj", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From cc23df7a13cf86074e28fcdcab0559e2506e3012 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:17:11 +0100 Subject: [PATCH 629/918] refactor: replaced rt.execute with proper function --- openpype/hosts/max/plugins/publish/extract_model_fbx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index e2bbac4ac2..0ffec94a59 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -39,8 +39,8 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute( - f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP' + rt.exportFile( + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP ) self.log.info("Performing Extraction ...") From dc39fafffd0cd0a5bb3940e9115e7d35e28eaec6 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:23:32 +0100 Subject: [PATCH 630/918] refactor: removed use of rt.execute and replaced with pymxs --- .../max/plugins/publish/extract_pointcache.py | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 75d8a7972c..0936a149f3 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -41,10 +41,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children class ExtractAlembic(publish.Extractor): @@ -66,35 +63,26 @@ class ExtractAlembic(publish.Extractor): path = os.path.join(parent_dir, file_name) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (file_name, - parent_dir)) - - abc_export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {abc_export_cmd}") + self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) with maintained_selection(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(abc_export_cmd) + rt.exportFile( + path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + ) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': file_name, + "name": "abc", + "ext": "abc", + "files": file_name, "stagingDir": parent_dir, } instance.data["representations"].append(representation) From b229b992a2a8dcbaa9865be2b3c423d757c30fbb Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:29:04 +0100 Subject: [PATCH 631/918] refactor: removed configs from maintained_selection() block --- .../max/plugins/publish/extract_camera_abc.py | 11 ++++++----- .../max/plugins/publish/extract_camera_fbx.py | 11 ++++++----- .../hosts/max/plugins/publish/extract_model.py | 13 +++++++------ .../max/plugins/publish/extract_model_fbx.py | 15 ++++++++------- .../max/plugins/publish/extract_pointcache.py | 9 +++++---- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index db96470f17..32d9ab9317 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -33,12 +33,13 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True + with maintained_selection(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - rt.AlembicExport.CustomAttributes = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 16dea0b41e..f865f7ac6a 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -28,12 +28,13 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) + with maintained_selection(): - rt.FBXExporterSetParam("Animation", True) - rt.FBXExporterSetParam("Cameras", True) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 56e791d2e7..d4d59df29c 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -31,13 +31,14 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True + with maintained_selection(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.CustomAttributes = True - rt.AlembicExport.UVs = True - rt.AlembicExport.VertexColors = True - rt.AlembicExport.PreserveInstances = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index 0ffec94a59..eefe5e7e72 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -29,14 +29,15 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) + with maintained_selection(): - rt.FBXExporterSetParam("Animation", False) - rt.FBXExporterSetParam("Cameras", False) - rt.FBXExporterSetParam("Lights", False) - rt.FBXExporterSetParam("PointCache", False) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 0936a149f3..84352b489e 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -65,11 +65,12 @@ class ExtractAlembic(publish.Extractor): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + with maintained_selection(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( From 4a128cc59b0f974181805dac69252fa4c559e916 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 18:06:33 +0100 Subject: [PATCH 632/918] refactor: updated black to be 79 charlines --- openpype/hosts/max/plugins/publish/extract_camera_abc.py | 5 ++++- openpype/hosts/max/plugins/publish/extract_camera_fbx.py | 9 +++++++-- .../hosts/max/plugins/publish/extract_max_scene_raw.py | 4 +++- openpype/hosts/max/plugins/publish/extract_model.py | 9 +++++++-- openpype/hosts/max/plugins/publish/extract_model_fbx.py | 9 +++++++-- openpype/hosts/max/plugins/publish/extract_model_obj.py | 9 +++++++-- openpype/hosts/max/plugins/publish/extract_pointcache.py | 5 ++++- 7 files changed, 39 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 32d9ab9317..6b3bb178a3 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -43,7 +43,10 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, ) self.log.info("Performing Extraction ...") diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index f865f7ac6a..4b4b349e19 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -38,7 +38,10 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, ) self.log.info("Performing Extraction ...") @@ -52,4 +55,6 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index 0f1f6f5b3b..f0c2aff7f3 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -45,4 +45,6 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, max_path)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, max_path) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index d4d59df29c..4c7c98e2cc 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -42,7 +42,10 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, ) self.log.info("Performing Extraction ...") @@ -56,4 +59,6 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index eefe5e7e72..e6ccb24cdd 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -41,7 +41,10 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, ) self.log.info("Performing Extraction ...") @@ -55,4 +58,6 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 3d98f37263..ed3d68c990 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -33,7 +33,10 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.ObjExp + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.ObjExp, ) self.log.info("Performing Extraction ...") @@ -48,4 +51,6 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 84352b489e..8658cecb1b 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -74,7 +74,10 @@ class ExtractAlembic(publish.Extractor): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, ) if "representations" not in instance.data: From 9ea2bb0c4d039636fd27142c12a7c0e87119d6cd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 May 2023 15:24:19 +0800 Subject: [PATCH 633/918] use pymxs instead of maxscript for fbx loader --- .../hosts/max/plugins/load/load_camera_fbx.py | 24 +++++++------------ .../hosts/max/plugins/load/load_model_fbx.py | 22 +++++++---------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index ce4dec32a0..9767321e3d 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -20,21 +20,15 @@ class FbxLoader(load.LoaderPlugin): from pymxs import runtime as rt filepath = os.path.normpath(self.fname) - - fbx_import_cmd = ( - f""" - -FBXImporterSetParam "Animation" true -FBXImporterSetParam "Cameras" true -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -importFile @"{filepath}" #noPrompt using:FBXIMP - """) - - self.log.debug(f"Executing command: {fbx_import_cmd}") - rt.execute(fbx_import_cmd) + rt.FBXImporterSetParam("Animation", True) + rt.FBXImporterSetParam("Camera", True) + rt.FBXImporterSetParam("AxisConversionMethod", True) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile( + filepath, + rt.name("noPrompt"), + using=rt.FBXIMP) container = rt.getNodeByName(f"{name}") if not container: diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 7532e3a8a0..bf51e3b191 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -21,21 +21,15 @@ class FbxModelLoader(load.LoaderPlugin): from pymxs import runtime as rt filepath = os.path.normpath(self.fname) + rt.FBXImporterSetParam("Animation", False) + rt.FBXImporterSetParam("Cameras", False) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile( + filepath, + rt.name("noPrompt"), + using=rt.FBXIMP) - fbx_import_cmd = ( - f""" - -FBXImporterSetParam "Animation" false -FBXImporterSetParam "Cameras" false -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -importFile @"{filepath}" #noPrompt using:FBXIMP - """) - - self.log.debug(f"Executing command: {fbx_import_cmd}") - rt.execute(fbx_import_cmd) container = rt.getNodeByName(f"{name}") if not container: container = rt.container() From 6fd4cbf2998f6d486858d01472d6385c8f2bc809 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 18 May 2023 16:47:58 +0800 Subject: [PATCH 634/918] fix the axis conversion issue --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 1 - openpype/hosts/max/plugins/load/load_model_fbx.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 9767321e3d..0c5dd762cf 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -23,7 +23,6 @@ class FbxLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Animation", True) rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("AxisConversionMethod", True) - rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile( filepath, diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index bf51e3b191..01e6acae12 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -23,7 +23,6 @@ class FbxModelLoader(load.LoaderPlugin): filepath = os.path.normpath(self.fname) rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) - rt.FBXImporterSetParam("UpAxis", "Y") rt.FBXImporterSetParam("Preserveinstances", True) rt.importFile( filepath, From 60efa939a86196749a1dab1f5afe2fbdc01991dc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 18 May 2023 10:59:13 +0200 Subject: [PATCH 635/918] OP-5714 - allow returning stub with not saved workfile (#4984) Without it is not possible to create first workile. --- openpype/hosts/aftereffects/api/pipeline.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 95f6f3235b..020022e263 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -60,9 +60,6 @@ class AfterEffectsHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): print("Not connected yet, ignoring") return - if not stub.get_active_document_name(): - return - self._stub = stub return self._stub From d95299a31b1d8aae602ac06993f57ce14dd397ee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 10:37:38 +0100 Subject: [PATCH 636/918] Added settings to make optional the setting the unit scale --- openpype/hosts/blender/api/pipeline.py | 27 ++++++++++++++---- .../defaults/project_settings/blender.json | 6 +++- .../schema_project_blender.json | 28 ++++++++++++++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 02b1560e56..3618c1f4c8 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -124,19 +124,34 @@ def set_start_end_frames(): scene.render.resolution_y = resolution_y -def set_base_file_unit_scale(): +def on_new(): + set_start_end_frames() + project = os.environ.get("AVALON_PROJECT") settings = get_project_settings(project) - unit_scale = settings.get("blender").get("base_file_unit_scale") - - bpy.context.scene.unit_settings.scale_length = unit_scale + unit_scale_settings = settings.get("blender").get("unit_scale_settings") + unit_scale_enabled = unit_scale_settings.get("enabled") + if unit_scale_enabled: + unit_scale = unit_scale_settings.get("base_file_unit_scale") + bpy.context.scene.unit_settings.scale_length = unit_scale -def on_new(): +def on_open(): set_start_end_frames() - set_base_file_unit_scale() + project = os.environ.get("AVALON_PROJECT") + settings = get_project_settings(project) + + unit_scale_settings = settings.get("blender").get("unit_scale_settings") + unit_scale_enabled = unit_scale_settings.get("enabled") + apply_on_opening = unit_scale_settings.get("apply_on_opening") + if unit_scale_enabled and apply_on_opening: + unit_scale = unit_scale_settings.get("base_file_unit_scale") + prev_unit_scale = bpy.context.scene.unit_settings.scale_length + + if unit_scale != prev_unit_scale: + bpy.context.scene.unit_settings.scale_length = unit_scale def on_open(): set_start_end_frames() diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 0b3f38a40f..41aebfa537 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -1,5 +1,9 @@ { - "base_file_unit_scale": 0.01, + "unit_scale_settings": { + "enabled": true, + "apply_on_opening": false, + "base_file_unit_scale": 0.01 + }, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 00414b3210..0d0952a70a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -6,10 +6,30 @@ "is_file": true, "children": [ { - "key": "base_file_unit_scale", - "type": "number", - "label": "Base File Unit Scale", - "decimal": 2 + "key": "unit_scale_settings", + "type": "dict", + "label": "Set Unit Scale", + "collapsible": true, + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "key": "apply_on_opening", + "type": "boolean", + "label": "Apply on Opening Existing Files" + }, + { + "key": "base_file_unit_scale", + "type": "number", + "label": "Base File Unit Scale", + "decimal": 2 + } + ] }, { "key": "imageio", From 0d4ae4efa7668ad5b5dc2a5a991b3e5cfcd92bf3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 10:38:22 +0100 Subject: [PATCH 637/918] Added message if base scale has been changed when opening a file --- openpype/hosts/blender/api/pipeline.py | 31 +++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 3618c1f4c8..9cc557c01a 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -85,6 +85,31 @@ def uninstall(): ops.unregister() +def show_message(title, message): + from openpype.widgets.message_window import Window + from .ops import BlenderApplication + + BlenderApplication.get_app() + + Window( + parent=None, + title=title, + message=message, + level="warning") + + +def message_window(title, message): + from .ops import ( + MainThreadItem, + execute_in_main_thread, + _process_app_events + ) + + mti = MainThreadItem(show_message, title, message) + execute_in_main_thread(mti) + _process_app_events() + + def set_start_end_frames(): project_name = legacy_io.active_project() asset_name = legacy_io.Session["AVALON_ASSET"] @@ -153,9 +178,9 @@ def on_open(): if unit_scale != prev_unit_scale: bpy.context.scene.unit_settings.scale_length = unit_scale -def on_open(): - set_start_end_frames() - set_base_file_unit_scale() + message_window( + "Base file unit scale changed", + "Base file unit scale changed to match the project settings.") @bpy.app.handlers.persistent From 87802d06203ad1cdad22d2aec5112fba1a04f0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 18 May 2023 14:39:27 +0200 Subject: [PATCH 638/918] Fix: Download last workfile doesn't work if wf not already downloaded (#4942) --- openpype/modules/sync_server/sync_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/sync_server.py b/openpype/modules/sync_server/sync_server.py index d1d5c2863d..98065b68a0 100644 --- a/openpype/modules/sync_server/sync_server.py +++ b/openpype/modules/sync_server/sync_server.py @@ -237,8 +237,7 @@ def download_last_published_workfile( last_published_workfile_path = get_representation_path_with_anatomy( workfile_representation, anatomy ) - if (not last_published_workfile_path or - not os.path.exists(last_published_workfile_path)): + if not last_published_workfile_path: return # If representation isn't available on remote site, then return. From 8b68371e0c399b6e8b80f5300a77223bfefc7007 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 14:33:26 +0100 Subject: [PATCH 639/918] Increased the number of decimals for the unit scale --- .../schemas/projects_schema/schema_project_blender.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 0d0952a70a..5b40169872 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -27,7 +27,7 @@ "key": "base_file_unit_scale", "type": "number", "label": "Base File Unit Scale", - "decimal": 2 + "decimal": 10 } ] }, From ec7c172fb2ec82d43ae014381829275581cf0ed2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 May 2023 15:26:12 +0100 Subject: [PATCH 640/918] Implement loading of abc camera --- .../blender/plugins/load/load_camera_abc.py | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 openpype/hosts/blender/plugins/load/load_camera_abc.py diff --git a/openpype/hosts/blender/plugins/load/load_camera_abc.py b/openpype/hosts/blender/plugins/load/load_camera_abc.py new file mode 100644 index 0000000000..21b48f409f --- /dev/null +++ b/openpype/hosts/blender/plugins/load/load_camera_abc.py @@ -0,0 +1,209 @@ +"""Load an asset in Blender from an Alembic file.""" + +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import bpy + +from openpype.pipeline import ( + get_representation_path, + AVALON_CONTAINER_ID, +) +from openpype.hosts.blender.api import plugin, lib +from openpype.hosts.blender.api.pipeline import ( + AVALON_CONTAINERS, + AVALON_PROPERTY, +) + + +class AbcCameraLoader(plugin.AssetLoader): + """Load a camera from Alembic file. + + Stores the imported asset in an empty named after the asset. + """ + + families = ["camera"] + representations = ["abc"] + + label = "Load Camera (ABC)" + icon = "code-fork" + color = "orange" + + def _remove(self, asset_group): + objects = list(asset_group.children) + + for obj in objects: + if obj.type == "CAMERA": + bpy.data.cameras.remove(obj.data) + elif obj.type == "EMPTY": + objects.extend(obj.children) + bpy.data.objects.remove(obj) + + def _process(self, libpath, asset_group, group_name): + plugin.deselect_all() + + bpy.ops.wm.alembic_import(filepath=libpath) + + objects = lib.get_selection() + + for obj in objects: + obj.parent = asset_group + + for obj in objects: + name = obj.name + obj.name = f"{group_name}:{name}" + if obj.type != "EMPTY": + name_data = obj.data.name + obj.data.name = f"{group_name}:{name_data}" + + if not obj.get(AVALON_PROPERTY): + obj[AVALON_PROPERTY] = dict() + + avalon_info = obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + plugin.deselect_all() + + return objects + + def process_asset( + self, + context: dict, + name: str, + namespace: Optional[str] = None, + options: Optional[Dict] = None, + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) + namespace = namespace or f"{asset}_{unique_number}" + + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) + + asset_group = bpy.data.objects.new(group_name, object_data=None) + avalon_container.objects.link(asset_group) + + objects = self._process(libpath, asset_group, group_name) + + objects = [] + nodes = list(asset_group.children) + + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) + + bpy.context.scene.collection.objects.link(asset_group) + + asset_group[AVALON_PROPERTY] = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or "", + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"], + "objectName": group_name, + } + + self[:] = objects + return objects + + def exec_update(self, container: Dict, representation: Dict): + """Update the loaded asset. + + This will remove all objects of the current collection, load the new + ones and add them to the collection. + If the objects of the collection are used in another collection they + will not be removed, only unlinked. Normally this should not be the + case though. + + Warning: + No nested collections are supported at the moment! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + libpath = Path(get_representation_path(representation)) + extension = libpath.suffix.lower() + + self.log.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert asset_group, ( + f"The asset is not loaded: {container['objectName']}") + assert libpath, ( + f"No existing library file found for {container['objectName']}") + assert libpath.is_file(), f"The file doesn't exist: {libpath}" + assert extension in plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}") + + metadata = asset_group.get(AVALON_PROPERTY) + group_libpath = metadata["libpath"] + + normalized_group_libpath = str( + Path(bpy.path.abspath(group_libpath)).resolve()) + normalized_libpath = str( + Path(bpy.path.abspath(str(libpath))).resolve()) + self.log.debug( + "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_group_libpath, + normalized_libpath, + ) + if normalized_group_libpath == normalized_libpath: + self.log.info("Library already loaded, not updating...") + return + + mat = asset_group.matrix_basis.copy() + + self._remove(asset_group) + self._process(str(libpath), asset_group, object_name) + + asset_group.matrix_basis = mat + + metadata["libpath"] = str(libpath) + metadata["representation"] = str(representation["_id"]) + + def exec_remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (openpype:container-1.0): Container to remove, + from `host.ls()`. + + Returns: + bool: Whether the container was deleted. + + Warning: + No nested collections are supported at the moment! + """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + + if not asset_group: + return False + + self._remove(asset_group) + + bpy.data.objects.remove(asset_group) + + return True From 51bba73ebe1801ae1c9443307277d6e0156783a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 May 2023 17:13:53 +0200 Subject: [PATCH 641/918] Unreal: Addon Py2 compatibility (#4994) * fix python 2 compatibility of unreal addon * added a comment --- openpype/hosts/unreal/addon.py | 9 +++++---- openpype/hosts/unreal/lib.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 9ded333d7d..1119b5c16c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,8 +1,5 @@ import os -from pathlib import Path - from openpype.modules import IHostAddon, OpenPypeModule -from .lib import get_compatible_integration UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -17,10 +14,14 @@ class UnrealAddon(OpenPypeModule, IHostAddon): def add_implementation_envs(self, env, app): """Modify environments to contain all required for implementation.""" # Set AYON_UNREAL_PLUGIN required for Unreal implementation + # Imports are in this method for Python 2 compatiblity of an addon + from pathlib import Path + + from .lib import get_compatible_integration ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", f"UE_{ue_version}", "Ayon" + UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" ) if not Path(unreal_plugin_path).exists(): compatible_versions = get_compatible_integration( diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 821b4daecc..97771472cf 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -7,7 +7,6 @@ import json from typing import List -import openpype from distutils import dir_util import subprocess import re From 8de85e94a7e42cf12b26b6d813dc375bfb0485b5 Mon Sep 17 00:00:00 2001 From: Sponge96 Date: Fri, 19 May 2023 16:15:09 +0100 Subject: [PATCH 642/918] 3dsmax: Refactored publish plugins to use proper implementation of pymxs (#4988) * refactor: rt.execute(saveNodes) replaces with pymxs function - changed the function to no longer use the selection and instead feed it the nodes directly from get_all_children function. - removed maintained_seclection() as we're no longer overriding the selection of the Max scene. - black also used to format. * refactor: replaced rt.export string with proper pymxs implementation - black used for formatting - moved the general flow around as each function call is now seperate instead of large string * refactor: replaced rt.execute with pymxs implementation * refactor: replaced rt.execute with proper pymxs * refactor: replaced rt.execute where possible * fix: pymxs terrible argument handling - noPrompt doesn't seem to work unless you call rt.name and is also positional - using doesn't work as a string you need to feed it the actual rt object * fix: exportFile to use correct arguments * fix: rt.exportfile args * fix: exportfile args * refactor: replaced rt.execute with proper function * refactor: removed use of rt.execute and replaced with pymxs * refactor: removed configs from maintained_selection() block * refactor: updated black to be 79 charlines --- .../max/plugins/publish/extract_camera_abc.py | 50 ++++++---------- .../max/plugins/publish/extract_camera_fbx.py | 57 +++++++----------- .../plugins/publish/extract_max_scene_raw.py | 38 ++++-------- .../max/plugins/publish/extract_model.py | 54 +++++++---------- .../max/plugins/publish/extract_model_fbx.py | 59 ++++++++----------- .../max/plugins/publish/extract_model_obj.py | 37 ++++++------ .../max/plugins/publish/extract_pointcache.py | 38 +++++------- 7 files changed, 131 insertions(+), 202 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 8c23ff9878..6b3bb178a3 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraAlembic(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport """ @@ -38,38 +31,33 @@ class ExtractCameraAlembic(publish.Extractor, path = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} -AlembicExport.CustomAttributes = true - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - path)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 7e92f355ed..4b4b349e19 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with FbxExporter """ @@ -33,43 +26,35 @@ class ExtractCameraFbx(publish.Extractor, filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing fbx file '%s' to '%s'" % (filename, - filepath)) + self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) - # Need to export: - # Animation = True - # Cameras = True - # AxisConversionMethod - fbx_export_cmd = ( - f""" - -FBXExporterSetParam "Animation" true -FBXExporterSetParam "Cameras" true -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {fbx_export_cmd}") + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(fbx_export_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index c14fcdbd0b..f0c2aff7f3 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import get_all_children -class ExtractMaxSceneRaw(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Raw Max Scene with SaveSelected """ @@ -20,9 +13,7 @@ class ExtractMaxSceneRaw(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] - families = ["camera", - "maxScene", - "model"] + families = ["camera", "maxScene", "model"] optional = True def process(self, instance): @@ -37,26 +28,23 @@ class ExtractMaxSceneRaw(publish.Extractor, filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) - self.log.info("Writing max file '%s' to '%s'" % (filename, - max_path)) + self.log.info("Writing max file '%s' to '%s'" % (filename, max_path)) if "representations" not in instance.data: instance.data["representations"] = [] - # saving max scene - with maintained_selection(): - # need to figure out how to select the camera - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'saveNodes selection "{max_path}" quiet:true') + nodes = get_all_children(rt.getNodeByName(container)) + rt.saveNodes(nodes, max_path, quiet=True) self.log.info("Performing Extraction ...") representation = { - 'name': 'max', - 'ext': 'max', - 'files': filename, + "name": "max", + "ext": "max", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - max_path)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, max_path) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 710ad5f97d..4c7c98e2cc 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModel(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in Alembic Format """ @@ -36,39 +29,36 @@ class ExtractModel(publish.Extractor, filepath = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.CustomAttributes = true -AlembicExport.UVs = true -AlembicExport.VertexColors = true -AlembicExport.PreserveInstances = true - -exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index ce58e8cc17..e6ccb24cdd 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in FBX Format """ @@ -33,42 +26,38 @@ class ExtractModelFbx(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing FBX '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) - export_fbx_cmd = ( - f""" -FBXExporterSetParam "Animation" false -FBXExporterSetParam "Cameras" false -FBXExporterSetParam "Lights" false -FBXExporterSetParam "PointCache" false -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {export_fbx_cmd}") + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_fbx_cmd) + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 7bda237880..ed3d68c990 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelObj(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in OBJ Format """ @@ -33,27 +26,31 @@ class ExtractModelObj(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing OBJ '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing OBJ '%s' to '%s'" % (filepath, stagingdir)) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa + rt.exportFile( + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.ObjExp, + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'obj', - 'ext': 'obj', - 'files': filename, + "name": "obj", + "ext": "obj", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 75d8a7972c..8658cecb1b 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -41,10 +41,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children class ExtractAlembic(publish.Extractor): @@ -66,35 +63,30 @@ class ExtractAlembic(publish.Extractor): path = os.path.join(parent_dir, file_name) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (file_name, - parent_dir)) + self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) - abc_export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {abc_export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(abc_export_cmd) + rt.exportFile( + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, + ) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': file_name, + "name": "abc", + "ext": "abc", + "files": file_name, "stagingDir": parent_dir, } instance.data["representations"].append(representation) From 2e7b0b9d954c606220f8ad89fdf86549dc38e3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 19 May 2023 18:20:03 +0200 Subject: [PATCH 643/918] fix raise --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 2d1b773c5f..525905f1ab 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -24,7 +24,7 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): # Test script path exists python_script_path = Path(python_script_path) if not python_script_path.exists(): - raise self.log.warning( + self.log.warning( f"Python script {python_script_path} doesn't exist. " "Skipped..." ) From eea816f0353b4dbcfed745061d83f61eb08cbcb2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 20 May 2023 03:25:02 +0000 Subject: [PATCH 644/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 954cfa945b..8874eb510d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.1" +__version__ = "3.15.8-nightly.2" From 0b8400bc8e5785cc23239bc432b10a7cc3de4f4c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 May 2023 03:25:46 +0000 Subject: [PATCH 645/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0f58d61881..244eb1a363 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.2 - 3.15.8-nightly.1 - 3.15.7 - 3.15.7-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.2 - 3.14.2-nightly.1 - 3.14.1 - - 3.14.1-nightly.4 validations: required: true - type: dropdown From dc7373408f8d586f854b99da7c9eb799441a3fec Mon Sep 17 00:00:00 2001 From: Felix David Date: Mon, 22 May 2023 10:39:30 +0200 Subject: [PATCH 646/918] continue if not python script path --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 525905f1ab..559e9ae0ce 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -28,6 +28,7 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): f"Python script {python_script_path} doesn't exist. " "Skipped..." ) + continue if "--" in self.launch_context.launch_args: # Insert before separator From e178244d46401e58fa398501e584e3a6360c0200 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Mon, 22 May 2023 10:43:11 +0200 Subject: [PATCH 647/918] Fusion - Loader plugins updates (#4920) * Added get_bmd_library to acces BMD's internal python library * Added the option to import image and online families. + black formatted * Added workfile loader To import the content of another workfile into your current comp * Fixed wrong family and extension in workfile loader * black formatting * Added missing formats to fbx importer Fusions fbx importer can import a bunch of different formats other then fbx (confusing I know but it's how it is) * Update openpype/hosts/fusion/plugins/load/load_workfile.py Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/api/__init__.py | 1 + openpype/hosts/fusion/api/lib.py | 6 ++ .../hosts/fusion/plugins/load/load_fbx.py | 34 +++++-- .../fusion/plugins/load/load_sequence.py | 92 +++++++++++-------- .../fusion/plugins/load/load_workfile.py | 32 +++++++ 5 files changed, 118 insertions(+), 47 deletions(-) create mode 100644 openpype/hosts/fusion/plugins/load/load_workfile.py diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index 495fe286d5..dba55a98d9 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -13,6 +13,7 @@ from .lib import ( update_frame_range, set_asset_framerange, get_current_comp, + get_bmd_library, comp_lock_and_undo_chunk ) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 40cc4d2963..c33209823e 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -309,6 +309,12 @@ def get_fusion_module(): return fusion +def get_bmd_library(): + """Get bmd library""" + bmd = getattr(sys.modules["__main__"], "bmd", None) + return bmd + + def get_current_comp(): """Get current comp in this session""" fusion = get_fusion_module() diff --git a/openpype/hosts/fusion/plugins/load/load_fbx.py b/openpype/hosts/fusion/plugins/load/load_fbx.py index b8f501ae7e..c73ad78394 100644 --- a/openpype/hosts/fusion/plugins/load/load_fbx.py +++ b/openpype/hosts/fusion/plugins/load/load_fbx.py @@ -1,4 +1,3 @@ - from openpype.pipeline import ( load, get_representation_path, @@ -6,7 +5,7 @@ from openpype.pipeline import ( from openpype.hosts.fusion.api import ( imprint_container, get_current_comp, - comp_lock_and_undo_chunk + comp_lock_and_undo_chunk, ) @@ -15,7 +14,21 @@ class FusionLoadFBXMesh(load.LoaderPlugin): families = ["*"] representations = ["*"] - extensions = {"fbx"} + extensions = { + "3ds", + "amc", + "aoa", + "asf", + "bvh", + "c3d", + "dae", + "dxf", + "fbx", + "htr", + "mcd", + "obj", + "trc", + } label = "Load FBX mesh" order = -10 @@ -27,23 +40,24 @@ class FusionLoadFBXMesh(load.LoaderPlugin): def load(self, context, name, namespace, data): # Fallback to asset name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["asset"]["name"] # Create the Loader with the filename path set comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create tool"): - path = self.fname args = (-32768, -32768) tool = comp.AddTool(self.tool_type, *args) tool["ImportFile"] = path - imprint_container(tool, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__) + imprint_container( + tool, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + ) def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 38fd41c8b2..552e282587 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -3,17 +3,14 @@ import contextlib import openpype.pipeline.load as load from openpype.pipeline.load import ( get_representation_context, - get_representation_path_from_context + get_representation_path_from_context, ) from openpype.hosts.fusion.api import ( imprint_container, get_current_comp, - comp_lock_and_undo_chunk -) -from openpype.lib.transcoding import ( - IMAGE_EXTENSIONS, - VIDEO_EXTENSIONS + comp_lock_and_undo_chunk, ) +from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS comp = get_current_comp() @@ -57,20 +54,23 @@ def preserve_trim(loader, log=None): try: yield finally: - length = loader.GetAttrs()["TOOLIT_Clip_Length"][1] - 1 if trim_from_start > length: trim_from_start = length if log: - log.warning("Reducing trim in to %d " - "(because of less frames)" % trim_from_start) + log.warning( + "Reducing trim in to %d " + "(because of less frames)" % trim_from_start + ) remainder = length - trim_from_start if trim_from_end > remainder: trim_from_end = remainder if log: - log.warning("Reducing trim in to %d " - "(because of less frames)" % trim_from_end) + log.warning( + "Reducing trim in to %d " + "(because of less frames)" % trim_from_end + ) loader["ClipTimeStart"][time] = trim_from_start loader["ClipTimeEnd"][time] = length - trim_from_end @@ -109,11 +109,15 @@ def loader_shift(loader, frame, relative=True): # Shifting global in will try to automatically compensate for the change # in the "ClipTimeStart" and "HoldFirstFrame" inputs, so we preserve those # input values to "just shift" the clip - with preserve_inputs(loader, inputs=["ClipTimeStart", - "ClipTimeEnd", - "HoldFirstFrame", - "HoldLastFrame"]): - + with preserve_inputs( + loader, + inputs=[ + "ClipTimeStart", + "ClipTimeEnd", + "HoldFirstFrame", + "HoldLastFrame", + ], + ): # GlobalIn cannot be set past GlobalOut or vice versa # so we must apply them in the order of the shift. if shift > 0: @@ -129,7 +133,14 @@ def loader_shift(loader, frame, relative=True): class FusionLoadSequence(load.LoaderPlugin): """Load image sequence into Fusion""" - families = ["imagesequence", "review", "render", "plate"] + families = [ + "imagesequence", + "review", + "render", + "plate", + "image", + "onilne", + ] representations = ["*"] extensions = set( ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) @@ -143,7 +154,7 @@ class FusionLoadSequence(load.LoaderPlugin): def load(self, context, name, namespace, data): # Fallback to asset name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["asset"]["name"] # Use the first file for now path = get_representation_path_from_context(context) @@ -151,7 +162,6 @@ class FusionLoadSequence(load.LoaderPlugin): # Create the Loader with the filename path set comp = get_current_comp() with comp_lock_and_undo_chunk(comp, "Create Loader"): - args = (-32768, -32768) tool = comp.AddTool("Loader", *args) tool["Clip"] = path @@ -160,11 +170,13 @@ class FusionLoadSequence(load.LoaderPlugin): start = self._get_start(context["version"], tool) loader_shift(tool, start, relative=False) - imprint_container(tool, - name=name, - namespace=namespace, - context=context, - loader=self.__class__.__name__) + imprint_container( + tool, + name=name, + namespace=namespace, + context=context, + loader=self.__class__.__name__, + ) def switch(self, container, representation): self.update(container, representation) @@ -222,24 +234,28 @@ class FusionLoadSequence(load.LoaderPlugin): start = self._get_start(context["version"], tool) with comp_lock_and_undo_chunk(comp, "Update Loader"): - # Update the loader's path whilst preserving some values with preserve_trim(tool, log=self.log): - with preserve_inputs(tool, - inputs=("HoldFirstFrame", - "HoldLastFrame", - "Reverse", - "Depth", - "KeyCode", - "TimeCodeOffset")): + with preserve_inputs( + tool, + inputs=( + "HoldFirstFrame", + "HoldLastFrame", + "Reverse", + "Depth", + "KeyCode", + "TimeCodeOffset", + ), + ): tool["Clip"] = path # Set the global in to the start frame of the sequence global_in_changed = loader_shift(tool, start, relative=False) if global_in_changed: # Log this change to the user - self.log.debug("Changed '%s' global in: %d" % (tool.Name, - start)) + self.log.debug( + "Changed '%s' global in: %d" % (tool.Name, start) + ) # Update the imprinted representation tool.SetData("avalon.representation", str(representation["_id"])) @@ -264,9 +280,11 @@ class FusionLoadSequence(load.LoaderPlugin): # Get frame start without handles start = data.get("frameStart") if start is None: - self.log.warning("Missing start frame for version " - "assuming starts at frame 0 for: " - "{}".format(tool.Name)) + self.log.warning( + "Missing start frame for version " + "assuming starts at frame 0 for: " + "{}".format(tool.Name) + ) return 0 # Use `handleStart` if the data is available diff --git a/openpype/hosts/fusion/plugins/load/load_workfile.py b/openpype/hosts/fusion/plugins/load/load_workfile.py new file mode 100644 index 0000000000..b49d104a15 --- /dev/null +++ b/openpype/hosts/fusion/plugins/load/load_workfile.py @@ -0,0 +1,32 @@ +"""Import workfiles into your current comp. +As all imported nodes are free floating and will probably be changed there +is no update or reload function added for this plugin +""" + +from openpype.pipeline import load + +from openpype.hosts.fusion.api import ( + get_current_comp, + get_bmd_library, +) + + +class FusionLoadWorkfile(load.LoaderPlugin): + """Load the content of a workfile into Fusion""" + + families = ["workfile"] + representations = ["*"] + extensions = {"comp"} + + label = "Load Workfile" + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name, namespace, data): + # Get needed elements + bmd = get_bmd_library() + comp = get_current_comp() + + # Paste the content of the file into the current comp + comp.Paste(bmd.readfile(self.fname)) From 136af34a7189e78fd8549ffe029285283b7997c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 22 May 2023 10:45:20 +0200 Subject: [PATCH 648/918] AfterEffects: set frame range and resolution (#4983) * OP-5660 - adding menu buttons to Set frame range in AE * OP-5660 - refactored location of scripts set_settings should be in lib as it is used elsewhere, but launch_logic and lib created circular dependency. Moved main to launch logic as it is actually handling launching. * OP-5660 - added set_settings to creator When instance gets created, set frame range and resolution from DB. * OP-5660 - minor fix * OP-5660 - updated extension zip * OP-5660 - updated documentation * OP-5660 - fixed missing exception * OP-5660 - fixed argument * OP-5560 - fix imports * OP-5660 - updated extension * OP-5660 - add js alert message for buttons * OP-5660 - repacked extension Without Anastasyi showed success, but extension wasn't loaded. * OP-5660 - make log message nicer * OP-5660 - added log if workfile not saved * OP-5660 - provide defaults to limit None exception * OP-5660 - updated error message --- openpype/hosts/aftereffects/api/__init__.py | 10 +- openpype/hosts/aftereffects/api/extension.zxp | Bin 101426 -> 101866 bytes .../api/extension/CSXS/manifest.xml | 4 +- .../aftereffects/api/extension/index.html | 72 +++++++--- .../aftereffects/api/extension/js/main.js | 56 +++++--- .../api/extension/jsx/hostscript.jsx | 134 ++++++++++++------ .../hosts/aftereffects/api/launch_logic.py | 86 ++++++++--- openpype/hosts/aftereffects/api/lib.py | 118 ++++++++------- openpype/hosts/aftereffects/api/pipeline.py | 6 +- openpype/hosts/aftereffects/api/ws_stub.py | 65 ++++++--- .../plugins/create/create_render.py | 19 ++- .../plugins/publish/collect_render.py | 16 +-- openpype/scripts/non_python_host_launch.py | 3 +- website/docs/artist_hosts_aftereffects.md | 37 ++++- .../docs/assets/aftereffects_extension.png | Bin 0 -> 12533 bytes 15 files changed, 414 insertions(+), 212 deletions(-) create mode 100644 website/docs/assets/aftereffects_extension.png diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index a7137ba8fb..28062cc35d 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -4,9 +4,8 @@ Anything that isn't defined here is INTERNAL and unreliable for external use. """ -from .launch_logic import ( +from .ws_stub import ( get_stub, - stub, ) from .pipeline import ( @@ -18,7 +17,8 @@ from .pipeline import ( from .lib import ( maintained_selection, get_extension_manifest_path, - get_asset_settings + get_asset_settings, + set_settings ) from .plugin import ( @@ -27,9 +27,8 @@ from .plugin import ( __all__ = [ - # launch_logic + # ws_stub "get_stub", - "stub", # pipeline "ls", @@ -39,6 +38,7 @@ __all__ = [ "maintained_selection", "get_extension_manifest_path", "get_asset_settings", + "set_settings", # plugin "AfterEffectsLoader" diff --git a/openpype/hosts/aftereffects/api/extension.zxp b/openpype/hosts/aftereffects/api/extension.zxp index b436f0ca0b67313cf4509d1ec5e7d4a1dfd2d106..50fda416f806515e8a6e90e745153e341f73c7b2 100644 GIT binary patch delta 14164 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 XY6T7RTV3K`-(5HW;4}pI>(~DQ$dj(< delta 13616 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 - + diff --git a/openpype/hosts/aftereffects/api/extension/index.html b/openpype/hosts/aftereffects/api/extension/index.html index 52a7c4964f..291965559f 100644 --- a/openpype/hosts/aftereffects/api/extension/index.html +++ b/openpype/hosts/aftereffects/api/extension/index.html @@ -2,7 +2,7 @@ - + @@ -25,11 +25,11 @@ - + - + - + - + - + + + + + + + - - + + + - - + @@ -107,6 +143,6 @@ - + - \ No newline at end of file + diff --git a/openpype/hosts/aftereffects/api/extension/js/main.js b/openpype/hosts/aftereffects/api/extension/js/main.js index bb0f3b1f0c..ffc41f0937 100644 --- a/openpype/hosts/aftereffects/api/extension/js/main.js +++ b/openpype/hosts/aftereffects/api/extension/js/main.js @@ -4,7 +4,7 @@ indent: 4, maxerr: 50 */ var csInterface = new CSInterface(); - + log.warn("script start"); WSRPC.DEBUG = false; @@ -14,7 +14,7 @@ WSRPC.TRACE = false; async function startUp(url){ promis = runEvalScript("getEnv('" + url + "')"); - var res = await promis; + var res = await promis; log.warn("res: " + res); promis = runEvalScript("getEnv('OPENPYPE_DEBUG')"); @@ -56,7 +56,7 @@ function get_extension_version(){ } function main(websocket_url){ - // creates connection to 'websocket_url', registers routes + // creates connection to 'websocket_url', registers routes var default_url = 'ws://localhost:8099/ws/'; if (websocket_url == ''){ @@ -66,7 +66,7 @@ function main(websocket_url){ RPC.connect(); - log.warn("connected"); + log.warn("connected"); RPC.addRoute('AfterEffects.open', function (data) { log.warn('Server called client route "open":', data); @@ -88,7 +88,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_name', function (data) { - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_name":', data); return runEvalScript("getActiveDocumentName()") .then(function(result){ @@ -98,7 +98,7 @@ function main(websocket_url){ }); RPC.addRoute('AfterEffects.get_active_document_full_name', function (data){ - log.warn('Server called client route ' + + log.warn('Server called client route ' + '"get_active_document_full_name":', data); return runEvalScript("getActiveDocumentFullName()") .then(function(result){ @@ -118,7 +118,7 @@ function main(websocket_url){ }); }); - + RPC.addRoute('AfterEffects.get_selected_items', function (data) { log.warn('Server called client route "get_selected_items":', data); return runEvalScript("getSelectedItems(" + data.comps + "," + @@ -194,23 +194,25 @@ function main(websocket_url){ }); }); - RPC.addRoute('AfterEffects.get_work_area', function (data) { - log.warn('Server called client route "get_work_area":', data); - return runEvalScript("getWorkArea(" + data.item_id + ")") + RPC.addRoute('AfterEffects.get_comp_properties', function (data) { + log.warn('Server called client route "get_comp_properties":', data); + return runEvalScript("getCompProperties(" + data.item_id + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("get_comp_properties: " + result); return result; }); }); - RPC.addRoute('AfterEffects.set_work_area', function (data) { + RPC.addRoute('AfterEffects.set_comp_properties', function (data) { log.warn('Server called client route "set_work_area":', data); - return runEvalScript("setWorkArea(" + data.item_id + ',' + + return runEvalScript("setCompProperties(" + data.item_id + ',' + data.start + ',' + data.duration + ',' + - data.frame_rate + ")") + data.frame_rate + ',' + + data.width + ',' + + data.height + ")") .then(function(result){ - log.warn("getWorkArea: " + result); + log.warn("set_comp_properties: " + result); return result; }); }); @@ -255,7 +257,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.import_background', function (data) { log.warn('Server called client route "import_background":', data); - return runEvalScript("importBackground(" + data.comp_id + ", " + + return runEvalScript("importBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -266,7 +268,7 @@ function main(websocket_url){ RPC.addRoute('AfterEffects.reload_background', function (data) { log.warn('Server called client route "reload_background":', data); - return runEvalScript("reloadBackground(" + data.comp_id + ", " + + return runEvalScript("reloadBackground(" + data.comp_id + ", " + "'" + data.comp_name + "', " + JSON.stringify(data.files) + ")") .then(function(result){ @@ -314,6 +316,16 @@ function main(websocket_url){ log.warn('Server called client route "close":', data); return runEvalScript("close()"); }); + + RPC.addRoute('AfterEffects.print_msg', function (data) { + log.warn('Server called client route "print_msg":', data); + var escaped_msg = EscapeStringForJSX(data.msg); + return runEvalScript("printMsg('" + escaped_msg +"')") + .then(function(result){ + log.warn("print_msg: " + result); + return result; + }); + }); } /** main entry point **/ @@ -323,17 +335,17 @@ startUp("WEBSOCKET_URL"); 'use strict'; var csInterface = new CSInterface(); - - + + function init() { - + themeManager.init(); - + $("#btn_test").click(function () { csInterface.evalScript('sayHello()'); }); } - + init(); }()); diff --git a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx index 5c1d163439..7d0b20bbb4 100644 --- a/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx +++ b/openpype/hosts/aftereffects/api/extension/jsx/hostscript.jsx @@ -1,7 +1,7 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ /*global $, Folder*/ -#include "../js/libs/json.js"; +//@include "../js/libs/json.js" /* All public API function should return JSON! */ @@ -29,13 +29,13 @@ function getEnv(variable){ function getMetadata(){ /** * Returns payload in 'Label' field of project's metadata - * + * **/ if (ExternalObject.AdobeXMPScript === undefined){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); @@ -53,7 +53,7 @@ function getMetadata(){ function imprint(payload){ /** * Stores payload in 'Label' field of project's metadata - * + * * Args: * payload (string): json content */ @@ -61,14 +61,14 @@ function imprint(payload){ ExternalObject.AdobeXMPScript = new ExternalObject('lib:AdobeXMPScript'); } - + var proj = app.project; var meta = new XMPMeta(app.project.xmpPacket); var schemaNS = XMPMeta.getNamespaceURI("xmp"); var label = "xmp:Label"; meta.setProperty(schemaNS, label, payload); - + app.project.xmpPacket = meta.serialize(); } @@ -116,14 +116,14 @@ function getItems(comps, folders, footages){ /** * Returns JSON representation of compositions and * if 'collectLayers' then layers in comps too. - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 1; i <= app.project.items.length; ++i){ var item = app.project.items[i]; @@ -142,14 +142,14 @@ function getItems(comps, folders, footages){ function getSelectedItems(comps, folders, footages){ /** * Returns list of selected items from Project menu - * + * * Args: * comps (bool): return selected compositions * folders (bool): return folders * footages (bool): return FootageItem * Returns: * (list) of JSON items - */ + */ var items = [] for (i = 0; i < app.project.selection.length; ++i){ var item = app.project.selection[i]; @@ -166,9 +166,9 @@ function getSelectedItems(comps, folders, footages){ function _getItem(item, comps, folders, footages){ /** - * Auxiliary function as project items and selections + * Auxiliary function as project items and selections * are indexed in different way :/ - * Refactor + * Refactor */ var item_type = ''; if (item instanceof FolderItem){ @@ -189,7 +189,7 @@ function _getItem(item, comps, folders, footages){ return "{}"; } } - + var item = {"name": item.name, "id": item.id, "type": item_type}; @@ -200,7 +200,7 @@ function importFile(path, item_name, import_options){ /** * Imports file (image tested for now) as a FootageItem. * Creates new composition - * + * * Args: * path (string): absolute path to image file * item_name (string): label for composition @@ -218,7 +218,7 @@ function importFile(path, item_name, import_options){ app.beginUndoGroup("Import File"); fp = new File(path); if (fp.exists){ - try { + try { im_opt = new ImportOptions(fp); importAsType = import_options["ImportAsType"]; @@ -234,18 +234,18 @@ function importFile(path, item_name, import_options){ } if (importAsType.indexOf('PROJECT') > 0){ im_opt.importAs = ImportAsType.PROJECT; - } - + } + } if ('sequence' in import_options){ im_opt.sequence = true; } - + comp = app.project.importFile(im_opt); if (app.project.selection.length == 2 && app.project.selection[0] instanceof FolderItem){ - comp.parentFolder = app.project.selection[0] + comp.parentFolder = app.project.selection[0] } } catch (error) { return _prepareError(error.toString() + importOptions.file.fsName); @@ -283,14 +283,14 @@ function setLabelColor(comp_id, color_idx){ function replaceItem(comp_id, path, item_name){ /** * Replaces loaded file with new file and updates name - * + * * Args: * comp_id (int): id of composition, not a index! * path (string): absolute path to new file * item_name (string): new composition name */ app.beginUndoGroup("Replace File"); - + fp = new File(path); if (!fp.exists){ return _prepareError("File " + path + " not found."); @@ -303,7 +303,7 @@ function replaceItem(comp_id, path, item_name){ }else{ item.replace(fp); } - + item.name = item_name; } catch (error) { return _prepareError(error.toString() + path); @@ -319,7 +319,7 @@ function replaceItem(comp_id, path, item_name){ function renameItem(item_id, new_name){ /** * Renames item with 'item_id' to 'new_name' - * + * * Args: * item_id (int): id to search item * new_name (str) @@ -335,7 +335,7 @@ function renameItem(item_id, new_name){ function deleteItem(item_id){ /** * Delete any 'item_id' - * + * * Not restricted only to comp, it could delete * any item with 'id' */ @@ -347,38 +347,76 @@ function deleteItem(item_id){ } } -function getWorkArea(comp_id){ +function getCompProperties(comp_id){ /** - * Returns information about workarea - are that will be - * rendered. All calculation will be done in OpenPype, - * easier to modify without redeploy of extension. - * + * Returns information about composition - are that will be + * rendered. + * * Returns * (dict) */ - var item = app.project.itemByID(comp_id); - if (item){ - return JSON.stringify({ - "workAreaStart": item.displayStartFrame, - "workAreaDuration": item.duration, - "frameRate": item.frameRate}); - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + return JSON.stringify({ + "id": comp.id, + "name": comp.name, + "frameStart": comp.displayStartFrame, + "framesDuration": comp.duration * comp.frameRate, + "frameRate": comp.frameRate, + "width": comp.width, + "height": comp.height}); } -function setWorkArea(comp_id, workAreaStart, workAreaDuration, frameRate){ +function setCompProperties(comp_id, frameStart, framesCount, frameRate, + width, height){ /** * Sets work area info from outside (from Ftrack via OpenPype) */ - var item = app.project.itemByID(comp_id); - if (item){ - item.displayStartTime = workAreaStart; - item.duration = workAreaDuration; - item.frameRate = frameRate; - }else{ + var comp = app.project.itemByID(comp_id); + if (!comp){ return _prepareError("There is no composition with "+ comp_id); } + + app.beginUndoGroup('change comp properties'); + if (frameStart && framesCount && frameRate){ + comp.displayStartFrame = frameStart; + comp.duration = framesCount / frameRate; + comp.frameRate = frameRate; + } + if (width && height){ + var widthOld = comp.width; + var widthNew = width; + var widthDelta = widthNew - widthOld; + + var heightOld = comp.height; + var heightNew = height; + var heightDelta = heightNew - heightOld; + + var offset = [widthDelta / 2, heightDelta / 2]; + + comp.width = widthNew; + comp.height = heightNew; + + for (var i = 1, il = comp.numLayers; i <= il; i++) { + var layer = comp.layer(i); + var positionProperty = layer.property('ADBE Transform Group').property('ADBE Position'); + + if (positionProperty.numKeys > 0) { + for (var j = 1, jl = positionProperty.numKeys; j <= jl; j++) { + var keyValue = positionProperty.keyValue(j); + positionProperty.setValueAtKey(j, keyValue + offset); + } + } else { + var positionValue = positionProperty.value; + positionProperty.setValue(positionValue + offset); + } + } + } + + app.endUndoGroup(); } function save(){ @@ -504,7 +542,7 @@ function addItemAsLayerToComp(comp_id, item_id, found_comp){ * Args: * comp_id (int): id of target composition * item_id (int): FootageItem.id - * found_comp (CompItem, optional): to limit querying if + * found_comp (CompItem, optional): to limit quering if * comp already found previously */ var comp = found_comp || app.project.itemByID(comp_id); @@ -749,7 +787,7 @@ function render(target_folder, comp_id){ var om1 = app.project.renderQueue.item(i).outputModule(1); var file_name = File.decode( om1.file.name ).replace('℗', ''); // Name contains special character, space? - + var omItem1_settable_str = app.project.renderQueue.item(i).outputModule(1).getSettings( GetSettingsFormat.STRING_SETTABLE ); var targetFolder = new Folder(target_folder); @@ -763,7 +801,7 @@ function render(target_folder, comp_id){ render_item.render = false; } } - + } app.beginSuppressDialogs(); app.project.renderQueue.render(); @@ -779,6 +817,10 @@ function getAppVersion(){ return _prepareSingleValue(app.version); } +function printMsg(msg){ + alert(msg); +} + function _prepareSingleValue(value){ return JSON.stringify({"result": value}) } diff --git a/openpype/hosts/aftereffects/api/launch_logic.py b/openpype/hosts/aftereffects/api/launch_logic.py index c428043d99..77c2b0b6ca 100644 --- a/openpype/hosts/aftereffects/api/launch_logic.py +++ b/openpype/hosts/aftereffects/api/launch_logic.py @@ -1,49 +1,77 @@ import os +import sys import subprocess import collections import logging import asyncio import functools +import traceback + from wsrpc_aiohttp import ( WebSocketRoute, WebSocketAsync ) -from qtpy import QtCore +from qtpy import QtCore, QtWidgets from openpype.lib import Logger -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools +from openpype.tests.lib import is_in_tests +from openpype.pipeline import install_host, legacy_io +from openpype.modules import ModulesManager from openpype.tools.adobe_webserver.app import WebServerTool -from .ws_stub import AfterEffectsServerStub +from .ws_stub import get_stub +from .lib import set_settings log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -class ConnectionNotEstablishedYet(Exception): - pass +def safe_excepthook(*args): + traceback.print_exception(*args) -def get_stub(): - """ - Convenience function to get server RPC stub to call methods directed - for host (Photoshop). - It expects already created connection, started from client. - Currently created when panel is opened (PS: Window>Extensions>Avalon) - :return: where functions could be called from - """ - ae_stub = AfterEffectsServerStub() - if not ae_stub.client: - raise ConnectionNotEstablishedYet("Connection is not created yet") +def main(*subprocess_args): + """Main entrypoint to AE launching, called from pre hook.""" + sys.excepthook = safe_excepthook - return ae_stub + from openpype.hosts.aftereffects.api import AfterEffectsHost + host = AfterEffectsHost() + install_host(host) -def stub(): - return get_stub() + os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" + app = QtWidgets.QApplication([]) + app.setQuitOnLastWindowClosed(False) + + launcher = ProcessLauncher(subprocess_args) + launcher.start() + + if os.environ.get("HEADLESS_PUBLISH"): + manager = ModulesManager() + webpublisher_addon = manager["webpublisher"] + + launcher.execute_in_main_thread( + functools.partial( + webpublisher_addon.headless_publish, + log, + "CloseAE", + is_in_tests() + ) + ) + + elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): + save = False + if os.getenv("WORKFILES_SAVE_AS"): + save = True + + launcher.execute_in_main_thread( + lambda: host_tools.show_tool_by_name("workfiles", save=save) + ) + + sys.exit(app.exec_()) def show_tool_by_name(tool_name): @@ -55,6 +83,7 @@ def show_tool_by_name(tool_name): class ProcessLauncher(QtCore.QObject): + """Launches webserver, connects to it, runs main thread.""" route_name = "AfterEffects" _main_thread_callbacks = collections.deque() @@ -296,6 +325,15 @@ class AfterEffectsRoute(WebSocketRoute): async def sceneinventory_route(self): self._tool_route("sceneinventory") + async def setresolution_route(self): + self._settings_route(False, True) + + async def setframes_route(self): + self._settings_route(True, False) + + async def setall_route(self): + self._settings_route(True, True) + async def experimental_tools_route(self): self._tool_route("experimental_tools") @@ -309,3 +347,13 @@ class AfterEffectsRoute(WebSocketRoute): # Required return statement. return "nothing" + + def _settings_route(self, frames, resolution): + partial_method = functools.partial(set_settings, + frames, + resolution) + + ProcessLauncher.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index a39af5c81f..e8352c382b 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -1,69 +1,17 @@ import os -import sys import re import json import contextlib -import traceback import logging -from functools import partial -from qtpy import QtWidgets - -from openpype.pipeline import install_host -from openpype.modules import ModulesManager - -from openpype.tools.utils import host_tools -from openpype.tests.lib import is_in_tests -from .launch_logic import ProcessLauncher, get_stub +from openpype.pipeline.context_tools import get_current_context +from openpype.client import get_asset_by_name +from .ws_stub import get_stub log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) -def safe_excepthook(*args): - traceback.print_exception(*args) - - -def main(*subprocess_args): - sys.excepthook = safe_excepthook - - from openpype.hosts.aftereffects.api import AfterEffectsHost - - host = AfterEffectsHost() - install_host(host) - - os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" - app = QtWidgets.QApplication([]) - app.setQuitOnLastWindowClosed(False) - - launcher = ProcessLauncher(subprocess_args) - launcher.start() - - if os.environ.get("HEADLESS_PUBLISH"): - manager = ModulesManager() - webpublisher_addon = manager["webpublisher"] - - launcher.execute_in_main_thread( - partial( - webpublisher_addon.headless_publish, - log, - "CloseAE", - is_in_tests() - ) - ) - - elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True): - save = False - if os.getenv("WORKFILES_SAVE_AS"): - save = True - - launcher.execute_in_main_thread( - lambda: host_tools.show_tool_by_name("workfiles", save=save) - ) - - sys.exit(app.exec_()) - - @contextlib.contextmanager def maintained_selection(): """Maintain selection during context.""" @@ -145,13 +93,13 @@ def get_asset_settings(asset_doc): """ asset_data = asset_doc["data"] - fps = asset_data.get("fps") - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - handle_start = asset_data.get("handleStart") - handle_end = asset_data.get("handleEnd") - resolution_width = asset_data.get("resolutionWidth") - resolution_height = asset_data.get("resolutionHeight") + fps = asset_data.get("fps", 0) + frame_start = asset_data.get("frameStart", 0) + frame_end = asset_data.get("frameEnd", 0) + handle_start = asset_data.get("handleStart", 0) + handle_end = asset_data.get("handleEnd", 0) + resolution_width = asset_data.get("resolutionWidth", 0) + resolution_height = asset_data.get("resolutionHeight", 0) duration = (frame_end - frame_start + 1) + handle_start + handle_end return { @@ -164,3 +112,49 @@ def get_asset_settings(asset_doc): "resolutionHeight": resolution_height, "duration": duration } + + +def set_settings(frames, resolution, comp_ids=None, print_msg=True): + """Sets number of frames and resolution to selected comps. + + Args: + frames (bool): True if set frame info + resolution (bool): True if set resolution + comp_ids (list): specific composition ids, if empty + it tries to look for currently selected + print_msg (bool): True throw JS alert with msg + """ + frame_start = frames_duration = fps = width = height = None + current_context = get_current_context() + + asset_doc = get_asset_by_name(current_context["project_name"], + current_context["asset_name"]) + settings = get_asset_settings(asset_doc) + + msg = '' + if frames: + frame_start = settings["frameStart"] - settings["handleStart"] + frames_duration = settings["duration"] + fps = settings["fps"] + msg += f"frame start:{frame_start}, duration:{frames_duration}, "\ + f"fps:{fps}" + if resolution: + width = settings["resolutionWidth"] + height = settings["resolutionHeight"] + msg += f"width:{width} and height:{height}" + + stub = get_stub() + if not comp_ids: + comps = stub.get_selected_items(True, False, False) + comp_ids = [comp.id for comp in comps] + if not comp_ids: + stub.print_msg("Select at least one composition to apply settings.") + return + + for comp_id in comp_ids: + msg = f"Setting for comp {comp_id} " + msg + log.debug(msg) + stub.set_comp_properties(comp_id, frame_start, frames_duration, + fps, width, height) + if print_msg: + stub.print_msg(msg) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 020022e263..27aee8c7ce 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -8,10 +8,7 @@ from openpype.lib import Logger, register_event_callback from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, AVALON_CONTAINER_ID, - legacy_io, ) from openpype.pipeline.load import any_outdated_containers import openpype.hosts.aftereffects @@ -23,7 +20,8 @@ from openpype.host import ( IPublishHost ) -from .launch_logic import get_stub, ConnectionNotEstablishedYet +from .launch_logic import get_stub +from .ws_stub import ConnectionNotEstablishedYet log = Logger.get_logger(__name__) diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index f094c7fa2a..576c997f49 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -11,6 +11,10 @@ from wsrpc_aiohttp import WebSocketAsync from openpype.tools.adobe_webserver.app import WebServerTool +class ConnectionNotEstablishedYet(Exception): + pass + + @attr.s class AEItem(object): """ @@ -24,8 +28,8 @@ class AEItem(object): # all imported elements, single for # regular image, array for Backgrounds members = attr.ib(factory=list) - workAreaStart = attr.ib(default=None) - workAreaDuration = attr.ib(default=None) + frameStart = attr.ib(default=None) + framesDuration = attr.ib(default=None) frameRate = attr.ib(default=None) file_name = attr.ib(default=None) instance_id = attr.ib(default=None) # New Publisher @@ -355,42 +359,50 @@ class AfterEffectsServerStub(): return self._handle_return(res) - def get_work_area(self, item_id): - """ Get work are information for render purposes + def get_comp_properties(self, comp_id): + """ Get composition information for render purposes + + Returns startFrame, frameDuration, fps, width, height. + Args: - item_id (int): + comp_id (int): Returns: (AEItem) """ res = self.websocketserver.call(self.client.call - ('AfterEffects.get_work_area', - item_id=item_id + ('AfterEffects.get_comp_properties', + item_id=comp_id )) records = self._to_records(self._handle_return(res)) if records: return records.pop() - def set_work_area(self, item, start, duration, frame_rate): + def set_comp_properties(self, comp_id, start, duration, frame_rate, + width, height): """ Set work area to predefined values (from Ftrack). Work area directs what gets rendered. Beware of rounding, AE expects seconds, not frames directly. Args: - item (dict): - start (float): workAreaStart in seconds - duration (float): in seconds + comp_id (int): + start (int): workAreaStart in frames + duration (int): in frames frame_rate (float): frames in seconds + width (int): resolution width + height (int): resolution height """ res = self.websocketserver.call(self.client.call - ('AfterEffects.set_work_area', - item_id=item.id, + ('AfterEffects.set_comp_properties', + item_id=comp_id, start=start, duration=duration, - frame_rate=frame_rate)) + frame_rate=frame_rate, + width=width, + height=height)) return self._handle_return(res) def save(self): @@ -554,6 +566,12 @@ class AfterEffectsServerStub(): return self._handle_return(res) + def print_msg(self, msg): + """Triggers Javascript alert dialog.""" + self.websocketserver.call(self.client.call + ('AfterEffects.print_msg', + msg=msg)) + def _handle_return(self, res): """Wraps return, throws ValueError if 'error' key is present.""" if res and isinstance(res, str) and res != "undefined": @@ -608,8 +626,8 @@ class AfterEffectsServerStub(): d.get('name'), d.get('type'), d.get('members'), - d.get('workAreaStart'), - d.get('workAreaDuration'), + d.get('frameStart'), + d.get('framesDuration'), d.get('frameRate'), d.get('file_name'), d.get("instance_id"), @@ -618,3 +636,18 @@ class AfterEffectsServerStub(): ret.append(item) return ret + + +def get_stub(): + """ + Convenience function to get server RPC stub to call methods directed + for host (Photoshop). + It expects already created connection, started from client. + Currently created when panel is opened (PS: Window>Extensions>Avalon) + :return: where functions could be called from + """ + ae_stub = AfterEffectsServerStub() + if not ae_stub.client: + raise ConnectionNotEstablishedYet("Connection is not created yet") + + return ae_stub diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 171d7053ce..fa79fac78f 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -9,6 +9,7 @@ from openpype.pipeline import ( CreatorError ) from openpype.hosts.aftereffects.api.pipeline import cache_and_get_instances +from openpype.hosts.aftereffects.api.lib import set_settings from openpype.lib import prepare_template_data from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS @@ -32,6 +33,14 @@ class RenderCreator(Creator): def create(self, subset_name_from_ui, data, pre_create_data): stub = api.get_stub() # only after After Effects is up + + try: + _ = stub.get_active_document_full_name() + except ValueError: + raise CreatorError( + "Please save workfile via Workfile app first!" + ) + if pre_create_data.get("use_selection"): comps = stub.get_selected_items( comps=True, folders=False, footages=False @@ -41,8 +50,8 @@ class RenderCreator(Creator): if not comps: raise CreatorError( - "Nothing to create. Select composition " - "if 'useSelection' or create at least " + "Nothing to create. Select composition in Project Bin if " + "'Use selection' is toggled or create at least " "one composition." ) use_composition_name = (pre_create_data.get("use_composition_name") or @@ -87,10 +96,14 @@ class RenderCreator(Creator): self._add_instance_to_context(new_instance) stub.rename_item(comp.id, subset_name) + set_settings(True, True, [comp.id], print_msg=False) def get_pre_create_attr_defs(self): output = [ - BoolDef("use_selection", default=True, label="Use selection"), + BoolDef("use_selection", + tooltip="Composition for publishable instance should be " + "selected by default.", + default=True, label="Use selection"), BoolDef("use_composition_name", label="Use composition name in subset"), UISeparatorDef(), diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index b01b707246..aa46461915 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -66,19 +66,19 @@ class CollectAERender(publish.AbstractCollectRender): comp_id = int(inst.data["members"][0]) - work_area_info = CollectAERender.get_stub().get_work_area(comp_id) + comp_info = CollectAERender.get_stub().get_comp_properties( + comp_id) - if not work_area_info: + if not comp_info: self.log.warning("Orphaned instance, deleting metadata") - inst_id = inst.get("instance_id") or str(comp_id) + inst_id = inst.data.get("instance_id") or str(comp_id) CollectAERender.get_stub().remove_instance(inst_id) continue - 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 + frame_start = comp_info.frameStart + frame_end = round(comp_info.frameStart + + comp_info.framesDuration) - 1 + fps = comp_info.frameRate # TODO add resolution when supported by extension task_name = inst.data.get("task") # legacy diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py index 79fb1cbb52..c95a9df314 100644 --- a/openpype/scripts/non_python_host_launch.py +++ b/openpype/scripts/non_python_host_launch.py @@ -81,9 +81,10 @@ def main(argv): host_name = os.environ["AVALON_APP"].lower() if host_name == "photoshop": + # TODO refactor launch logic according to AE from openpype.hosts.photoshop.api.lib import main elif host_name == "aftereffects": - from openpype.hosts.aftereffects.api.lib import main + from openpype.hosts.aftereffects.api.launch_logic import main elif host_name == "harmony": from openpype.hosts.harmony.api.lib import main else: diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index d9522d5765..d415a1d47d 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -15,18 +15,18 @@ sidebar_label: AfterEffects ## Setup -To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. +To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. -Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. +Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. ## Implemented functionality AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.) -and send prepared composition for rendering to Deadline or render locally. +and send prepared composition for rendering to Deadline or render locally. ## Usage -When you launch AfterEffects you will be met with the Workfiles app. If don't +When you launch AfterEffects you will be met with the Workfiles app. If don't have any previous workfiles, you can just close this window. Workfiles tools takes care of saving your .AEP files in the correct location and under @@ -34,7 +34,7 @@ a correct name. You should use it instead of standard file saving dialog. In AfterEffects you'll find the tools in the `OpenPype` extension: -![Extension](assets/photoshop_extension.png) +![Extension](assets/photoshop_extension.png) You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`. @@ -58,6 +58,9 @@ Name of publishable instance (eg. subset name) could be configured with a templa Trash icon under the list of instances allows to delete any selected `render` instance. +Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically! +(Eg. number of rendered frames is controlled by settings inserted from supervisor. Artist can override this by disabling validation only in special cases.) + Workfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item. If you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable @@ -67,7 +70,7 @@ Publisher allows publishing into different context, just click on any instance, #### RenderQueue -AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item per composition in the Render Queue. @@ -151,3 +154,25 @@ You can switch to a previous version of the image or update to the latest. ![Loader](assets/photoshop_manage_switch.gif) ![Loader](assets/photoshop_manage_update.gif) + + +### Setting section + +Composition properties should be controlled by state in Asset Management System (Ftrack etc). Extension provides couple of buttons to trigger this propagation. + +#### Set Resolution + +Set width and height from AMS to composition. + +#### Set Frame Range + +Start frame and duration in workarea is set according to the settings in AMS. Handles are incorporated (not inclusive). +It is expected that composition(s) is selected first before pushing this button! + +#### Apply All Settings + +Both previous settings are triggered at same time. + +### Experimental tools + +Currently empty. Could contain special tools available only for specific hosts for early access testing. diff --git a/website/docs/assets/aftereffects_extension.png b/website/docs/assets/aftereffects_extension.png new file mode 100644 index 0000000000000000000000000000000000000000..b14992471a1e42cb07c16e93858f180240f3f2e3 GIT binary patch literal 12533 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 Date: Mon, 22 May 2023 11:31:56 +0200 Subject: [PATCH 649/918] files are missing issue --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 6697a1e59a..dbd0963f0a 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -133,11 +133,12 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, else: representation['files'] = collected_frames - # inject colorspace data - self.set_representation_colorspace( - representation, instance.context, - colorspace=colorspace - ) + self.log.debug("_ representation: {}".format(representation)) + # inject colorspace data + self.set_representation_colorspace( + representation, instance.context, + colorspace=colorspace + ) instance.data["representations"].append(representation) self.log.info("Publishing rendered frames ...") From 30fe6759c32fb879fe9ddb3594aabe651c057697 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 May 2023 13:28:09 +0200 Subject: [PATCH 650/918] Publish: Enhance automated publish plugin settings (#4986) * prepared helper functions for custom settings apply method * publish plugin can have 'settings_category' attribute to define settings category * Better 'settings_category' comment Co-authored-by: Roy Nieterau * fix trailing spaces * added more information about pyblish plugins to dev docs --------- Co-authored-by: Roy Nieterau --- openpype/pipeline/publish/__init__.py | 6 ++ openpype/pipeline/publish/lib.py | 90 ++++++++++++++++++++------- website/docs/dev_publishing.md | 63 ++++++++++++++++++- 3 files changed, 136 insertions(+), 23 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 36252c9f3d..72f3774e1a 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -36,6 +36,9 @@ from .lib import ( context_plugin_should_run, get_instance_staging_dir, get_publish_repre_path, + + apply_plugin_settings_automatically, + get_plugin_settings, ) from .abstract_expected_files import ExpectedFiles @@ -80,6 +83,9 @@ __all__ = ( "get_instance_staging_dir", "get_publish_repre_path", + "apply_plugin_settings_automatically", + "get_plugin_settings", + "ExpectedFiles", "RenderInstance", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 8b6212b3ef..080f93e514 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -355,29 +355,55 @@ def publish_plugins_discover(paths=None): return result -def _get_plugin_settings(host_name, project_settings, plugin, log): +def get_plugin_settings(plugin, project_settings, log, category=None): """Get plugin settings based on host name and plugin name. + Note: + Default implementation of automated settings is passing host name + into 'category'. + Args: - host_name (str): Name of host. + plugin (pyblish.Plugin): Plugin where settings are applied. project_settings (dict[str, Any]): Project settings. - plugin (pyliblish.Plugin): Plugin where settings are applied. log (logging.Logger): Logger to log messages. + category (Optional[str]): Settings category key where to look + for plugin settings. Returns: dict[str, Any]: Plugin settings {'attribute': 'value'}. """ - # Use project settings from host name category when available - try: - return ( - project_settings - [host_name] - ["publish"] - [plugin.__name__] - ) - except KeyError: - pass + # Plugin can define settings category by class attribute + # - it's impossible to set `settings_category` via settings because + # obviously settings are not applied before it. + # - if `settings_category` is set the fallback category method is ignored + settings_category = getattr(plugin, "settings_category", None) + if settings_category: + try: + return ( + project_settings + [settings_category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + log.warning(( + "Couldn't find plugin '{}' settings" + " under settings category '{}'" + ).format(plugin.__name__, settings_category)) + return {} + + # Use project settings based on a category name + if category: + try: + return ( + project_settings + [category] + ["publish"] + [plugin.__name__] + ) + except KeyError: + pass # Settings category determined from path # - usually path is './/plugins/publish/' @@ -386,9 +412,10 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): split_path = filepath.rsplit(os.path.sep, 5) if len(split_path) < 4: - log.warning( - 'plugin path too short to extract host {}'.format(filepath) - ) + log.debug(( + "Plugin path is too short to automatically" + " extract settings category. {}" + ).format(filepath)) return {} category_from_file = split_path[-4] @@ -410,6 +437,28 @@ def _get_plugin_settings(host_name, project_settings, plugin, log): return {} +def apply_plugin_settings_automatically(plugin, settings, logger=None): + """Automatically apply plugin settings to a plugin object. + + Note: + This function was created to be able to use it in custom overrides of + 'apply_settings' class method. + + Args: + plugin (type[pyblish.api.Plugin]): Class of a plugin. + settings (dict[str, Any]): Plugin specific settings. + logger (Optional[logging.Logger]): Logger to log debug messages about + applied settings values. + """ + + for option, value in settings.items(): + if logger: + logger.debug("Plugin {} - Attr: {} -> {}".format( + option, value, plugin.__name__ + )) + setattr(plugin, option, value) + + def filter_pyblish_plugins(plugins): """Pyblish plugin filter which applies OpenPype settings. @@ -453,13 +502,10 @@ def filter_pyblish_plugins(plugins): ) else: # Automated - plugin_settins = _get_plugin_settings( - host_name, project_settings, plugin, log + plugin_settins = get_plugin_settings( + plugin, project_settings, log, host_name ) - for option, value in plugin_settins.items(): - log.info("setting {}:{} on plugin {}".format( - option, value, plugin.__name__)) - setattr(plugin, option, value) + apply_plugin_settings_automatically(plugin, plugin_settins, log) # Remove disabled plugins if getattr(plugin, "enabled", True) is False: diff --git a/website/docs/dev_publishing.md b/website/docs/dev_publishing.md index 2c57537223..3ef6272373 100644 --- a/website/docs/dev_publishing.md +++ b/website/docs/dev_publishing.md @@ -506,6 +506,67 @@ or the scene file was copy pasted from different context. #### *Known errors* When there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown. +### Plugins +Plugin is a single processing unit that can work with publish context and instances. + +#### Plugin types +There are 2 types of plugins - `InstancePlugin` and `ContextPlugin`. Be aware that inheritance of plugin from `InstancePlugin` or `ContextPlugin` actually does not affect if plugin is instance or context plugin, that is affected by argument name in `process` method. + +```python +import pyblish.api + + +# Context plugin +class MyContextPlugin(pyblish.api.ContextPlugin): + def process(self, context): + ... + +# Instance plugin +class MyInstancePlugin(pyblish.api.InstancePlugin): + def process(self, instance): + ... + +# Still an instance plugin +class MyOtherInstancePlugin(pyblish.api.ContextPlugin): + def process(self, instance): + ... +``` + +#### Plugin filtering +By pyblish logic, plugins have predefined filtering class attributes `hosts`, `targets` and `families`. Filter by `hosts` and `targets` are filters that are applied for current publishing process. Both filters are registered in `pyblish` module, `hosts` filtering may not match OpenPype host name (e.g. farm publishing uses `shell` in pyblish). Filter `families` works only on instance plugins and is dynamic during publish process by changing families of an instance. + +All filters are list of a strings `families = ["image"]`. Empty list is invalid filter and plugin will be skipped, to allow plugin for all values use a start `families = ["*"]`. For more detailed filtering options check [pyblish documentation](https://api.pyblish.com/pluginsystem). + +Each plugin must have order, there are 4 order milestones - Collect, Validate, Extract, Integration. Any plugin below collection order won't be processed. for more details check [pyblish documentation](https://api.pyblish.com/ordering). + +#### Plugin settings +Pyblish plugins may have settings. There are 2 ways how settings are applied, first is automated, and it's logic is based on function `filter_pyblish_plugins` in `./openpype/pipeline/publish/lib.py`, second is explicit by implementing class method `apply_settings` on a plugin. + + +Automated logic is expecting specific structure of project settings `project_settings[{category}]["plugins"]["publish"][{plugin class name}]`. The category is a key in root of project settings. There are currently 3 ways how the category key is received. +1. Use `settings_category` class attribute value from plugin. If `settings_category` is not `None` there is not any fallback to other way. +2. Use currently registered pyblish host. This will be probably deprecated soon. +3. Use 3rd folder name from a plugin filepath. From path `./maya/plugins/publish/collect_render.py` is used `maya` as the key. + +For any other use-case is recommended to use explicit approach by implementing `apply_settings` method. Must use `@classmethod` decorator and expect arguments for project settings and system settings. We're planning to support single argument with only project settings. +```python +import pyblish.api + + +class MyPlugin(pyblish.api.InstancePlugin): + profiles = [] + + @classmethod + def apply_settings(cls, project_settings, system_settings): + cls.profiles = ( + project_settings + ["addon"] + ["plugins"] + ["publish"] + ["vfx_profiles"] + ) +``` + ### Plugin extension Publish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins. @@ -596,4 +657,4 @@ Publish attributes work the same way as create attributes but the source of attr ### Create dialog ![Publisher UI - Create dialog](assets/publisher_create_dialog.png) -Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. \ No newline at end of file +Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator. From cfa64b58e84024d7ad3b7adef8e38dd4b5dfd493 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 13:38:56 +0100 Subject: [PATCH 651/918] Fix the frame range when loading camera --- .../hosts/unreal/plugins/load/load_camera.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 2303ed1ffc..84d025b37e 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -286,6 +286,26 @@ class CameraLoader(plugin.Loader): self.fname ) + # Set range of all sections + # Changing the range of the section is not enough. We need to change + # the frame of all the keys in the section. + for possessable in cam_seq.get_possessables(): + for tracks in possessable.get_tracks(): + for section in tracks.get_sections(): + section.set_range( + data.get('clipIn'), + data.get('clipOut') + 1) + for channel in section.get_all_channels(): + for key in channel.get_keys(): + old_time = key.get_time().get_editor_property( + 'frame_number') + old_time_value = old_time.get_editor_property( + 'value') + new_time = old_time_value + ( + data.get('clipIn') - data.get('frameStart') + ) + key.set_time(unreal.FrameNumber(value = new_time)) + # Create Asset Container unreal_pipeline.create_container( container=container_name, path=asset_dir) From bf77a9e5b9cf1c0be9b2988941c3e06378698bf5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 13:43:27 +0100 Subject: [PATCH 652/918] Hound fixes --- openpype/hosts/unreal/plugins/load/load_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 84d025b37e..1bd398349f 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -304,7 +304,7 @@ class CameraLoader(plugin.Loader): new_time = old_time_value + ( data.get('clipIn') - data.get('frameStart') ) - key.set_time(unreal.FrameNumber(value = new_time)) + key.set_time(unreal.FrameNumber(value=new_time)) # Create Asset Container unreal_pipeline.create_container( From 41fbe3031f67b9ed9141939c1e70a955b8240d4d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 14:59:58 +0200 Subject: [PATCH 653/918] fusion: asset_db is collecting by default. --- .../fusion/plugins/publish/collect_instances.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 458f00c7ed..6016baa2a9 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -25,15 +25,16 @@ class CollectInstanceData(pyblish.api.InstancePlugin): frame_range_source = creator_attributes.get("frame_range_source") instance.data["frame_range_source"] = frame_range_source - if frame_range_source == "asset_db": - # get asset frame ranges - start = context.data["frameStart"] - end = context.data["frameEnd"] - handle_start = context.data["handleStart"] - handle_end = context.data["handleEnd"] - start_with_handle = start - handle_start - end_with_handle = end + handle_end + # get asset frame ranges to all instances + # render family instances `asset_db` render target + start = context.data["frameStart"] + end = context.data["frameEnd"] + handle_start = context.data["handleStart"] + handle_end = context.data["handleEnd"] + start_with_handle = start - handle_start + end_with_handle = end + handle_end + # conditions for render family instances if frame_range_source == "render_range": # set comp render frame ranges start = context.data["renderFrameStart"] From 72fee37af6293b93605b323cfd191b46130c812b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 May 2023 16:42:12 +0200 Subject: [PATCH 654/918] Allow to open with djv by extension instead of representation name (#5004) --- openpype/plugins/load/open_djv.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index bc5fd64b87..5bb7a6aaa5 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -19,7 +19,8 @@ class OpenInDJV(load.LoaderPlugin): djv_list = existing_djv_path() families = ["*"] if djv_list else [] - representations = [ + representations = ["*"] + extensions = [ "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", From bad6aa2d96f86dee6291ed664a4ccf447242d821 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 22 May 2023 16:50:38 +0200 Subject: [PATCH 655/918] DJV open action `extensions` as `set` (#5005) * Allow to open with djv by extension instead of representation name * Turn extensions into `set` like on base loader class --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/load/open_djv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/load/open_djv.py b/openpype/plugins/load/open_djv.py index 5bb7a6aaa5..9c36e7f405 100644 --- a/openpype/plugins/load/open_djv.py +++ b/openpype/plugins/load/open_djv.py @@ -20,12 +20,12 @@ class OpenInDJV(load.LoaderPlugin): djv_list = existing_djv_path() families = ["*"] if djv_list else [] representations = ["*"] - extensions = [ + extensions = { "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img", "h264", - ] + } label = "Open in DJV" order = -10 From 267bad5ba6bd0d0b7558d6d3a106fd7c2c106998 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 16:59:12 +0200 Subject: [PATCH 656/918] adding multiple reposition nodes attribute to settings --- .../defaults/project_settings/nuke.json | 38 +++++++++++++++++-- .../schemas/schema_nuke_publish.json | 35 ++++++++++++++++- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 85dee73176..f01bdf7d50 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -358,12 +358,12 @@ "optional": true, "active": true }, - "ValidateGizmo": { + "ValidateBackdrop": { "enabled": true, "optional": true, "active": true }, - "ValidateBackdrop": { + "ValidateGizmo": { "enabled": true, "optional": true, "active": true @@ -401,7 +401,39 @@ false ] ] - } + }, + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": [ + { + "type": "text", + "name": "type", + "value": "to format" + }, + { + "type": "text", + "name": "format", + "value": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "value": "Lanczos6" + }, + { + "type": "bool", + "name": "black_outside", + "value": true + }, + { + "type": "bool", + "name": "pbb", + "value": false + } + ] + } + ] }, "ExtractReviewData": { "enabled": false diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index ce9fa04c6a..3019c9b1b5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -158,10 +158,43 @@ "label": "Nodes", "collapsible": true, "children": [ + { + "type": "label", + "label": "Nodes attribute will be deprecated in future releases. Use reposition_nodes instead." + }, { "type": "raw-json", "key": "nodes", - "label": "Nodes" + "label": "Nodes [depricated]" + }, + { + "type": "label", + "label": "Reposition knobs supported only. You can add multiple reformat nodes
and set their knobs. Order of reformat nodes is important. First reformat node
will be applied first and last reformat node will be applied last." + }, + { + "key": "reposition_nodes", + "type": "list", + "label": "Reposition nodes", + "object_type": { + "type": "dict", + "children": [ + { + "key": "node_class", + "label": "Node class", + "type": "text" + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + } + ] + } } ] } From b62d066390861ac9aa13e10fb58f2e33d17bdb58 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 22 May 2023 17:30:09 +0200 Subject: [PATCH 657/918] adding multi repositional nodes support to thumbnail exporter --- .../nuke/plugins/publish/extract_thumbnail.py | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index f391ca1e7c..2336487b37 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -5,6 +5,8 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.nuke import api as napi +from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings + if sys.version_info[0] >= 3: unicode = str @@ -28,7 +30,7 @@ class ExtractThumbnail(publish.Extractor): bake_viewer_process = True bake_viewer_input_process = True nodes = {} - + reposition_nodes = [] def process(self, instance): if instance.data.get("farm"): @@ -123,18 +125,32 @@ class ExtractThumbnail(publish.Extractor): temporary_nodes.append(rnode) previous_node = rnode - reformat_node = nuke.createNode("Reformat") - ref_node = self.nodes.get("Reformat", None) - if ref_node: - for k, v in ref_node: - self.log.debug("k, v: {0}:{1}".format(k, v)) - if isinstance(v, unicode): - v = str(v) - reformat_node[k].setValue(v) + if not self.reposition_nodes: + # [deprecated] create reformat node old way + reformat_node = nuke.createNode("Reformat") + ref_node = self.nodes.get("Reformat", None) + if ref_node: + for k, v in ref_node: + self.log.debug("k, v: {0}:{1}".format(k, v)) + if isinstance(v, unicode): + v = str(v) + reformat_node[k].setValue(v) - reformat_node.setInput(0, previous_node) - previous_node = reformat_node - temporary_nodes.append(reformat_node) + reformat_node.setInput(0, previous_node) + previous_node = reformat_node + temporary_nodes.append(reformat_node) + else: + # create reformat node new way + for repo_node in self.reposition_nodes: + node_class = repo_node["node_class"] + knobs = repo_node["knobs"] + node = nuke.createNode(node_class) + set_node_knobs_from_settings(node, knobs) + + # connect in order + node.setInput(0, previous_node) + previous_node = node + temporary_nodes.append(node) # only create colorspace baking if toggled on if bake_viewer_process: From fa74cae511070afb4edd87028ecbcffd5c7f6142 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 16:55:39 +0100 Subject: [PATCH 658/918] Implemented creator, loader and extractor for Unreal Levels --- .../unreal/plugins/create/create_umap.py | 46 ++++++ .../hosts/unreal/plugins/load/load_umap.py | 140 ++++++++++++++++++ .../publish/collect_instance_members.py | 2 +- .../unreal/plugins/publish/extract_umap.py | 48 ++++++ 4 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py create mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py create mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py new file mode 100644 index 0000000000..34aa8cdc00 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_umap.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from pathlib import Path + +import unreal + +from openpype.pipeline import CreatorError +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) + + +class CreateUMap(UnrealAssetCreator): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + icon = "cube" + + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + if len(selection) != 1: + raise CreatorError("Please select only one object.") + + obj = selection[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + + if not sys_path: + raise CreatorError( + f"{Path(obj).name} is not on the disk. Likely it needs to" + "be saved first.") + + if Path(sys_path).suffix != ".umap": + raise CreatorError(f"{Path(sys_path).name} is not a Level.") + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py new file mode 100644 index 0000000000..f467fe6b3b --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_umap.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +"""Load Level.""" +from pathlib import Path +import shutil + +from openpype.pipeline import ( + get_representation_path, + AYON_CONTAINER_ID +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as unreal_pipeline +import unreal # noqa + + +class UMapLoader(plugin.Loader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + options (dict): Those would be data to be imprinted. This is not + used now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + asset_name = f"{asset}_{name}" if asset else f"{name}" + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{root}/{asset}/{name}", suffix="" + ) + + container_name += suffix + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) + + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset_dir = container["namespace"] + name = representation["context"]["subset"] + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=False, include_folder=True + ) + + for asset in asset_content: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() != 'AyonAssetContainer': + unreal.EditorAssetLibrary.delete_asset(asset) + + update_filepath = get_representation_path(representation) + + shutil.copy(update_filepath, f"{destination_path}/{name}.umap") + + container_path = f'{container["namespace"]}/{container["objectName"]}' + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = Path(path).parent.as_posix() + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py index 46ca51ab7e..de10e7b119 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -24,7 +24,7 @@ class CollectInstanceMembers(pyblish.api.InstancePlugin): ar = unreal.AssetRegistryHelpers.get_asset_registry() inst_path = instance.data.get('instance_path') - inst_name = instance.data.get('objectName') + inst_name = inst_path.split('/')[-1] pub_instance = ar.get_asset_by_object_path( f"{inst_path}.{inst_name}").get_asset() diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py new file mode 100644 index 0000000000..3812834430 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_umap.py @@ -0,0 +1,48 @@ +from pathlib import Path +import shutil + +import unreal + +from openpype.pipeline import publish + + +class ExtractUMap(publish.Extractor): + """Extract a UMap.""" + + label = "Extract Level" + hosts = ["unreal"] + families = ["uasset"] + optional = True + + def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + self.log.info("Performing extraction..") + + staging_dir = self.staging_dir(instance) + filename = f"{instance.name}.umap" + + members = instance.data.get("members", []) + + if not members: + raise RuntimeError("No members found in instance.") + + # UAsset publishing supports only one member + obj = members[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + filename = Path(sys_path).name + + shutil.copy(sys_path, staging_dir) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'umap', + 'ext': 'umap', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) From afb6bd9ba53f6fb030eaed635ae18d456a04e0d5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 23 May 2023 00:57:15 +0800 Subject: [PATCH 659/918] restore the code in abc extractor --- openpype/hosts/max/plugins/publish/extract_camera_abc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 0a4f64508a..d53c47fb51 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -19,8 +19,8 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): def process(self, instance): if not self.is_active(instance.data): return - start = float(instance.data.get("frameStart", 1)) - end = float(instance.data.get("frameEnd", 1)) + start = float(instance.data.get("frameStartHandle", 1)) + end = float(instance.data.get("frameEndHandle", 1)) container = instance.data["instance_node"] From 14360f02176b3eb5fbb5a1bc9ba7ab54e33e6bd0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 18:11:13 +0100 Subject: [PATCH 660/918] Changed name and path of the camera levels to fix problem with sequencer --- openpype/hosts/unreal/api/__init__.py | 4 + openpype/hosts/unreal/api/pipeline.py | 130 ++++++++++++ .../hosts/unreal/plugins/load/load_camera.py | 192 +++++++----------- .../hosts/unreal/plugins/load/load_layout.py | 152 ++------------ 4 files changed, 228 insertions(+), 250 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index de0fce13d5..ac6a91eae9 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -22,6 +22,8 @@ from .pipeline import ( show_tools_popup, instantiate, UnrealHost, + set_sequence_hierarchy, + generate_sequence, maintained_selection ) @@ -41,5 +43,7 @@ __all__ = [ "show_tools_popup", "instantiate", "UnrealHost", + "set_sequence_hierarchy", + "generate_sequence", "maintained_selection" ] diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index bb45fa8c01..5030e8ee86 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -9,12 +9,14 @@ import time import pyblish.api +from openpype.client import get_asset_by_name, get_assets from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, AYON_CONTAINER_ID, + legacy_io, ) from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -512,6 +514,134 @@ def get_subsequences(sequence: unreal.LevelSequence): return [] +def set_sequence_hierarchy( + seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths +): + # Get existing sequencer tracks or create them if they don't exist + tracks = seq_i.get_master_tracks() + subscene_track = None + visibility_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t + if not subscene_track: + subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) + if not visibility_track: + visibility_track = seq_i.add_master_track( + unreal.MovieSceneLevelVisibilityTrack) + + # Create the sub-scene section + subscenes = subscene_track.get_sections() + subscene = None + for s in subscenes: + if s.get_editor_property('sub_sequence') == seq_j: + subscene = s + break + if not subscene: + subscene = subscene_track.add_section() + subscene.set_row_index(len(subscene_track.get_sections())) + subscene.set_editor_property('sub_sequence', seq_j) + subscene.set_range( + min_frame_j, + max_frame_j + 1) + + # Create the visibility section + ar = unreal.AssetRegistryHelpers.get_asset_registry() + maps = [] + for m in map_paths: + # Unreal requires to load the level to get the map name + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(m) + maps.append(str(ar.get_asset_by_object_path(m).asset_name)) + + vis_section = visibility_track.add_section() + index = len(visibility_track.get_sections()) + + vis_section.set_range( + min_frame_j, + max_frame_j + 1) + vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) + vis_section.set_row_index(index) + vis_section.set_level_names(maps) + + if min_frame_j > 1: + hid_section = visibility_track.add_section() + hid_section.set_range( + 1, + min_frame_j) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + if max_frame_j < max_frame_i: + hid_section = visibility_track.add_section() + hid_section.set_range( + max_frame_j + 1, + max_frame_i + 1) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + + +def generate_sequence(h, h_dir): + tools = unreal.AssetToolsHelpers().get_asset_tools() + + sequence = tools.create_asset( + asset_name=h, + package_path=h_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + project_name = legacy_io.active_project() + asset_data = get_asset_by_name( + project_name, + h_dir.split('/')[-1], + fields=["_id", "data.fps"] + ) + + start_frames = [] + end_frames = [] + + elements = list(get_assets( + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(get_assets( + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + + min_frame = min(start_frames) + max_frame = max(end_frames) + + sequence.set_display_rate( + unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + sequence.set_playback_start(min_frame) + sequence.set_playback_end(max_frame) + + tracks = sequence.get_master_tracks() + track = None + for t in tracks: + if (t.get_class() == + unreal.MovieSceneCameraCutTrack.static_class()): + track = t + break + if not track: + track = sequence.add_master_track( + unreal.MovieSceneCameraCutTrack) + + return sequence, (min_frame, max_frame) + + @contextmanager def maintained_selection(): """Stub to be either implemented or replaced. diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 1bd398349f..02634104e7 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -6,13 +6,18 @@ import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary from unreal import EditorLevelUtils -from openpype.client import get_assets, get_asset_by_name +from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, +) class CameraLoader(plugin.Loader): @@ -24,32 +29,6 @@ class CameraLoader(plugin.Loader): icon = "cube" color = "orange" - def _set_sequence_hierarchy( - self, seq_i, seq_j, min_frame_j, max_frame_j - ): - tracks = seq_i.get_master_tracks() - track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - track = t - break - if not track: - track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - - subscenes = track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = track.add_section() - subscene.set_row_index(len(track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - def _import_camera( self, world, sequence, bindings, import_fbx_settings, import_filename ): @@ -156,9 +135,9 @@ class CameraLoader(plugin.Loader): if not EditorAssetLibrary.does_asset_exist(master_level): EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - level = f"{asset_path_parent}/{asset}_map.{asset}_map" + level = f"{asset_dir}/{asset}_map_camera.{asset}_map_camera" if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map") + EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map_camera") EditorLevelLibrary.load_level(master_level) EditorLevelUtils.add_level_to_world( @@ -169,27 +148,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(level) - project_name = legacy_io.active_project() - # TODO refactor - # - Creating of hierarchy should be a function in unreal integration - # - it's used in multiple loaders but must not be loader's logic - # - hard to say what is purpose of the loop - # - variables does not match their meaning - # - why scene is stored to sequences? - # - asset documents vs. elements - # - cleanup variable names in whole function - # - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc' - # - really inefficient queries of asset documents - # - existing asset in scene is considered as "with correct values" - # - variable 'elements' is modified during it's loop # Get all the sequences in the hierarchy. It will create them, if # they don't exist. - sequences = [] frame_ranges = [] - i = 0 - for h in hierarchy_dir_list: + sequences = [] + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): root_content = EditorAssetLibrary.list_assets( - h, recursive=False, include_folder=False) + h_dir, recursive=False, include_folder=False) existing_sequences = [ EditorAssetLibrary.find_asset_data(asset) @@ -199,48 +164,10 @@ class CameraLoader(plugin.Loader): ] if not existing_sequences: - scene = tools.create_asset( - asset_name=hierarchy[i], - package_path=h, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) + sequence, frame_range = generate_sequence(h, h_dir) - asset_data = get_asset_by_name( - project_name, - h.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - scene.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min_frame) - scene.set_playback_end(max_frame) - - sequences.append(scene) - frame_ranges.append((min_frame, max_frame)) + sequences.append(sequence) + frame_ranges.append(frame_range) else: for e in existing_sequences: sequences.append(e.get_asset()) @@ -248,8 +175,6 @@ class CameraLoader(plugin.Loader): e.get_asset().get_playback_start(), e.get_asset().get_playback_end())) - i += 1 - EditorAssetLibrary.make_directory(asset_dir) cam_seq = tools.create_asset( @@ -260,19 +185,24 @@ class CameraLoader(plugin.Loader): ) # Add sequences data to hierarchy - for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + for i in range(len(sequences) - 1): + set_sequence_hierarchy( sequences[i], sequences[i + 1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1]) + frame_ranges[i][1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1], + [level]) + project_name = legacy_io.active_project() data = get_asset_by_name(project_name, asset)["data"] cam_seq.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) cam_seq.set_playback_start(data.get('clipIn')) cam_seq.set_playback_end(data.get('clipOut') + 1) - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], cam_seq, - data.get('clipIn'), data.get('clipOut')) + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + [level]) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) @@ -307,7 +237,7 @@ class CameraLoader(plugin.Loader): key.set_time(unreal.FrameNumber(value=new_time)) # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -322,7 +252,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) EditorLevelLibrary.save_all_dirty_levels() @@ -360,7 +290,7 @@ class CameraLoader(plugin.Loader): sequences = ar.get_assets(filter) filter = unreal.ARFilter( class_names=["World"], - package_paths=[str(Path(asset_dir).parent.as_posix())], + package_paths=[asset_dir], recursive_paths=True) maps = ar.get_assets(filter) @@ -470,7 +400,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -484,15 +414,15 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) def remove(self, container): - path = Path(container.get("namespace")) - parent_path = str(path.parent.as_posix()) + asset_dir = container.get('namespace') + path = Path(asset_dir) ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"{str(path.as_posix())}"], + package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) if not sequences: raise Exception("Could not find sequence.") @@ -500,11 +430,11 @@ class CameraLoader(plugin.Loader): world = ar.get_asset_by_object_path( EditorLevelLibrary.get_editor_world().get_path_name()) - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"{parent_path}"], + package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list if not maps: @@ -534,12 +464,18 @@ class CameraLoader(plugin.Loader): root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_full_name() sequences = [master_sequence] @@ -547,10 +483,13 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: @@ -565,18 +504,45 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map_camera") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break assert parent, "Could not find the parent sequence" - EditorAssetLibrary.delete_directory(str(path.as_posix())) + # Create a temporary level to delete the layout level. + EditorLevelLibrary.save_all_dirty_levels() + EditorAssetLibrary.make_directory(f"{root}/tmp") + tmp_level = f"{root}/tmp/temp_map" + if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + EditorLevelLibrary.new_level(tmp_level) + else: + EditorLevelLibrary.load_level(tmp_level) + + # Delete the layout directory. + EditorAssetLibrary.delete_directory(asset_dir) + + EditorLevelLibrary.load_level(master_level) + EditorAssetLibrary.delete_directory(f"{root}/tmp") # Check if there isn't any more assets in the parent folder, and # delete it if not. asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True + path.parent.as_posix(), recursive=False, include_folder=True ) if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) + EditorAssetLibrary.delete_directory(path.parent.as_posix()) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e5f32c3412..1dfe6f1f5c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -13,7 +13,7 @@ from unreal import FBXImportType from unreal import MovieSceneLevelVisibilityTrack from unreal import MovieSceneSubTrack -from openpype.client import get_asset_by_name, get_assets, get_representations +from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -25,7 +25,13 @@ from openpype.pipeline import ( from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, + ls, +) class LayoutLoader(plugin.Loader): @@ -91,77 +97,6 @@ class LayoutLoader(plugin.Loader): return None - @staticmethod - def _set_sequence_hierarchy( - seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths - ): - # Get existing sequencer tracks or create them if they don't exist - tracks = seq_i.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if not subscene_track: - subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - if not visibility_track: - visibility_track = seq_i.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) - - # Create the sub-scene section - subscenes = subscene_track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = subscene_track.add_section() - subscene.set_row_index(len(subscene_track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - - # Create the visibility section - ar = unreal.AssetRegistryHelpers.get_asset_registry() - maps = [] - for m in map_paths: - # Unreal requires to load the level to get the map name - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(m) - maps.append(str(ar.get_asset_by_object_path(m).asset_name)) - - vis_section = visibility_track.add_section() - index = len(visibility_track.get_sections()) - - vis_section.set_range( - min_frame_j, - max_frame_j + 1) - vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) - vis_section.set_row_index(index) - vis_section.set_level_names(maps) - - if min_frame_j > 1: - hid_section = visibility_track.add_section() - hid_section.set_range( - 1, - min_frame_j) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - if max_frame_j < max_frame_i: - hid_section = visibility_track.add_section() - hid_section.set_range( - max_frame_j + 1, - max_frame_i + 1) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - def _transform_from_basis(self, transform, basis): """Transform a transform from a basis to a new basis.""" # Get the basis matrix @@ -352,63 +287,6 @@ class LayoutLoader(plugin.Loader): sec_params = section.get_editor_property('params') sec_params.set_editor_property('animation', animation) - @staticmethod - def _generate_sequence(h, h_dir): - tools = unreal.AssetToolsHelpers().get_asset_tools() - - sequence = tools.create_asset( - asset_name=h, - package_path=h_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - project_name = legacy_io.active_project() - asset_data = get_asset_by_name( - project_name, - h_dir.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) - - tracks = sequence.get_master_tracks() - track = None - for t in tracks: - if (t.get_class() == - unreal.MovieSceneCameraCutTrack.static_class()): - track = t - break - if not track: - track = sequence.add_master_track( - unreal.MovieSceneCameraCutTrack) - - return sequence, (min_frame, max_frame) - def _get_repre_docs_by_version_id(self, data): version_ids = { element.get("version") @@ -696,7 +574,7 @@ class LayoutLoader(plugin.Loader): ] if not existing_sequences: - sequence, frame_range = self._generate_sequence(h, h_dir) + sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) @@ -716,7 +594,7 @@ class LayoutLoader(plugin.Loader): # sequences and frame_ranges have the same length for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[i], sequences[i + 1], frame_ranges[i][1], frame_ranges[i + 1][0], frame_ranges[i + 1][1], @@ -729,7 +607,7 @@ class LayoutLoader(plugin.Loader): shot.set_playback_start(0) shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) if sequences: - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], shot, frame_ranges[-1][1], data.get('clipIn'), data.get('clipOut'), @@ -745,7 +623,7 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -761,7 +639,7 @@ class LayoutLoader(plugin.Loader): "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) asset_content = EditorAssetLibrary.list_assets( @@ -843,7 +721,7 @@ class LayoutLoader(plugin.Loader): "parent": str(representation["parent"]), "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -870,7 +748,7 @@ class LayoutLoader(plugin.Loader): root = "/Game/Ayon" path = Path(container.get("namespace")) - containers = unreal_pipeline.ls() + containers = ls() layout_containers = [ c for c in containers if (c.get('asset_name') != container.get('asset_name') and From 63d820436599fb7f3dfe54b6d86b58cbb1e35639 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 23 May 2023 15:46:16 +0800 Subject: [PATCH 661/918] Jakub's comment --- openpype/hosts/max/plugins/create/create_render.py | 2 +- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 9b677a615f..4523d3d411 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -23,6 +23,6 @@ class CreateRender(plugin.MaxCreator): sel_obj = self.selected_nodes if sel_obj: # set viewport camera for rendering(mandatory for deadline) - RenderSettings().set_render_camera(sel_obj) + RenderSettings(self.project_settings).set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index c807885859..9e1e7fdc72 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -5,7 +5,6 @@ from pymxs import runtime as rt from openpype.hosts.max.api import ( maintained_selection ) -from openpype.settings import get_project_settings class ExtractPointCloud(publish.Extractor): @@ -148,9 +147,7 @@ class ExtractPointCloud(publish.Extractor): @staticmethod def get_setting(instance): - project_setting = get_project_settings( - instance.context.data["projectName"] - ) + project_setting = instance.context.data["project_settings"] return project_setting["max"]["PointCloud"] def get_custom_attr(self, operator): From 4adf8388b4c08738c99b3f4fd8dfb778fb841dee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 09:17:54 +0100 Subject: [PATCH 662/918] Fix problem with updating camera --- .../hosts/unreal/plugins/load/load_camera.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 02634104e7..9361ed2ac5 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -345,17 +345,21 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if ss.get_sequence().get_name() == sequence_name: + if (ss.get_sequence().get_name() == + container.get('asset')): parent = s sub_scene = ss - # subscene_track.remove_section(ss) + subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) # Update subscenes indexes. @@ -364,10 +368,24 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break - assert parent, "Could not find the parent sequence" + assert parent, "Could not find the parent sequence" EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) From 341dc16701a147807388df3467ce6c84094f7c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 11:22:06 +0200 Subject: [PATCH 663/918] Update openpype/hosts/nuke/plugins/publish/collect_writes.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index dbd0963f0a..2d1caacdc3 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -133,7 +133,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, else: representation['files'] = collected_frames - self.log.debug("_ representation: {}".format(representation)) # inject colorspace data self.set_representation_colorspace( representation, instance.context, From b7556f76d5cbc61841ec4593c99fa20dd42d7023 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:23:14 +0100 Subject: [PATCH 664/918] Fix sequence when updating camera... again --- .../hosts/unreal/plugins/load/load_camera.py | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 9361ed2ac5..6f20a466a9 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,7 +270,7 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/ayon" + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -283,16 +283,16 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) - filter = unreal.ARFilter( + sequences = ar.get_assets(_filter) + _filter = unreal.ARFilter( class_names=["World"], package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list EditorLevelLibrary.load_level(maps[0].get_full_name()) @@ -328,14 +328,13 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. - root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() sequences = [master_sequence] @@ -345,43 +344,19 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None - visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if (ss.get_sequence().get_name() == - container.get('asset')): + if ss.get_sequence().get_name() == sequence_name: parent = s sub_scene = ss - subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) - # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() if parent: break From bf816562f09dc53510a8834e8e3c5ae9ba8b6597 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:48:17 +0100 Subject: [PATCH 665/918] Fix old camera not being deleted on update --- .../hosts/unreal/plugins/load/load_camera.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 6f20a466a9..8fd6cc3e80 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,17 +270,8 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/Ayon" - asset_dir = container.get('namespace') - context = representation.get("context") - - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - EditorLevelLibrary.save_current_level() _filter = unreal.ARFilter( @@ -295,7 +286,7 @@ class CameraLoader(plugin.Loader): maps = ar.get_assets(_filter) # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) level_sequence = sequences[0].get_asset() @@ -328,6 +319,7 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] _filter = unreal.ARFilter( @@ -336,6 +328,12 @@ class CameraLoader(plugin.Loader): recursive_paths=False) sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_asset().get_path_name() sequences = [master_sequence] From a762b310e8407a233185b2de18de09c4b0f44a27 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 11:51:23 +0200 Subject: [PATCH 666/918] inverting logic for `ignoreFrameHandleCheck` this was ignoring settings in frame range target. --- openpype/hosts/fusion/plugins/publish/collect_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index d0b7f1c4ff..551a365099 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -84,7 +84,7 @@ class CollectFusionRender( handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], ignoreFrameHandleCheck=( - inst.data["frame_range_source"] == "render_range"), + inst.data["frame_range_source"] == "asset_db"), frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, From a68aa029e4246aa77dcb67b003b3da7e7b650430 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 11:52:09 +0200 Subject: [PATCH 667/918] Renaming attribute to make more sense in Fusion context --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index f1e7791972..04898d0a45 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -233,7 +233,7 @@ class CreateSaver(NewCreator): def _get_frame_range_enum(self): frame_range_options = { "asset_db": "Current asset context", - "render_range": "From viewer render in/out", + "render_range": "From render in/out", "comp_range": "From composition timeline" } From 1b3b7b1a737004416042c00f8ab48ac263200351 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 12:41:33 +0200 Subject: [PATCH 668/918] Render instances with their explicit frame ranges --- .../plugins/publish/extract_render_local.py | 125 ++++++++++++------ 1 file changed, 85 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index f801f30577..1663ca04fa 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -1,6 +1,7 @@ import os import logging import contextlib +import collections import pyblish.api from openpype.pipeline import publish @@ -52,11 +53,14 @@ class FusionRenderLocal( hosts = ["fusion"] families = ["render.local"] + is_rendered_key = "_fusionrenderlocal_has_rendered" + def process(self, instance): - context = instance.context # Start render - self.render_once(context) + result = self.render(instance) + if result is False: + raise RuntimeError(f"Comp render failed for {instance}") self._add_representation(instance) @@ -69,52 +73,61 @@ class FusionRenderLocal( ) ) - def render_once(self, context): - """Render context comp only once, even with more render instances""" + def render(self, instance): + """Render instance. - # This plug-in assumes all render nodes get rendered at the same time - # to speed up the rendering. The check below makes sure that we only - # execute the rendering once and not for each instance. - key = f"__hasRun{self.__class__.__name__}" + We try to render the minimal amount of times by combining the instances + that have a matching frame range in one Fusion render. Then for the + batch of instances we store whether the - savers_to_render = [ - # Get the saver tool from the instance - instance.data["tool"] for instance in context if - # Only active instances - instance.data.get("publish", True) and - # Only render.local instances - "render.local" in instance.data.get("families", []) - ] + """ - if key not in context.data: - # We initialize as false to indicate it wasn't successful yet - # so we can keep track of whether Fusion succeeded - context.data[key] = False + if self.is_rendered_key in instance.data: + # This instance was already processed in batch with another + # instance, so we just return the render result directly + self.log.debug(f"Instance {instance} was already rendered") + return instance.data[self.is_rendered_key] - current_comp = context.data["currentComp"] - frame_start = context.data["frameStartHandle"] - frame_end = context.data["frameEndHandle"] + instances_by_frame_range = self.get_render_instances_by_frame_range( + instance.context + ) - self.log.info("Starting Fusion render") - self.log.info(f"Start frame: {frame_start}") - self.log.info(f"End frame: {frame_end}") - saver_names = ", ".join(saver.Name for saver in savers_to_render) - self.log.info(f"Rendering tools: {saver_names}") + # Render matching batch of instances that share the same frame range + frame_range = self.get_instance_render_frame_range(instance) + render_instances = instances_by_frame_range[frame_range] - with comp_lock_and_undo_chunk(current_comp): - with enabled_savers(current_comp, savers_to_render): - result = current_comp.Render( - { - "Start": frame_start, - "End": frame_end, - "Wait": True, - } - ) + # We initialize render state false to indicate it wasn't successful + # yet to keep track of whether Fusion succeeded. This is for cases + # where an error below this might cause the comp render result not + # to be stored for the instances of this batch + for render_instance in render_instances: + render_instance.data[self.is_rendered_key] = False - context.data[key] = bool(result) + savers_to_render = [inst.data["tool"] for inst in render_instances] + current_comp = instance.context.data["currentComp"] + frame_start, frame_end = frame_range - if context.data[key] is False: - raise RuntimeError("Comp render failed") + self.log.info( + f"Starting Fusion render frame range {frame_start}-{frame_end}" + ) + saver_names = ", ".join(saver.Name for saver in savers_to_render) + self.log.info(f"Rendering tools: {saver_names}") + + with comp_lock_and_undo_chunk(current_comp): + with enabled_savers(current_comp, savers_to_render): + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) + + # Store the render state for all the rendered instances + for render_instance in render_instances: + render_instance.data[self.is_rendered_key] = bool(result) + + return result def _add_representation(self, instance): """Add representation to instance""" @@ -151,3 +164,35 @@ class FusionRenderLocal( instance.data["representations"].append(repre) return instance + + def get_render_instances_by_frame_range(self, context): + """Return enabled render.local instances grouped by their frame range. + + Arguments: + context (pyblish.Context): The pyblish context + + Returns: + dict: (start, end): instances mapping + + """ + + instances_to_render = [ + instance for instance in context if + # Only active instances + instance.data.get("publish", True) and + # Only render.local instances + "render.local" in instance.data.get("families", []) + ] + + # Instances by frame ranges + instances_by_frame_range = collections.defaultdict(list) + for instance in instances_to_render: + start, end = self.get_instance_render_frame_range(instance) + instances_by_frame_range[(start, end)].append(instance) + + return dict(instances_by_frame_range) + + def get_instance_render_frame_range(self, instance): + start = instance.data["frameStartHandle"] + end = instance.data["frameEndHandle"] + return start, end From a2007887299de2f65b21e93522fbbe4de3277c63 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 11:50:34 +0100 Subject: [PATCH 669/918] Removed get_full_name() calls because of unexpected behaviour --- openpype/hosts/unreal/plugins/load/load_animation.py | 4 ++-- openpype/hosts/unreal/plugins/load/load_camera.py | 6 +++--- openpype/hosts/unreal/plugins/load/load_layout.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 778ddf693d..a5ecb677e8 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -156,7 +156,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{root}/{hierarchy[0]}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() + master_level = levels[0].get_asset().get_path_name() hierarchy_dir = root for h in hierarchy: @@ -168,7 +168,7 @@ class AnimationFBXLoader(plugin.Loader): package_paths=[f"{hierarchy_dir}/"], recursive_paths=True) levels = ar.get_assets(_filter) - level = levels[0].get_full_name() + level = levels[0].get_asset().get_path_name() unreal.EditorLevelLibrary.save_all_dirty_levels() unreal.EditorLevelLibrary.load_level(level) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 1bd398349f..072b3b1467 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -365,7 +365,7 @@ class CameraLoader(plugin.Loader): maps = ar.get_assets(filter) # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) level_sequence = sequences[0].get_asset() @@ -513,7 +513,7 @@ class CameraLoader(plugin.Loader): map = maps[0] EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(map.get_full_name()) + EditorLevelLibrary.load_level(map.get_asset().get_path_name()) # Remove the camera from the level. actors = EditorLevelLibrary.get_all_level_actors() @@ -523,7 +523,7 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.destroy_actor(a) EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(world.get_full_name()) + EditorLevelLibrary.load_level(world.get_asset().get_path_name()) # There should be only one sequence in the path. sequence_name = sequences[0].asset_name diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e5f32c3412..d94e6e5837 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -740,7 +740,7 @@ class LayoutLoader(plugin.Loader): loaded_assets = self._process(self.fname, asset_dir, shot) for s in sequences: - EditorAssetLibrary.save_asset(s.get_full_name()) + EditorAssetLibrary.save_asset(s.get_path_name()) EditorLevelLibrary.save_current_level() @@ -819,7 +819,7 @@ class LayoutLoader(plugin.Loader): recursive_paths=False) levels = ar.get_assets(filter) - layout_level = levels[0].get_full_name() + layout_level = levels[0].get_asset().get_path_name() EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(layout_level) @@ -919,7 +919,7 @@ class LayoutLoader(plugin.Loader): package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) levels = ar.get_assets(_filter) - master_level = levels[0].get_full_name() + master_level = levels[0].get_asset().get_path_name() sequences = [master_sequence] From 915c0934854f53914013291ef7d91ee39a8a23f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 13:55:58 +0200 Subject: [PATCH 670/918] Update openpype/hosts/resolve/hooks/pre_resolve_setup.py Co-authored-by: Roy Nieterau --- openpype/hosts/resolve/hooks/pre_resolve_setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 6747e773a3..d066fc2da2 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -101,7 +101,8 @@ class ResolvePrelaunch(PreLaunchHook): self.log.debug(f"PYTHONPATH: {self.launch_context.env['PYTHONPATH']}") - # add to the python path to PATH + # add the pythonhome folder to PATH because on Windows + # this is needed for Py3 to be correctly detected within Resolve env_path = self.launch_context.env["PATH"] self.log.info(f"Adding `{python3_home_str}` to the PATH variable") self.launch_context.env[ From 4f24356139425df00ad13e5dce943d739c2f63ee Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 14:20:30 +0200 Subject: [PATCH 671/918] Add validator for instance frame range to be within comp global in/out --- .../publish/validate_instance_frame_range.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py diff --git a/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py new file mode 100644 index 0000000000..06cd0ca186 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py @@ -0,0 +1,41 @@ +import pyblish.api + +from openpype.pipeline import PublishValidationError + + +class ValidateInstanceFrameRange(pyblish.api.InstancePlugin): + """Validate instance frame range is within comp's global render range.""" + + order = pyblish.api.ValidatorOrder + label = "Validate Filename Has Extension" + families = ["render"] + hosts = ["fusion"] + + def process(self, instance): + + context = instance.context + global_start = context.data["compFrameStart"] + global_end = context.data["compFrameEnd"] + + render_start = instance.data["frameStartHandle"] + render_end = instance.data["frameEndHandle"] + + if render_start < global_start or render_end > global_end: + + message = ( + f"Instance {instance} render frame range " + f"({render_start}-{render_end}) is outside of the comp's " + f"global render range ({global_start}-{global_end}) and thus " + f"can't be rendered. " + ) + description = ( + f"{message}\n\n" + f"Either update the comp's global range or the instance's " + f"frame range to ensure the comp's frame range includes the " + f"to render frame range for the instance." + ) + raise PublishValidationError( + title="Frame range outside of comp range", + message=message, + description=description + ) From af6ce0bf9fd9ac34f9afb7dfd39168f3258ea76f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 14:21:38 +0200 Subject: [PATCH 672/918] Fix docstring --- openpype/hosts/fusion/plugins/publish/extract_render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 1663ca04fa..564dca1796 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -78,7 +78,7 @@ class FusionRenderLocal( We try to render the minimal amount of times by combining the instances that have a matching frame range in one Fusion render. Then for the - batch of instances we store whether the + batch of instances we store whether the render succeeded or failed. """ From 8ac4cb499e9106c6f08b5beb2e933102a6b3ca3e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 23 May 2023 15:50:07 +0200 Subject: [PATCH 673/918] :rotating_light: style changes --- openpype/hosts/max/api/lib.py | 20 ++- openpype/hosts/max/api/plugin.py | 52 ++++--- .../hosts/max/plugins/create/create_render.py | 5 +- .../hosts/max/plugins/load/load_camera_fbx.py | 30 ++-- .../hosts/max/plugins/load/load_max_scene.py | 37 ++--- openpype/hosts/max/plugins/load/load_model.py | 45 +++--- .../hosts/max/plugins/load/load_model_fbx.py | 34 ++-- .../hosts/max/plugins/load/load_model_obj.py | 40 +++-- .../hosts/max/plugins/load/load_model_usd.py | 28 ++-- .../hosts/max/plugins/load/load_pointcache.py | 52 +++---- .../hosts/max/plugins/load/load_pointcloud.py | 33 ++-- .../max/plugins/publish/collect_members.py | 7 +- .../max/plugins/publish/extract_camera_abc.py | 24 +-- .../max/plugins/publish/extract_camera_fbx.py | 22 ++- .../max/plugins/publish/extract_model_usd.py | 17 +- .../max/plugins/publish/extract_pointcloud.py | 13 +- .../publish/validate_model_contents.py | 3 +- poetry.lock | 24 +-- setup.cfg | 146 +----------------- 19 files changed, 221 insertions(+), 411 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 132896805f..5718d8f112 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -1,16 +1,13 @@ # -*- coding: utf-8 -*- """Library of functions useful for 3dsmax pipeline.""" -import json -import six -from pymxs import runtime as rt -from typing import Union, Any, Dict import contextlib +import json +from typing import Any, Dict, Union +import six from openpype.pipeline.context_tools import ( - get_current_project_asset, - get_current_project -) - + get_current_project, get_current_project_asset,) +from pymxs import runtime as rt JSON_PREFIX = "JSON::" @@ -22,7 +19,7 @@ def imprint(node_name: str, data: dict) -> bool: for k, v in data.items(): if isinstance(v, (dict, list)): - rt.SetUserProp(node, k, f'{JSON_PREFIX}{json.dumps(v)}') + rt.SetUserProp(node, k, f"{JSON_PREFIX}{json.dumps(v)}") else: rt.SetUserProp(node, k, v) @@ -171,7 +168,7 @@ def set_scene_resolution(width: int, height: int): """ # make sure the render dialog is closed # for the update of resolution - # Changing the Render Setup dialog settingsshould be done + # Changing the Render Setup dialog settings should be done # with the actual Render Setup dialog in a closed state. if rt.renderSceneDialog.isOpen(): rt.renderSceneDialog.close() @@ -179,6 +176,7 @@ def set_scene_resolution(width: int, height: int): rt.renderWidth = width rt.renderHeight = height + def reset_scene_resolution(): """Apply the scene resolution from the project definition @@ -248,7 +246,7 @@ def reset_frame_range(fps: bool = True): frange_cmd = ( f"animationRange = interval {frame_start_handle} {frame_end_handle}" ) - rt.execute(frange_cmd) + rt.Execute(frange_cmd) set_render_frame_range(frame_start_handle, frame_end_handle) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 8df620b913..583e9dc1fb 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- """3dsmax specific Avalon/Pyblish plugin definitions.""" -from pymxs import runtime as rt -from typing import Union -import six from abc import ABCMeta -from openpype.pipeline import ( - CreatorError, - Creator, - CreatedInstance -) + +import six +from pymxs import runtime as rt + from openpype.lib import BoolDef -from .lib import imprint, read, lsattr +from openpype.pipeline import CreatedInstance, Creator, CreatorError + +from .lib import imprint, lsattr, read MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( @@ -100,17 +98,18 @@ class MaxCreatorBase(object): @staticmethod def cache_subsets(shared_data): - if shared_data.get("max_cached_subsets") is None: - shared_data["max_cached_subsets"] = {} - cached_instances = lsattr("id", "pyblish.avalon.instance") - for i in cached_instances: - creator_id = rt.getUserProp(i, "creator_identifier") - if creator_id not in shared_data["max_cached_subsets"]: - shared_data["max_cached_subsets"][creator_id] = [i.name] - else: - shared_data[ - "max_cached_subsets"][creator_id].append( - i.name) # noqa + if shared_data.get("max_cached_subsets"): + return shared_data + + shared_data["max_cached_subsets"] = {} + cached_instances = lsattr("id", "pyblish.avalon.instance") + for i in cached_instances: + creator_id = rt.GetUserProp(i, "creator_identifier") + if creator_id not in shared_data["max_cached_subsets"]: + shared_data["max_cached_subsets"][creator_id] = [i.name] + else: + shared_data[ + "max_cached_subsets"][creator_id].append(i.name) return shared_data @staticmethod @@ -127,9 +126,9 @@ class MaxCreatorBase(object): instance """ if isinstance(node, str): - node = rt.container(name=node) + node = rt.Container(name=node) - attrs = rt.execute(MS_CUSTOM_ATTRIB) + attrs = rt.Execute(MS_CUSTOM_ATTRIB) rt.custAttributes.add(node.baseObject, attrs) return node @@ -141,7 +140,7 @@ class MaxCreator(Creator, MaxCreatorBase): def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): - self.selected_nodes = rt.getCurrentSelection() + self.selected_nodes = rt.GetCurrentSelection() instance_node = self.create_instance_node(subset_name) instance_data["instance_node"] = instance_node.name @@ -196,9 +195,12 @@ class MaxCreator(Creator, MaxCreatorBase): """ for instance in instances: - if instance_node := rt.GetNodeByName(instance.data.get("instance_node")): # noqa + if instance_node := rt.GetNodeByName( + instance.data.get("instance_node")): rt.Select(instance_node) - rt.execute(f'for o in selection do for c in o.children do c.parent = undefined') # noqa + rt.Execute( + ("for o in selection do " + "for c in o.children do c.parent = undefined")) rt.Delete(instance_node) self._remove_instance_from_context(instance) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 4523d3d411..5b35453579 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -18,10 +18,7 @@ class CreateRender(plugin.MaxCreator): instance_data, pre_create_data) container_name = instance.data.get("instance_node") - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container - sel_obj = self.selected_nodes - if sel_obj: + if sel_obj := self.selected_nodes: # set viewport camera for rendering(mandatory for deadline) RenderSettings(self.project_settings).set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 35d7f4bad2..c51900dbb7 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -1,14 +1,12 @@ import os -from openpype.pipeline import ( - load, - get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib, maintained_selection +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class FbxLoader(load.LoaderPlugin): - """Fbx Loader""" + """Fbx Loader.""" families = ["camera"] representations = ["fbx"] @@ -24,17 +22,17 @@ class FbxLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Camera", True) rt.FBXImporterSetParam("AxisConversionMethod", True) rt.FBXImporterSetParam("Preserveinstances", True) - rt.importFile( + rt.ImportFile( filepath, rt.name("noPrompt"), using=rt.FBXIMP) - container = rt.getNodeByName(f"{name}") + container = rt.GetNodeByName(f"{name}") if not container: - container = rt.container() + container = rt.Container() container.name = f"{name}" - for selection in rt.getCurrentSelection(): + for selection in rt.GetCurrentSelection(): selection.Parent = container return containerise( @@ -44,8 +42,8 @@ class FbxLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) - rt.select(node.Children) + node = rt.GetNodeByName(container["instance_node"]) + rt.Select(node.Children) fbx_reimport_cmd = ( f""" @@ -57,10 +55,10 @@ FbxExporterSetParam "Preserveinstances" true importFile @"{path}" #noPrompt using:FBXIMP """) - rt.execute(fbx_reimport_cmd) + rt.Execute(fbx_reimport_cmd) with maintained_selection(): - rt.select(node) + rt.Select(node) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) @@ -72,5 +70,5 @@ importFile @"{path}" #noPrompt using:FBXIMP def remove(self, container): from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index cffc4ae559..4d9367b16f 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,13 +1,12 @@ import os -from openpype.pipeline import ( - load, get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class MaxSceneLoader(load.LoaderPlugin): - """Max Scene Loader""" + """Max Scene Loader.""" families = ["camera", "maxScene", @@ -24,16 +23,12 @@ class MaxSceneLoader(load.LoaderPlugin): # import the max scene by using "merge file" path = path.replace('\\', '/') - merge_before = { - c for c in rt.rootNode.Children - } - rt.mergeMaxFile(path) + merge_before = set(rt.RootNode.Children) + rt.MergeMaxFile(path) - merge_after = { - c for c in rt.rootNode.Children - } + merge_after = set(rt.RootNode.Children) max_objects = merge_after.difference(merge_before) - max_container = rt.container(name=f"{name}") + max_container = rt.Container(name=f"{name}") for max_object in max_objects: max_object.Parent = max_container @@ -46,18 +41,14 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] instance_name, _ = node_name.split("_") - merge_before = { - c for c in rt.rootNode.Children - } - rt.mergeMaxFile(path, + merge_before = set(rt.RootNode.Children) + rt.MergeMaxFile(path, rt.Name("noRedraw"), rt.Name("deleteOldDups"), rt.Name("useSceneMtlDups")) - merge_after = { - c for c in rt.rootNode.Children - } + merge_after = set(rt.EootNode.Children) max_objects = merge_after.difference(merge_before) - container_node = rt.getNodeByName(instance_name) + container_node = rt.GetNodeByName(instance_name) for max_object in max_objects: max_object.Parent = container_node @@ -71,5 +62,5 @@ class MaxSceneLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 95ee014e07..662b9fcb87 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -1,11 +1,10 @@ import os -from openpype.pipeline import ( - load, get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class ModelAbcLoader(load.LoaderPlugin): @@ -24,8 +23,8 @@ class ModelAbcLoader(load.LoaderPlugin): file_path = os.path.normpath(self.fname) abc_before = { - c for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer + c for c in rt.RootNode.Children + if rt.ClassOf(c) == rt.AlembicContainer } abc_import_cmd = (f""" @@ -38,11 +37,11 @@ importFile @"{file_path}" #noPrompt """) self.log.debug(f"Executing command: {abc_import_cmd}") - rt.execute(abc_import_cmd) + rt.Execute(abc_import_cmd) abc_after = { - c for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer + c for c in rt.RootNode.Children + if rt.ClassOf(c) == rt.AlembicContainer } # This should yield new AlembicContainer node @@ -59,22 +58,22 @@ importFile @"{file_path}" #noPrompt def update(self, container, representation): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) - rt.select(node.Children) + node = rt.GetNodeByName(container["instance_node"]) + rt.Select(node.Children) - for alembic in rt.selection: - abc = rt.getNodeByName(alembic.name) - rt.select(abc.Children) - for abc_con in rt.selection: - container = rt.getNodeByName(abc_con.name) + for alembic in rt.Selection: + abc = rt.GetNodeByName(alembic.name) + rt.Select(abc.Children) + for abc_con in rt.Selection: + container = rt.GetNodeByName(abc_con.name) container.source = path - rt.select(container.Children) - for abc_obj in rt.selection: - alembic_obj = rt.getNodeByName(abc_obj.name) + rt.Select(container.Children) + for abc_obj in rt.Selection: + alembic_obj = rt.GetNodeByName(abc_obj.name) alembic_obj.source = path with maintained_selection(): - rt.select(node) + rt.Select(node) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) @@ -86,8 +85,8 @@ importFile @"{file_path}" #noPrompt def remove(self, container): from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) @staticmethod def get_container_children(parent, type_name): @@ -102,7 +101,7 @@ importFile @"{file_path}" #noPrompt filtered = [] for child in list_children(parent): - class_type = str(rt.classOf(child.baseObject)) + class_type = str(rt.ClassOf(child.baseObject)) if class_type == type_name: filtered.append(child) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 01e6acae12..2aef6f02c2 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -1,15 +1,13 @@ import os -from openpype.pipeline import ( - load, - get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class FbxModelLoader(load.LoaderPlugin): - """Fbx Model Loader""" + """Fbx Model Loader.""" families = ["model"] representations = ["fbx"] @@ -24,17 +22,17 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Preserveinstances", True) - rt.importFile( + rt.ImportFile( filepath, - rt.name("noPrompt"), + rt.Name("noPrompt"), using=rt.FBXIMP) - container = rt.getNodeByName(f"{name}") + container = rt.GetNodeByName(name) if not container: - container = rt.container() - container.name = f"{name}" + container = rt.Container() + container.name = name - for selection in rt.getCurrentSelection(): + for selection in rt.GetCurrentSelection(): selection.Parent = container return containerise( @@ -44,8 +42,8 @@ class FbxModelLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) - rt.select(node.Children) + node = rt.GetNodeByName(container["instance_node"]) + rt.Select(node.Children) fbx_reimport_cmd = ( f""" FBXImporterSetParam "Animation" false @@ -56,10 +54,10 @@ FbxExporterSetParam "Preserveinstances" true importFile @"{path}" #noPrompt using:FBXIMP """) - rt.execute(fbx_reimport_cmd) + rt.Execute(fbx_reimport_cmd) with maintained_selection(): - rt.select(node) + rt.Select(node) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) @@ -71,5 +69,5 @@ importFile @"{path}" #noPrompt using:FBXIMP def remove(self, container): from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_model_obj.py b/openpype/hosts/max/plugins/load/load_model_obj.py index ae42e1f3d3..77d4e08cfb 100644 --- a/openpype/hosts/max/plugins/load/load_model_obj.py +++ b/openpype/hosts/max/plugins/load/load_model_obj.py @@ -1,15 +1,13 @@ import os -from openpype.pipeline import ( - load, - get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class ObjLoader(load.LoaderPlugin): - """Obj Loader""" + """Obj Loader.""" families = ["model"] representations = ["obj"] @@ -21,18 +19,18 @@ class ObjLoader(load.LoaderPlugin): from pymxs import runtime as rt filepath = os.path.normpath(self.fname) - self.log.debug(f"Executing command to import..") + self.log.debug("Executing command to import..") - rt.execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') + rt.Execute(f'importFile @"{filepath}" #noPrompt using:ObjImp') # create "missing" container for obj import - container = rt.container() - container.name = f"{name}" + container = rt.Container() + container.name = name # get current selection - for selection in rt.getCurrentSelection(): + for selection in rt.GetCurrentSelection(): selection.Parent = container - asset = rt.getNodeByName(f"{name}") + asset = rt.GetNodeByName(name) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -42,20 +40,20 @@ class ObjLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] - node = rt.getNodeByName(node_name) + node = rt.GetNodeByName(node_name) instance_name, _ = node_name.split("_") - container = rt.getNodeByName(instance_name) - for n in container.Children: - rt.delete(n) + container = rt.GetNodeByName(instance_name) + for child in container.Children: + rt.Delete(child) - rt.execute(f'importFile @"{path}" #noPrompt using:ObjImp') + rt.Execute(f'importFile @"{path}" #noPrompt using:ObjImp') # get current selection - for selection in rt.getCurrentSelection(): + for selection in rt.GetCurrentSelection(): selection.Parent = container with maintained_selection(): - rt.select(node) + rt.Select(node) lib.imprint(node_name, { "representation": str(representation["_id"]) @@ -67,5 +65,5 @@ class ObjLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_model_usd.py b/openpype/hosts/max/plugins/load/load_model_usd.py index 143f91f40b..2b34669278 100644 --- a/openpype/hosts/max/plugins/load/load_model_usd.py +++ b/openpype/hosts/max/plugins/load/load_model_usd.py @@ -1,10 +1,9 @@ import os -from openpype.pipeline import ( - load, get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class ModelUSDLoader(load.LoaderPlugin): @@ -19,6 +18,7 @@ class ModelUSDLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt + # asset_filepath filepath = os.path.normpath(self.fname) import_options = rt.USDImporter.CreateOptions() @@ -27,11 +27,11 @@ class ModelUSDLoader(load.LoaderPlugin): log_filepath = filepath.replace(ext, "txt") rt.LogPath = log_filepath - rt.LogLevel = rt.name('info') + rt.LogLevel = rt.Name("info") rt.USDImporter.importFile(filepath, importOptions=import_options) - asset = rt.getNodeByName(f"{name}") + asset = rt.GetNodeByName(name) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -41,11 +41,11 @@ class ModelUSDLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] - node = rt.getNodeByName(node_name) + node = rt.GetNodeByName(node_name) for n in node.Children: for r in n.Children: - rt.delete(r) - rt.delete(n) + rt.Delete(r) + rt.Delete(n) instance_name, _ = node_name.split("_") import_options = rt.USDImporter.CreateOptions() @@ -54,15 +54,15 @@ class ModelUSDLoader(load.LoaderPlugin): log_filepath = path.replace(ext, "txt") rt.LogPath = log_filepath - rt.LogLevel = rt.name('info') + rt.LogLevel = rt.Name("info") rt.USDImporter.importFile(path, importOptions=import_options) - asset = rt.getNodeByName(f"{instance_name}") + asset = rt.GetNodeByName(instance_name) asset.Parent = node with maintained_selection(): - rt.select(node) + rt.Select(node) lib.imprint(node_name, { "representation": str(representation["_id"]) @@ -74,5 +74,5 @@ class ModelUSDLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 37cb6791db..4f7773d967 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -5,11 +5,10 @@ Because of limited api, alembics can be only loaded, but not easily updated. """ import os -from openpype.pipeline import ( - load, get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib, maintained_selection +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class AbcLoader(load.LoaderPlugin): @@ -30,29 +29,28 @@ class AbcLoader(load.LoaderPlugin): file_path = os.path.normpath(self.fname) abc_before = { - c for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer + c for c in rt.RootNode.Children + if rt.ClassOf(c) == rt.AlembicContainer } rt.AlembicImport.ImportToRoot = False rt.AlembicImport.StartFrame = True rt.AlembicImport.EndFrame = True - rt.importFile(file_path, rt.name("noPrompt")) + rt.ImportFile(file_path, rt.Name("noPrompt")) abc_after = { - c for c in rt.rootNode.Children - if rt.classOf(c) == rt.AlembicContainer + c for c in rt.RootNode.Children + if rt.ClassOf(c) == rt.AlembicContainer } # This should yield new AlembicContainer node abc_containers = abc_after.difference(abc_before) - if len(abc_containers) != 1: self.log.error("Something failed when loading.") abc_container = abc_containers.pop() - for abc in rt.getCurrentSelection(): + for abc in rt.GetCurrentSelection(): for cam_shape in abc.Children: cam_shape.playbackType = 2 @@ -63,27 +61,25 @@ class AbcLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) + node = rt.GetNodeByName(container["instance_node"]) lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) }) - rt.select(node.Children) - - for alembic in rt.selection: - abc = rt.getNodeByName(alembic.name) - rt.select(abc.Children) - for abc_con in rt.selection: - container = rt.getNodeByName(abc_con.name) - container.source = path - rt.select(container.Children) - for abc_obj in rt.selection: - alembic_obj = rt.getNodeByName(abc_obj.name) - alembic_obj.source = path - with maintained_selection(): - rt.select(node) + rt.Select(node.Children) + + for alembic in rt.Selection: + abc = rt.GetNodeByName(alembic.name) + rt.Select(abc.Children) + for abc_con in rt.Selection: + container = rt.GetNodeByName(abc_con.name) + container.source = path + rt.Select(container.Children) + for abc_obj in rt.Selection: + alembic_obj = rt.GetNodeByName(abc_obj.name) + alembic_obj.source = path def switch(self, container, representation): self.update(container, representation) @@ -91,8 +87,8 @@ class AbcLoader(load.LoaderPlugin): def remove(self, container): from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) @staticmethod def get_container_children(parent, type_name): diff --git a/openpype/hosts/max/plugins/load/load_pointcloud.py b/openpype/hosts/max/plugins/load/load_pointcloud.py index d4ae721c8a..8634e1d51f 100644 --- a/openpype/hosts/max/plugins/load/load_pointcloud.py +++ b/openpype/hosts/max/plugins/load/load_pointcloud.py @@ -1,13 +1,12 @@ import os -from openpype.pipeline import ( - load, get_representation_path -) -from openpype.hosts.max.api.pipeline import containerise + from openpype.hosts.max.api import lib, maintained_selection +from openpype.hosts.max.api.pipeline import containerise +from openpype.pipeline import get_representation_path, load class PointCloudLoader(load.LoaderPlugin): - """Point Cloud Loader""" + """Point Cloud Loader.""" families = ["pointcloud"] representations = ["prt"] @@ -23,7 +22,7 @@ class PointCloudLoader(load.LoaderPlugin): obj = rt.tyCache() obj.filename = filepath - prt_container = rt.getNodeByName(f"{obj.name}") + prt_container = rt.GetNodeByName(obj.name) return containerise( name, [prt_container], context, loader=self.__class__.__name__) @@ -33,18 +32,16 @@ class PointCloudLoader(load.LoaderPlugin): from pymxs import runtime as rt path = get_representation_path(representation) - node = rt.getNodeByName(container["instance_node"]) - rt.select(node.Children) - for prt in rt.selection: - prt_object = rt.getNodeByName(prt.name) - prt_object.filename = path - + node = rt.GetNodeByName(container["instance_node"]) with maintained_selection(): - rt.select(node) + rt.Select(node.Children) + for prt in rt.Selection: + prt_object = rt.GetNodeByName(prt.name) + prt_object.filename = path - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) def switch(self, container, representation): self.update(container, representation) @@ -53,5 +50,5 @@ class PointCloudLoader(load.LoaderPlugin): """remove the container""" from pymxs import runtime as rt - node = rt.getNodeByName(container["instance_node"]) - rt.delete(node) + node = rt.GetNodeByName(container["instance_node"]) + rt.Delete(node) diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py index 4ceb6cdadf..54020d7dae 100644 --- a/openpype/hosts/max/plugins/publish/collect_members.py +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -"""Collect instance members""" +"""Collect instance members.""" import pyblish.api from pymxs import runtime as rt class CollectMembers(pyblish.api.InstancePlugin): - """Collect Render for Deadline""" + """Collect Render for Deadline.""" order = pyblish.api.CollectorOrder + 0.01 label = "Collect Instance Members" @@ -16,5 +16,6 @@ class CollectMembers(pyblish.api.InstancePlugin): if instance.data.get("instance_node"): container = rt.GetNodeByName(instance.data["instance_node"]) instance.data["members"] = [ - i.node for i in container.openPypeData.all_handles + member.node for member + in container.openPypeData.all_handles ] diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index d53c47fb51..c526de8960 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -1,14 +1,14 @@ import os + import pyblish.api -from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection, get_all_children + +from openpype.hosts.max.api import get_all_children, maintained_selection +from openpype.pipeline import OptionalPyblishPluginMixin, publish class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): - """ - Extract Camera with AlembicExport - """ + """Extract Camera with AlembicExport.""" order = pyblish.api.ExtractorOrder - 0.1 label = "Extract Alembic Camera" @@ -31,20 +31,20 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): path = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) + self.log.info(f"Writing alembic '{filename}' to '{stagingdir}'") - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.ArchiveType = rt.Name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.Name("maya") rt.AlembicExport.StartFrame = start rt.AlembicExport.EndFrame = end rt.AlembicExport.CustomAttributes = True with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.exportFile( + rt.Select(get_all_children(rt.GetNodeByName(container))) + rt.ExportFile( path, - rt.name("noPrompt"), + rt.Name("noPrompt"), selectedOnly=True, using=rt.AlembicExport, ) @@ -62,4 +62,4 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): "frameEnd": end, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) + self.log.info(f"Extracted instance '{instance.name}' to: {path}") diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 4b4b349e19..0c8a82dcaa 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -1,14 +1,14 @@ import os + import pyblish.api -from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection, get_all_children + +from openpype.hosts.max.api import get_all_children, maintained_selection +from openpype.pipeline import OptionalPyblishPluginMixin, publish class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): - """ - Extract Camera with FbxExporter - """ + """Extract Camera with FbxExporter.""" order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Fbx Camera" @@ -26,7 +26,7 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) + self.log.info(f"Writing fbx file '{filename}' to '{filepath}'") rt.FBXExporterSetParam("Animation", True) rt.FBXExporterSetParam("Cameras", True) @@ -36,10 +36,10 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.exportFile( + rt.Select(get_all_children(rt.GetNodeByName(container))) + rt.ExportFile( filepath, - rt.name("noPrompt"), + rt.Name("noPrompt"), selectedOnly=True, using=rt.FBXEXP, ) @@ -55,6 +55,4 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info( - "Extracted instance '%s' to: %s" % (instance.name, filepath) - ) + self.log.info(f"Extracted instance '{instance.name}' to: {filepath}") diff --git a/openpype/hosts/max/plugins/publish/extract_model_usd.py b/openpype/hosts/max/plugins/publish/extract_model_usd.py index 2500e6c905..da37c77bf7 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_usd.py +++ b/openpype/hosts/max/plugins/publish/extract_model_usd.py @@ -1,20 +1,15 @@ import os + import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection -) + +from openpype.hosts.max.api import maintained_selection +from openpype.pipeline import OptionalPyblishPluginMixin, publish class ExtractModelUSD(publish.Extractor, OptionalPyblishPluginMixin): - """ - Extract Geometry in USDA Format - """ + """Extract Geometry in USDA Format.""" order = pyblish.api.ExtractorOrder - 0.05 label = "Extract Geometry (USD)" @@ -44,7 +39,7 @@ class ExtractModelUSD(publish.Extractor, with maintained_selection(): # select and export node_list = instance.data["members"] - rt.select(node_list) + rt.Select(node_list) rt.USDExporter.ExportFile(asset_filepath, exportOptions=export_options, contentSource=rt.Name("selected"), diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 9e1e7fdc72..618f9856fd 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -1,15 +1,15 @@ import os + import pyblish.api -from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection -) + +from openpype.hosts.max.api import maintained_selection +from openpype.pipeline import publish class ExtractPointCloud(publish.Extractor): """ - Extract PRT format with tyFlow operators + Extract PRT format with tyFlow operators. Notes: Currently only works for the default partition setting @@ -112,8 +112,7 @@ class ExtractPointCloud(publish.Extractor): job_args.append(mode) additional_args = self.get_custom_attr(operator) - for args in additional_args: - job_args.append(args) + job_args.extend(iter(additional_args)) prt_export = f"{operator}.exportPRT()" job_args.append(prt_export) diff --git a/openpype/hosts/max/plugins/publish/validate_model_contents.py b/openpype/hosts/max/plugins/publish/validate_model_contents.py index 1d834292b8..1ec08d9c5f 100644 --- a/openpype/hosts/max/plugins/publish/validate_model_contents.py +++ b/openpype/hosts/max/plugins/publish/validate_model_contents.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import pyblish.api -from openpype.pipeline import PublishValidationError from pymxs import runtime as rt +from openpype.pipeline import PublishValidationError + class ValidateModelContent(pyblish.api.InstancePlugin): """Validates Model instance contents. diff --git a/poetry.lock b/poetry.lock index 563f905fad..f71611cb6f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry and should not be changed by hand. [[package]] name = "acre" @@ -1456,13 +1456,11 @@ python-versions = ">=3.6" files = [ {file = "lief-0.12.3-cp310-cp310-macosx_10_14_arm64.whl", hash = "sha256:66724f337e6a36cea1a9380f13b59923f276c49ca837becae2e7be93a2e245d9"}, {file = "lief-0.12.3-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:6d18aafa2028587c98f6d4387bec94346e92f2b5a8a5002f70b1cf35b1c045cc"}, - {file = "lief-0.12.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f69d125caaa8d5ddb574f29cc83101e165ebea1a9f18ad042eb3544081a797"}, {file = "lief-0.12.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c078d6230279ffd3bca717c79664fb8368666f610b577deb24b374607936e9c1"}, {file = "lief-0.12.3-cp310-cp310-win32.whl", hash = "sha256:e3a6af926532d0aac9e7501946134513d63217bacba666e6f7f5a0b7e15ba236"}, {file = "lief-0.12.3-cp310-cp310-win_amd64.whl", hash = "sha256:0750b72e3aa161e1fb0e2e7f571121ae05d2428aafd742ff05a7656ad2288447"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_arm64.whl", hash = "sha256:b5c123cb99a7879d754c059e299198b34e7e30e3b64cf22e8962013db0099f47"}, {file = "lief-0.12.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:8bc58fa26a830df6178e36f112cb2bbdd65deff593f066d2d51434ff78386ba5"}, - {file = "lief-0.12.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74ac6143ac6ccd813c9b068d9c5f1f9d55c8813c8b407387eb57de01c3db2d74"}, {file = "lief-0.12.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04eb6b70d646fb5bd6183575928ee23715550f161f2832cbcd8c6ff2071fb408"}, {file = "lief-0.12.3-cp311-cp311-win32.whl", hash = "sha256:7e2d0a53c403769b04adcf8df92e83c5e25f9103a052aa7f17b0a9cf057735fb"}, {file = "lief-0.12.3-cp311-cp311-win_amd64.whl", hash = "sha256:7f6395c12ee1bc4a5162f567cba96d0c72dfb660e7902e84d4f3029daf14fe33"}, @@ -1482,7 +1480,6 @@ files = [ {file = "lief-0.12.3-cp38-cp38-win_amd64.whl", hash = "sha256:b00667257b43e93d94166c959055b6147d46d302598f3ee55c194b40414c89cc"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_arm64.whl", hash = "sha256:e6a1b5b389090d524621c2455795e1262f62dc9381bedd96f0cd72b878c4066d"}, {file = "lief-0.12.3-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:ae773196df814202c0c51056163a1478941b299512b09660a3c37be3c7fac81e"}, - {file = "lief-0.12.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:66ddf88917ec7b00752687c476bb2771dc8ec19bd7e4c0dcff1f8ef774cad4e9"}, {file = "lief-0.12.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:4a47f410032c63ac3be051d963d0337d6b47f0e94bfe8e946ab4b6c428f4d0f8"}, {file = "lief-0.12.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbd11367c2259bd1131a6c8755dcde33314324de5ea029227bfbc7d3755871e6"}, {file = "lief-0.12.3-cp39-cp39-win32.whl", hash = "sha256:2ce53e311918c3e5b54c815ef420a747208d2a88200c41cd476f3dd1eb876bcf"}, @@ -2355,7 +2352,7 @@ files = [ cffi = ">=1.4.1" [package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +docs = ["sphinx (>=1.6.5)", "sphinx_rtd_theme"] tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] @@ -3248,21 +3245,6 @@ files = [ [package.dependencies] six = "*" -[[package]] -name = "wemake-python-styleguide" -version = "0.0.1" -description = "Opinionated styleguide that we use in wemake.services projects" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "wemake-python-styleguide-0.0.1.tar.gz", hash = "sha256:e1f47a2be6aa79ca8a1cfbbbffdd67bf4df32b76306f4c3dd2a620a2af78e671"}, - {file = "wemake_python_styleguide-0.0.1-py2.py3-none-any.whl", hash = "sha256:505a19d82f9c4f450c6f06bb8c74d86c99cabcc4d5e6d8ea70e90b13b049f34f"}, -] - -[package.dependencies] -flake8 = "*" - [[package]] name = "wheel" version = "0.38.4" @@ -3480,4 +3462,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.9.1,<3.10" -content-hash = "45e91b47f9e6697b0eb9fdbe76981f691d389ce74bc5a0e98d72e1109b39bc63" +content-hash = "02daca205796a0f29a0d9f50707544e6804f32027eba493cd2aa7f175a00dcea" diff --git a/setup.cfg b/setup.cfg index 1d57657a19..42cacdc93c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [flake8] +# ignore = D203 +ignore = BLK100, W504, W503 max-line-length = 79 -strictness = short exclude = .git, __pycache__, @@ -9,149 +10,8 @@ exclude = website, openpype/vendor, *deadline/repository/custom/plugins -max-complexity = 30 -ignore = - # line break before binary operator - W503, - # line break occurred after a binary operator - W504, - # wemake-python-styleguide warnings - # See https://wemake-python-stylegui.de/en/latest/pages/usage/violations/index.html for doc - # Found incorrect module name pattern - WPS102, - # Found wrong variable name - WPS110, - # Found too short name - WPS111, - # Found upper-case constant in a class - WPS115, - # Found module with too many imports - WPS201, - # Found too many module members - WPS202, - # Found overused expression - WPS204, - # Found too many local variables - WPS210, - # Found too many arguments - WPS211, - # Found too many return statements - WPS212, - # Found too many expressions - WPS213, - # Found too many methods - WPS214, - # Found too many await expressions - WPS217, - # Found line with high Jones Complexity - WPS221, - # Found too many `elif` branches - WPS223, - # Found string constant over-use - WPS226, - # Found too long try body length - WPS229, - # Found too many public instance attributes - WPS230, - # Found function with too much cognitive complexity - WPS231, - # Found module cognitive complexity that is too high - WPS232, - # Found too many imported names from a module - WPS235, - # Found too many raises in a function - WPS238, - # Found too deep nesting - WPS220, - # Found `f` string - WPS305, - # Found incorrect multi-line parameters - WPS317, - # Found extra indentation - WPS318, - # Found bracket in wrong position - WPS319, - # Found percent string formatting - WPS323, - # Found implicit string concatenation - WPS326, - # Found variables that are only used for `return` - WPS331, - # Found explicit string concatenation - WPS336, - # Found multiline conditions - WPS337, - # Found incorrect order of methods in a class - WPS338, - # Found line starting with a dot - WPS348, - # Found multiline loop - WPS352, - # Found incorrect unpacking target - WPS414, - # Found wrong keyword - WPS420, - # Found wrong function - WPS421, - # Found statement that has no effect - WPS428, - # Found nested function - WPS430, - # Found magic number - WPS432, - # Found protected attribute usage - WPS437, - # Found block variables overlap - WPS440, - # Found an infinite while loop - WPS457, - # Found a getter without a return value - WPS463, - # Found negated condition - WPS504, - # flake8-quotes warnings - # Remove bad quotes - Q000, - # Remove bad quotes from multiline string - Q001, - # Darglint warnings - # Incorrect indentation - DAR003, - # Excess parameter(s) in Docstring - DAR102, - # Excess exception(s) in Raises section - DAR402, - # pydocstyle warnings - # Missing docstring in __init_ - D107, - # White space formatting for doc strings - D2, - # First line should end with a period - D400, - # Others - # function name - N802, - # Found backslash that is used for line breaking - N400, - E501, - S105, - RST, - # Black would make changes error - BLK100, - # Imperative mood of the first line on docstrings - D401, -[isort] -profile=wemake -src_paths=isort,test -# isort configuration: -# https://github.com/timothycrosley/isort/wiki/isort-Settings -include_trailing_comma = true -use_parentheses = true -# See https://github.com/timothycrosley/isort#multi-line-output-modes -multi_line_output = 3 -# Is the same as 80 in flake8: -line_length = 79 +max-complexity = 30 [pylint.'MESSAGES CONTROL'] disable = no-member From cccc0acd1dc9c6fd9d00fa56abfaebc1de1d327d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 23 May 2023 22:00:40 +0800 Subject: [PATCH 674/918] add docs --- website/docs/artist_hosts_houdini.md | 24 +++++++++++++----- .../assets/houdini_render_publish_creator.png | Bin 0 -> 82356 bytes 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 website/docs/assets/houdini_render_publish_creator.png diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index 8874a0b5cf..0471765365 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -14,7 +14,7 @@ sidebar_label: Houdini - [Library Loader](artist_tools_library-loader) ## Publishing Alembic Cameras -You can publish baked camera in Alembic format. +You can publish baked camera in Alembic format. Select your camera and go **OpenPype -> Create** and select **Camera (abc)**. This will create Alembic ROP in **out** with path and frame range already set. This node will have a name you've @@ -30,7 +30,7 @@ You can use any COP node and publish the image sequence generated from it. For e ![Noise COP](assets/houdini_imagesequence_cop.png) To publish the output of the `radialblur1` go to **OpenPype -> Create** and -select **Composite (Image Sequence)**. If you name the variant *Noise* this will create the `/out/imagesequenceNoise` Composite ROP with the frame range set. +select **Composite (Image Sequence)**. If you name the variant *Noise* this will create the `/out/imagesequenceNoise` Composite ROP with the frame range set. When you hit **Publish** it will render image sequence from selected node. @@ -56,14 +56,14 @@ Now select the `output0` node and go **OpenPype -> Create** and select **Point C Alembic ROP `/out/pointcacheStrange` ## Publishing Reviews (OpenGL) -To generate a review output from Houdini you need to create a **review** instance. +To generate a review output from Houdini you need to create a **review** instance. Go to **OpenPype -> Create** and select **Review**. ![Houdini Create Review](assets/houdini_review_create_attrs.png) -On create, with the **Use Selection** checkbox enabled it will set up the first -camera found in your selection as the camera for the OpenGL ROP node and other -non-cameras are set in **Force Objects**. It will then render those even if +On create, with the **Use Selection** checkbox enabled it will set up the first +camera found in your selection as the camera for the OpenGL ROP node and other +non-cameras are set in **Force Objects**. It will then render those even if their display flag is disabled in your scene. ## Redshift @@ -71,6 +71,18 @@ their display flag is disabled in your scene. This part of documentation is still work in progress. ::: +## Publishing Render to Deadline +Five Renderers(Arnold, Redshift, Mantra, Karma, VRay) are supported for Render Publishing. +They are named with the suffix("_ROP") +To submit render to deadline, you need to create a **Render** instance. +Go to **Openpype -> Create** and select **Publish**. Before clicking **Create** button, +you need select your preferred image rendering format. You can also enable the **Use selection** to +select your render camera. +![Houdini Create Render](assets/houdini_render_publish_creator.png) + +All the render outputs are stored in the pyblish/render directory within your project path.\ +For Karma-specific render, it also outputs the USD render as default. + ## USD (experimental support) ### Publishing USD You can publish your Solaris Stage as USD file. diff --git a/website/docs/assets/houdini_render_publish_creator.png b/website/docs/assets/houdini_render_publish_creator.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd73d296a416a49291c2bb8729541780178ac60 GIT binary patch literal 82356 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 Date: Tue, 23 May 2023 16:18:58 +0200 Subject: [PATCH 675/918] fixing frame range data passing from instance --- .../hosts/fusion/plugins/publish/collect_render.py | 6 ++++-- .../pipeline/publish/abstract_collect_render.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index 551a365099..a20a142701 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -17,6 +17,8 @@ class FusionRenderInstance(RenderInstance): tool = attr.ib(default=None) workfileComp = attr.ib(default=None) publish_attributes = attr.ib(default={}) + frameStartHandle = attr.ib(default=None) + frameEndHandle = attr.ib(default=None) class CollectFusionRender( @@ -83,8 +85,8 @@ class CollectFusionRender( frameEnd=inst.data["frameEnd"], handleStart=inst.data["handleStart"], handleEnd=inst.data["handleEnd"], - ignoreFrameHandleCheck=( - inst.data["frame_range_source"] == "asset_db"), + frameStartHandle=inst.data["frameStartHandle"], + frameEndHandle=inst.data["frameEndHandle"], frameStep=1, fps=comp_frame_format_prefs.get("Rate"), app_version=comp.GetApp().Version, diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index fd35ddb719..1e392d25e3 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -167,16 +167,27 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): frame_start_render = int(render_instance.frameStart) frame_end_render = int(render_instance.frameEnd) + """ TODO: Needs to be refofactored because this + seems to be very hacky + """ if (render_instance.ignoreFrameHandleCheck or int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 - + # only for Harmony where frame range cannot be set by DB handle_start = context.data['handleStart'] handle_end = context.data['handleEnd'] frame_start = context.data['frameStart'] frame_end = context.data['frameEnd'] frame_start_handle = context.data['frameStartHandle'] frame_end_handle = context.data['frameEndHandle'] + elif (hasattr(render_instance, "frameStartHandle") + and hasattr(render_instance, "frameEndHandle")): + handle_start = int(render_instance.handleStart) + handle_end = int(render_instance.handleEnd) + frame_start = int(render_instance.frameStart) + frame_end = int(render_instance.frameEnd) + frame_start_handle = int(render_instance.frameStartHandle) + frame_end_handle = int(render_instance.frameEndHandle) else: handle_start = 0 handle_end = 0 From 561b4cb5d52e2f201a82f029a09b9b450a02085b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 23 May 2023 16:31:28 +0200 Subject: [PATCH 676/918] Update openpype/pipeline/publish/abstract_collect_render.py Co-authored-by: Roy Nieterau --- openpype/pipeline/publish/abstract_collect_render.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/abstract_collect_render.py b/openpype/pipeline/publish/abstract_collect_render.py index 1e392d25e3..6877d556c3 100644 --- a/openpype/pipeline/publish/abstract_collect_render.py +++ b/openpype/pipeline/publish/abstract_collect_render.py @@ -167,9 +167,7 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): frame_start_render = int(render_instance.frameStart) frame_end_render = int(render_instance.frameEnd) - """ TODO: Needs to be refofactored because this - seems to be very hacky - """ + # TODO: Refactor hacky frame range workaround below if (render_instance.ignoreFrameHandleCheck or int(context.data['frameStartHandle']) == frame_start_render and int(context.data['frameEndHandle']) == frame_end_render): # noqa: W503, E501 From 902e6a9b61a7290e910c30009beb23dad9337c58 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 May 2023 15:54:11 +0100 Subject: [PATCH 677/918] Long names fixes. --- .../hosts/maya/plugins/publish/validate_rig_output_ids.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 499bfd4e37..cba70a21b7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -55,7 +55,8 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if shapes: instance_nodes.extend(shapes) - scene_nodes = cmds.ls(type="transform") + cmds.ls(type="mesh") + scene_nodes = cmds.ls(type="transform", long=True) + scene_nodes += cmds.ls(type="mesh", long=True) scene_nodes = set(scene_nodes) - set(instance_nodes) scene_nodes_by_basename = defaultdict(list) @@ -76,7 +77,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if len(ids) > 1: cls.log.error( "\"{}\" id mismatch to: {}".format( - instance_node.longName(), matches + instance_node, matches ) ) invalid[instance_node] = matches From b70051b768e1b4b126b3f17fd2a734ab486edc58 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 17:55:54 +0200 Subject: [PATCH 678/918] Preserve comp frame range after rendering --- openpype/hosts/fusion/api/lib.py | 33 +++++++++++++++++-- .../plugins/publish/extract_render_local.py | 19 ++++++----- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index c33209823e..1c486783be 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -256,8 +256,11 @@ def switch_item(container, @contextlib.contextmanager -def maintained_selection(): - comp = get_current_comp() +def maintained_selection(comp=None): + """Reset comp selection from before the context after the context""" + if comp is None: + comp = get_current_comp() + previous_selection = comp.GetToolList(True).values() try: yield @@ -268,6 +271,32 @@ def maintained_selection(): for tool in previous_selection: flow.Select(tool, True) +@contextlib.contextmanager +def maintained_comp_range(comp=None, + global_start=True, + global_end=True, + render_start=True, + render_end=True): + """Reset comp frame ranges from before the context after the context""" + if comp is None: + comp = get_current_comp() + + comp_attrs = comp.GetAttrs() + preserve_attrs = {} + if global_start: + preserve_attrs["COMPN_GlobalStart"] = comp_attrs["COMPN_GlobalStart"] + if global_end: + preserve_attrs["COMPN_GlobalEnd"] = comp_attrs["COMPN_GlobalEnd"] + if render_start: + preserve_attrs["COMPN_RenderStart"] = comp_attrs["COMPN_RenderStart"] + if render_end: + preserve_attrs["COMPN_RenderEnd"] = comp_attrs["COMPN_RenderEnd"] + + try: + yield + finally: + comp.SetAttrs(preserve_attrs) + def get_frame_path(path): """Get filename for the Fusion Saver with padded number as '#' diff --git a/openpype/hosts/fusion/plugins/publish/extract_render_local.py b/openpype/hosts/fusion/plugins/publish/extract_render_local.py index 564dca1796..25c101cf00 100644 --- a/openpype/hosts/fusion/plugins/publish/extract_render_local.py +++ b/openpype/hosts/fusion/plugins/publish/extract_render_local.py @@ -6,7 +6,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk -from openpype.hosts.fusion.api.lib import get_frame_path +from openpype.hosts.fusion.api.lib import get_frame_path, maintained_comp_range log = logging.getLogger(__name__) @@ -114,14 +114,15 @@ class FusionRenderLocal( self.log.info(f"Rendering tools: {saver_names}") with comp_lock_and_undo_chunk(current_comp): - with enabled_savers(current_comp, savers_to_render): - result = current_comp.Render( - { - "Start": frame_start, - "End": frame_end, - "Wait": True, - } - ) + with maintained_comp_range(current_comp): + with enabled_savers(current_comp, savers_to_render): + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) # Store the render state for all the rendered instances for render_instance in render_instances: From 409929c3b8233d922463410a12f452766ee94123 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 May 2023 17:57:23 +0200 Subject: [PATCH 679/918] Cosmetics --- openpype/hosts/fusion/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 1c486783be..cba8c38c2f 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -271,6 +271,7 @@ def maintained_selection(comp=None): for tool in previous_selection: flow.Select(tool, True) + @contextlib.contextmanager def maintained_comp_range(comp=None, global_start=True, From a73d19b612c6c4d423a8784b0598e71263213c9f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 23 May 2023 18:16:05 +0200 Subject: [PATCH 680/918] Publisher: Show instances in report page (#4915) * renamed 'validations_widget.py' to 'report_page.py' * Implemented base logic and widgets for logs * make one report page * added missing imports * added missing constants * move and rename 'VerticallScrollArea' to 'VerticalScrollArea' * Validation erro item have id * use 'ReportPageWidget' in window * change 'bg-button-hover' key to 'bg-buttons-hover' in style colors * move publish actions widgets * Refactored how validation error title is showed * remove item id from validation error item but add id to group items * remove margins from actions widget * shrink publish frame on finished publishing * fix dash line draw * add missing styles * fix dash line in thumbnail widget * added crash widget and changed layout a little * added infor overlay message * export and copy report happens in main window * fix docstrings * added per plugin filtering for validation errors * added implementation of 'FlowLayout' * actions buttons are in flow layout * fix actions order * implemented expanding text edit widget * expand button has some signals and properties * description and details are separated widgets * fix typo * added constans to '__all__' * parse icon def is a function * change layout of widgets * fix log filtering * added state icon to instances * fix pyside6 issues * implemented 'ClassicExpandBtnLabel' with arrow images * modified details separator * added some spacing to layouts * fix syle of description inputs and progress color * removed unused import * add 'is_validation_error' to errored result * validation error has different icon in logs view * added plugin name to ValueError if happens * spacer before detail inputs moved out of detals widget * fix actions visible in craash report * ignore pyblish base classes * filter base plugins in discovery * use 'is' comparison instead of '__eq__' * fix action error handling * Fix handling of 'None' values in comparison * formatting fix * Report instance card have same margins as in create mode * publish instances are grouped by family * log messages are rstripped --- openpype/pipeline/publish/lib.py | 8 + openpype/style/data.json | 9 +- openpype/style/style.css | 64 +- openpype/tools/attribute_defs/files_widget.py | 24 +- openpype/tools/publisher/constants.py | 6 + openpype/tools/publisher/control.py | 41 +- .../publish_report_viewer/widgets.py | 6 +- openpype/tools/publisher/widgets/__init__.py | 4 +- .../publisher/widgets/card_view_widgets.py | 6 +- .../tools/publisher/widgets/images/error.png | Bin 0 -> 14667 bytes .../publisher/widgets/images/success.png | Bin 0 -> 14514 bytes .../publisher/widgets/images/warning.png | Bin 9748 -> 11546 bytes .../tools/publisher/widgets/publish_frame.py | 39 +- .../tools/publisher/widgets/report_page.py | 1876 +++++++++++++++++ .../publisher/widgets/thumbnail_widget.py | 21 +- .../publisher/widgets/validations_widget.py | 715 ------- openpype/tools/publisher/widgets/widgets.py | 65 +- openpype/tools/publisher/window.py | 58 +- openpype/tools/utils/__init__.py | 7 + openpype/tools/utils/layouts.py | 150 ++ openpype/tools/utils/widgets.py | 108 +- 21 files changed, 2373 insertions(+), 834 deletions(-) create mode 100644 openpype/tools/publisher/widgets/images/error.png create mode 100644 openpype/tools/publisher/widgets/images/success.png create mode 100644 openpype/tools/publisher/widgets/report_page.py delete mode 100644 openpype/tools/publisher/widgets/validations_widget.py create mode 100644 openpype/tools/utils/layouts.py diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 080f93e514..40186238aa 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -320,6 +320,14 @@ def publish_plugins_discover(paths=None): continue for plugin in pyblish.plugin.plugins_from_module(module): + # Ignore base plugin classes + # NOTE 'pyblish.api.discover' does not ignore them! + if ( + plugin is pyblish.api.Plugin + or plugin is pyblish.api.ContextPlugin + or plugin is pyblish.api.InstancePlugin + ): + continue if not allow_duplicates and plugin.__name__ in plugin_names: result.duplicated_plugins.append(plugin) log.debug("Duplicate plug-in found: %s", plugin) diff --git a/openpype/style/data.json b/openpype/style/data.json index bea2a3d407..7389387d97 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -26,8 +26,8 @@ "bg": "#2C313A", "bg-inputs": "#21252B", - "bg-buttons": "#434a56", - "bg-button-hover": "rgb(81, 86, 97)", + "bg-buttons": "rgb(67, 74, 86)", + "bg-buttons-hover": "rgb(81, 86, 97)", "bg-inputs-disabled": "#2C313A", "bg-buttons-disabled": "#434a56", @@ -66,7 +66,9 @@ "bg-success": "#458056", "bg-success-hover": "#55a066", "bg-error": "#AD2E2E", - "bg-error-hover": "#C93636" + "bg-error-hover": "#C93636", + "bg-info": "rgb(63, 98, 121)", + "bg-info-hover": "rgb(81, 146, 181)" }, "tab-widget": { "bg": "#21252B", @@ -94,6 +96,7 @@ "crash": "#FF6432", "success": "#458056", "warning": "#ffc671", + "progress": "rgb(194, 226, 236)", "tab-bg": "#16191d", "list-view-group": { "bg": "#434a56", diff --git a/openpype/style/style.css b/openpype/style/style.css index 827b103f94..5ce55aa658 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -136,7 +136,7 @@ QPushButton { } QPushButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -166,7 +166,7 @@ QToolButton { } QToolButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -722,6 +722,13 @@ OverlayMessageWidget[type="error"]:hover { background: {color:overlay-messages:bg-error-hover}; } +OverlayMessageWidget[type="info"] { + background: {color:overlay-messages:bg-info}; +} +OverlayMessageWidget[type="info"]:hover { + background: {color:overlay-messages:bg-info-hover}; +} + OverlayMessageWidget QWidget { background: transparent; } @@ -749,10 +756,11 @@ OverlayMessageWidget QWidget { } #InfoText { - padding-left: 30px; - padding-top: 20px; + padding-left: 0px; + padding-top: 0px; + padding-right: 20px; background: transparent; - border: 1px solid {color:border}; + border: none; } #TypeEditor, #ToolEditor, #NameEditor, #NumberEditor { @@ -914,7 +922,7 @@ PixmapButton{ background: {color:bg-buttons}; } PixmapButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } PixmapButton:disabled { background: {color:bg-buttons-disabled}; @@ -925,7 +933,7 @@ PixmapButton:disabled { background: {color:bg-view}; } #ThumbnailPixmapHoverButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CreatorDetailedDescription { @@ -946,7 +954,7 @@ PixmapButton:disabled { } #CreateDialogHelpButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CreateDialogHelpButton QWidget { background: transparent; @@ -1005,7 +1013,7 @@ PixmapButton:disabled { border-radius: 0.2em; } #CardViewWidget:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; } #CardViewWidget[state="selected"] { background: {color:bg-view-selection}; @@ -1032,7 +1040,7 @@ PixmapButton:disabled { } #PublishInfoFrame[state="3"], #PublishInfoFrame[state="4"] { - background: rgb(194, 226, 236); + background: {color:publisher:progress}; } #PublishInfoFrame QLabel { @@ -1040,6 +1048,11 @@ PixmapButton:disabled { font-style: bold; } +#PublishReportHeader { + font-size: 14pt; + font-weight: bold; +} + #PublishInfoMainLabel { font-size: 12pt; } @@ -1060,7 +1073,7 @@ ValidationArtistMessage QLabel { } #ValidationActionButton:hover { - background: {color:bg-button-hover}; + background: {color:bg-buttons-hover}; color: {color:font-hover}; } @@ -1090,6 +1103,35 @@ ValidationArtistMessage QLabel { border-left: 1px solid {color:border}; } +#PublishInstancesDetails { + border: 1px solid {color:border}; + border-radius: 0.3em; +} + +#InstancesLogsView { + border: 1px solid {color:border}; + background: {color:bg-view}; + border-radius: 0.3em; +} + +#PublishLogMessage { + font-family: "Noto Sans Mono"; +} + +#PublishInstanceLogsLabel { + font-weight: bold; +} + +#PublishCrashMainLabel{ + font-weight: bold; + font-size: 16pt; +} + +#PublishCrashReportLabel { + font-weight: bold; + font-size: 13pt; +} + #AssetNameInputWidget { background: {color:bg-inputs}; border: 1px solid {color:border}; diff --git a/openpype/tools/attribute_defs/files_widget.py b/openpype/tools/attribute_defs/files_widget.py index 067866035f..076b33fb7c 100644 --- a/openpype/tools/attribute_defs/files_widget.py +++ b/openpype/tools/attribute_defs/files_widget.py @@ -198,29 +198,33 @@ class DropEmpty(QtWidgets.QWidget): def paintEvent(self, event): super(DropEmpty, self).paintEvent(event) - painter = QtGui.QPainter(self) + pen = QtGui.QPen() - pen.setWidth(1) pen.setBrush(QtCore.Qt.darkGray) pen.setStyle(QtCore.Qt.DashLine) - painter.setPen(pen) - content_margins = self.layout().contentsMargins() + pen.setWidth(1) - left_m = content_margins.left() - top_m = content_margins.top() - rect = QtCore.QRect( + content_margins = self.layout().contentsMargins() + rect = self.rect() + left_m = content_margins.left() + pen.width() + top_m = content_margins.top() + pen.width() + new_rect = QtCore.QRect( left_m, top_m, ( - self.rect().width() + rect.width() - (left_m + content_margins.right() + pen.width()) ), ( - self.rect().height() + rect.height() - (top_m + content_margins.bottom() + pen.width()) ) ) - painter.drawRect(rect) + + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.setPen(pen) + painter.drawRect(new_rect) class FilesModel(QtGui.QStandardItemModel): diff --git a/openpype/tools/publisher/constants.py b/openpype/tools/publisher/constants.py index 660fccecf1..4630eb144b 100644 --- a/openpype/tools/publisher/constants.py +++ b/openpype/tools/publisher/constants.py @@ -35,9 +35,13 @@ ResetKeySequence = QtGui.QKeySequence( __all__ = ( "CONTEXT_ID", + "CONTEXT_LABEL", "VARIANT_TOOLTIP", + "INPUTS_LAYOUT_HSPACING", + "INPUTS_LAYOUT_VSPACING", + "INSTANCE_ID_ROLE", "SORT_VALUE_ROLE", "IS_GROUP_ROLE", @@ -47,4 +51,6 @@ __all__ = ( "FAMILY_ROLE", "GROUP_ROLE", "CONVERTER_IDENTIFIER_ROLE", + + "ResetKeySequence", ) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 4b083d4bc8..8095d00103 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -47,6 +47,7 @@ PLUGIN_ORDER_OFFSET = 0.5 class CardMessageTypes: standard = None + info = "info" error = "error" @@ -220,7 +221,12 @@ class PublishReportMaker: def _add_plugin_data_item(self, plugin): if plugin in self._stored_plugins: - raise ValueError("Plugin is already stored") + # A plugin would be processed more than once. What can cause it: + # - there is a bug in controller + # - plugin class is imported into multiple files + # - this can happen even with base classes from 'pyblish' + raise ValueError( + "Plugin '{}' is already stored".format(str(plugin))) self._stored_plugins.append(plugin) @@ -239,6 +245,7 @@ class PublishReportMaker: label = plugin.label return { + "id": plugin.id, "name": plugin.__name__, "label": label, "order": plugin.order, @@ -324,7 +331,7 @@ class PublishReportMaker: "instances": instances_details, "context": self._extract_context_data(self._current_context), "crashed_file_paths": crashed_file_paths, - "id": str(uuid.uuid4()), + "id": uuid.uuid4().hex, "report_version": "1.0.0" } @@ -342,7 +349,9 @@ class PublishReportMaker: "label": instance.data.get("label"), "family": instance.data["family"], "families": instance.data.get("families") or [], - "exists": exists + "exists": exists, + "creator_identifier": instance.data.get("creator_identifier"), + "instance_id": instance.data.get("instance_id"), } def _extract_instance_log_items(self, result): @@ -388,8 +397,11 @@ class PublishReportMaker: exception = result.get("error") if exception: fname, line_no, func, exc = exception.traceback + # Action result does not have 'is_validation_error' + is_validation_error = result.get("is_validation_error", False) output.append({ "type": "error", + "is_validation_error": is_validation_error, "msg": str(exception), "filename": str(fname), "lineno": str(line_no), @@ -426,13 +438,15 @@ class PublishPluginsProxy: plugin_id = plugin.id plugins_by_id[plugin_id] = plugin - action_ids = set() + action_ids = [] action_ids_by_plugin_id[plugin_id] = action_ids actions = getattr(plugin, "actions", None) or [] for action in actions: action_id = action.id - action_ids.add(action_id) + if action_id in actions_by_id: + continue + action_ids.append(action_id) actions_by_id[action_id] = action self._plugins_by_id = plugins_by_id @@ -461,7 +475,7 @@ class PublishPluginsProxy: return plugin.id def get_plugin_action_items(self, plugin_id): - """Get plugin action items for plugin by it's id. + """Get plugin action items for plugin by its id. Args: plugin_id (str): Publish plugin id. @@ -568,7 +582,7 @@ class ValidationErrorItem: context_validation, title, description, - detail, + detail ): self.instance_id = instance_id self.instance_label = instance_label @@ -677,6 +691,8 @@ class PublishValidationErrorsReport: for title in titles: grouped_error_items.append({ + "id": uuid.uuid4().hex, + "plugin_id": plugin_id, "plugin_action_items": list(plugin_action_items), "error_items": error_items_by_title[title], "title": title @@ -2379,7 +2395,8 @@ class PublisherController(BasePublisherController): yield MainThreadItem(self.stop_publish) # Add plugin to publish report - self._publish_report.add_plugin_iter(plugin, self._publish_context) + self._publish_report.add_plugin_iter( + plugin, self._publish_context) # WARNING This is hack fix for optional plugins if not self._is_publish_plugin_active(plugin): @@ -2461,14 +2478,14 @@ class PublisherController(BasePublisherController): plugin, self._publish_context, instance ) - self._publish_report.add_result(result) - exception = result.get("error") if exception: + has_validation_error = False if ( isinstance(exception, PublishValidationError) and not self.publish_has_validated ): + has_validation_error = True self._add_validation_error(result) else: @@ -2482,6 +2499,10 @@ class PublisherController(BasePublisherController): self.publish_error_msg = msg self.publish_has_crashed = True + result["is_validation_error"] = has_validation_error + + self._publish_report.add_result(result) + self._publish_next_process() diff --git a/openpype/tools/publisher/publish_report_viewer/widgets.py b/openpype/tools/publisher/publish_report_viewer/widgets.py index dc449b6b69..02c9b63a4e 100644 --- a/openpype/tools/publisher/publish_report_viewer/widgets.py +++ b/openpype/tools/publisher/publish_report_viewer/widgets.py @@ -163,7 +163,11 @@ class ZoomPlainText(QtWidgets.QPlainTextEdit): super(ZoomPlainText, self).wheelEvent(event) return - degrees = float(event.delta()) / 8 + if hasattr(event, "angleDelta"): + delta = event.angleDelta().y() + else: + delta = event.delta() + degrees = float(delta) / 8 steps = int(ceil(degrees / 5)) self._scheduled_scalings += steps if (self._scheduled_scalings * steps < 0): diff --git a/openpype/tools/publisher/widgets/__init__.py b/openpype/tools/publisher/widgets/__init__.py index f18e6cc61e..87a5f3914a 100644 --- a/openpype/tools/publisher/widgets/__init__.py +++ b/openpype/tools/publisher/widgets/__init__.py @@ -18,7 +18,7 @@ from .help_widget import ( from .publish_frame import PublishFrame from .tabs_widget import PublisherTabsWidget from .overview_widget import OverviewWidget -from .validations_widget import ValidationsWidget +from .report_page import ReportPageWidget __all__ = ( @@ -40,5 +40,5 @@ __all__ = ( "PublisherTabsWidget", "OverviewWidget", - "ValidationsWidget", + "ReportPageWidget", ) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 13715bc73c..eae8e0420a 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -93,7 +93,7 @@ class BaseGroupWidget(QtWidgets.QWidget): return self._group def get_widget_by_item_id(self, item_id): - """Get instance widget by it's id.""" + """Get instance widget by its id.""" return self._widgets_by_id.get(item_id) @@ -702,8 +702,8 @@ class InstanceCardView(AbstractInstanceView): for group_name in sorted_group_names: group_icons = { - idenfier: self._controller.get_creator_icon(idenfier) - for idenfier in identifiers_by_group[group_name] + identifier: self._controller.get_creator_icon(identifier) + for identifier in identifiers_by_group[group_name] } if group_name in self._widgets_by_group: group_widget = self._widgets_by_group[group_name] diff --git a/openpype/tools/publisher/widgets/images/error.png b/openpype/tools/publisher/widgets/images/error.png new file mode 100644 index 0000000000000000000000000000000000000000..7b09a57d7dff44b31b5f18746fa22a42b7d9f15d GIT binary patch literal 14667 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%0DjRy{e1C?ePOpy6!-*e$z;|Xnn(?D zpXQkrLy+DjASj+-wx6t?@ebwUR`0uxctB97l~{FM{J+PyxI!(i%57)%H{wYr^v%Sc zuD@pzD{>YcX>>qj2wnglaUl5^|1)Jgx$pYJQuoBT27 z!HS(8dUMi^=z60EypoUky_~5%M$mxL8 z2R1h>A<69oL)PUk=F_}VsHXhV+s);w-(V*MX|aT0=GY2jm{4v#fZN-9d$Km~UqOX; zb4%faGqpee0khM`5FU*t_mgJ9$((#;S(c_SEOkqj11tove#5k(1E=MV zrt04k$G+Mx3_*o})b*Wd0Tx^67_|8Kbh~i57CU1&5hHe!NzG}N+55G7pG6cut$r(O z4`N7-ISsy2R9lg_^yq^L@IC1WEefb-@J^4rG0Uu=!QK&opnN zp9Ov5g677MUB?iLBadN4ze7gkKA(A3dw?-YbCO;^yE3E{k;+^}a=ZkyG3Er=WF2o% z_YoURGSAYFvox+4-E*~yQ2uE&P1>*h6y9kqk)6)^p_|)Dos;y%7NLy3$FvKj-p6qF zKbBh{;^VXEG-<+f=CI~1SATBqWEYk1_F^4+7DpOS@4{m7{!fL6P`W!M?$F{i_asl~ zQ852cI~rG;wI*`@!?9b@rR6o>c!JHN9iNnJ$}t!@#|ub$Plbj!n|9?_*e1zVzXp!< zkGb~@8NgR?I-Mzs=&?{Y0oZz`a za)sOnG`CdsbCC;y2$yf?K)_2})y=DD(K9a^h>ay?D@VxO4xY8!JkA6P@!|`(Naao) zz=C{oXdbkqw0X)xO)=U?V-Aq#6o8F5y{N`k=dA#023iQ}db)3j3&;G{ z?w@D<_AUX&p~aE|uZ zR?*dVYJ($J-9kr$N(2W5bWQNVL%zu=$)*07y{&LyzIKC)?Z}!t!CuxTQ*X5&j8||HAB_g{)ipOgnE^QtdUK57#q#rTJRX|9ioH!!{ zT42+F$UZ!5EC^V$_fW~>rJ=mnhS2w^$-D?E#Vted$&DFTMVO8X&uaM zFKtAPP<8iv?z{TS(6+$8 zsG#&F1r|-_tV=odOOeSW_LJ@!0E+tM{=d1zt>}-+*Psc&qiuBPmd10&gDOl7c{?AV zwaDkqZ91*NP3n#wTOg=7svLK5C8N{PfyeU zIJHO?boR3Z+?3n^qF?ayxa3#KoZzuXH{$0#PEr?1fcd!LTk;LR=?OfLov4PjNr0}N z(+5#zu)TKSRR`EJcWL2!Tcs{Jd-9^I^vgph=Q=_;71;2hO z5~HB@*;fYUBlW5atjaO=P&6pEr^WE-0=uFwD_VXpOD!(SK;?C;M zCdwc~`WSCt`V{cn=~>ZG%jk1YaP2~jBBQ1j zq`W5VQ201AOgVy8RU99DCzDjeXPFh?dnBw;>&Y-RE+$92Bo%Gf98o5Yp9m?a*lG-dU4LeVWjy^;ab* zkMsD z#hFH5zT)j5cpQB2jvMqK#D}Fdc|U8TZFkw}W+B|ApQ}lwMdno*|L)h&`WHq^VC$!m zef-xD(*iat+noRFk`41}j&uLAG{=P-Ki)VVua-Hy8I>Nl;qc>4&P*WUwqt{kBOBd3 zgZmL#)RHUi;-%8RJ?OcvOc~7{q|t?Rm4y&Bf7V!LQeGB|}rpv{T`-Q!kF=hAMZ=WmoNg-)j+rm`& z1nqA*`7e=1A|K8G-su!9bYkSz%{vybEp1(G7Tjin1#aG{(5i#7tCr$RJOx_?3$E|2 zx+H-)EqQpgws;E#-YSFpN;BGsA>JX<(1V}m@cN0i>YUw*!-_Ozzo=rgFKD5$kAV*T zZd(x$%bnWt(o6^mCGKL6@Pe3!pW9s7c@RAO#hbPz!yOZv?fUrwcAzY<3RBD3j}#JQ zd3bGY9_@P!l`lIbzqr18a(gW|A%07PQ+LHYA%X42(EsQD81vPM&7IXwpVa_16%lan O4Z@q-n^l_-F8mKWCTVE^ literal 0 HcmV?d00001 diff --git a/openpype/tools/publisher/widgets/images/warning.png b/openpype/tools/publisher/widgets/images/warning.png index 76d1e34b6ccf951b166db53cd59d8183cded7586..531f62b741804a2fa48769c17f9e29fbb0cd5429 100644 GIT binary patch literal 11546 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 E3l)7W-~a#s diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index e4e6740532..d21130deff 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -468,45 +468,14 @@ class PublishFrame(QtWidgets.QWidget): widget.setProperty("state", state) widget.style().polish(widget) - def _copy_report(self): - logs = self._controller.get_publish_report() - logs_string = json.dumps(logs, indent=4) - - mime_data = QtCore.QMimeData() - mime_data.setText(logs_string) - QtWidgets.QApplication.instance().clipboard().setMimeData( - mime_data - ) - - def _export_report(self): - default_filename = "publish-report-{}".format( - time.strftime("%y%m%d-%H-%M") - ) - default_filepath = os.path.join( - os.path.expanduser("~"), - default_filename - ) - new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName( - self, "Save report", default_filepath, ".json" - ) - if not ext or not new_filepath: - return - - logs = self._controller.get_publish_report() - full_path = new_filepath + ext - dir_path = os.path.dirname(full_path) - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - with open(full_path, "w") as file_stream: - json.dump(logs, file_stream) - def _on_report_triggered(self, identifier): if identifier == "export_report": - self._export_report() + self._controller.event_system.emit( + "export_report.request", {}, "publish_frame") elif identifier == "copy_report": - self._copy_report() + self._controller.event_system.emit( + "copy_report.request", {}, "publish_frame") elif identifier == "go_to_report": self.details_page_requested.emit() diff --git a/openpype/tools/publisher/widgets/report_page.py b/openpype/tools/publisher/widgets/report_page.py new file mode 100644 index 0000000000..50a619f0a8 --- /dev/null +++ b/openpype/tools/publisher/widgets/report_page.py @@ -0,0 +1,1876 @@ +# -*- coding: utf-8 -*- +import collections +import logging + +try: + import commonmark +except Exception: + commonmark = None + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.style import get_objected_colors +from openpype.tools.utils import ( + BaseClickableFrame, + ClickableFrame, + ExpandingTextEdit, + FlowLayout, + ClassicExpandBtn, + paint_image_with_color, + SeparatorWidget, +) +from .widgets import IconValuePixmapLabel +from .icons import ( + get_pixmap, + get_image, +) +from ..constants import ( + INSTANCE_ID_ROLE, + CONTEXT_ID, + CONTEXT_LABEL, +) + +LOG_DEBUG_VISIBLE = 1 << 0 +LOG_INFO_VISIBLE = 1 << 1 +LOG_WARNING_VISIBLE = 1 << 2 +LOG_ERROR_VISIBLE = 1 << 3 +LOG_CRITICAL_VISIBLE = 1 << 4 +ERROR_VISIBLE = 1 << 5 +INFO_VISIBLE = 1 << 6 + + +class VerticalScrollArea(QtWidgets.QScrollArea): + """Scroll area for validation error titles. + + The biggest difference is that the scroll area has scroll bar on left side + and resize of content will also resize scrollarea itself. + + Resize if deferred by 100ms because at the moment of resize are not yet + propagated sizes and visibility of scroll bars. + """ + + def __init__(self, *args, **kwargs): + super(VerticalScrollArea, self).__init__(*args, **kwargs) + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.setLayoutDirection(QtCore.Qt.RightToLeft) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + # Background of scrollbar will be transparent + scrollbar_bg = self.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setViewportMargins(0, 0, 0, 0) + + self.verticalScrollBar().installEventFilter(self) + + # Timer with 100ms offset after changing size + size_changed_timer = QtCore.QTimer() + size_changed_timer.setInterval(100) + size_changed_timer.setSingleShot(True) + + size_changed_timer.timeout.connect(self._on_timer_timeout) + self._size_changed_timer = size_changed_timer + + def setVerticalScrollBar(self, widget): + old_widget = self.verticalScrollBar() + if old_widget: + old_widget.removeEventFilter(self) + + super(VerticalScrollArea, self).setVerticalScrollBar(widget) + if widget: + widget.installEventFilter(self) + + def setWidget(self, widget): + old_widget = self.widget() + if old_widget: + old_widget.removeEventFilter(self) + + super(VerticalScrollArea, self).setWidget(widget) + if widget: + widget.installEventFilter(self) + + def _on_timer_timeout(self): + width = self.widget().width() + if self.verticalScrollBar().isVisible(): + width += self.verticalScrollBar().width() + self.setMinimumWidth(width) + + def eventFilter(self, obj, event): + if ( + event.type() == QtCore.QEvent.Resize + and (obj is self.widget() or obj is self.verticalScrollBar()) + ): + self._size_changed_timer.start() + return super(VerticalScrollArea, self).eventFilter(obj, event) + + +# --- Publish actions widget --- +class ActionButton(BaseClickableFrame): + """Plugin's action callback button. + + Action may have label or icon or both. + + Args: + plugin_action_item (PublishPluginActionItem): Action item that can be + triggered by its id. + """ + + action_clicked = QtCore.Signal(str, str) + + def __init__(self, plugin_action_item, parent): + super(ActionButton, self).__init__(parent) + + self.setObjectName("ValidationActionButton") + + self.plugin_action_item = plugin_action_item + + action_label = plugin_action_item.label + action_icon = plugin_action_item.icon + label_widget = QtWidgets.QLabel(action_label, self) + icon_label = None + if action_icon: + icon_label = IconValuePixmapLabel(action_icon, self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(5, 0, 5, 0) + layout.addWidget(label_widget, 1) + if icon_label: + layout.addWidget(icon_label, 0) + + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + self.sizePolicy().verticalPolicy() + ) + + def _mouse_release_callback(self): + self.action_clicked.emit( + self.plugin_action_item.plugin_id, + self.plugin_action_item.action_id + ) + + +class ValidateActionsWidget(QtWidgets.QFrame): + """Wrapper widget for plugin actions. + + Change actions based on selected validation error. + """ + + def __init__(self, controller, parent): + super(ValidateActionsWidget, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_widget = QtWidgets.QWidget(self) + content_layout = FlowLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(content_widget) + + self._controller = controller + self._content_widget = content_widget + self._content_layout = content_layout + + self._actions_mapping = {} + + self._visible_mode = True + + def _update_visibility(self): + self.setVisible( + self._visible_mode + and self._content_layout.count() > 0 + ) + + def set_visible_mode(self, visible): + if self._visible_mode is visible: + return + self._visible_mode = visible + self._update_visibility() + + def _clear(self): + """Remove actions from widget.""" + while self._content_layout.count(): + item = self._content_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._actions_mapping = {} + + def set_error_info(self, error_info): + """Set selected plugin and show it's actions. + + Clears current actions from widget and recreate them from the plugin. + + Args: + Dict[str, Any]: Object holding error items, title and possible + actions to run. + """ + + self._clear() + + if not error_info: + self.setVisible(False) + return + + plugin_action_items = error_info["plugin_action_items"] + for plugin_action_item in plugin_action_items: + if not plugin_action_item.active: + continue + + if plugin_action_item.on_filter not in ("failed", "all"): + continue + + action_id = plugin_action_item.action_id + self._actions_mapping[action_id] = plugin_action_item + + action_btn = ActionButton(plugin_action_item, self._content_widget) + action_btn.action_clicked.connect(self._on_action_click) + self._content_layout.addWidget(action_btn) + + self._update_visibility() + + def _on_action_click(self, plugin_id, action_id): + self._controller.run_action(plugin_id, action_id) + + +# --- Validation error titles --- +class ValidationErrorInstanceList(QtWidgets.QListView): + """List of publish instances that caused a validation error. + + Instances are collected per plugin's validation error title. + """ + def __init__(self, *args, **kwargs): + super(ValidationErrorInstanceList, self).__init__(*args, **kwargs) + + self.setObjectName("ValidationErrorInstanceList") + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + + def minimumSizeHint(self): + return self.sizeHint() + + def sizeHint(self): + result = super(ValidationErrorInstanceList, self).sizeHint() + row_count = self.model().rowCount() + height = 0 + if row_count > 0: + height = self.sizeHintForRow(0) * row_count + result.setHeight(height) + return result + + +class ValidationErrorTitleWidget(QtWidgets.QWidget): + """Title of validation error. + + Widget is used as radio button so requires clickable functionality and + changing style on selection/deselection. + + Has toggle button to show/hide instances on which validation error happened + if there is a list (Valdation error may happen on context). + """ + + selected = QtCore.Signal(str) + instance_changed = QtCore.Signal(str) + + def __init__(self, title_id, error_info, parent): + super(ValidationErrorTitleWidget, self).__init__(parent) + + self._title_id = title_id + self._error_info = error_info + self._selected = False + + title_frame = ClickableFrame(self) + title_frame.setObjectName("ValidationErrorTitleFrame") + + toggle_instance_btn = QtWidgets.QToolButton(title_frame) + toggle_instance_btn.setObjectName("ArrowBtn") + toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) + toggle_instance_btn.setMaximumWidth(14) + + label_widget = QtWidgets.QLabel(error_info["title"], title_frame) + + title_frame_layout = QtWidgets.QHBoxLayout(title_frame) + title_frame_layout.addWidget(label_widget, 1) + title_frame_layout.addWidget(toggle_instance_btn, 0) + + instances_model = QtGui.QStandardItemModel() + + instance_ids = [] + + items = [] + context_validation = False + for error_item in error_info["error_items"]: + context_validation = error_item.context_validation + if context_validation: + toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) + instance_ids.append(CONTEXT_ID) + # Add fake item to have minimum size hint of view widget + items.append(QtGui.QStandardItem(CONTEXT_LABEL)) + continue + + label = error_item.instance_label + item = QtGui.QStandardItem(label) + item.setFlags( + QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + ) + item.setData(label, QtCore.Qt.ToolTipRole) + item.setData(error_item.instance_id, INSTANCE_ID_ROLE) + items.append(item) + instance_ids.append(error_item.instance_id) + + if items: + root_item = instances_model.invisibleRootItem() + root_item.appendRows(items) + + instances_view = ValidationErrorInstanceList(self) + instances_view.setModel(instances_model) + + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + view_widget = QtWidgets.QWidget(self) + view_layout = QtWidgets.QHBoxLayout(view_widget) + view_layout.setContentsMargins(0, 0, 0, 0) + view_layout.setSpacing(0) + view_layout.addSpacing(14) + view_layout.addWidget(instances_view, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(0) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(title_frame, 0) + layout.addWidget(view_widget, 0) + view_widget.setVisible(False) + + if not context_validation: + toggle_instance_btn.clicked.connect(self._on_toggle_btn_click) + + title_frame.clicked.connect(self._mouse_release_callback) + instances_view.selectionModel().selectionChanged.connect( + self._on_selection_change + ) + + self._title_frame = title_frame + + self._toggle_instance_btn = toggle_instance_btn + + self._view_widget = view_widget + + self._instances_model = instances_model + self._instances_view = instances_view + + self._context_validation = context_validation + + self._instance_ids = instance_ids + self._expanded = False + + def sizeHint(self): + result = super(ValidationErrorTitleWidget, self).sizeHint() + expected_width = max( + self._view_widget.minimumSizeHint().width(), + self._view_widget.sizeHint().width() + ) + + if expected_width < 200: + expected_width = 200 + + if result.width() < expected_width: + result.setWidth(expected_width) + + return result + + def minimumSizeHint(self): + return self.sizeHint() + + def _mouse_release_callback(self): + """Mark this widget as selected on click.""" + + self.set_selected(True) + + @property + def is_selected(self): + """Is widget marked a selected. + + Returns: + bool: Item is selected or not. + """ + + return self._selected + + @property + def id(self): + return self._title_id + + def _change_style_property(self, selected): + """Change style of widget based on selection.""" + + value = "1" if selected else "" + self._title_frame.setProperty("selected", value) + self._title_frame.style().polish(self._title_frame) + + def set_selected(self, selected=None): + """Change selected state of widget.""" + + if selected is None: + selected = not self._selected + + # Clear instance view selection on deselect + if not selected: + self._instances_view.clearSelection() + + # Skip if has same value + if selected == self._selected: + return + + self._selected = selected + self._change_style_property(selected) + if selected: + self.selected.emit(self._title_id) + self._set_expanded(True) + + def _on_toggle_btn_click(self): + """Show/hide instances list.""" + + self._set_expanded() + + def _set_expanded(self, expanded=None): + if expanded is None: + expanded = not self._expanded + + elif expanded is self._expanded: + return + + if expanded and self._context_validation: + return + + self._expanded = expanded + self._view_widget.setVisible(expanded) + if expanded: + self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow) + else: + self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) + + def _on_selection_change(self): + self.instance_changed.emit(self._title_id) + + def get_selected_instances(self): + if self._context_validation: + return [CONTEXT_ID] + sel_model = self._instances_view.selectionModel() + return [ + index.data(INSTANCE_ID_ROLE) + for index in sel_model.selectedIndexes() + if index.isValid() + ] + + def get_available_instances(self): + return list(self._instance_ids) + + +class ValidationArtistMessage(QtWidgets.QWidget): + def __init__(self, message, parent): + super(ValidationArtistMessage, self).__init__(parent) + + artist_msg_label = QtWidgets.QLabel(message, self) + artist_msg_label.setAlignment(QtCore.Qt.AlignCenter) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget( + artist_msg_label, 1, QtCore.Qt.AlignCenter + ) + + +class ValidationErrorsView(QtWidgets.QWidget): + selection_changed = QtCore.Signal() + + def __init__(self, parent): + super(ValidationErrorsView, self).__init__(parent) + + errors_scroll = VerticalScrollArea(self) + errors_scroll.setWidgetResizable(True) + + errors_widget = QtWidgets.QWidget(errors_scroll) + errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + errors_scroll.setWidget(errors_widget) + + errors_layout = QtWidgets.QVBoxLayout(errors_widget) + errors_layout.setContentsMargins(0, 0, 0, 0) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(errors_scroll, 1) + + self._errors_widget = errors_widget + self._errors_layout = errors_layout + self._title_widgets = {} + self._previous_select = None + + def _clear(self): + """Delete all dynamic widgets and hide all wrappers.""" + + self._title_widgets = {} + self._previous_select = None + while self._errors_layout.count(): + item = self._errors_layout.takeAt(0) + widget = item.widget() + if widget: + widget.deleteLater() + + def set_errors(self, grouped_error_items): + """Set errors into context and created titles. + + Args: + validation_error_report (PublishValidationErrorsReport): Report + with information about validation errors and publish plugin + actions. + """ + + self._clear() + + first_id = None + for title_item in grouped_error_items: + title_id = title_item["id"] + if first_id is None: + first_id = title_id + widget = ValidationErrorTitleWidget(title_id, title_item, self) + widget.selected.connect(self._on_select) + widget.instance_changed.connect(self._on_instance_change) + self._errors_layout.addWidget(widget) + self._title_widgets[title_id] = widget + + self._errors_layout.addStretch(1) + + if first_id: + self._title_widgets[first_id].set_selected(True) + else: + self.selection_changed.emit() + + self.updateGeometry() + + def _on_select(self, title_id): + if self._previous_select: + if self._previous_select.id == title_id: + return + self._previous_select.set_selected(False) + + self._previous_select = self._title_widgets[title_id] + self.selection_changed.emit() + + def _on_instance_change(self, title_id): + if self._previous_select and self._previous_select.id != title_id: + self._title_widgets[title_id].set_selected(True) + else: + self.selection_changed.emit() + + def get_selected_items(self): + if not self._previous_select: + return None, [] + + title_id = self._previous_select.id + instance_ids = self._previous_select.get_selected_instances() + if not instance_ids: + instance_ids = self._previous_select.get_available_instances() + return title_id, instance_ids + + +# ----- Publish instance report ----- +class _InstanceItem: + """Publish instance item for report UI. + + Contains only data related to an instance in publishing. Has implemented + sorting methods and prepares information, e.g. if contains error or + warnings. + """ + + _attrs = ( + "creator_identifier", + "family", + "label", + "name", + ) + + def __init__( + self, + instance_id, + creator_identifier, + family, + name, + label, + exists, + logs, + errored, + warned + ): + self.id = instance_id + self.creator_identifier = creator_identifier + self.family = family + self.name = name + self.label = label + self.exists = exists + self.logs = logs + self.errored = errored + self.warned = warned + + def __eq__(self, other): + for attr in self._attrs: + if getattr(self, attr) != getattr(other, attr): + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __gt__(self, other): + for attr in self._attrs: + self_value = getattr(self, attr) + other_value = getattr(other, attr) + if self_value == other_value: + continue + values = [self_value, other_value] + values.sort() + return values[0] == other_value + return None + + def __lt__(self, other): + for attr in self._attrs: + self_value = getattr(self, attr) + other_value = getattr(other, attr) + if self_value == other_value: + continue + if self_value is None: + return False + if other_value is None: + return True + values = [self_value, other_value] + values.sort() + return values[0] == self_value + return None + + def __ge__(self, other): + if self == other: + return True + return self.__gt__(other) + + def __le__(self, other): + if self == other: + return True + return self.__lt__(other) + + @classmethod + def from_report(cls, instance_id, instance_data, logs): + errored, warned = cls.extract_basic_log_info(logs) + + return cls( + instance_id, + instance_data["creator_identifier"], + instance_data["family"], + instance_data["name"], + instance_data["label"], + instance_data["exists"], + logs, + errored, + warned, + ) + + @classmethod + def create_context_item(cls, context_label, logs): + errored, warned = cls.extract_basic_log_info(logs) + return cls( + CONTEXT_ID, + None, + "", + CONTEXT_LABEL, + context_label, + True, + logs, + errored, + warned + ) + + @staticmethod + def extract_basic_log_info(logs): + warned = False + errored = False + for log in logs: + if log["type"] == "error": + errored = True + elif log["type"] == "record": + level_no = log["levelno"] + if level_no and level_no >= logging.WARNING: + warned = True + + if warned and errored: + break + return errored, warned + + +class FamilyGroupLabel(QtWidgets.QWidget): + def __init__(self, family, parent): + super(FamilyGroupLabel, self).__init__(parent) + + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + label_widget = QtWidgets.QLabel(family, self) + + line_widget = QtWidgets.QWidget(self) + line_widget.setObjectName("Separator") + line_widget.setMinimumHeight(2) + line_widget.setMaximumHeight(2) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setAlignment(QtCore.Qt.AlignVCenter) + main_layout.setSpacing(10) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(label_widget, 0) + main_layout.addWidget(line_widget, 1) + + +class PublishInstanceCardWidget(BaseClickableFrame): + selection_requested = QtCore.Signal(str) + + _warning_pix = None + _error_pix = None + _success_pix = None + _in_progress_pix = None + + def __init__(self, instance, icon, publish_finished, parent): + super(PublishInstanceCardWidget, self).__init__(parent) + + self.setObjectName("CardViewWidget") + + icon_widget = IconValuePixmapLabel(icon, self) + icon_widget.setObjectName("FamilyIconLabel") + + label_widget = QtWidgets.QLabel(instance.label, self) + + if instance.errored: + state_pix = self.get_error_pix() + elif instance.warned: + state_pix = self.get_warning_pix() + elif publish_finished: + state_pix = self.get_success_pix() + else: + state_pix = self.get_in_progress_pix() + + state_label = IconValuePixmapLabel(state_pix, self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(10, 7, 10, 7) + layout.addWidget(icon_widget, 0) + layout.addWidget(label_widget, 1) + layout.addWidget(state_label, 0) + + # Change direction -> parent is scroll area where scrolls are on + # left side + self.setLayoutDirection(QtCore.Qt.LeftToRight) + + self._id = instance.id + + self._selected = False + + self._update_style_state() + + @classmethod + def _prepare_pixes(cls): + publisher_colors = get_objected_colors("publisher") + cls._warning_pix = paint_image_with_color( + get_image("warning"), + publisher_colors["warning"].get_qcolor() + ) + cls._error_pix = paint_image_with_color( + get_image("error"), + publisher_colors["error"].get_qcolor() + ) + cls._success_pix = paint_image_with_color( + get_image("success"), + publisher_colors["success"].get_qcolor() + ) + cls._in_progress_pix = paint_image_with_color( + get_image("success"), + publisher_colors["progress"].get_qcolor() + ) + + @classmethod + def get_warning_pix(cls): + if cls._warning_pix is None: + cls._prepare_pixes() + return cls._warning_pix + + @classmethod + def get_error_pix(cls): + if cls._error_pix is None: + cls._prepare_pixes() + return cls._error_pix + + @classmethod + def get_success_pix(cls): + if cls._success_pix is None: + cls._prepare_pixes() + return cls._success_pix + + @classmethod + def get_in_progress_pix(cls): + if cls._in_progress_pix is None: + cls._prepare_pixes() + return cls._in_progress_pix + + @property + def id(self): + """Id of card. + + Returns: + str: Id of item. + """ + + return self._id + + @property + def is_selected(self): + """Is card selected. + + Returns: + bool: Item widget is marked as selected. + """ + + return self._selected + + def set_selected(self, selected): + """Set card as selected. + + Args: + selected (bool): Item should be marked as selected. + """ + + if selected == self._selected: + return + self._selected = selected + self._update_style_state() + + def _update_style_state(self): + state = "" + if self._selected: + state = "selected" + + self.setProperty("state", state) + self.style().polish(self) + + def _mouse_release_callback(self): + """Trigger selected signal.""" + + self.selection_requested.emit(self.id) + + +class PublishInstancesViewWidget(QtWidgets.QWidget): + # Sane minimum width of instance cards - size calulated using font metrics + _min_width_measure_string = 24 * "O" + selection_changed = QtCore.Signal() + + def __init__(self, controller, parent): + super(PublishInstancesViewWidget, self).__init__(parent) + + scroll_area = VerticalScrollArea(self) + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + scrollbar_bg = scroll_area.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + scroll_area.setViewportMargins(0, 0, 0, 0) + + instance_view = QtWidgets.QWidget(scroll_area) + + scroll_area.setWidget(instance_view) + + instance_layout = QtWidgets.QVBoxLayout(instance_view) + instance_layout.setContentsMargins(0, 0, 0, 0) + instance_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(scroll_area, 1) + + self._controller = controller + self._scroll_area = scroll_area + self._instance_view = instance_view + self._instance_layout = instance_layout + + self._context_widget = None + + self._widgets_by_instance_id = {} + self._group_widgets = [] + self._ordered_widgets = [] + + self._explicitly_selected_instance_ids = [] + + self.setSizePolicy( + QtWidgets.QSizePolicy.Minimum, + self.sizePolicy().verticalPolicy() + ) + + def sizeHint(self): + """Modify sizeHint based on visibility of scroll bars.""" + # Calculate width hint by content widget and vertical scroll bar + scroll_bar = self._scroll_area.verticalScrollBar() + view_size = self._instance_view.sizeHint().width() + fm = self._instance_view.fontMetrics() + width = ( + max(view_size, fm.width(self._min_width_measure_string)) + + scroll_bar.sizeHint().width() + ) + + result = super(PublishInstancesViewWidget, self).sizeHint() + result.setWidth(width) + return result + + def _get_selected_widgets(self): + return [ + widget + for widget in self._ordered_widgets + if widget.is_selected + ] + + def get_selected_instance_ids(self): + return [ + widget.id + for widget in self._get_selected_widgets() + ] + + def clear(self): + """Remove actions from widget.""" + while self._instance_layout.count(): + item = self._instance_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._ordered_widgets = [] + self._group_widgets = [] + self._widgets_by_instance_id = {} + + def update_instances(self, instance_items): + self.clear() + identifiers = { + instance_item.creator_identifier + for instance_item in instance_items + } + identifier_icons = { + identifier: self._controller.get_creator_icon(identifier) + for identifier in identifiers + } + + widgets = [] + group_widgets = [] + + publish_finished = ( + self._controller.publish_has_crashed + or self._controller.publish_has_validation_errors + or self._controller.publish_has_finished + ) + instances_by_family = collections.defaultdict(list) + for instance_item in instance_items: + if not instance_item.exists: + continue + instances_by_family[instance_item.family].append(instance_item) + + sorted_by_family = sorted( + instances_by_family.items(), key=lambda i: i[0] + ) + for family, instance_items in sorted_by_family: + # Only instance without family is context + if family: + group_widget = FamilyGroupLabel(family, self._instance_view) + self._instance_layout.addWidget(group_widget, 0) + group_widgets.append(group_widget) + + sorted_items = sorted(instance_items, key=lambda i: i.label) + for instance_item in sorted_items: + icon = identifier_icons[instance_item.creator_identifier] + + widget = PublishInstanceCardWidget( + instance_item, icon, publish_finished, self._instance_view + ) + widget.selection_requested.connect(self._on_selection_request) + self._instance_layout.addWidget(widget, 0) + + widgets.append(widget) + self._widgets_by_instance_id[widget.id] = widget + self._instance_layout.addStretch(1) + self._ordered_widgets = widgets + self._group_widgets = group_widgets + + def _on_selection_request(self, instance_id): + instance_widget = self._widgets_by_instance_id[instance_id] + selected_widgets = self._get_selected_widgets() + if instance_widget in selected_widgets: + instance_widget.set_selected(False) + else: + instance_widget.set_selected(True) + for widget in selected_widgets: + widget.set_selected(False) + self.selection_changed.emit() + + +class LogIconFrame(QtWidgets.QFrame): + """Draw log item icon next to message. + + Todos: + Paint event could be slow, maybe we could cache the image into pixmaps + so each item does not have to redraw it again. + """ + + info_color = QtGui.QColor("#ffffff") + error_color = QtGui.QColor("#ff4a4a") + level_to_color = dict(( + (10, QtGui.QColor("#ff66e8")), + (20, QtGui.QColor("#66abff")), + (30, QtGui.QColor("#ffba66")), + (40, QtGui.QColor("#ff4d58")), + (50, QtGui.QColor("#ff4f75")), + )) + _error_pix = None + _validation_error_pix = None + + def __init__(self, parent, log_type, log_level, is_validation_error): + super(LogIconFrame, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self._is_record = log_type == "record" + self._is_error = log_type == "error" + self._is_validation_error = bool(is_validation_error) + self._log_color = self.level_to_color.get(log_level) + + @classmethod + def get_validation_error_icon(cls): + if cls._validation_error_pix is None: + cls._validation_error_pix = get_pixmap("warning") + return cls._validation_error_pix + + @classmethod + def get_error_icon(cls): + if cls._error_pix is None: + cls._error_pix = get_pixmap("error") + return cls._error_pix + + def minimumSizeHint(self): + fm = self.fontMetrics() + size = fm.height() + return QtCore.QSize(size, size) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.setRenderHints( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + painter.setPen(QtCore.Qt.NoPen) + rect = self.rect() + new_size = min(rect.width(), rect.height()) + new_rect = QtCore.QRect(1, 1, new_size - 2, new_size - 2) + if self._is_error: + if self._is_validation_error: + error_icon = self.get_validation_error_icon() + else: + error_icon = self.get_error_icon() + scaled_error_icon = error_icon.scaled( + new_rect.size(), + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + painter.drawPixmap(new_rect, scaled_error_icon) + + else: + if self._is_record: + color = self._log_color + else: + color = QtGui.QColor(255, 255, 255) + painter.setBrush(color) + painter.drawEllipse(new_rect) + painter.end() + + +class LogItemWidget(QtWidgets.QWidget): + log_level_to_flag = { + 10: LOG_DEBUG_VISIBLE, + 20: LOG_INFO_VISIBLE, + 30: LOG_WARNING_VISIBLE, + 40: LOG_ERROR_VISIBLE, + 50: LOG_CRITICAL_VISIBLE, + } + + def __init__(self, log, parent): + super(LogItemWidget, self).__init__(parent) + + type_flag, level_n = self._get_log_info(log) + icon_label = LogIconFrame( + self, log["type"], level_n, log.get("is_validation_error")) + message_label = QtWidgets.QLabel(log["msg"].rstrip(), self) + message_label.setObjectName("PublishLogMessage") + message_label.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction) + message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + message_label.setWordWrap(True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(8) + main_layout.addWidget(icon_label, 0) + main_layout.addWidget(message_label, 1) + + self._type_flag = type_flag + self._plugin_id = log["plugin_id"] + self._log_type_filtered = False + self._plugin_filtered = False + + @property + def type_flag(self): + return self._type_flag + + @property + def plugin_id(self): + return self._plugin_id + + def _get_log_info(self, log): + log_type = log["type"] + if log_type == "error": + return ERROR_VISIBLE, None + + if log_type != "record": + return INFO_VISIBLE, None + + level_n = log["levelno"] + if level_n < 10: + level_n = 10 + elif level_n % 10 != 0: + level_n -= (level_n % 10) + 10 + + flag = self.log_level_to_flag.get(level_n, LOG_CRITICAL_VISIBLE) + return flag, level_n + + def _update_visibility(self): + self.setVisible( + not self._log_type_filtered + and not self._plugin_filtered + ) + + def set_log_type_filtered(self, filtered): + if filtered is self._log_type_filtered: + return + self._log_type_filtered = filtered + self._update_visibility() + + def set_plugin_filtered(self, filtered): + if filtered is self._plugin_filtered: + return + self._plugin_filtered = filtered + self._update_visibility() + + +class LogsWithIconsView(QtWidgets.QWidget): + """Show logs in a grid with 2 columns. + + First column is for icon second is for message. + + Todos: + Add filtering by type (exception, debug, info, etc.). + """ + + def __init__(self, logs, parent): + super(LogsWithIconsView, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + logs_layout = QtWidgets.QVBoxLayout(self) + logs_layout.setContentsMargins(0, 0, 0, 0) + logs_layout.setSpacing(4) + + widgets_by_flag = collections.defaultdict(list) + widgets_by_plugins_id = collections.defaultdict(list) + + for log in logs: + widget = LogItemWidget(log, self) + widgets_by_flag[widget.type_flag].append(widget) + widgets_by_plugins_id[widget.plugin_id].append(widget) + logs_layout.addWidget(widget, 0) + + self._widgets_by_flag = widgets_by_flag + self._widgets_by_plugins_id = widgets_by_plugins_id + + self._visibility_by_flags = { + LOG_DEBUG_VISIBLE: True, + LOG_INFO_VISIBLE: True, + LOG_WARNING_VISIBLE: True, + LOG_ERROR_VISIBLE: True, + LOG_CRITICAL_VISIBLE: True, + ERROR_VISIBLE: True, + INFO_VISIBLE: True, + } + self._flags_filter = sum(self._visibility_by_flags.keys()) + self._plugin_ids_filter = None + + def _update_flags_filtering(self): + for flag in ( + LOG_DEBUG_VISIBLE, + LOG_INFO_VISIBLE, + LOG_WARNING_VISIBLE, + LOG_ERROR_VISIBLE, + LOG_CRITICAL_VISIBLE, + ERROR_VISIBLE, + INFO_VISIBLE, + ): + visible = (self._flags_filter & flag) != 0 + if visible is not self._visibility_by_flags[flag]: + self._visibility_by_flags[flag] = visible + for widget in self._widgets_by_flag[flag]: + widget.set_log_type_filtered(not visible) + + def _update_plugin_filtering(self): + if self._plugin_ids_filter is None: + for widgets in self._widgets_by_plugins_id.values(): + for widget in widgets: + widget.set_plugin_filtered(False) + + else: + for plugin_id, widgets in self._widgets_by_plugins_id.items(): + filtered = plugin_id not in self._plugin_ids_filter + for widget in widgets: + widget.set_plugin_filtered(filtered) + + def set_log_filters(self, visibility_filter, plugin_ids): + if self._flags_filter != visibility_filter: + self._flags_filter = visibility_filter + self._update_flags_filtering() + + if self._plugin_ids_filter != plugin_ids: + if plugin_ids is not None: + plugin_ids = set(plugin_ids) + self._plugin_ids_filter = plugin_ids + self._update_plugin_filtering() + + +class InstanceLogsWidget(QtWidgets.QWidget): + """Widget showing logs of one publish instance. + + Args: + instance (_InstanceItem): Item of instance used as data source. + parent (QtWidgets.QWidget): Parent widget. + """ + + def __init__(self, instance, parent): + super(InstanceLogsWidget, self).__init__(parent) + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + label_widget = QtWidgets.QLabel(instance.label, self) + label_widget.setObjectName("PublishInstanceLogsLabel") + logs_grid = LogsWithIconsView(instance.logs, self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(label_widget, 0) + layout.addWidget(logs_grid, 0) + + self._logs_grid = logs_grid + + def set_log_filters(self, visibility_filter, plugin_ids): + """Change logs filter. + + Args: + visibility_filter (int): Number contained of flags for each log + type and level. + plugin_ids (Iterable[str]): Plugin ids to which are logs filtered. + """ + + self._logs_grid.set_log_filters(visibility_filter, plugin_ids) + + +class InstancesLogsView(QtWidgets.QFrame): + """Publish instances logs view widget.""" + + def __init__(self, parent): + super(InstancesLogsView, self).__init__(parent) + self.setObjectName("InstancesLogsView") + + scroll_area = QtWidgets.QScrollArea(self) + scroll_area.setWidgetResizable(True) + scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + scroll_area.setAttribute(QtCore.Qt.WA_TranslucentBackground) + scrollbar_bg = scroll_area.verticalScrollBar().parent() + if scrollbar_bg: + scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_wrap_widget = QtWidgets.QWidget(scroll_area) + content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + content_widget = QtWidgets.QWidget(content_wrap_widget) + content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setSpacing(15) + + scroll_area.setWidget(content_wrap_widget) + + content_wrap_layout = QtWidgets.QVBoxLayout(content_wrap_widget) + content_wrap_layout.setContentsMargins(0, 0, 0, 0) + content_wrap_layout.addWidget(content_widget, 0) + content_wrap_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(scroll_area, 1) + + self._visible_filters = ( + LOG_INFO_VISIBLE + | LOG_WARNING_VISIBLE + | LOG_ERROR_VISIBLE + | LOG_CRITICAL_VISIBLE + | ERROR_VISIBLE + | INFO_VISIBLE + ) + + self._content_widget = content_widget + self._content_layout = content_layout + + self._instances_order = [] + self._instances_by_id = {} + self._views_by_instance_id = {} + self._is_showed = False + self._clear_needed = False + self._update_needed = False + self._instance_ids_filter = [] + self._plugin_ids_filter = None + + def showEvent(self, event): + super(InstancesLogsView, self).showEvent(event) + self._is_showed = True + self._update_instances() + + def hideEvent(self, event): + super(InstancesLogsView, self).hideEvent(event) + self._is_showed = False + + def closeEvent(self, event): + super(InstancesLogsView, self).closeEvent(event) + self._is_showed = False + + def _update_instances(self): + if not self._is_showed: + return + + if self._clear_needed: + self._clear_widgets() + self._clear_needed = False + + if not self._update_needed: + return + self._update_needed = False + + instance_ids = self._instance_ids_filter + to_hide = set() + if not instance_ids: + instance_ids = self._instances_by_id + else: + to_hide = set(self._instances_by_id) - set(instance_ids) + + for instance_id in instance_ids: + widget = self._views_by_instance_id.get(instance_id) + if widget is None: + instance = self._instances_by_id[instance_id] + widget = InstanceLogsWidget(instance, self._content_widget) + self._views_by_instance_id[instance_id] = widget + self._content_layout.addWidget(widget, 0) + + widget.setVisible(True) + widget.set_log_filters( + self._visible_filters, self._plugin_ids_filter + ) + + for instance_id in to_hide: + widget = self._views_by_instance_id.get(instance_id) + if widget is not None: + widget.setVisible(False) + + def _clear_widgets(self): + """Remove all widgets from layout and from cache.""" + + while self._content_layout.count(): + item = self._content_layout.takeAt(0) + widget = item.widget() + if widget: + widget.setVisible(False) + widget.deleteLater() + self._views_by_instance_id = {} + + def update_instances(self, instances): + """Update publish instance from report. + + Args: + instances (list[_InstanceItem]): Instance data from report. + """ + + self._instances_order = [ + instance.id for instance in instances + ] + self._instances_by_id = { + instance.id: instance + for instance in instances + } + self._instance_ids_filter = [] + self._plugin_ids_filter = None + self._clear_needed = True + self._update_needed = True + self._update_instances() + + def set_instances_filter(self, instance_ids=None): + """Set instance filter. + + Args: + instance_ids (Optional[list[str]]): List of instances to keep + visible. Pass empty list to hide all items. + """ + + self._instance_ids_filter = instance_ids + self._update_needed = True + self._update_instances() + + def set_plugins_filter(self, plugin_ids=None): + if self._plugin_ids_filter == plugin_ids: + return + self._plugin_ids_filter = plugin_ids + self._update_needed = True + self._update_instances() + + +class CrashWidget(QtWidgets.QWidget): + """Widget shown when publishing crashes. + + Contains only minimal information for artist with easy access to report + actions. + """ + + def __init__(self, controller, parent): + super(CrashWidget, self).__init__(parent) + + main_label = QtWidgets.QLabel("This is not your fault", self) + main_label.setAlignment(QtCore.Qt.AlignCenter) + main_label.setObjectName("PublishCrashMainLabel") + + report_label = QtWidgets.QLabel( + ( + "Please report the error to your pipeline support" + " using one of the options below." + ), + self + ) + report_label.setAlignment(QtCore.Qt.AlignCenter) + report_label.setWordWrap(True) + report_label.setObjectName("PublishCrashReportLabel") + + btns_widget = QtWidgets.QWidget(self) + copy_clipboard_btn = QtWidgets.QPushButton( + "Copy to clipboard", btns_widget) + save_to_disk_btn = QtWidgets.QPushButton( + "Save to disk", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.addStretch(1) + btns_layout.addWidget(copy_clipboard_btn, 0) + btns_layout.addSpacing(20) + btns_layout.addWidget(save_to_disk_btn, 0) + btns_layout.addStretch(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.addStretch(1) + layout.addWidget(main_label, 0) + layout.addSpacing(20) + layout.addWidget(report_label, 0) + layout.addSpacing(20) + layout.addWidget(btns_widget, 0) + layout.addStretch(2) + + copy_clipboard_btn.clicked.connect(self._on_copy_to_clipboard) + save_to_disk_btn.clicked.connect(self._on_save_to_disk_click) + + self._controller = controller + + def _on_copy_to_clipboard(self): + self._controller.event_system.emit( + "copy_report.request", {}, "report_page") + + def _on_save_to_disk_click(self): + self._controller.event_system.emit( + "export_report.request", {}, "report_page") + + +class ErrorDetailsWidget(QtWidgets.QWidget): + def __init__(self, parent): + super(ErrorDetailsWidget, self).__init__(parent) + + inputs_widget = QtWidgets.QWidget(self) + # Error 'Description' input + error_description_input = ExpandingTextEdit(inputs_widget) + error_description_input.setObjectName("InfoText") + error_description_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + # Error 'Details' widget -> Collapsible + error_details_widget = QtWidgets.QWidget(inputs_widget) + + error_details_top = ClickableFrame(error_details_widget) + + error_details_expand_btn = ClassicExpandBtn(error_details_top) + error_details_expand_label = QtWidgets.QLabel( + "Details", error_details_top) + + line_widget = SeparatorWidget(1, parent=error_details_top) + + error_details_top_l = QtWidgets.QHBoxLayout(error_details_top) + error_details_top_l.setContentsMargins(0, 0, 10, 0) + error_details_top_l.addWidget(error_details_expand_btn, 0) + error_details_top_l.addWidget(error_details_expand_label, 0) + error_details_top_l.addWidget(line_widget, 1) + + error_details_input = ExpandingTextEdit(error_details_widget) + error_details_input.setObjectName("InfoText") + error_details_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + error_details_input.setVisible(not error_details_expand_btn.collapsed) + + error_details_layout = QtWidgets.QVBoxLayout(error_details_widget) + error_details_layout.setContentsMargins(0, 0, 0, 0) + error_details_layout.addWidget(error_details_top, 0) + error_details_layout.addWidget(error_details_input, 0) + error_details_layout.addStretch(1) + + # Description and Details layout + inputs_layout = QtWidgets.QVBoxLayout(inputs_widget) + inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.setSpacing(10) + inputs_layout.addWidget(error_description_input, 0) + inputs_layout.addWidget(error_details_widget, 1) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(inputs_widget, 1) + + error_details_top.clicked.connect(self._on_detail_toggle) + + self._error_details_widget = error_details_widget + self._error_description_input = error_description_input + self._error_details_expand_btn = error_details_expand_btn + self._error_details_input = error_details_input + + def _on_detail_toggle(self): + self._error_details_expand_btn.set_collapsed() + self._error_details_input.setVisible( + not self._error_details_expand_btn.collapsed) + + def set_error_item(self, error_item): + detail = "" + description = "" + if error_item: + description = error_item.description or description + detail = error_item.detail or detail + + if commonmark: + self._error_description_input.setHtml( + commonmark.commonmark(description) + ) + self._error_details_input.setHtml( + commonmark.commonmark(detail) + ) + + elif hasattr(self._error_details_input, "setMarkdown"): + self._error_description_input.setMarkdown(description) + self._error_details_input.setMarkdown(detail) + + else: + self._error_description_input.setText(description) + self._error_details_input.setText(detail) + + self._error_details_widget.setVisible(bool(detail)) + + +class ReportsWidget(QtWidgets.QWidget): + """ + # Crash layout + ┌──────┬─────────┬─────────┐ + │Views │ Logs │ Details │ + │ │ │ │ + │ │ │ │ + └──────┴─────────┴─────────┘ + # Success layout + ┌──────┬───────────────────┐ + │View │ Logs │ + │ │ │ + │ │ │ + └──────┴───────────────────┘ + # Validation errors layout + ┌──────┬─────────┬─────────┐ + │Views │ Actions │ │ + │ ├─────────┤ Details │ + │ │ Logs │ │ + │ │ │ │ + └──────┴─────────┴─────────┘ + """ + + def __init__(self, controller, parent): + super(ReportsWidget, self).__init__(parent) + + # Instances view + views_widget = QtWidgets.QWidget(self) + + instances_view = PublishInstancesViewWidget(controller, views_widget) + + validation_error_view = ValidationErrorsView(views_widget) + + views_layout = QtWidgets.QStackedLayout(views_widget) + views_layout.setContentsMargins(0, 0, 0, 0) + views_layout.addWidget(instances_view) + views_layout.addWidget(validation_error_view) + + views_layout.setCurrentWidget(instances_view) + + # Error description with actions and optional detail + details_widget = QtWidgets.QFrame(self) + details_widget.setObjectName("PublishInstancesDetails") + + # Actions widget + actions_widget = ValidateActionsWidget(controller, details_widget) + + pages_widget = QtWidgets.QWidget(details_widget) + + # Logs view + logs_view = InstancesLogsView(pages_widget) + + # Validation details + # Description and details inputs are in scroll + # - single scroll for both inputs, they are forced to not use theirs + detail_inputs_spacer = QtWidgets.QWidget(pages_widget) + detail_inputs_spacer.setMinimumWidth(30) + detail_inputs_spacer.setMaximumWidth(30) + + detail_input_scroll = QtWidgets.QScrollArea(pages_widget) + + detail_inputs_widget = ErrorDetailsWidget(detail_input_scroll) + detail_inputs_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + detail_input_scroll.setWidget(detail_inputs_widget) + detail_input_scroll.setWidgetResizable(True) + detail_input_scroll.setViewportMargins(0, 0, 0, 0) + + # Crash information + crash_widget = CrashWidget(controller, details_widget) + + # Layout pages + pages_layout = QtWidgets.QHBoxLayout(pages_widget) + pages_layout.setContentsMargins(0, 0, 0, 0) + pages_layout.addWidget(logs_view, 1) + pages_layout.addWidget(detail_inputs_spacer, 0) + pages_layout.addWidget(detail_input_scroll, 1) + pages_layout.addWidget(crash_widget, 1) + + details_layout = QtWidgets.QVBoxLayout(details_widget) + margins = details_layout.contentsMargins() + margins.setTop(margins.top() * 2) + margins.setBottom(margins.bottom() * 2) + details_layout.setContentsMargins(margins) + details_layout.setSpacing(margins.top()) + details_layout.addWidget(actions_widget, 0) + details_layout.addWidget(pages_widget, 1) + + content_layout = QtWidgets.QHBoxLayout(self) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.addWidget(views_widget, 0) + content_layout.addWidget(details_widget, 1) + + instances_view.selection_changed.connect(self._on_instance_selection) + validation_error_view.selection_changed.connect( + self._on_error_selection) + + self._views_layout = views_layout + self._instances_view = instances_view + self._validation_error_view = validation_error_view + + self._actions_widget = actions_widget + self._detail_inputs_widget = detail_inputs_widget + self._logs_view = logs_view + self._detail_inputs_spacer = detail_inputs_spacer + self._detail_input_scroll = detail_input_scroll + self._crash_widget = crash_widget + + self._controller = controller + + self._validation_errors_by_id = {} + + def _get_instance_items(self): + report = self._controller.get_publish_report() + context_label = report["context"]["label"] or CONTEXT_LABEL + instances_by_id = report["instances"] + plugins_info = report["plugins_data"] + logs_by_instance_id = collections.defaultdict(list) + for plugin_info in plugins_info: + plugin_id = plugin_info["id"] + for instance_info in plugin_info["instances_data"]: + instance_id = instance_info["id"] or CONTEXT_ID + for log in instance_info["logs"]: + log["plugin_id"] = plugin_id + logs_by_instance_id[instance_id].extend(instance_info["logs"]) + + context_item = _InstanceItem.create_context_item( + context_label, logs_by_instance_id[CONTEXT_ID]) + instance_items = [ + _InstanceItem.from_report( + instance_id, instance, logs_by_instance_id[instance_id] + ) + for instance_id, instance in instances_by_id.items() + if instance["exists"] + ] + instance_items.sort() + instance_items.insert(0, context_item) + return instance_items + + def update_data(self): + view = self._instances_view + validation_error_mode = False + if ( + not self._controller.publish_has_crashed + and self._controller.publish_has_validation_errors + ): + view = self._validation_error_view + validation_error_mode = True + + self._actions_widget.set_visible_mode(validation_error_mode) + self._detail_inputs_spacer.setVisible(validation_error_mode) + self._detail_input_scroll.setVisible(validation_error_mode) + self._views_layout.setCurrentWidget(view) + + self._crash_widget.setVisible(self._controller.publish_has_crashed) + self._logs_view.setVisible(not self._controller.publish_has_crashed) + + # Instance view & logs update + instance_items = self._get_instance_items() + self._instances_view.update_instances(instance_items) + self._logs_view.update_instances(instance_items) + + # Validation errors + validation_errors = self._controller.get_validation_errors() + grouped_error_items = validation_errors.group_items_by_title() + + validation_errors_by_id = { + title_item["id"]: title_item + for title_item in grouped_error_items + } + + self._validation_errors_by_id = validation_errors_by_id + self._validation_error_view.set_errors(grouped_error_items) + + def _on_instance_selection(self): + instance_ids = self._instances_view.get_selected_instance_ids() + self._logs_view.set_instances_filter(instance_ids) + + def _on_error_selection(self): + title_id, instance_ids = ( + self._validation_error_view.get_selected_items()) + error_info = self._validation_errors_by_id.get(title_id) + if error_info is None: + self._actions_widget.set_error_info(None) + self._detail_inputs_widget.set_error_item(None) + return + + self._logs_view.set_instances_filter(instance_ids) + self._logs_view.set_plugins_filter([error_info["plugin_id"]]) + + match_error_item = None + for error_item in error_info["error_items"]: + instance_id = error_item.instance_id or CONTEXT_ID + if instance_id in instance_ids: + match_error_item = error_item + break + + self._actions_widget.set_error_info(error_info) + self._detail_inputs_widget.set_error_item(match_error_item) + + +class ReportPageWidget(QtWidgets.QFrame): + """Widgets showing report for artis. + + There are 5 possible states: + 1. Publishing did not start yet. > Only label. + 2. Publishing is paused. ┐ + 3. Publishing successfully finished. │> Instances with logs. + 4. Publishing crashed. ┘ + 5. Crashed because of validation error. > Errors with logs. + + This widget is shown if validation errors happened during validation part. + + Shows validation error titles with instances on which they happened + and validation error detail with possible actions (repair). + """ + + def __init__(self, controller, parent): + super(ReportPageWidget, self).__init__(parent) + + header_label = QtWidgets.QLabel(self) + header_label.setAlignment(QtCore.Qt.AlignCenter) + header_label.setObjectName("PublishReportHeader") + + publish_instances_widget = ReportsWidget(controller, self) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(header_label, 0) + layout.addWidget(publish_instances_widget, 0) + + controller.event_system.add_callback( + "publish.process.started", self._on_publish_start + ) + controller.event_system.add_callback( + "publish.reset.finished", self._on_publish_reset + ) + controller.event_system.add_callback( + "publish.process.stopped", self._on_publish_stop + ) + + self._header_label = header_label + self._publish_instances_widget = publish_instances_widget + + self._controller = controller + + def _update_label(self): + if not self._controller.publish_has_started: + # This probably never happen when this widget is visible + header_label = "Nothing to report until you run publish" + elif self._controller.publish_has_crashed: + header_label = "Publish error report" + elif self._controller.publish_has_validation_errors: + header_label = "Publish validation report" + elif self._controller.publish_has_finished: + header_label = "Publish success report" + else: + header_label = "Publish report" + self._header_label.setText(header_label) + + def _update_state(self): + self._update_label() + publish_started = self._controller.publish_has_started + self._publish_instances_widget.setVisible(publish_started) + if publish_started: + self._publish_instances_widget.update_data() + + self.updateGeometry() + + def _on_publish_start(self): + self._update_state() + + def _on_publish_reset(self): + self._update_state() + + def _on_publish_stop(self): + self._update_state() diff --git a/openpype/tools/publisher/widgets/thumbnail_widget.py b/openpype/tools/publisher/widgets/thumbnail_widget.py index e234f4cdc1..b17ca0adc8 100644 --- a/openpype/tools/publisher/widgets/thumbnail_widget.py +++ b/openpype/tools/publisher/widgets/thumbnail_widget.py @@ -75,6 +75,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): painter = QtGui.QPainter() painter.begin(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) painter.drawPixmap(0, 0, self._cached_pix) painter.end() @@ -183,6 +184,18 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): backgrounded_images.append(new_pix) return backgrounded_images + def _paint_dash_line(self, painter, rect): + pen = QtGui.QPen() + pen.setWidth(1) + pen.setBrush(QtCore.Qt.darkGray) + pen.setStyle(QtCore.Qt.DashLine) + + new_rect = rect.adjusted(1, 1, -1, -1) + painter.setPen(pen) + painter.setBrush(QtCore.Qt.transparent) + # painter.drawRect(rect) + painter.drawRect(new_rect) + def _cache_pix(self): rect = self.rect() rect_width = rect.width() @@ -264,13 +277,7 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): # Draw drop enabled dashes if used_default_pix: - pen = QtGui.QPen() - pen.setWidth(1) - pen.setBrush(QtCore.Qt.darkGray) - pen.setStyle(QtCore.Qt.DashLine) - final_painter.setPen(pen) - final_painter.setBrush(QtCore.Qt.transparent) - final_painter.drawRect(rect) + self._paint_dash_line(final_painter, rect) final_painter.end() diff --git a/openpype/tools/publisher/widgets/validations_widget.py b/openpype/tools/publisher/widgets/validations_widget.py deleted file mode 100644 index 0abe85c0b8..0000000000 --- a/openpype/tools/publisher/widgets/validations_widget.py +++ /dev/null @@ -1,715 +0,0 @@ -# -*- coding: utf-8 -*- -try: - import commonmark -except Exception: - commonmark = None - -from qtpy import QtWidgets, QtCore, QtGui - -from openpype.tools.utils import BaseClickableFrame, ClickableFrame -from .widgets import ( - IconValuePixmapLabel -) -from ..constants import ( - INSTANCE_ID_ROLE -) - - -class ValidationErrorInstanceList(QtWidgets.QListView): - """List of publish instances that caused a validation error. - - Instances are collected per plugin's validation error title. - """ - def __init__(self, *args, **kwargs): - super(ValidationErrorInstanceList, self).__init__(*args, **kwargs) - - self.setObjectName("ValidationErrorInstanceList") - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - - def minimumSizeHint(self): - return self.sizeHint() - - def sizeHint(self): - result = super(ValidationErrorInstanceList, self).sizeHint() - row_count = self.model().rowCount() - height = 0 - if row_count > 0: - height = self.sizeHintForRow(0) * row_count - result.setHeight(height) - return result - - -class ValidationErrorTitleWidget(QtWidgets.QWidget): - """Title of validation error. - - Widget is used as radio button so requires clickable functionality and - changing style on selection/deselection. - - Has toggle button to show/hide instances on which validation error happened - if there is a list (Valdation error may happen on context). - """ - - selected = QtCore.Signal(int) - instance_changed = QtCore.Signal(int) - - def __init__(self, index, error_info, parent): - super(ValidationErrorTitleWidget, self).__init__(parent) - - self._index = index - self._error_info = error_info - self._selected = False - - title_frame = ClickableFrame(self) - title_frame.setObjectName("ValidationErrorTitleFrame") - - toggle_instance_btn = QtWidgets.QToolButton(title_frame) - toggle_instance_btn.setObjectName("ArrowBtn") - toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) - toggle_instance_btn.setMaximumWidth(14) - - label_widget = QtWidgets.QLabel(error_info["title"], title_frame) - - title_frame_layout = QtWidgets.QHBoxLayout(title_frame) - title_frame_layout.addWidget(label_widget, 1) - title_frame_layout.addWidget(toggle_instance_btn, 0) - - instances_model = QtGui.QStandardItemModel() - - help_text_by_instance_id = {} - - items = [] - context_validation = False - for error_item in error_info["error_items"]: - context_validation = error_item.context_validation - if context_validation: - toggle_instance_btn.setArrowType(QtCore.Qt.NoArrow) - description = self._prepare_description(error_item) - help_text_by_instance_id[None] = description - # Add fake item to have minimum size hint of view widget - items.append(QtGui.QStandardItem("Context")) - continue - - label = error_item.instance_label - item = QtGui.QStandardItem(label) - item.setFlags( - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable - ) - item.setData(label, QtCore.Qt.ToolTipRole) - item.setData(error_item.instance_id, INSTANCE_ID_ROLE) - items.append(item) - description = self._prepare_description(error_item) - help_text_by_instance_id[error_item.instance_id] = description - - if items: - root_item = instances_model.invisibleRootItem() - root_item.appendRows(items) - - instances_view = ValidationErrorInstanceList(self) - instances_view.setModel(instances_model) - - self.setLayoutDirection(QtCore.Qt.LeftToRight) - - view_widget = QtWidgets.QWidget(self) - view_layout = QtWidgets.QHBoxLayout(view_widget) - view_layout.setContentsMargins(0, 0, 0, 0) - view_layout.setSpacing(0) - view_layout.addSpacing(14) - view_layout.addWidget(instances_view, 0) - - layout = QtWidgets.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(title_frame, 0) - layout.addWidget(view_widget, 0) - view_widget.setVisible(False) - - if not context_validation: - toggle_instance_btn.clicked.connect(self._on_toggle_btn_click) - - title_frame.clicked.connect(self._mouse_release_callback) - instances_view.selectionModel().selectionChanged.connect( - self._on_seleciton_change - ) - - self._title_frame = title_frame - - self._toggle_instance_btn = toggle_instance_btn - - self._view_widget = view_widget - - self._instances_model = instances_model - self._instances_view = instances_view - - self._context_validation = context_validation - self._help_text_by_instance_id = help_text_by_instance_id - - self._expanded = False - - def sizeHint(self): - result = super(ValidationErrorTitleWidget, self).sizeHint() - expected_width = max( - self._view_widget.minimumSizeHint().width(), - self._view_widget.sizeHint().width() - ) - - if expected_width < 200: - expected_width = 200 - - if result.width() < expected_width: - result.setWidth(expected_width) - - return result - - def minimumSizeHint(self): - return self.sizeHint() - - def _prepare_description(self, error_item): - """Prepare description text for detail intput. - - Args: - error_item (ValidationErrorItem): Item which hold information about - validation error. - - Returns: - str: Prepared detailed description. - """ - - dsc = error_item.description - detail = error_item.detail - if detail: - dsc += "

{}".format(detail) - - description = dsc - if commonmark: - description = commonmark.commonmark(dsc) - return description - - def _mouse_release_callback(self): - """Mark this widget as selected on click.""" - - self.set_selected(True) - - def current_description_text(self): - if self._context_validation: - return self._help_text_by_instance_id[None] - index = self._instances_view.currentIndex() - # TODO make sure instance is selected - if not index.isValid(): - index = self._instances_model.index(0, 0) - - indence_id = index.data(INSTANCE_ID_ROLE) - return self._help_text_by_instance_id[indence_id] - - @property - def is_selected(self): - """Is widget marked a selected. - - Returns: - bool: Item is selected or not. - """ - - return self._selected - - @property - def index(self): - """Widget's index set by parent. - - Returns: - int: Index of widget. - """ - - return self._index - - def set_index(self, index): - """Set index of widget (called by parent). - - Args: - int: New index of widget. - """ - - self._index = index - - def _change_style_property(self, selected): - """Change style of widget based on selection.""" - - value = "1" if selected else "" - self._title_frame.setProperty("selected", value) - self._title_frame.style().polish(self._title_frame) - - def set_selected(self, selected=None): - """Change selected state of widget.""" - - if selected is None: - selected = not self._selected - - # Clear instance view selection on deselect - if not selected: - self._instances_view.clearSelection() - - # Skip if has same value - if selected == self._selected: - return - - self._selected = selected - self._change_style_property(selected) - if selected: - self.selected.emit(self._index) - self._set_expanded(True) - - def _on_toggle_btn_click(self): - """Show/hide instances list.""" - - self._set_expanded() - - def _set_expanded(self, expanded=None): - if expanded is None: - expanded = not self._expanded - - elif expanded is self._expanded: - return - - if expanded and self._context_validation: - return - - self._expanded = expanded - self._view_widget.setVisible(expanded) - if expanded: - self._toggle_instance_btn.setArrowType(QtCore.Qt.DownArrow) - else: - self._toggle_instance_btn.setArrowType(QtCore.Qt.RightArrow) - - def _on_seleciton_change(self): - sel_model = self._instances_view.selectionModel() - if sel_model.selectedIndexes(): - self.instance_changed.emit(self._index) - - -class ActionButton(BaseClickableFrame): - """Plugin's action callback button. - - Action may have label or icon or both. - - Args: - plugin_action_item (PublishPluginActionItem): Action item that can be - triggered by it's id. - """ - - action_clicked = QtCore.Signal(str, str) - - def __init__(self, plugin_action_item, parent): - super(ActionButton, self).__init__(parent) - - self.setObjectName("ValidationActionButton") - - self.plugin_action_item = plugin_action_item - - action_label = plugin_action_item.label - action_icon = plugin_action_item.icon - label_widget = QtWidgets.QLabel(action_label, self) - icon_label = None - if action_icon: - icon_label = IconValuePixmapLabel(action_icon, self) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(5, 0, 5, 0) - layout.addWidget(label_widget, 1) - if icon_label: - layout.addWidget(icon_label, 0) - - self.setSizePolicy( - QtWidgets.QSizePolicy.Minimum, - self.sizePolicy().verticalPolicy() - ) - - def _mouse_release_callback(self): - self.action_clicked.emit( - self.plugin_action_item.plugin_id, - self.plugin_action_item.action_id - ) - - -class ValidateActionsWidget(QtWidgets.QFrame): - """Wrapper widget for plugin actions. - - Change actions based on selected validation error. - """ - - def __init__(self, controller, parent): - super(ValidateActionsWidget, self).__init__(parent) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - - content_widget = QtWidgets.QWidget(self) - content_layout = QtWidgets.QVBoxLayout(content_widget) - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(content_widget) - - self._controller = controller - self._content_widget = content_widget - self._content_layout = content_layout - self._actions_mapping = {} - - def clear(self): - """Remove actions from widget.""" - while self._content_layout.count(): - item = self._content_layout.takeAt(0) - widget = item.widget() - if widget: - widget.setVisible(False) - widget.deleteLater() - self._actions_mapping = {} - - def set_error_item(self, error_item): - """Set selected plugin and show it's actions. - - Clears current actions from widget and recreate them from the plugin. - - Args: - Dict[str, Any]: Object holding error items, title and possible - actions to run. - """ - - self.clear() - - if not error_item: - self.setVisible(False) - return - - plugin_action_items = error_item["plugin_action_items"] - for plugin_action_item in plugin_action_items: - if not plugin_action_item.active: - continue - - if plugin_action_item.on_filter not in ("failed", "all"): - continue - - action_id = plugin_action_item.action_id - self._actions_mapping[action_id] = plugin_action_item - - action_btn = ActionButton(plugin_action_item, self._content_widget) - action_btn.action_clicked.connect(self._on_action_click) - self._content_layout.addWidget(action_btn) - - if self._content_layout.count() > 0: - self.setVisible(True) - self._content_layout.addStretch(1) - else: - self.setVisible(False) - - def _on_action_click(self, plugin_id, action_id): - self._controller.run_action(plugin_id, action_id) - - -class VerticallScrollArea(QtWidgets.QScrollArea): - """Scroll area for validation error titles. - - The biggest difference is that the scroll area has scroll bar on left side - and resize of content will also resize scrollarea itself. - - Resize if deferred by 100ms because at the moment of resize are not yet - propagated sizes and visibility of scroll bars. - """ - - def __init__(self, *args, **kwargs): - super(VerticallScrollArea, self).__init__(*args, **kwargs) - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.setLayoutDirection(QtCore.Qt.RightToLeft) - - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - # Background of scrollbar will be transparent - scrollbar_bg = self.verticalScrollBar().parent() - if scrollbar_bg: - scrollbar_bg.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.setViewportMargins(0, 0, 0, 0) - - self.verticalScrollBar().installEventFilter(self) - - # Timer with 100ms offset after changing size - size_changed_timer = QtCore.QTimer() - size_changed_timer.setInterval(100) - size_changed_timer.setSingleShot(True) - - size_changed_timer.timeout.connect(self._on_timer_timeout) - self._size_changed_timer = size_changed_timer - - def setVerticalScrollBar(self, widget): - old_widget = self.verticalScrollBar() - if old_widget: - old_widget.removeEventFilter(self) - - super(VerticallScrollArea, self).setVerticalScrollBar(widget) - if widget: - widget.installEventFilter(self) - - def setWidget(self, widget): - old_widget = self.widget() - if old_widget: - old_widget.removeEventFilter(self) - - super(VerticallScrollArea, self).setWidget(widget) - if widget: - widget.installEventFilter(self) - - def _on_timer_timeout(self): - width = self.widget().width() - if self.verticalScrollBar().isVisible(): - width += self.verticalScrollBar().width() - self.setMinimumWidth(width) - - def eventFilter(self, obj, event): - if ( - event.type() == QtCore.QEvent.Resize - and (obj is self.widget() or obj is self.verticalScrollBar()) - ): - self._size_changed_timer.start() - return super(VerticallScrollArea, self).eventFilter(obj, event) - - -class ValidationArtistMessage(QtWidgets.QWidget): - def __init__(self, message, parent): - super(ValidationArtistMessage, self).__init__(parent) - - artist_msg_label = QtWidgets.QLabel(message, self) - artist_msg_label.setAlignment(QtCore.Qt.AlignCenter) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget( - artist_msg_label, 1, QtCore.Qt.AlignCenter - ) - - -class ValidationsWidget(QtWidgets.QFrame): - """Widgets showing validation error. - - This widget is shown if validation error/s happened during validation part. - - Shows validation error titles with instances on which happened and - validation error detail with possible actions (repair). - - ┌──────┬────────────────┬───────┐ - │titles│ │actions│ - │ │ │ │ - │ │ Error detail │ │ - │ │ │ │ - │ │ │ │ - └──────┴────────────────┴───────┘ - """ - - def __init__(self, controller, parent): - super(ValidationsWidget, self).__init__(parent) - - # Before publishing - before_publish_widget = ValidationArtistMessage( - "Nothing to report until you run publish", self - ) - # After success publishing - publish_started_widget = ValidationArtistMessage( - "So far so good", self - ) - # After success publishing - publish_stop_ok_widget = ValidationArtistMessage( - "Publishing finished successfully", self - ) - # After failed publishing (not with validation error) - publish_stop_fail_widget = ValidationArtistMessage( - "This is not your fault...", self - ) - - # Validation errors - validations_widget = QtWidgets.QWidget(self) - - content_widget = QtWidgets.QWidget(validations_widget) - - errors_scroll = VerticallScrollArea(content_widget) - errors_scroll.setWidgetResizable(True) - - errors_widget = QtWidgets.QWidget(errors_scroll) - errors_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - errors_layout = QtWidgets.QVBoxLayout(errors_widget) - errors_layout.setContentsMargins(0, 0, 0, 0) - - errors_scroll.setWidget(errors_widget) - - error_details_frame = QtWidgets.QFrame(content_widget) - error_details_input = QtWidgets.QTextEdit(error_details_frame) - error_details_input.setObjectName("InfoText") - error_details_input.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - - actions_widget = ValidateActionsWidget(controller, content_widget) - actions_widget.setMinimumWidth(140) - - error_details_layout = QtWidgets.QHBoxLayout(error_details_frame) - error_details_layout.addWidget(error_details_input, 1) - error_details_layout.addWidget(actions_widget, 0) - - content_layout = QtWidgets.QHBoxLayout(content_widget) - content_layout.setSpacing(0) - content_layout.setContentsMargins(0, 0, 0, 0) - - content_layout.addWidget(errors_scroll, 0) - content_layout.addWidget(error_details_frame, 1) - - top_label = QtWidgets.QLabel( - "Publish validation report", content_widget - ) - top_label.setObjectName("PublishInfoMainLabel") - top_label.setAlignment(QtCore.Qt.AlignCenter) - - validation_layout = QtWidgets.QVBoxLayout(validations_widget) - validation_layout.setContentsMargins(0, 0, 0, 0) - validation_layout.addWidget(top_label, 0) - validation_layout.addWidget(content_widget, 1) - - main_layout = QtWidgets.QStackedLayout(self) - main_layout.addWidget(before_publish_widget) - main_layout.addWidget(publish_started_widget) - main_layout.addWidget(publish_stop_ok_widget) - main_layout.addWidget(publish_stop_fail_widget) - main_layout.addWidget(validations_widget) - - main_layout.setCurrentWidget(before_publish_widget) - - controller.event_system.add_callback( - "publish.process.started", self._on_publish_start - ) - controller.event_system.add_callback( - "publish.reset.finished", self._on_publish_reset - ) - controller.event_system.add_callback( - "publish.process.stopped", self._on_publish_stop - ) - - self._main_layout = main_layout - - self._before_publish_widget = before_publish_widget - self._publish_started_widget = publish_started_widget - self._publish_stop_ok_widget = publish_stop_ok_widget - self._publish_stop_fail_widget = publish_stop_fail_widget - self._validations_widget = validations_widget - - self._top_label = top_label - self._errors_widget = errors_widget - self._errors_layout = errors_layout - self._error_details_frame = error_details_frame - self._error_details_input = error_details_input - self._actions_widget = actions_widget - - self._title_widgets = {} - self._error_info = {} - self._previous_select = None - - self._controller = controller - - def clear(self): - """Delete all dynamic widgets and hide all wrappers.""" - self._title_widgets = {} - self._error_info = {} - self._previous_select = None - while self._errors_layout.count(): - item = self._errors_layout.takeAt(0) - widget = item.widget() - if widget: - widget.deleteLater() - - self._top_label.setVisible(False) - self._error_details_frame.setVisible(False) - self._errors_widget.setVisible(False) - self._actions_widget.setVisible(False) - - def _set_errors(self, validation_error_report): - """Set errors into context and created titles. - - Args: - validation_error_report (PublishValidationErrorsReport): Report - with information about validation errors and publish plugin - actions. - """ - - self.clear() - if not validation_error_report: - return - - self._top_label.setVisible(True) - self._error_details_frame.setVisible(True) - self._errors_widget.setVisible(True) - - grouped_error_items = validation_error_report.group_items_by_title() - for idx, error_info in enumerate(grouped_error_items): - widget = ValidationErrorTitleWidget(idx, error_info, self) - widget.selected.connect(self._on_select) - widget.instance_changed.connect(self._on_instance_change) - self._errors_layout.addWidget(widget) - self._title_widgets[idx] = widget - self._error_info[idx] = error_info - - self._errors_layout.addStretch(1) - - if self._title_widgets: - self._title_widgets[0].set_selected(True) - - self.updateGeometry() - - def _set_current_widget(self, widget): - self._main_layout.setCurrentWidget(widget) - - def _on_publish_start(self): - self._set_current_widget(self._publish_started_widget) - - def _on_publish_reset(self): - self._set_current_widget(self._before_publish_widget) - - def _on_publish_stop(self): - if self._controller.publish_has_crashed: - self._set_current_widget(self._publish_stop_fail_widget) - return - - if self._controller.publish_has_validation_errors: - validation_errors = self._controller.get_validation_errors() - self._set_current_widget(self._validations_widget) - self._set_errors(validation_errors) - return - - if self._controller.publish_has_finished: - self._set_current_widget(self._publish_stop_ok_widget) - return - - self._set_current_widget(self._publish_started_widget) - - def _on_select(self, index): - if self._previous_select: - if self._previous_select.index == index: - return - self._previous_select.set_selected(False) - - self._previous_select = self._title_widgets[index] - - error_item = self._error_info[index] - - self._actions_widget.set_error_item(error_item) - - self._update_description() - - def _on_instance_change(self, index): - if self._previous_select and self._previous_select.index != index: - self._title_widgets[index].set_selected(True) - else: - self._update_description() - - def _update_description(self): - description = self._previous_select.current_description_text() - if commonmark: - html = commonmark.commonmark(description) - self._error_details_input.setHtml(html) - elif hasattr(self._error_details_input, "setMarkdown"): - self._error_details_input.setMarkdown(description) - else: - self._error_details_input.setText(description) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index cd1f1f5a96..0b13f26d57 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -40,6 +40,41 @@ from ..constants import ( INPUTS_LAYOUT_VSPACING, ) +FA_PREFIXES = ["", "fa.", "fa5.", "fa5b.", "fa5s.", "ei.", "mdi."] + + +def parse_icon_def( + icon_def, default_width=None, default_height=None, color=None +): + if not icon_def: + return None + + if isinstance(icon_def, QtGui.QPixmap): + return icon_def + + color = color or "white" + default_width = default_width or 512 + default_height = default_height or 512 + + if isinstance(icon_def, QtGui.QIcon): + return icon_def.pixmap(default_width, default_height) + + try: + if os.path.exists(icon_def): + return QtGui.QPixmap(icon_def) + except Exception: + # TODO logging + pass + + for prefix in FA_PREFIXES: + try: + icon_name = "{}{}".format(prefix, icon_def) + icon = qtawesome.icon(icon_name, color=color) + return icon.pixmap(default_width, default_height) + except Exception: + # TODO logging + continue + class PublishPixmapLabel(PixmapLabel): def _get_pix_size(self): @@ -54,7 +89,6 @@ class IconValuePixmapLabel(PublishPixmapLabel): Handle icon parsing from creators/instances. Using of QAwesome module of path to images. """ - fa_prefixes = ["", "fa."] default_size = 200 def __init__(self, icon_def, parent): @@ -77,31 +111,9 @@ class IconValuePixmapLabel(PublishPixmapLabel): return pix def _parse_icon_def(self, icon_def): - if not icon_def: - return self._default_pixmap() - - if isinstance(icon_def, QtGui.QPixmap): - return icon_def - - if isinstance(icon_def, QtGui.QIcon): - return icon_def.pixmap(self.default_size, self.default_size) - - try: - if os.path.exists(icon_def): - return QtGui.QPixmap(icon_def) - except Exception: - # TODO logging - pass - - for prefix in self.fa_prefixes: - try: - icon_name = "{}{}".format(prefix, icon_def) - icon = qtawesome.icon(icon_name, color="white") - return icon.pixmap(self.default_size, self.default_size) - except Exception: - # TODO logging - continue - + icon = parse_icon_def(icon_def, self.default_size, self.default_size) + if icon: + return icon return self._default_pixmap() @@ -692,6 +704,7 @@ class TasksCombobox(QtWidgets.QComboBox): style.drawControl( QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self ) + painter.end() def is_valid(self): """Are all selected items valid.""" diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b3471163ae..fc90e66f21 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -1,3 +1,6 @@ +import os +import json +import time import collections import copy from qtpy import QtWidgets, QtCore, QtGui @@ -15,10 +18,11 @@ from openpype.tools.utils import ( from .constants import ResetKeySequence from .publish_report_viewer import PublishReportViewerWidget +from .control import CardMessageTypes from .control_qt import QtPublisherController from .widgets import ( OverviewWidget, - ValidationsWidget, + ReportPageWidget, PublishFrame, PublisherTabsWidget, @@ -182,7 +186,7 @@ class PublisherWindow(QtWidgets.QDialog): controller, content_stacked_widget ) - report_widget = ValidationsWidget(controller, parent) + report_widget = ReportPageWidget(controller, parent) # Details - Publish details publish_details_widget = PublishReportViewerWidget( @@ -313,6 +317,13 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "convertors.find.failed", self._on_convertor_error ) + controller.event_system.add_callback( + "export_report.request", self._export_report + ) + controller.event_system.add_callback( + "copy_report.request", self._copy_report + ) + # Store extra header widget for TrayPublisher # - can be used to add additional widgets to header between context @@ -825,6 +836,9 @@ class PublisherWindow(QtWidgets.QDialog): self._validate_btn.setEnabled(validate_enabled) self._publish_btn.setEnabled(publish_enabled) + if not publish_enabled: + self._publish_frame.set_shrunk_state(True) + self._update_publish_details_widget() def _validate_create_instances(self): @@ -941,6 +955,46 @@ class PublisherWindow(QtWidgets.QDialog): under_mouse = widget_x < global_pos.x() self._create_overlay_button.set_under_mouse(under_mouse) + def _copy_report(self): + logs = self._controller.get_publish_report() + logs_string = json.dumps(logs, indent=4) + + mime_data = QtCore.QMimeData() + mime_data.setText(logs_string) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) + self._controller.emit_card_message( + "Report added to clipboard", + CardMessageTypes.info) + + def _export_report(self): + default_filename = "publish-report-{}".format( + time.strftime("%y%m%d-%H-%M") + ) + default_filepath = os.path.join( + os.path.expanduser("~"), + default_filename + ) + new_filepath, ext = QtWidgets.QFileDialog.getSaveFileName( + self, "Save report", default_filepath, ".json" + ) + if not ext or not new_filepath: + return + + logs = self._controller.get_publish_report() + full_path = new_filepath + ext + dir_path = os.path.dirname(full_path) + if not os.path.exists(dir_path): + os.makedirs(dir_path) + + with open(full_path, "w") as file_stream: + json.dump(logs, file_stream) + + self._controller.emit_card_message( + "Report saved", + CardMessageTypes.info) + class ErrorsMessageBox(ErrorMessageBox): def __init__(self, error_title, failed_info, message_start, parent): diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 4149763f80..10bd527692 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -1,13 +1,16 @@ +from .layouts import FlowLayout from .widgets import ( FocusSpinBox, FocusDoubleSpinBox, ComboBox, CustomTextComboBox, PlaceholderLineEdit, + ExpandingTextEdit, BaseClickableFrame, ClickableFrame, ClickableLabel, ExpandBtn, + ClassicExpandBtn, PixmapLabel, IconButton, PixmapButton, @@ -37,15 +40,19 @@ from .overlay_messages import ( __all__ = ( + "FlowLayout", + "FocusSpinBox", "FocusDoubleSpinBox", "ComboBox", "CustomTextComboBox", "PlaceholderLineEdit", + "ExpandingTextEdit", "BaseClickableFrame", "ClickableFrame", "ClickableLabel", "ExpandBtn", + "ClassicExpandBtn", "PixmapLabel", "IconButton", "PixmapButton", diff --git a/openpype/tools/utils/layouts.py b/openpype/tools/utils/layouts.py new file mode 100644 index 0000000000..65ea087c27 --- /dev/null +++ b/openpype/tools/utils/layouts.py @@ -0,0 +1,150 @@ +from qtpy import QtWidgets, QtCore + + +class FlowLayout(QtWidgets.QLayout): + """Layout that organize widgets by minimum size into a flow layout. + + Layout is putting widget from left to right and top to bottom. When widget + can't fit a row it is added to next line. Minimum size matches widget with + biggest 'sizeHint' width and height using calculated geometry. + + Content margins are part of calculations. It is possible to define + horizontal and vertical spacing. + + Layout does not support stretch and spacing items. + + Todos: + Unified width concept -> use width of largest item so all of them are + same. This could allow to have minimum columns option too. + """ + + def __init__(self, parent=None): + super(FlowLayout, self).__init__(parent) + + # spaces between each item + self._horizontal_spacing = 5 + self._vertical_spacing = 5 + + self._items = [] + + def __del__(self): + while self.count(): + self.takeAt(0, False) + + def isEmpty(self): + for item in self._items: + if not item.isEmpty(): + return False + return True + + def setSpacing(self, spacing): + self._horizontal_spacing = spacing + self._vertical_spacing = spacing + self.invalidate() + + def setHorizontalSpacing(self, spacing): + self._horizontal_spacing = spacing + self.invalidate() + + def setVerticalSpacing(self, spacing): + self._vertical_spacing = spacing + self.invalidate() + + def addItem(self, item): + self._items.append(item) + self.invalidate() + + def count(self): + return len(self._items) + + def itemAt(self, index): + if 0 <= index < len(self._items): + return self._items[index] + return None + + def takeAt(self, index, invalidate=True): + if 0 <= index < len(self._items): + item = self._items.pop(index) + if invalidate: + self.invalidate() + return item + return None + + def expandingDirections(self): + return QtCore.Qt.Orientations(QtCore.Qt.Vertical) + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + return self._setup_geometry(QtCore.QRect(0, 0, width, 0), True) + + def setGeometry(self, rect): + super(FlowLayout, self).setGeometry(rect) + self._setup_geometry(rect) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize(0, 0) + for item in self._items: + widget = item.widget() + if widget is not None: + parent = widget.parent() + if not widget.isVisibleTo(parent): + continue + size = size.expandedTo(item.minimumSize()) + + if size.width() < 1 or size.height() < 1: + return size + l_margin, t_margin, r_margin, b_margin = self.getContentsMargins() + size += QtCore.QSize(l_margin + r_margin, t_margin + b_margin) + return size + + def _setup_geometry(self, rect, only_calculate=False): + h_spacing = self._horizontal_spacing + v_spacing = self._vertical_spacing + l_margin, t_margin, r_margin, b_margin = self.getContentsMargins() + + left_x = rect.x() + l_margin + top_y = rect.y() + t_margin + pos_x = left_x + pos_y = top_y + row_height = 0 + for item in self._items: + item_hint = item.sizeHint() + item_width = item_hint.width() + item_height = item_hint.height() + if item_width < 1 or item_height < 1: + continue + + end_x = pos_x + item_width + + wrap = ( + row_height > 0 + and ( + end_x > rect.right() + or (end_x + r_margin) > rect.right() + ) + ) + if not wrap: + next_pos_x = end_x + h_spacing + else: + pos_x = left_x + pos_y += row_height + v_spacing + next_pos_x = pos_x + item_width + h_spacing + row_height = 0 + + if not only_calculate: + item.setGeometry( + QtCore.QRect(pos_x, pos_y, item_width, item_height) + ) + + pos_x = next_pos_x + row_height = max(row_height, item_height) + + height = (pos_y - top_y) + row_height + if height > 0: + height += b_margin + return height diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index bae89aeb09..5a8104611b 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -101,6 +101,46 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit): self.setPalette(filter_palette) +class ExpandingTextEdit(QtWidgets.QTextEdit): + """QTextEdit which does not have sroll area but expands height.""" + + def __init__(self, parent=None): + super(ExpandingTextEdit, self).__init__(parent) + + size_policy = self.sizePolicy() + size_policy.setHeightForWidth(True) + size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred) + self.setSizePolicy(size_policy) + + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + doc = self.document() + doc.contentsChanged.connect(self._on_doc_change) + + def _on_doc_change(self): + self.updateGeometry() + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + margins = self.contentsMargins() + + document_width = 0 + if width >= margins.left() + margins.right(): + document_width = width - margins.left() - margins.right() + + document = self.document().clone() + document.setTextWidth(document_width) + + return margins.top() + document.size().height() + margins.bottom() + + def sizeHint(self): + width = super(ExpandingTextEdit, self).sizeHint().width() + return QtCore.QSize(width, self.heightForWidth(width)) + + class BaseClickableFrame(QtWidgets.QFrame): """Widget that catch left mouse click and can trigger a callback. @@ -161,19 +201,34 @@ class ClickableLabel(QtWidgets.QLabel): class ExpandBtnLabel(QtWidgets.QLabel): """Label showing expand icon meant for ExpandBtn.""" + state_changed = QtCore.Signal() + + def __init__(self, parent): super(ExpandBtnLabel, self).__init__(parent) - self._source_collapsed_pix = QtGui.QPixmap( - get_style_image_path("branch_closed") - ) - self._source_expanded_pix = QtGui.QPixmap( - get_style_image_path("branch_open") - ) + self._source_collapsed_pix = self._create_collapsed_pixmap() + self._source_expanded_pix = self._create_expanded_pixmap() self._current_image = self._source_collapsed_pix self._collapsed = True - def set_collapsed(self, collapsed): + def _create_collapsed_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("branch_closed") + ) + + def _create_expanded_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("branch_open") + ) + + @property + def collapsed(self): + return self._collapsed + + def set_collapsed(self, collapsed=None): + if collapsed is None: + collapsed = not self._collapsed if self._collapsed == collapsed: return self._collapsed = collapsed @@ -182,6 +237,7 @@ class ExpandBtnLabel(QtWidgets.QLabel): else: self._current_image = self._source_expanded_pix self._set_resized_pix() + self.state_changed.emit() def resizeEvent(self, event): self._set_resized_pix() @@ -203,21 +259,55 @@ class ExpandBtnLabel(QtWidgets.QLabel): class ExpandBtn(ClickableFrame): + state_changed = QtCore.Signal() + def __init__(self, parent=None): super(ExpandBtn, self).__init__(parent) - pixmap_label = ExpandBtnLabel(self) + pixmap_label = self._create_pix_widget(self) layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(pixmap_label) + pixmap_label.state_changed.connect(self.state_changed) + self._pixmap_label = pixmap_label - def set_collapsed(self, collapsed): + def _create_pix_widget(self, parent=None): + if parent is None: + parent = self + return ExpandBtnLabel(parent) + + @property + def collapsed(self): + return self._pixmap_label.collapsed + + def set_collapsed(self, collapsed=None): self._pixmap_label.set_collapsed(collapsed) +class ClassicExpandBtnLabel(ExpandBtnLabel): + def _create_collapsed_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("right_arrow") + ) + + def _create_expanded_pixmap(self): + return QtGui.QPixmap( + get_style_image_path("down_arrow") + ) + + +class ClassicExpandBtn(ExpandBtn): + """Same as 'ExpandBtn' but with arrow images.""" + + def _create_pix_widget(self, parent=None): + if parent is None: + parent = self + return ClassicExpandBtnLabel(parent) + + class ImageButton(QtWidgets.QPushButton): """PushButton with icon and size of font. From 4a8d6f7e8c26d2e9b2439c1ce017eb784acbfa75 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 24 May 2023 00:42:34 +0800 Subject: [PATCH 681/918] remove custom attributes while removing instance --- openpype/hosts/max/api/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 8df620b913..553031d72e 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -197,8 +197,8 @@ class MaxCreator(Creator, MaxCreatorBase): """ for instance in instances: if instance_node := rt.GetNodeByName(instance.data.get("instance_node")): # noqa - rt.Select(instance_node) - rt.execute(f'for o in selection do for c in o.children do c.parent = undefined') # noqa + count = rt.custAttributes.count(instance_node) + rt.custAttributes.delete(instance_node, count) rt.Delete(instance_node) self._remove_instance_from_context(instance) From 96a4edf8cb412906047be7435a742ec80e2f4b94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 23 May 2023 23:02:52 +0200 Subject: [PATCH 682/918] Resolve: fixing the issue with no active timeline during bootstrap of loader --- openpype/hosts/resolve/api/lib.py | 32 ++++++++++++++++--- .../hosts/resolve/plugins/load/load_clip.py | 1 + 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index b3ad20df39..1c33749a77 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,39 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(new=False): +def get_current_timeline(any=False, new=False): + """Get current timeline object. + + Args: + any (bool, optional): return any even new if no timeline available. + Defaults to False. + new (bool, optional): return only new timeline. Defaults to False. + + Returns: + _type_: _description_ + """ # get current project project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline only if it is not new + if timeline and not new: + return timeline + + # if any is True then return any timeline + if any: + timeline_count = project.GetTimelineCount() + if timeline_count == 0: + # if there is no timeline then create a new one + new = True + + # create new timeline if new is True if new: media_pool = project.GetMediaPool() new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) project.SetCurrentTimeline(new_timeline) - - return project.GetCurrentTimeline() + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -312,7 +335,8 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - timeline = get_current_timeline() + # make sure some timeline will be active with `any` argument + timeline = get_current_timeline(any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index d30a7ea272..05bfb003d6 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -19,6 +19,7 @@ from openpype.lib.transcoding import ( IMAGE_EXTENSIONS ) + class LoadClip(plugin.TimelineItemLoader): """Load a subset to timeline as clip From 179dc65f501faa6e71d5e783ef0d01f0d1ac09aa Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 24 May 2023 03:25:50 +0000 Subject: [PATCH 683/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 8874eb510d..3d7f64b991 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.2" +__version__ = "3.15.8-nightly.3" From ea2d87d903ed95200e1ceb121440e690b65907e0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 May 2023 03:26:39 +0000 Subject: [PATCH 684/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 244eb1a363..a9f1f1cc02 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8-nightly.3 - 3.15.8-nightly.2 - 3.15.8-nightly.1 - 3.15.7 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.3 - 3.14.2-nightly.2 - 3.14.2-nightly.1 - - 3.14.1 validations: required: true - type: dropdown From fe02a093128b17a02d0c2e87405c8beca7bc23e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 May 2023 10:01:59 +0200 Subject: [PATCH 685/918] Deadline: fix selection from multiple webservices (#5015) * OP-4380 - override default DL from project settings * OP-4380 - updated documentation --- .../collect_default_deadline_server.py | 26 ++++++++++++++++++- website/docs/module_deadline.md | 3 +++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py index e6ad6a9aa1..cb2b0cf156 100644 --- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -4,7 +4,18 @@ import pyblish.api class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): - """Collect default Deadline Webservice URL.""" + """Collect default Deadline Webservice URL. + + DL webservice addresses must be configured first in System Settings for + project settings enum to work. + + Default webservice could be overriden by + `project_settings/deadline/deadline_servers`. Currently only single url + is expected. + + This url could be overriden by some hosts directly on instances with + `CollectDeadlineServerFromInstance`. + """ order = pyblish.api.CollectorOrder + 0.410 label = "Default Deadline Webservice" @@ -23,3 +34,16 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): context.data["defaultDeadline"] = deadline_module.deadline_urls["default"] # noqa: E501 context.data["deadlinePassMongoUrl"] = self.pass_mongo_url + + deadline_servers = (context.data + ["project_settings"] + ["deadline"] + ["deadline_servers"]) + if deadline_servers: + deadline_server_name = deadline_servers[0] + deadline_webservice = deadline_module.deadline_urls.get( + deadline_server_name) + if deadline_webservice: + context.data["defaultDeadline"] = deadline_webservice + self.log.debug("Overriding from project settings with {}".format( # noqa: E501 + deadline_webservice)) diff --git a/website/docs/module_deadline.md b/website/docs/module_deadline.md index 94b6a381c2..bca2a83936 100644 --- a/website/docs/module_deadline.md +++ b/website/docs/module_deadline.md @@ -22,6 +22,9 @@ For [AWS Thinkbox Deadline](https://www.awsthinkbox.com/deadline) support you ne 5. Install our custom plugin and scripts to your deadline repository. It should be as simple as copying content of `openpype/modules/deadline/repository/custom` to `path/to/your/deadline/repository/custom`. +Multiple different DL webservice could be configured. First set them in point 4., then they could be configured per project in `project_settings/deadline/deadline_servers`. +Only single webservice could be a target of publish though. + ## Configuration From 07a969df6ec384af473e3034eb678208aa5dfc77 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 10:29:41 +0200 Subject: [PATCH 686/918] adding resolve host to the pre ocio hook --- openpype/hooks/pre_ocio_hook.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 49a042caa8..eac7d2696f 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -18,7 +18,8 @@ class OCIOEnvHook(PreLaunchHook): "houdini", "maya", "nuke", - "hiero" + "hiero", + "resolve" ] def execute(self): From 248336bb0ddf2a61632e579600021b093c24440f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 May 2023 10:51:35 +0200 Subject: [PATCH 687/918] General: Lib code cleanup (#5003) * implemented 'is_func_signature_supported' function * 'WeakMethod' can be imported from 'python_2_comp' all the time * simplified events logic for callback registration * modified docstrings in publish lib * removed unused imports * fixed 'run_openpype_process' docstring --- openpype/lib/__init__.py | 6 ++- openpype/lib/events.py | 43 +++--------------- openpype/lib/execute.py | 2 +- openpype/lib/python_2_comp.py | 65 +++++++++++++++------------- openpype/lib/python_module_tools.py | 67 +++++++++++++++++++++++++++++ openpype/pipeline/publish/lib.py | 34 ++++++++------- 6 files changed, 129 insertions(+), 88 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 9eb7724a60..06de486f2e 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # flake8: noqa E402 -"""Pype module API.""" +"""OpenPype lib functions.""" # add vendor to sys path based on Python version import sys import os @@ -94,7 +94,8 @@ from .python_module_tools import ( modules_from_path, recursive_bases_from_class, classes_from_module, - import_module_from_dirpath + import_module_from_dirpath, + is_func_signature_supported, ) from .profiles_filtering import ( @@ -243,6 +244,7 @@ __all__ = [ "recursive_bases_from_class", "classes_from_module", "import_module_from_dirpath", + "is_func_signature_supported", "get_transcode_temp_directory", "should_convert_for_ffmpeg", diff --git a/openpype/lib/events.py b/openpype/lib/events.py index bed00fe659..dca58fcf93 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -6,10 +6,9 @@ import inspect import logging import weakref from uuid import uuid4 -try: - from weakref import WeakMethod -except Exception: - from openpype.lib.python_2_comp import WeakMethod + +from .python_2_comp import WeakMethod +from .python_module_tools import is_func_signature_supported class MissingEventSystem(Exception): @@ -80,40 +79,8 @@ class EventCallback(object): # Get expected arguments from function spec # - positional arguments are always preferred - expect_args = False - expect_kwargs = False - fake_event = "fake" - if hasattr(inspect, "signature"): - # Python 3 using 'Signature' object where we try to bind arg - # or kwarg. Using signature is recommended approach based on - # documentation. - sig = inspect.signature(func) - try: - sig.bind(fake_event) - expect_args = True - except TypeError: - pass - - try: - sig.bind(event=fake_event) - expect_kwargs = True - except TypeError: - pass - - else: - # In Python 2 'signature' is not available so 'getcallargs' is used - # - 'getcallargs' is marked as deprecated since Python 3.0 - try: - inspect.getcallargs(func, fake_event) - expect_args = True - except TypeError: - pass - - try: - inspect.getcallargs(func, event=fake_event) - expect_kwargs = True - except TypeError: - pass + expect_args = is_func_signature_supported(func, "fake") + expect_kwargs = is_func_signature_supported(func, event="fake") self._func_ref = func_ref self._func_name = func_name diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index ef456395e7..6f52efdfcc 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -190,7 +190,7 @@ def run_openpype_process(*args, **kwargs): Example: ``` - run_openpype_process("run", "") + run_detached_process("run", "") ``` Args: diff --git a/openpype/lib/python_2_comp.py b/openpype/lib/python_2_comp.py index d7137dbe9c..091c51a6f6 100644 --- a/openpype/lib/python_2_comp.py +++ b/openpype/lib/python_2_comp.py @@ -1,41 +1,44 @@ import weakref -class _weak_callable: - def __init__(self, obj, func): - self.im_self = obj - self.im_func = func +WeakMethod = getattr(weakref, "WeakMethod", None) - def __call__(self, *args, **kws): - if self.im_self is None: - return self.im_func(*args, **kws) - else: - return self.im_func(self.im_self, *args, **kws) +if WeakMethod is None: + class _WeakCallable: + def __init__(self, obj, func): + self.im_self = obj + self.im_func = func + + def __call__(self, *args, **kws): + if self.im_self is None: + return self.im_func(*args, **kws) + else: + return self.im_func(self.im_self, *args, **kws) -class WeakMethod: - """ Wraps a function or, more importantly, a bound method in - a way that allows a bound method's object to be GCed, while - providing the same interface as a normal weak reference. """ + class WeakMethod: + """ Wraps a function or, more importantly, a bound method in + a way that allows a bound method's object to be GCed, while + providing the same interface as a normal weak reference. """ - def __init__(self, fn): - try: - self._obj = weakref.ref(fn.im_self) - self._meth = fn.im_func - except AttributeError: - # It's not a bound method - self._obj = None - self._meth = fn + def __init__(self, fn): + try: + self._obj = weakref.ref(fn.im_self) + self._meth = fn.im_func + except AttributeError: + # It's not a bound method + self._obj = None + self._meth = fn - def __call__(self): - if self._dead(): - return None - return _weak_callable(self._getobj(), self._meth) + def __call__(self): + if self._dead(): + return None + return _WeakCallable(self._getobj(), self._meth) - def _dead(self): - return self._obj is not None and self._obj() is None + def _dead(self): + return self._obj is not None and self._obj() is None - def _getobj(self): - if self._obj is None: - return None - return self._obj() + def _getobj(self): + if self._obj is None: + return None + return self._obj() diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index 9e8e94842c..a10263f991 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -230,3 +230,70 @@ def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None): dirpath, folder_name, dst_module_name ) return module + + +def is_func_signature_supported(func, *args, **kwargs): + """Check if a function signature supports passed args and kwargs. + + This check does not actually call the function, just look if function can + be called with the arguments. + + Notes: + This does NOT check if the function would work with passed arguments + only if they can be passed in. If function have *args, **kwargs + in paramaters, this will always return 'True'. + + Example: + >>> def my_function(my_number): + ... return my_number + 1 + ... + >>> is_func_signature_supported(my_function, 1) + True + >>> is_func_signature_supported(my_function, 1, 2) + False + >>> is_func_signature_supported(my_function, my_number=1) + True + >>> is_func_signature_supported(my_function, number=1) + False + >>> is_func_signature_supported(my_function, "string") + True + >>> def my_other_function(*args, **kwargs): + ... my_function(*args, **kwargs) + ... + >>> is_func_signature_supported( + ... my_other_function, + ... "string", + ... 1, + ... other=None + ... ) + True + + Args: + func (function): A function where the signature should be tested. + *args (tuple[Any]): Positional arguments for function signature. + **kwargs (dict[str, Any]): Keyword arguments for function signature. + + Returns: + bool: Function can pass in arguments. + """ + + if hasattr(inspect, "signature"): + # Python 3 using 'Signature' object where we try to bind arg + # or kwarg. Using signature is recommended approach based on + # documentation. + sig = inspect.signature(func) + try: + sig.bind(*args, **kwargs) + return True + except TypeError: + pass + + else: + # In Python 2 'signature' is not available so 'getcallargs' is used + # - 'getcallargs' is marked as deprecated since Python 3.0 + try: + inspect.getcallargs(func, *args, **kwargs) + return True + except TypeError: + pass + return False diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 40186238aa..63a856e326 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1,12 +1,10 @@ import os import sys -import types import inspect import copy import tempfile import xml.etree.ElementTree -import six import pyblish.util import pyblish.plugin import pyblish.api @@ -42,7 +40,9 @@ def get_template_name_profiles( Args: project_name (str): Name of project where to look for templates. - project_settings(Dic[str, Any]): Prepared project settings. + project_settings (Dict[str, Any]): Prepared project settings. + logger (Optional[logging.Logger]): Logger object to be used instead + of default logger. Returns: List[Dict[str, Any]]: Publish template profiles. @@ -103,7 +103,9 @@ def get_hero_template_name_profiles( Args: project_name (str): Name of project where to look for templates. - project_settings(Dic[str, Any]): Prepared project settings. + project_settings (Dict[str, Any]): Prepared project settings. + logger (Optional[logging.Logger]): Logger object to be used instead + of default logger. Returns: List[Dict[str, Any]]: Publish template profiles. @@ -172,9 +174,10 @@ def get_publish_template_name( project_name (str): Name of project where to look for settings. host_name (str): Name of host integration. family (str): Family for which should be found template. - task_name (str): Task name on which is intance working. - task_type (str): Task type on which is intance working. - project_setting (Dict[str, Any]): Prepared project settings. + task_name (str): Task name on which is instance working. + task_type (str): Task type on which is instance working. + project_settings (Dict[str, Any]): Prepared project settings. + hero (bool): Template is for hero version publishing. logger (logging.Logger): Custom logger used for 'filter_profiles' function. @@ -264,19 +267,18 @@ def load_help_content_from_plugin(plugin): def publish_plugins_discover(paths=None): """Find and return available pyblish plug-ins - Overridden function from `pyblish` module to be able collect crashed files - and reason of their crash. + Overridden function from `pyblish` module to be able to collect + crashed files and reason of their crash. Arguments: paths (list, optional): Paths to discover plug-ins from. If no paths are provided, all paths are searched. - """ # The only difference with `pyblish.api.discover` result = DiscoverResult(pyblish.api.Plugin) - plugins = dict() + plugins = {} plugin_names = [] allow_duplicates = pyblish.plugin.ALLOW_DUPLICATES @@ -302,7 +304,7 @@ def publish_plugins_discover(paths=None): mod_name, mod_ext = os.path.splitext(fname) - if not mod_ext == ".py": + if mod_ext != ".py": continue try: @@ -533,10 +535,10 @@ def find_close_plugin(close_plugin_name, log): def remote_publish(log, close_plugin_name=None, raise_error=False): """Loops through all plugins, logs to console. Used for tests. - Args: - log (openpype.lib.Logger) - close_plugin_name (str): name of plugin with responsibility to - close host app + Args: + log (Logger) + close_plugin_name (str): name of plugin with responsibility to + close host app """ # Error exit as soon as any error occurs. error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" From 17a38c32a4ba6e33798a097418c1f91c732d1fb8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 24 May 2023 10:54:31 +0200 Subject: [PATCH 688/918] Enhancement: Improve logging levels and messages for artist facing publish reports (#5018) * Tweak log levels and message to be more informative to artist in report page * Tweak levels and clarity of logs * Tweak levels and clarity of logs + tweak grammar * Cosmetics * Improve logging * Simplify logging * Convert to debug log if it's skipping thumbnail integration if there's no thumbnail whatsoever to integrate * Tweak to debug since they only show representation ids hardly understandable to the artist * Match logging message across hosts + include filepath for full clarity * Tweak message to clarify it only starts checking and not that it requires filling + to debug log * Tweak to debug log if there's basically no thumbnail to integrate at the end * Tweak log levels - Artist doesn't care what's prepared, especially since afterwards it's logged what gets written to the database anyway * Log clearly it's processing a legacy instance * Cosmetics --- .../fusion/plugins/publish/collect_inputs.py | 2 +- .../fusion/plugins/publish/save_scene.py | 2 +- .../houdini/plugins/publish/collect_frames.py | 7 ++-- .../houdini/plugins/publish/collect_inputs.py | 2 +- .../plugins/publish/collect_instances.py | 4 +- .../plugins/publish/collect_workfile.py | 3 +- .../houdini/plugins/publish/save_scene.py | 2 +- .../publish/validate_workfile_paths.py | 41 ++++++++++++++----- .../maya/plugins/publish/collect_inputs.py | 2 +- .../hosts/maya/plugins/publish/save_scene.py | 2 +- .../plugins/publish/save_workfile.py | 5 ++- .../plugins/publish/submit_fusion_deadline.py | 2 +- .../plugins/publish/submit_nuke_deadline.py | 2 +- .../plugins/publish/submit_publish_job.py | 2 +- .../publish/validate_deadline_pools.py | 2 +- openpype/pipeline/publish/publish_plugins.py | 13 +++--- openpype/plugins/publish/cleanup.py | 9 ++-- .../publish/collect_anatomy_context_data.py | 5 ++- .../publish/collect_anatomy_instance_data.py | 8 ++-- .../plugins/publish/collect_anatomy_object.py | 2 +- .../publish/collect_custom_staging_dir.py | 2 +- .../publish/collect_from_create_context.py | 4 +- .../plugins/publish/collect_scene_version.py | 5 ++- openpype/plugins/publish/extract_burnin.py | 4 +- .../publish/extract_color_transcode.py | 6 +-- openpype/plugins/publish/extract_review.py | 6 +-- openpype/plugins/publish/extract_thumbnail.py | 14 +++---- .../publish/extract_thumbnail_from_source.py | 4 +- openpype/plugins/publish/integrate.py | 8 ++-- openpype/plugins/publish/integrate_legacy.py | 6 ++- .../plugins/publish/integrate_thumbnail.py | 4 +- 31 files changed, 106 insertions(+), 74 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index 1bb3cd1220..a6628300db 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -113,4 +113,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/fusion/plugins/publish/save_scene.py b/openpype/hosts/fusion/plugins/publish/save_scene.py index a249c453d8..0798e7c8b7 100644 --- a/openpype/hosts/fusion/plugins/publish/save_scene.py +++ b/openpype/hosts/fusion/plugins/publish/save_scene.py @@ -17,5 +17,5 @@ class FusionSaveComp(pyblish.api.ContextPlugin): current = comp.GetAttrs().get("COMPS_FileName", "") assert context.data['currentFile'] == current - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) comp.Save() diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 6c695f64e9..059793e3c5 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -8,7 +8,6 @@ import pyblish.api from openpype.hosts.houdini.api import lib - class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" @@ -34,8 +33,10 @@ class CollectFrames(pyblish.api.InstancePlugin): self.log.warning("Using current frame: {}".format(hou.frame())) output = output_parm.eval() - _, ext = lib.splitext(output, - allowed_multidot_extensions=[".ass.gz"]) + _, ext = lib.splitext( + output, + allowed_multidot_extensions=[".ass.gz"] + ) file_name = os.path.basename(output) result = file_name diff --git a/openpype/hosts/houdini/plugins/publish/collect_inputs.py b/openpype/hosts/houdini/plugins/publish/collect_inputs.py index 6411376ea3..e92a42f2e8 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_inputs.py +++ b/openpype/hosts/houdini/plugins/publish/collect_inputs.py @@ -117,4 +117,4 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index bb85630552..5d5347f96e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -55,7 +55,9 @@ class CollectInstances(pyblish.api.ContextPlugin): has_family = node.evalParm("family") assert has_family, "'%s' is missing 'family'" % node.name() - self.log.info("processing {}".format(node)) + self.log.info( + "Processing legacy instance node {}".format(node.path()) + ) data = lib.read(node) # Check bypass state and reverse diff --git a/openpype/hosts/houdini/plugins/publish/collect_workfile.py b/openpype/hosts/houdini/plugins/publish/collect_workfile.py index a6e94ec29e..aa533bcf1b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_workfile.py +++ b/openpype/hosts/houdini/plugins/publish/collect_workfile.py @@ -32,5 +32,4 @@ class CollectWorkfile(pyblish.api.InstancePlugin): "stagingDir": folder, }] - self.log.info('Collected instance: {}'.format(file)) - self.log.info('staging Dir: {}'.format(folder)) + self.log.debug('Collected workfile instance: {}'.format(file)) diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index d6e07ccab0..703d3e4895 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -20,7 +20,7 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): ) if host.has_unsaved_changes(): - self.log.info("Saving current file {}...".format(current_file)) + self.log.info("Saving current file: {}".format(current_file)) host.save_workfile(current_file) else: self.log.debug("No unsaved changes, skipping file save..") diff --git a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py index 7707cc2dba..543c8e1407 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py +++ b/openpype/hosts/houdini/plugins/publish/validate_workfile_paths.py @@ -28,18 +28,37 @@ class ValidateWorkfilePaths( if not self.is_active(instance.data): return invalid = self.get_invalid() - self.log.info( - "node types to check: {}".format(", ".join(self.node_types))) - self.log.info( - "prohibited vars: {}".format(", ".join(self.prohibited_vars)) + self.log.debug( + "Checking node types: {}".format(", ".join(self.node_types))) + self.log.debug( + "Searching prohibited vars: {}".format( + ", ".join(self.prohibited_vars) + ) ) - if invalid: - for param in invalid: - self.log.error( - "{}: {}".format(param.path(), param.unexpandedString())) - raise PublishValidationError( - "Invalid paths found", title=self.label) + if invalid: + all_container_vars = set() + for param in invalid: + value = param.unexpandedString() + contained_vars = [ + var for var in self.prohibited_vars + if var in value + ] + all_container_vars.update(contained_vars) + + self.log.error( + "Parm {} contains prohibited vars {}: {}".format( + param.path(), + ", ".join(contained_vars), + value) + ) + + message = ( + "Prohibited vars {} found in parameter values".format( + ", ".join(all_container_vars) + ) + ) + raise PublishValidationError(message, title=self.label) @classmethod def get_invalid(cls): @@ -63,7 +82,7 @@ class ValidateWorkfilePaths( def repair(cls, instance): invalid = cls.get_invalid() for param in invalid: - cls.log.info("processing: {}".format(param.path())) + cls.log.info("Processing: {}".format(param.path())) cls.log.info("Replacing {} for {}".format( param.unexpandedString(), hou.text.expandString(param.unexpandedString()))) diff --git a/openpype/hosts/maya/plugins/publish/collect_inputs.py b/openpype/hosts/maya/plugins/publish/collect_inputs.py index 9c3f0f5efa..895c92762b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_inputs.py +++ b/openpype/hosts/maya/plugins/publish/collect_inputs.py @@ -166,7 +166,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) + self.log.debug("Collected inputs: %s" % inputs) def _collect_renderlayer_inputs(self, scene_containers, instance): """Collects inputs from nodes in renderlayer, incl. shaders + camera""" diff --git a/openpype/hosts/maya/plugins/publish/save_scene.py b/openpype/hosts/maya/plugins/publish/save_scene.py index 45e62e7b44..495c339731 100644 --- a/openpype/hosts/maya/plugins/publish/save_scene.py +++ b/openpype/hosts/maya/plugins/publish/save_scene.py @@ -31,5 +31,5 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): # remove lockfile before saving if is_workfile_lock_enabled("maya", project_name, project_settings): remove_workfile_lock(current) - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) cmds.file(save=True, force=True) diff --git a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py index 4874b5e5c7..9662f31922 100644 --- a/openpype/hosts/substancepainter/plugins/publish/save_workfile.py +++ b/openpype/hosts/substancepainter/plugins/publish/save_workfile.py @@ -16,11 +16,12 @@ class SaveCurrentWorkfile(pyblish.api.ContextPlugin): def process(self, context): host = registered_host() - if context.data["currentFile"] != host.get_current_workfile(): + current = host.get_current_workfile() + if context.data["currentFile"] != current: raise KnownPublishError("Workfile has changed during publishing!") if host.has_unsaved_changes(): - self.log.info("Saving current file..") + self.log.info("Saving current file: {}".format(current)) host.save_workfile() else: self.log.debug("Skipping workfile save because there are no " diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 717391100d..a48596c6bf 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -73,7 +73,7 @@ class FusionSubmitDeadline( def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return attribute_values = self.get_attr_values_from_data( diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 5c598df94b..4900231783 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -86,7 +86,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return instance.data["attributeValues"] = self.get_attr_values_from_data( diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index eeb813cb62..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -762,7 +762,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return data = instance.data.copy() diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 7c8ab62d4d..e1c0595830 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -26,7 +26,7 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, def process(self, instance): if not instance.data.get("farm"): - self.log.info("Skipping local instance.") + self.log.debug("Skipping local instance.") return # get default deadline webservice url from deadline module diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index a38896ec8e..a67c8397b1 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -379,7 +379,9 @@ class ColormanagedPyblishPluginMixin(object): # check if ext in lower case is in self.allowed_ext if ext.lstrip(".").lower() not in self.allowed_ext: - self.log.debug("Extension is not in allowed extensions.") + self.log.debug( + "Extension '{}' is not in allowed extensions.".format(ext) + ) return if colorspace_settings is None: @@ -393,8 +395,7 @@ class ColormanagedPyblishPluginMixin(object): self.log.warning("No colorspace management was defined") return - self.log.info("Config data is : `{}`".format( - config_data)) + self.log.debug("Config data is: `{}`".format(config_data)) project_name = context.data["projectName"] host_name = context.data["hostName"] @@ -405,8 +406,7 @@ class ColormanagedPyblishPluginMixin(object): if isinstance(filename, list): filename = filename[0] - self.log.debug("__ filename: `{}`".format( - filename)) + self.log.debug("__ filename: `{}`".format(filename)) # get matching colorspace from rules colorspace = colorspace or get_imageio_colorspace_from_filepath( @@ -415,8 +415,7 @@ class ColormanagedPyblishPluginMixin(object): file_rules=file_rules, project_settings=project_settings ) - self.log.debug("__ colorspace: `{}`".format( - colorspace)) + self.log.debug("__ colorspace: `{}`".format(colorspace)) # infuse data to representation if colorspace: diff --git a/openpype/plugins/publish/cleanup.py b/openpype/plugins/publish/cleanup.py index b90c88890d..57cc9c0ab5 100644 --- a/openpype/plugins/publish/cleanup.py +++ b/openpype/plugins/publish/cleanup.py @@ -81,7 +81,8 @@ class CleanUp(pyblish.api.InstancePlugin): staging_dir = instance.data.get("stagingDir", None) if not staging_dir: - self.log.info("Staging dir not set.") + self.log.debug("Skipping cleanup. Staging dir not set " + "on instance: {}.".format(instance)) return if not os.path.normpath(staging_dir).startswith(temp_root): @@ -90,7 +91,7 @@ class CleanUp(pyblish.api.InstancePlugin): return if not os.path.exists(staging_dir): - self.log.info("No staging directory found: %s" % staging_dir) + self.log.debug("No staging directory found at: %s" % staging_dir) return if instance.data.get("stagingDir_persistent"): @@ -131,7 +132,9 @@ class CleanUp(pyblish.api.InstancePlugin): try: os.remove(src) except PermissionError: - self.log.warning("Insufficient permission to delete {}".format(src)) + self.log.warning( + "Insufficient permission to delete {}".format(src) + ) continue # add dir for cleanup diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 55ce8e06f4..508b01447b 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -67,5 +67,6 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): # Store context.data["anatomyData"] = anatomy_data - self.log.info("Global anatomy Data collected") - self.log.debug(json.dumps(anatomy_data, indent=4)) + self.log.debug("Global Anatomy Context Data collected:\n{}".format( + json.dumps(anatomy_data, indent=4) + )) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 4fbb93324b..128ad90b4f 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -46,17 +46,17 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): follow_workfile_version = False def process(self, context): - self.log.info("Collecting anatomy data for all instances.") + self.log.debug("Collecting anatomy data for all instances.") project_name = context.data["projectName"] self.fill_missing_asset_docs(context, project_name) self.fill_latest_versions(context, project_name) self.fill_anatomy_data(context) - self.log.info("Anatomy Data collection finished.") + self.log.debug("Anatomy Data collection finished.") def fill_missing_asset_docs(self, context, project_name): - self.log.debug("Qeurying asset documents for instances.") + self.log.debug("Querying asset documents for instances.") context_asset_doc = context.data.get("assetEntity") @@ -271,7 +271,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): instance_name = instance.data["name"] instance_label = instance.data.get("label") if instance_label: - instance_name += "({})".format(instance_label) + instance_name += " ({})".format(instance_label) self.log.debug("Anatomy data for instance {}: {}".format( instance_name, json.dumps(anatomy_data, indent=4) diff --git a/openpype/plugins/publish/collect_anatomy_object.py b/openpype/plugins/publish/collect_anatomy_object.py index 725cae2b14..f792cf3abd 100644 --- a/openpype/plugins/publish/collect_anatomy_object.py +++ b/openpype/plugins/publish/collect_anatomy_object.py @@ -30,6 +30,6 @@ class CollectAnatomyObject(pyblish.api.ContextPlugin): context.data["anatomy"] = Anatomy(project_name) - self.log.info( + self.log.debug( "Anatomy object collected for project \"{}\".".format(project_name) ) diff --git a/openpype/plugins/publish/collect_custom_staging_dir.py b/openpype/plugins/publish/collect_custom_staging_dir.py index b749b251c0..669c4873e0 100644 --- a/openpype/plugins/publish/collect_custom_staging_dir.py +++ b/openpype/plugins/publish/collect_custom_staging_dir.py @@ -65,6 +65,6 @@ class CollectCustomStagingDir(pyblish.api.InstancePlugin): else: result_str = "Not adding" - self.log.info("{} custom staging dir for instance with '{}'".format( + self.log.debug("{} custom staging dir for instance with '{}'".format( result_str, family )) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 5fcf8feb56..4888476fff 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -92,5 +92,5 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): instance.data["transientData"] = transient_data - self.log.info("collected instance: {}".format(instance.data)) - self.log.info("parsing data: {}".format(in_data)) + self.log.debug("collected instance: {}".format(instance.data)) + self.log.debug("parsing data: {}".format(in_data)) diff --git a/openpype/plugins/publish/collect_scene_version.py b/openpype/plugins/publish/collect_scene_version.py index fdbcb3cb9d..cd3231a07d 100644 --- a/openpype/plugins/publish/collect_scene_version.py +++ b/openpype/plugins/publish/collect_scene_version.py @@ -48,10 +48,13 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if '' in filename: return + self.log.debug( + "Collecting scene version from filename: {}".format(filename) + ) + version = get_version_from_path(filename) assert version, "Cannot determine version" rootVersion = int(version) context.data['version'] = rootVersion - self.log.info("{}".format(type(rootVersion))) self.log.info('Scene Version: %s' % context.data.get('version')) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index a12e8d18b4..10b366dcd6 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -517,8 +517,8 @@ class ExtractBurnin(publish.Extractor): """ if "burnin" not in (repre.get("tags") or []): - self.log.info(( - "Representation \"{}\" don't have \"burnin\" tag. Skipped." + self.log.debug(( + "Representation \"{}\" does not have \"burnin\" tag. Skipped." ).format(repre["name"])) return False diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 58e0350a2e..45b10620d1 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -336,13 +336,13 @@ class ExtractOIIOTranscode(publish.Extractor): if repre.get("ext") not in self.supported_exts: self.log.debug(( - "Representation '{}' of unsupported extension. Skipped." - ).format(repre["name"])) + "Representation '{}' has unsupported extension: '{}'. Skipped." + ).format(repre["name"], repre.get("ext"))) return False if not repre.get("files"): self.log.debug(( - "Representation '{}' have empty files. Skipped." + "Representation '{}' has empty files. Skipped." ).format(repre["name"])) return False diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 1062683319..a68addda7d 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -92,8 +92,8 @@ class ExtractReview(pyblish.api.InstancePlugin): host_name = instance.context.data["hostName"] family = self.main_family_from_instance(instance) - self.log.info("Host: \"{}\"".format(host_name)) - self.log.info("Family: \"{}\"".format(family)) + self.log.debug("Host: \"{}\"".format(host_name)) + self.log.debug("Family: \"{}\"".format(family)) profile = filter_profiles( self.profiles, @@ -351,7 +351,7 @@ class ExtractReview(pyblish.api.InstancePlugin): temp_data = self.prepare_temp_data(instance, repre, output_def) files_to_clean = [] if temp_data["input_is_sequence"]: - self.log.info("Filling gaps in sequence.") + self.log.debug("Checking sequence to fill gaps in sequence..") files_to_clean = self.fill_sequence_gaps( files=temp_data["origin_repre"]["files"], staging_dir=new_repre["stagingDir"], diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 54b933a76d..b98ab64f56 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -36,7 +36,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): ).format(subset_name)) return - self.log.info( + self.log.debug( "Processing instance with subset name {}".format(subset_name) ) @@ -89,13 +89,13 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): src_staging = os.path.normpath(repre["stagingDir"]) full_input_path = os.path.join(src_staging, input_file) - self.log.info("input {}".format(full_input_path)) + self.log.debug("input {}".format(full_input_path)) filename = os.path.splitext(input_file)[0] jpeg_file = filename + "_thumb.jpg" full_output_path = os.path.join(dst_staging, jpeg_file) if oiio_supported: - self.log.info("Trying to convert with OIIO") + self.log.debug("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( @@ -148,7 +148,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): def _already_has_thumbnail(self, repres): for repre in repres: - self.log.info("repre {}".format(repre)) + self.log.debug("repre {}".format(repre)) if repre["name"] == "thumbnail": return True return False @@ -173,20 +173,20 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return filtered_repres def create_thumbnail_oiio(self, src_path, dst_path): - self.log.info("outputting {}".format(dst_path)) + self.log.info("Extracting thumbnail {}".format(dst_path)) oiio_tool_path = get_oiio_tools_path() oiio_cmd = [ oiio_tool_path, "-a", src_path, "-o", dst_path ] - self.log.info("running: {}".format(" ".join(oiio_cmd))) + self.log.debug("running: {}".format(" ".join(oiio_cmd))) try: run_subprocess(oiio_cmd, logger=self.log) return True except Exception: self.log.warning( - "Failed to create thubmnail using oiiotool", + "Failed to create thumbnail using oiiotool", exc_info=True ) return False diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index a92f762cde..a9c95d6065 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -39,7 +39,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): self._create_context_thumbnail(instance.context) subset_name = instance.data["subset"] - self.log.info( + self.log.debug( "Processing instance with subset name {}".format(subset_name) ) thumbnail_source = instance.data.get("thumbnailSource") @@ -104,7 +104,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): full_output_path = os.path.join(dst_staging, dst_filename) if oiio_supported: - self.log.info("Trying to convert with OIIO") + self.log.debug("Trying to convert with OIIO") # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 8e984a9e97..f392cf67f7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -267,7 +267,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): instance_stagingdir = instance.data.get("stagingDir") if not instance_stagingdir: - self.log.info(( + self.log.debug(( "{0} is missing reference to staging directory." " Will try to get it from representation." ).format(instance)) @@ -480,7 +480,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): update_data ) - self.log.info("Prepared subset: {}".format(subset_name)) + self.log.debug("Prepared subset: {}".format(subset_name)) return subset_doc def prepare_version(self, instance, op_session, subset_doc, project_name): @@ -521,7 +521,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): project_name, version_doc["type"], version_doc ) - self.log.info("Prepared version: v{0:03d}".format(version_doc["name"])) + self.log.debug( + "Prepared version: v{0:03d}".format(version_doc["name"]) + ) return version_doc diff --git a/openpype/plugins/publish/integrate_legacy.py b/openpype/plugins/publish/integrate_legacy.py index c67ce62bf6..c238cca633 100644 --- a/openpype/plugins/publish/integrate_legacy.py +++ b/openpype/plugins/publish/integrate_legacy.py @@ -147,7 +147,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): def process(self, instance): if instance.data.get("processedWithNewIntegrator"): - self.log.info("Instance was already processed with new integrator") + self.log.debug( + "Instance was already processed with new integrator" + ) return for ef in self.exclude_families: @@ -274,7 +276,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): stagingdir = instance.data.get("stagingDir") if not stagingdir: - self.log.info(( + self.log.debug(( "{0} is missing reference to staging directory." " Will try to get it from representation." ).format(instance)) diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index 16cc47d432..f6d4f654f5 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -41,7 +41,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): # Filter instances which can be used for integration filtered_instance_items = self._prepare_instances(context) if not filtered_instance_items: - self.log.info( + self.log.debug( "All instances were filtered. Thumbnail integration skipped." ) return @@ -162,7 +162,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): # Skip instance if thumbnail path is not available for it if not thumbnail_path: - self.log.info(( + self.log.debug(( "Skipping thumbnail integration for instance \"{}\"." " Instance and context" " thumbnail paths are not available." From 22e7f9bd8497bdda99eea9e560788fdea35cb21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 24 May 2023 10:58:40 +0200 Subject: [PATCH 689/918] Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 2336487b37..34f4b4e8cf 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -125,7 +125,7 @@ class ExtractThumbnail(publish.Extractor): temporary_nodes.append(rnode) previous_node = rnode - if not self.reposition_nodes: + if self.reposition_nodes is None: # [deprecated] create reformat node old way reformat_node = nuke.createNode("Reformat") ref_node = self.nodes.get("Reformat", None) From 084a15ec8c2433e2584dd2c0646417cf811262cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 24 May 2023 10:58:47 +0200 Subject: [PATCH 690/918] Update openpype/hosts/nuke/plugins/publish/extract_thumbnail.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 34f4b4e8cf..21eefda249 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -30,7 +30,7 @@ class ExtractThumbnail(publish.Extractor): bake_viewer_process = True bake_viewer_input_process = True nodes = {} - reposition_nodes = [] + reposition_nodes = None def process(self, instance): if instance.data.get("farm"): From e5733450e428f7f26e5bfe76fc9fe1e80b42b9f2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 May 2023 12:18:57 +0200 Subject: [PATCH 691/918] Global: plugins cleanup plugin will leave beauty rendered files (#4790) * OP-1066 - add expected files in Deadline into explicit cleanup Implicit cleanup doesn't work correctly, safest option is for DL submissions to mark only files that should be rendered to be deleted after successful publish. * OP-1066 - moved collecting of expected files into collector Parsing of json didn't have context implemented, it is easier to mark expected files in collector. * OP-4793 - delete full stagingDir Reviews might be extracted into staging dir, should be removed too. * Revert "OP-4793 - delete full stagingDir" This reverts commit 8b002191e1ad3b31a0cbe439ca1158946c43b049. * OP-1066 - added function to mark representation files to be cleaned up Should be applicable for all new representations, as reviews, thumbnails, to clean up their intermediate files. * OP-1066 - moved files to better file Cleaned up occurences where not necessary. * OP-1066 - removed unused import * OP-1066 - removed unnecessary setdefault * OP-1066 - removed unnecessary logging * OP-1066 - cleanup metadata json Try to cleanup parent folder if empty. --- openpype/pipeline/publish/lib.py | 19 +++++++++++++++++++ .../plugins/publish/collect_rendered_files.py | 6 ++++++ openpype/plugins/publish/extract_burnin.py | 3 +++ openpype/plugins/publish/extract_review.py | 3 +++ 4 files changed, 31 insertions(+) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 63a856e326..b55f813b5e 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -847,3 +847,22 @@ def _validate_transient_template(project_name, template_name, anatomy): raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa " for project \"{}\"." ).format(template_name, project_name)) + + +def add_repre_files_for_cleanup(instance, repre): + """ Explicitly mark repre files to be deleted. + + Should be used on intermediate files (eg. review, thumbnails) to be + explicitly deleted. + """ + files = repre["files"] + staging_dir = repre.get("stagingDir") + if not staging_dir: + return + + if isinstance(files, str): + files = [files] + + for file_name in files: + expected_file = os.path.join(staging_dir, file_name) + instance.context.data["cleanupFullPaths"].append(expected_file) diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index 8f8d0a5eeb..6c8d1e9ca5 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -13,6 +13,7 @@ import json import pyblish.api from openpype.pipeline import legacy_io, KnownPublishError +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class CollectRenderedFiles(pyblish.api.ContextPlugin): @@ -89,6 +90,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # now we can just add instances from json file and we are done for instance_data in data.get("instances"): + self.log.info(" - processing instance for {}".format( instance_data.get("subset"))) instance = self._context.create_instance( @@ -107,6 +109,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): self._fill_staging_dir(repre_data, anatomy) representations.append(repre_data) + add_repre_files_for_cleanup(instance, repre_data) + instance.data["representations"] = representations # add audio if in metadata data @@ -157,6 +161,8 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): os.environ.update(session_data) session_is_set = True self._process_path(data, anatomy) + context.data["cleanupFullPaths"].append(path) + context.data["cleanupEmptyDirs"].append(os.path.dirname(path)) except Exception as e: self.log.error(e, exc_info=True) raise Exception("Error") from e diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index 10b366dcd6..6a8ae958d2 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -19,6 +19,7 @@ from openpype.lib import ( should_convert_for_ffmpeg ) from openpype.lib.profiles_filtering import filter_profiles +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class ExtractBurnin(publish.Extractor): @@ -353,6 +354,8 @@ class ExtractBurnin(publish.Extractor): # Add new representation to instance instance.data["representations"].append(new_repre) + add_repre_files_for_cleanup(instance, new_repre) + # Cleanup temp staging dir after procesisng of output definitions if do_convert: temp_dir = repre["stagingDir"] diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index a68addda7d..fa58c03df1 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -24,6 +24,7 @@ from openpype.lib.transcoding import ( get_transcode_temp_directory, ) from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish.lib import add_repre_files_for_cleanup class ExtractReview(pyblish.api.InstancePlugin): @@ -425,6 +426,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ) instance.data["representations"].append(new_repre) + add_repre_files_for_cleanup(instance, new_repre) + def input_is_sequence(self, repre): """Deduce from representation data if input is sequence.""" # TODO GLOBAL ISSUE - Find better way how to find out if input From 8410055b2499e30c80cc7d3bd8c4d10cf76369bb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 24 May 2023 10:21:38 +0000 Subject: [PATCH 692/918] [Automated] Release --- CHANGELOG.md | 298 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 300 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bba6b64bfe..a33904735b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,304 @@ # Changelog +## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.7...3.15.8) + +### **🆕 New features** + + +

+Publisher: Show instances in report page #4915 + +Show publish instances in report page. Also added basic log view with logs grouped by instance. Validation error detail now have 2 colums, one with erro details second with logs. Crashed state shows fast access to report action buttons. Success will show only logs. Publish frame is shrunked automatically on publish stop. + + +___ + +
+ + +
+Fusion - Loader plugins updates #4920 + +Update to some Fusion loader plugins:The sequence loader can now load footage from the image and online family.The FBX loader can now import all formats Fusions FBX node can read.You can now import the content of another workfile into your current comp with the workfile loader. + + +___ + +
+ + +
+Fusion: deadline farm rendering #4955 + +Enabling Fusion for deadline farm rendering. + + +___ + +
+ + +
+AfterEffects: set frame range and resolution #4983 + +Frame information (frame start, duration, fps) and resolution (width and height) is applied to selected composition from Asset Management System (Ftrack or DB) automatically when published instance is created.It is also possible explicitly propagate both values from DB to selected composition by newly added menu buttons. + + +___ + +
+ + +
+Publish: Enhance automated publish plugin settings #4986 + +Added plugins option to define settings category where to look for settings of a plugin and added public helper functions to apply settings `get_plugin_settings` and `apply_plugin_settings_automatically`. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Load Rig References - Change Rig to Animation in Animation instance #4877 + +We are using the template builder to build an animation scene. All the rig placeholders are imported correctly, but the automatically created animation instances retain the rig family in their names and subsets. In our example, we need animationMain instead of rigMain, because this name will be used in the following steps like lighting.Here is the result we need. I checked, and it's not a template builder problem, because even if I load a rig as a reference, the result is the same. For me, since we are in the animation instance, it makes more sense to have animation instead of rig in the name. The naming is just fine if we use create from the Openpype menu. + + +___ + +
+ + +
+Enhancement: Resolve prelaunch code refactoring and update defaults #4916 + +The main reason of this PR is wrong default settings in `openpype/settings/defaults/system_settings/applications.json` for Resolve host. The `bin` folder should not be a part of the macos and Linux `RESOLVE_PYTHON3_PATH` variable.The rest of this PR is some code cleanups for Resolve prelaunch hook to simplify further development.Also added a .gitignore for vscode workspace files. + + +___ + +
+ + +
+Unreal: 🚚 move Unreal plugin to separate repository #4980 + +To support Epic Marketplace have to move AYON Unreal integration plugins to separate repository. This is replacing current files with git submodule, so the change should be functionally without impact.New repository lives here: https://github.com/ynput/ayon-unreal-plugin + + +___ + +
+ + +
+General: Lib code cleanup #5003 + +Small cleanup in lib files in openpype. + + +___ + +
+ + +
+Allow to open with djv by extension instead of representation name #5004 + +Filter open in djv action by extension instead of representation. + + +___ + +
+ + +
+DJV open action `extensions` as `set` #5005 + +Change `extensions` attribute to `set`. + + +___ + +
+ + +
+Nuke: extract thumbnail with multiple reposition nodes #5011 + +Added support for multiple reposition nodes. + + +___ + +
+ + +
+Enhancement: Improve logging levels and messages for artist facing publish reports #5018 + +Tweak the logging levels and messages to try and only show those logs that an artist should see and could understand. Move anything that's slightly more involved into a "debug" message instead. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Bugfix/frame variable fix #4978 + +Renamed variables to match OpenPype terminology to reduce confusion and add consistency. +___ + +
+ + +
+Global: plugins cleanup plugin will leave beauty rendered files #4790 + +Attempt to mark more files to be cleaned up explicitly in intermediate `renders` folder in work area for farm jobs. + + +___ + +
+ + +
+Fix: Download last workfile doesn't work if not already downloaded #4942 + +Some optimization condition is messing with the feature: if the published workfile is not already downloaded, it won't download it... + + +___ + +
+ + +
+Unreal: Fix transform when loading layout to match existing assets #4972 + +Fixed transform when loading layout to match existing assets. + + +___ + +
+ + +
+fix the bug of fbx loaders in Max #4977 + +bug fix of fbx loaders for not being able to parent to the CON instances while importing cameras(and models) which is published from other DCCs such as Maya. + + +___ + +
+ + +
+AfterEffects: allow returning stub with not saved workfile #4984 + +Allows to use Workfile app to Save first empty workfile. + + +___ + +
+ + +
+Blender: Fix Alembic loading #4985 + +Fixed problem occurring when trying to load an Alembic model in Blender. + + +___ + +
+ + +
+Unreal: Addon Py2 compatibility #4994 + +Fixed Python 2 compatibility of unreal addon. + + +___ + +
+ + +
+Nuke: fixed missing files key in representation #4999 + +Issue with missing keys once rendering target set to existing frames is fixed. Instance has to be evaluated in validation for missing files. + + +___ + +
+ + +
+Unreal: Fix the frame range when loading camera #5002 + +The keyframes of the camera, when loaded, were not using the correct frame range. + + +___ + +
+ + +
+Fusion: fixing frame range targeting #5013 + +Frame range targeting at Rendering instances is now following configured options. + + +___ + +
+ + +
+Deadline: fix selection from multiple webservices #5015 + +Multiple different DL webservice could be configured. First they must by configured in System Settings., then they could be configured per project in `project_settings/deadline/deadline_servers`.Only single webservice could be a target of publish though. + + +___ + +
+ +### **Merged pull requests** + + +
+3dsmax: Refactored publish plugins to use proper implementation of pymxs #4988 + + +___ + +
+ + + + ## [3.15.7](https://github.com/ynput/OpenPype/tree/3.15.7) diff --git a/openpype/version.py b/openpype/version.py index 3d7f64b991..342bbfc85a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8-nightly.3" +__version__ = "3.15.8" diff --git a/pyproject.toml b/pyproject.toml index 190ecb9329..a72a3d66d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.7" # OpenPype +version = "3.15.8" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 4647e39142da5c0a1a5cc62844a38c105c166dc9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 May 2023 10:22:35 +0000 Subject: [PATCH 693/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a9f1f1cc02..4d7d06a2c8 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.8 - 3.15.8-nightly.3 - 3.15.8-nightly.2 - 3.15.8-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.4 - 3.14.2-nightly.3 - 3.14.2-nightly.2 - - 3.14.2-nightly.1 validations: required: true - type: dropdown From ca9defe36975ec373b4c247819e4b9d0bf1fb382 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 13:11:13 +0200 Subject: [PATCH 694/918] label improvements --- openpype/pipeline/colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 652304ef33..1cf7e2e192 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -360,7 +360,7 @@ def get_imageio_config( # if global settings are disabled return empty dict because # it is expected that no colorspace management is needed log.info( - "Colorspace management is disabled." + "Colorspace management is disabled globally." ) return {} @@ -482,7 +482,7 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): if not activate_global_rules: log.info( - "Global File Rules are disabled." + "Colorspace global file rules are disabled." ) return {} From b5827e8cdfcde48a7a9eff6aa29a21f48541ab66 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:18:05 +0100 Subject: [PATCH 695/918] Fix section range on update camera --- .../hosts/unreal/plugins/load/load_camera.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index d198be29f4..c4fe9df70b 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -387,6 +387,30 @@ class CameraLoader(plugin.Loader): str(representation["data"]["path"]) ) + # Set range of all sections + # Changing the range of the section is not enough. We need to change + # the frame of all the keys in the section. + project_name = legacy_io.active_project() + asset = container.get('asset') + data = get_asset_by_name(project_name, asset)["data"] + + for possessable in new_sequence.get_possessables(): + for tracks in possessable.get_tracks(): + for section in tracks.get_sections(): + section.set_range( + data.get('clipIn'), + data.get('clipOut') + 1) + for channel in section.get_all_channels(): + for key in channel.get_keys(): + old_time = key.get_time().get_editor_property( + 'frame_number') + old_time_value = old_time.get_editor_property( + 'value') + new_time = old_time_value + ( + data.get('clipIn') - data.get('frameStart') + ) + key.set_time(unreal.FrameNumber(value=new_time)) + data = { "representation": str(representation["_id"]), "parent": str(representation["parent"]) From 7cfbb972a5ad41eea6e7bac6775ec8697edcb342 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:40:28 +0100 Subject: [PATCH 696/918] Fix sequence frames validator to use correct data --- .../hosts/unreal/plugins/publish/validate_sequence_frames.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py index e6584e130f..76bb25fac3 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/openpype/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -31,8 +31,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): frames = list(collection.indexes) current_range = (frames[0], frames[-1]) - required_range = (data["frameStart"], - data["frameEnd"]) + required_range = (data["clipIn"], + data["clipOut"]) if current_range != required_range: raise ValueError(f"Invalid frame range: {current_range} - " From 7e692ad5acc284540f1d29672ea86386b0f22468 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 24 May 2023 14:01:50 +0200 Subject: [PATCH 697/918] added option to nest settings templates --- openpype/settings/entities/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 1c7dc9bed0..93abc27b0e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -323,7 +323,10 @@ class SchemasHub: filled_template = self._fill_template( schema_data, template_def ) - return filled_template + new_template_def = [] + for item in filled_template: + new_template_def.extend(self.resolve_schema_data(item)) + return new_template_def def create_schema_object(self, schema_data, *args, **kwargs): """Create entity for passed schema data. From 45e1dbc8410fb66dd7e685e77946db2952b35672 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 13:14:46 +0100 Subject: [PATCH 698/918] Fix render instances collection to use correct data --- .../hosts/unreal/plugins/publish/collect_render_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index a352b2c3f3..dad0310dfc 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -103,8 +103,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): new_instance.data["representations"] = [] repr = { - 'frameStart': s.get('frame_range')[0], - 'frameEnd': s.get('frame_range')[1], + 'frameStart': instance.data["frameStart"], + 'frameEnd': instance.data["frameEnd"], 'name': 'png', 'ext': 'png', 'files': frames, From 379f838f03160f0b3ae4d24df980c06be06efc59 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 14:50:59 +0200 Subject: [PATCH 699/918] distributing settings via template schemas --- .../schema_project_aftereffects.json | 18 +------- .../schema_project_blender.json | 18 +------- .../schema_project_celaction.json | 18 +------- .../projects_schema/schema_project_flame.json | 17 +------- .../schema_project_fusion.json | 17 +------- .../schema_project_harmony.json | 18 +------- .../projects_schema/schema_project_hiero.json | 17 +------- .../schema_project_houdini.json | 18 +------- .../projects_schema/schema_project_max.json | 18 +------- .../projects_schema/schema_project_maya.json | 17 +------- .../schema_project_photoshop.json | 18 +------- .../schema_project_resolve.json | 18 +------- .../schema_project_substancepainter.json | 11 ++--- .../schema_project_traypublisher.json | 18 +------- .../schema_project_tvpaint.json | 18 +------- .../schema_project_unreal.json | 18 +------- .../schema_project_webpublisher.json | 18 +------- .../schemas/schema_imageio_config.json | 20 --------- .../schemas/schema_imageio_file_rules.json | 40 ------------------ .../schemas/schema_nuke_imageio.json | 17 +------- .../template_colorspace_remapping.json | 29 +++++++++++++ ...emplate_host_color_management_derived.json | 19 +++++++++ .../template_host_color_management_ocio.json | 19 +++++++++ ...mplate_host_color_management_remapped.json | 23 ++++++++++ .../schemas/template_imageio_config.json | 22 ++++++++++ .../schemas/template_imageio_file_rules.json | 42 +++++++++++++++++++ 26 files changed, 191 insertions(+), 335 deletions(-) delete mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json delete mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_derived.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_ocio.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_remapped.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_config.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 7262c17dd5..d4f52b50d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 79eea3f192..78a1552ac3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json index 915f199b6e..9d50e85631 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_celaction.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_derived" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index 16c9378194..102e2bdcc3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -13,21 +13,8 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_remapped" }, { "key": "project", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 550e7a3cf4..656c50dd98 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -13,21 +13,8 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 276b321b24..98a815f2d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json index c2339d8200..d80edf902b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_hiero.json @@ -13,21 +13,8 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" }, { "key": "workfile", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index a7032775c1..7f782e3647 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index ee2bbd4ffa..e314174dff 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index fe7c262603..dca955dab4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -53,21 +53,8 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" }, { "key": "workfile", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 7ddd575dde..20d4ff0aa3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_remapped" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index aea019b77b..2f10cc900a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.

Related documentation.." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_remapped" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json index 79a39b8e6e..6be8cecad3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_substancepainter.json @@ -8,18 +8,13 @@ { "key": "imageio", "type": "dict", - "label": "Color Management (ImageIO)", + "label": "Color Management (OCIO managed)", "is_group": true, "children": [ { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 6b55837f12..3703d82856 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_derived" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index ed8887f93e..45fc13bdde 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_derived" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index aa2fe40b4a..b23744f406 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json index 7b65dddda6..87de732d69 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_webpublisher.json @@ -13,23 +13,9 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_derived" } - ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json deleted file mode 100644 index bc65dd7826..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "key": "ocio_config", - "type": "dict", - "label": "OCIO config", - "collapsible": true, - "children": [ - { - "type": "boolean", - "key": "override_global_config", - "label": "Override global OCIO config" - }, - { - "type": "path", - "key": "filepath", - "label": "Config path", - "multiplatform": false, - "multipath": true - } - ] -} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json deleted file mode 100644 index 62b72c2518..0000000000 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_imageio_file_rules.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "key": "file_rules", - "type": "dict", - "label": "File Rules (OCIO v1 only)", - "collapsible": true, - "children": [ - { - "type": "boolean", - "key": "override_global_rules", - "label": "Override global File Rules" - }, - { - "key": "rules", - "label": "Rules", - "type": "dict-modifiable", - "highlight_content": true, - "collapsible": false, - "object_type": { - "type": "dict", - "children": [ - { - "key": "pattern", - "label": "Regex pattern", - "type": "text" - }, - { - "key": "colorspace", - "label": "Colorspace name", - "type": "text" - }, - { - "key": "ext", - "label": "File extension", - "type": "text" - } - ] - } - } - ] -} diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json index 864e084bde..d4cd332ef8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_imageio.json @@ -6,21 +6,8 @@ "is_group": true, "children": [ { - "type": "label", - "label": "This application's colorspace management can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." - }, - { - "type": "boolean", - "key": "activate_host_color_management", - "label": "Enable Color Management" - }, - { - "type": "schema", - "name": "schema_imageio_config" - }, - { - "type": "schema", - "name": "schema_imageio_file_rules" + "type": "template", + "name": "template_host_color_management_ocio" }, { "key": "viewer", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json new file mode 100644 index 0000000000..9ae504fa81 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json @@ -0,0 +1,29 @@ +[ + { + "key": "remapping", + "type": "dict", + "label": "Remapping colorspace names", + "collapsible": true, + "children": [ + { + "type": "list", + "key": "inputs", + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "internal_name", + "label": "Internal colorspace name" + }, + { + "type": "text", + "key": "ocio_name", + "label": "OCIO colorspace name" + } + ] + } + } + ] + } +] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_derived.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_derived.json new file mode 100644 index 0000000000..a129d470c0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_derived.json @@ -0,0 +1,19 @@ +[ + { + "type": "label", + "label": "The application does not include any built-in color management capabilities, OpenPype offers a solution
to this limitation by deriving valid colorspace names for the OpenColorIO (OCIO) color management
system from file paths, using File Rules feature only during Publishing.

Related documentation." + }, + { + "type": "boolean", + "key": "activate_host_color_management", + "label": "Enable Color Management" + }, + { + "type": "template", + "name": "template_imageio_config" + }, + { + "type": "template", + "name": "template_imageio_file_rules" + } +] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_ocio.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_ocio.json new file mode 100644 index 0000000000..88c22fa762 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_ocio.json @@ -0,0 +1,19 @@ +[ + { + "type": "label", + "label": "Colorspace management for the application can be controlled through OpenPype settings.
Specifically, the configured OpenColorIO (OCIO) config path is utilized in the application's workfile.
Additionally, the File Rules feature can be leveraged for both publishing and loading procedures.

Related documentation." + }, + { + "type": "boolean", + "key": "activate_host_color_management", + "label": "Enable Color Management" + }, + { + "type": "template", + "name": "template_imageio_config" + }, + { + "type": "template", + "name": "template_imageio_file_rules" + } +] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_remapped.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_remapped.json new file mode 100644 index 0000000000..780264947f --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_host_color_management_remapped.json @@ -0,0 +1,23 @@ +[ + { + "type": "label", + "label": "The application includes internal color management functionality, but it does not offer external control
over this feature. To address this limitation, OpenPype uses mapping rules to remap the native
colorspace names used in the internal color management system to the OpenColorIO (OCIO)
color management system. Remapping feature is used in Publishing and Loading procedures.

Related documentation.." + }, + { + "type": "boolean", + "key": "activate_host_color_management", + "label": "Enable Color Management" + }, + { + "type": "template", + "name": "template_colorspace_remapping" + }, + { + "type": "template", + "name": "template_imageio_config" + }, + { + "type": "template", + "name": "template_imageio_file_rules" + } +] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_config.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_config.json new file mode 100644 index 0000000000..0550e5093c --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_config.json @@ -0,0 +1,22 @@ +[ + { + "key": "ocio_config", + "type": "dict", + "label": "OCIO config", + "collapsible": true, + "children": [ + { + "type": "boolean", + "key": "override_global_config", + "label": "Override global OCIO config" + }, + { + "type": "path", + "key": "filepath", + "label": "Config path", + "multiplatform": false, + "multipath": true + } + ] + } +] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json new file mode 100644 index 0000000000..829fd02489 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json @@ -0,0 +1,42 @@ +[ + { + "key": "file_rules", + "type": "dict", + "label": "File Rules (OCIO v1 only)", + "collapsible": true, + "children": [ + { + "type": "boolean", + "key": "override_global_rules", + "label": "Override global File Rules" + }, + { + "key": "rules", + "label": "Rules", + "type": "dict-modifiable", + "highlight_content": true, + "collapsible": false, + "object_type": { + "type": "dict", + "children": [ + { + "key": "pattern", + "label": "Regex pattern", + "type": "text" + }, + { + "key": "colorspace", + "label": "Colorspace name", + "type": "text" + }, + { + "key": "ext", + "label": "File extension", + "type": "text" + } + ] + } + } + ] + } +] From c2055622ab98880eeb5a1a3475b7c9303bdcd93d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 15:51:29 +0200 Subject: [PATCH 700/918] adding remapping functionality --- openpype/pipeline/colorspace.py | 50 +++++++++++++++++++ .../projects_schema/schema_project_flame.json | 6 ++- .../template_colorspace_remapping.json | 6 +-- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 1cf7e2e192..fa7ad5133c 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -17,6 +17,8 @@ from openpype.pipeline import Anatomy log = Logger.get_logger(__name__) +class cashed_data: + remapping: dict = None @contextlib.contextmanager def _make_temp_json_file(): @@ -497,6 +499,54 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): return frules_global["rules"] +def get_remapped_colorspace_to_native( + ocio_colorspace_name, host_name, imageio_host_settings): + """Return native colorspace name. + + Args: + ocio_colorspace_name (str | None): ocio colorspace name + + Returns: + str: native colorspace name defined in remapping or None + """ + + if not cashed_data.remapping.get(host_name): + remapping_rules = imageio_host_settings["remapping"]["rules"] + cashed_data.remapping[host_name] = { + "to_native": { + rule["ocio_name"]: input["host_native_name"] + for rule in remapping_rules + } + } + + return cashed_data.remapping[host_name]["to_native"].get( + ocio_colorspace_name) + + +def get_remapped_colorspace_from_native( + host_native_colorspace_name, host_name, imageio_host_settings): + """Return ocio colorspace name remapped from host native used name. + + Args: + host_native_colorspace_name (str): host native colorspace name + + Returns: + str: ocio colorspace name defined in remapping or None + """ + + if not cashed_data.remapping.get(host_name): + remapping_rules = imageio_host_settings["remapping"]["rules"] + cashed_data.remapping[host_name] = { + "from_native": { + input["host_native_name"]: rule["ocio_name"] + for rule in remapping_rules + } + } + + return cashed_data.remapping[host_name]["from_native"].get( + host_native_colorspace_name) + + def _get_imageio_settings(project_settings, host_name): """Get ImageIO settings for global and host diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json index 102e2bdcc3..06f818966f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_flame.json @@ -44,10 +44,14 @@ } ] }, + { + "type": "label", + "label": "Profile names mapping settings is deprecated use ./imagio/remapping instead" + }, { "key": "profilesMapping", "type": "dict", - "label": "Profile names mapping", + "label": "Profile names mapping [deprecated]", "collapsible": true, "children": [ { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json index 9ae504fa81..acd36ece9d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_colorspace_remapping.json @@ -7,14 +7,14 @@ "children": [ { "type": "list", - "key": "inputs", + "key": "rules", "object_type": { "type": "dict", "children": [ { "type": "text", - "key": "internal_name", - "label": "Internal colorspace name" + "key": "host_native_name", + "label": "Application native colorspace name" }, { "type": "text", From 05b0f61e0bad1db6226812c9d77f6dadef49ded5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 16:22:33 +0200 Subject: [PATCH 701/918] flame: adding remapping implementation --- openpype/hosts/flame/api/plugin.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index df8c1ac887..3289187fa0 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -10,6 +10,7 @@ from qtpy import QtCore, QtWidgets from openpype import style from openpype.lib import Logger, StringTemplate from openpype.pipeline import LegacyCreator, LoaderPlugin +from openpype.pipeline.colorspace import get_remapped_colorspace_to_native from openpype.settings import get_current_project_settings from . import constants @@ -701,6 +702,7 @@ class ClipLoader(LoaderPlugin): ] _mapping = None + _host_settings = None def apply_settings(cls, project_settings, system_settings): @@ -769,15 +771,26 @@ class ClipLoader(LoaderPlugin): Returns: str: native colorspace name defined in mapping or None """ + # TODO: rewrite to support only pipeline's remapping + if not cls._host_settings: + cls._host_settings = get_current_project_settings()["flame"] + + # [Deprecated] way of remapping if not cls._mapping: - settings = get_current_project_settings()["flame"] - mapping = settings["imageio"]["profilesMapping"]["inputs"] + mapping = ( + cls._host_settings["imageio"]["profilesMapping"]["inputs"]) cls._mapping = { input["ocioName"]: input["flameName"] for input in mapping } - return cls._mapping.get(input_colorspace) + native_name = cls._mapping.get(input_colorspace) + + if not native_name: + native_name = get_remapped_colorspace_to_native( + input_colorspace, "flame", cls._host_settings["imageio"]) + + return native_name class OpenClipSolver(flib.MediaInfoFile): From 6473fac9042e9257c05c2c4020c3352c178da5f5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 16:25:34 +0200 Subject: [PATCH 702/918] fixing cashing nested data structure --- openpype/pipeline/colorspace.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index fa7ad5133c..5c449e0a4e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -17,9 +17,11 @@ from openpype.pipeline import Anatomy log = Logger.get_logger(__name__) + class cashed_data: remapping: dict = None + @contextlib.contextmanager def _make_temp_json_file(): """Wrapping function for json temp file @@ -510,7 +512,7 @@ def get_remapped_colorspace_to_native( str: native colorspace name defined in remapping or None """ - if not cashed_data.remapping.get(host_name): + if not cashed_data.remapping.get(host_name, {}).get("to_native"): remapping_rules = imageio_host_settings["remapping"]["rules"] cashed_data.remapping[host_name] = { "to_native": { @@ -534,7 +536,7 @@ def get_remapped_colorspace_from_native( str: ocio colorspace name defined in remapping or None """ - if not cashed_data.remapping.get(host_name): + if not cashed_data.remapping.get(host_name, {}).get("from_native"): remapping_rules = imageio_host_settings["remapping"]["rules"] cashed_data.remapping[host_name] = { "from_native": { From 31d04e492b8277bcc6c982111bb783632a8c8e1d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 16:28:23 +0200 Subject: [PATCH 703/918] cosmetics --- openpype/pipeline/colorspace.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 5c449e0a4e..f0fb7cf7f5 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -18,8 +18,8 @@ from openpype.pipeline import Anatomy log = Logger.get_logger(__name__) -class cashed_data: - remapping: dict = None +class CashedData: + remapping = None @contextlib.contextmanager @@ -512,16 +512,16 @@ def get_remapped_colorspace_to_native( str: native colorspace name defined in remapping or None """ - if not cashed_data.remapping.get(host_name, {}).get("to_native"): + if not CashedData.remapping.get(host_name, {}).get("to_native"): remapping_rules = imageio_host_settings["remapping"]["rules"] - cashed_data.remapping[host_name] = { + CashedData.remapping[host_name] = { "to_native": { rule["ocio_name"]: input["host_native_name"] for rule in remapping_rules } } - return cashed_data.remapping[host_name]["to_native"].get( + return CashedData.remapping[host_name]["to_native"].get( ocio_colorspace_name) @@ -536,16 +536,16 @@ def get_remapped_colorspace_from_native( str: ocio colorspace name defined in remapping or None """ - if not cashed_data.remapping.get(host_name, {}).get("from_native"): + if not CashedData.remapping.get(host_name, {}).get("from_native"): remapping_rules = imageio_host_settings["remapping"]["rules"] - cashed_data.remapping[host_name] = { + CashedData.remapping[host_name] = { "from_native": { input["host_native_name"]: rule["ocio_name"] for rule in remapping_rules } } - return cashed_data.remapping[host_name]["from_native"].get( + return CashedData.remapping[host_name]["from_native"].get( host_native_colorspace_name) From 0f8a998e3aeac42e44c137ca837b75ab1cda15d1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 16:30:32 +0200 Subject: [PATCH 704/918] old docstring fix --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index f0fb7cf7f5..5af313c570 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -333,7 +333,7 @@ def get_imageio_config( Defaults to None. Returns: - dict or bool: config path data or None + dict: config path data or empty dict """ project_settings = project_settings or get_project_settings(project_name) anatomy = anatomy or Anatomy(project_name) From e6b301fecbfc6ec8e5bd784808f7654123f07384 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 16:45:43 +0200 Subject: [PATCH 705/918] defaults update --- openpype/settings/defaults/project_settings/flame.json | 3 +++ openpype/settings/defaults/project_settings/photoshop.json | 3 +++ openpype/settings/defaults/project_settings/resolve.json | 3 +++ .../settings/defaults/project_settings/substancepainter.json | 5 +++-- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index 64021baeef..19773727ca 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -1,6 +1,9 @@ { "imageio": { "activate_host_color_management": true, + "remapping": { + "rules": [] + }, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 17da5dd738..ffcf87d8a5 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -1,6 +1,9 @@ { "imageio": { "activate_host_color_management": true, + "remapping": { + "rules": [] + }, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 7379e74200..f2d3727be1 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -1,6 +1,9 @@ { "imageio": { "activate_host_color_management": true, + "remapping": { + "rules": [] + }, "ocio_config": { "override_global_config": false, "filepath": [] diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json index 60929e85fd..4a1b86f3f4 100644 --- a/openpype/settings/defaults/project_settings/substancepainter.json +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -1,11 +1,12 @@ { "imageio": { + "activate_host_color_management": true, "ocio_config": { - "enabled": true, + "override_global_config": true, "filepath": [] }, "file_rules": { - "enabled": true, + "override_global_rules": true, "rules": {} } }, From 1eef778b04e9505e09da3fe3ef7e6ca5d83d5594 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 24 May 2023 17:43:35 +0200 Subject: [PATCH 706/918] Collect frame range for all rop nodes --- .../houdini/plugins/publish/collect_frames.py | 4 +- .../plugins/publish/collect_instances.py | 6 --- .../publish/collect_rop_frame_range.py | 41 +++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 059793e3c5..91a3d9d170 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -11,15 +11,13 @@ from openpype.hosts.houdini.api import lib class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.01 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review"] def process(self, instance): ropnode = hou.node(instance.data["instance_node"]) - frame_data = lib.get_frame_data(ropnode) - instance.data.update(frame_data) start_frame = instance.data.get("frameStart", None) end_frame = instance.data.get("frameEnd", None) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 5d5347f96e..5fa3e9655e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -70,16 +70,10 @@ class CollectInstances(pyblish.api.ContextPlugin): if "active" in data: data["publish"] = data["active"] - data.update(self.get_frame_data(node)) - # Create nice name if the instance has a frame range. label = data.get("name", node.name()) label += " (%s)" % data["asset"] # include asset in name - if "frameStart" in data and "frameEnd" in data: - frames = "[{frameStart} - {frameEnd}]".format(**data) - label = "{} {}".format(label, frames) - instance = context.create_instance(label) # Include `families` using `family` data diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py new file mode 100644 index 0000000000..2a6be6b9f1 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""Collector plugin for frames data on ROP instances.""" +import hou # noqa +import pyblish.api +from openpype.hosts.houdini.api import lib + + +class CollectRopFrameRange(pyblish.api.InstancePlugin): + """Collect all frames which would be saved from the ROP nodes""" + + order = pyblish.api.CollectorOrder + label = "Collect RopNode Frame Range" + + def process(self, instance): + + node_path = instance.data.get("instance_node") + if node_path is None: + # Instance without instance node like a workfile instance + return + + ropnode = hou.node(node_path) + frame_data = lib.get_frame_data(ropnode) + + if "frameStart" in frame_data and "frameEnd" in frame_data: + + # Log artist friendly message about the collected frame range + message = ( + "Frame range {0[frameStart]} - {0[frameEnd]}" + ).format(frame_data) + if frame_data.get("step", 1.0) != 1.0: + message += " with step {0[step]}".format(frame_data) + self.log.info(message) + + instance.data.update(frame_data) + + # Add frame range to label if the instance has a frame range. + label = instance.data.get("label", instance.data["name"]) + instance.data["label"] = ( + "{0} [{1[frameStart]} - {1[frameEnd]}]".format(label, + frame_data) + ) From d4212ef9918e805025fb93fdfdaf5b5fa82f2d7c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:04:42 +0200 Subject: [PATCH 707/918] Return any timeline in case none is detected as active also adding in host test --- openpype/hosts/resolve/api/lib.py | 16 +++++++++------- .../utility_scripts/tests/testing_timeline_op.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 1c33749a77..d42521200a 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -91,16 +91,16 @@ def get_current_project(): return self.project_manager.GetCurrentProject() -def get_current_timeline(any=False, new=False): +def get_current_timeline(new=False, get_any=False): """Get current timeline object. Args: - any (bool, optional): return any even new if no timeline available. - Defaults to False. new (bool, optional): return only new timeline. Defaults to False. + get_any (bool, optional): return any even new if no timeline available. + Defaults to False. Returns: - _type_: _description_ + object: resolve.Timeline """ # get current project project = get_current_project() @@ -111,12 +111,14 @@ def get_current_timeline(any=False, new=False): if timeline and not new: return timeline - # if any is True then return any timeline - if any: + # if get_any is True then return any timeline + if get_any: timeline_count = project.GetTimelineCount() if timeline_count == 0: # if there is no timeline then create a new one new = True + else: + return project.GetTimelineByIndex(1) # create new timeline if new is True if new: @@ -336,7 +338,7 @@ def get_current_timeline_items( selecting_color = selecting_color or "Chocolate" project = get_current_project() # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(any=True) + timeline = get_current_timeline(get_any=True) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py new file mode 100644 index 0000000000..8270496f64 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_timeline_op.py @@ -0,0 +1,13 @@ +#! python3 +from openpype.pipeline import install_host +from openpype.hosts.resolve import api as bmdvr +from openpype.hosts.resolve.api.lib import get_current_project + +if __name__ == "__main__": + install_host(bmdvr) + project = get_current_project() + timeline_count = project.GetTimelineCount() + print(f"Timeline count: {timeline_count}") + timeline = project.GetTimelineByIndex(timeline_count) + print(f"Timeline name: {timeline.GetName()}") + print(timeline.GetTrackCount("video")) From 99a1be366e77db5549b294b8e37bb3089061cdd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 24 May 2023 22:19:46 +0200 Subject: [PATCH 708/918] nuke: callback for dirmapping is on demand --- openpype/hosts/nuke/api/pipeline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d649ffae7f..75b0f80d21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -151,6 +151,7 @@ class NukeHost( def add_nuke_callbacks(): """ Adding all available nuke callbacks """ + nuke_settings = get_current_project_settings()["nuke"] workfile_settings = WorkfileSettings() # Set context settings. nuke.addOnCreate( @@ -169,7 +170,10 @@ def add_nuke_callbacks(): # # set apply all workfile settings on script load and save nuke.addOnScriptLoad(WorkfileSettings().set_context_settings) - nuke.addFilenameFilter(dirmap_file_name_filter) + if nuke_settings["nuke-dirmap"]["enabled"]: + log.info("Added Nuke's dirmaping callback ...") + # Add dirmap for file paths. + nuke.addFilenameFilter(dirmap_file_name_filter) log.info("Added Nuke callbacks ...") From 41ae41d7512ebdc3270d49904f264cdde6c44f75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 25 May 2023 10:00:29 +0200 Subject: [PATCH 709/918] Enhancement/publisher: Remove "hit play to continue" label on continue (#5029) * Clear message label on publish so that on "continue" it does not persist the "Hit play to continue" message * Clear main label on reset since the label isn't visible anyway * Remove unnecessary comment --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/publisher/widgets/publish_frame.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/publish_frame.py b/openpype/tools/publisher/widgets/publish_frame.py index d21130deff..d423f97047 100644 --- a/openpype/tools/publisher/widgets/publish_frame.py +++ b/openpype/tools/publisher/widgets/publish_frame.py @@ -310,7 +310,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property() self._set_progress_visibility(True) - self._main_label.setText("Hit publish (play button)! If you want") + self._main_label.setText("") self._message_label_top.setText("") self._reset_btn.setEnabled(True) @@ -331,6 +331,7 @@ class PublishFrame(QtWidgets.QWidget): self._set_success_property(3) self._set_progress_visibility(True) self._set_main_label("Publishing...") + self._message_label_top.setText("") self._reset_btn.setEnabled(False) self._stop_btn.setEnabled(True) From 55040e6f74116b23531fa87f8965b83ab68d1316 Mon Sep 17 00:00:00 2001 From: Thomas Fricard <51854004+friquette@users.noreply.github.com> Date: Thu, 25 May 2023 10:32:36 +0200 Subject: [PATCH 710/918] Drop-down menu to list all families in create placeholder (#4928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * convert family text field to enum field * get families from loaders and not creators * refactor the list families part * remove discover_loader_plugins call since there is already a variable with loaders plugins --------- Co-authored-by: Thomas Fricard Co-authored-by: Clément Hector --- .../workfile/workfile_template_builder.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index a3d7340367..896ed40f2d 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -43,6 +43,7 @@ from openpype.pipeline.load import ( get_contexts_for_repre_docs, load_with_repre_context, ) + from openpype.pipeline.create import ( discover_legacy_creator_plugins, CreateContext, @@ -1246,6 +1247,16 @@ class PlaceholderLoadMixin(object): loader_items = list(sorted(loader_items, key=lambda i: i["label"])) options = options or {} + + # Get families from all loaders excluding "*" + families = set() + for loader in loaders_by_name.values(): + families.update(loader.families) + families.discard("*") + + # Sort for readability + families = list(sorted(families)) + return [ attribute_definitions.UISeparatorDef(), attribute_definitions.UILabelDef("Main attributes"), @@ -1272,11 +1283,11 @@ class PlaceholderLoadMixin(object): " field \"inputLinks\"" ) ), - attribute_definitions.TextDef( + attribute_definitions.EnumDef( "family", label="Family", default=options.get("family"), - placeholder="model, look, ..." + items=families ), attribute_definitions.TextDef( "representation", From 124493affd3e3b34563e35c83e34c7c66b5ee99b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 11:51:44 +0200 Subject: [PATCH 711/918] Publisher: UI works with instances without label (#5032) * implemented helper function to get instance label * use 'get_publish_instance_label' in some of existing plugins * use 'get_publish_instance_label' in publisher controller --- openpype/pipeline/publish/__init__.py | 2 ++ openpype/pipeline/publish/lib.py | 32 +++++++++++++++++++ openpype/plugins/publish/extract_review.py | 16 +++------- .../plugins/publish/integrate_thumbnail.py | 12 ++----- openpype/tools/publisher/control.py | 3 +- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 72f3774e1a..0c57915c05 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -39,6 +39,7 @@ from .lib import ( apply_plugin_settings_automatically, get_plugin_settings, + get_publish_instance_label, ) from .abstract_expected_files import ExpectedFiles @@ -85,6 +86,7 @@ __all__ = ( "apply_plugin_settings_automatically", "get_plugin_settings", + "get_publish_instance_label", "ExpectedFiles", diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index b55f813b5e..e87b865dce 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -30,6 +30,8 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) +_ARG_PLACEHOLDER = object() + def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -866,3 +868,33 @@ def add_repre_files_for_cleanup(instance, repre): for file_name in files: expected_file = os.path.join(staging_dir, file_name) instance.context.data["cleanupFullPaths"].append(expected_file) + + +def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): + """Try to get label from pyblish instance. + + First are checked 'label' and 'name' keys in instance data. If are not set + a default value is returned. Instance object is converted to string + if default value is not specific. + + Todos: + Maybe 'subset' key could be used too. + + Args: + instance (pyblish.api.Instance): Pyblish instance. + default (Optional[Any]): Default value to return if any + + Returns: + Union[Any]: Instance label or default label. + """ + + label = ( + instance.data.get("label") + or instance.data.get("name") + ) + if label: + return label + + if default is _ARG_PLACEHOLDER: + return str(instance) + return default diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index fa58c03df1..d04893fa7e 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -23,7 +23,10 @@ from openpype.lib.transcoding import ( convert_input_paths_for_ffmpeg, get_transcode_temp_directory, ) -from openpype.pipeline.publish import KnownPublishError +from openpype.pipeline.publish import ( + KnownPublishError, + get_publish_instance_label, +) from openpype.pipeline.publish.lib import add_repre_files_for_cleanup @@ -203,17 +206,8 @@ class ExtractReview(pyblish.api.InstancePlugin): return filtered_defs - @staticmethod - def get_instance_label(instance): - return ( - getattr(instance, "label", None) - or instance.data.get("label") - or instance.data.get("name") - or str(instance) - ) - def main_process(self, instance): - instance_label = self.get_instance_label(instance) + instance_label = get_publish_instance_label(instance) self.log.debug("Processing instance \"{}\"".format(instance_label)) profile_outputs = self._get_outputs_for_instance(instance) if not profile_outputs: diff --git a/openpype/plugins/publish/integrate_thumbnail.py b/openpype/plugins/publish/integrate_thumbnail.py index f6d4f654f5..2e87d8fc86 100644 --- a/openpype/plugins/publish/integrate_thumbnail.py +++ b/openpype/plugins/publish/integrate_thumbnail.py @@ -20,6 +20,7 @@ import pyblish.api from openpype.client import get_versions from openpype.client.operations import OperationsSession, new_thumbnail_doc +from openpype.pipeline.publish import get_publish_instance_label InstanceFilterResult = collections.namedtuple( "InstanceFilterResult", @@ -133,7 +134,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): filtered_instances = [] for instance in context: - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) # Skip instances without published representations # - there is no place where to put the thumbnail published_repres = instance.data.get("published_representations") @@ -248,7 +249,7 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): for instance_item in filtered_instance_items: instance, thumbnail_path, version_id = instance_item - instance_label = self._get_instance_label(instance) + instance_label = get_publish_instance_label(instance) version_doc = version_docs_by_str_id.get(version_id) if not version_doc: self.log.warning(( @@ -339,10 +340,3 @@ class IntegrateThumbnails(pyblish.api.ContextPlugin): )) op_session.commit() - - def _get_instance_label(self, instance): - return ( - instance.data.get("label") - or instance.data.get("name") - or "N/A" - ) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 8095d00103..89c2343ef7 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -40,6 +40,7 @@ from openpype.pipeline.create.context import ( CreatorsOperationFailed, ConvertorsOperationFailed, ) +from openpype.pipeline.publish import get_publish_instance_label # Define constant for plugin orders offset PLUGIN_ORDER_OFFSET = 0.5 @@ -346,7 +347,7 @@ class PublishReportMaker: def _extract_instance_data(self, instance, exists): return { "name": instance.data.get("name"), - "label": instance.data.get("label"), + "label": get_publish_instance_label(instance), "family": instance.data["family"], "families": instance.data.get("families") or [], "exists": exists, From 48b4934ee36628f57a895ff9e8cf7f349ddcea6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 May 2023 13:33:11 +0200 Subject: [PATCH 712/918] limit number of ftrack events to query at once (#5033) --- openpype/modules/ftrack/ftrack_server/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py index eb64063fab..2226c85ef9 100644 --- a/openpype/modules/ftrack/ftrack_server/lib.py +++ b/openpype/modules/ftrack/ftrack_server/lib.py @@ -196,7 +196,7 @@ class ProcessEventHub(SocketBaseEventHub): {"pype_data.is_processed": False} ).sort( [("pype_data.stored", pymongo.ASCENDING)] - ) + ).limit(100) found = False for event_data in not_processed_events: From 318237ded65c42e04a61cc38ba91886c0becf7a4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 16:38:01 +0200 Subject: [PATCH 713/918] breaking get_current_timeline into more functions --- openpype/hosts/resolve/api/__init__.py | 4 ++ openpype/hosts/resolve/api/lib.py | 83 ++++++++++++++++---------- openpype/hosts/resolve/api/plugin.py | 5 +- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 00a598548e..2b4546f8d6 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -24,6 +24,8 @@ from .lib import ( get_project_manager, get_current_project, get_current_timeline, + get_any_timeline, + get_new_timeline, create_bin, get_media_pool_item, create_media_pool_item, @@ -95,6 +97,8 @@ __all__ = [ "get_project_manager", "get_current_project", "get_current_timeline", + "get_any_timeline", + "get_new_timeline", "create_bin", "get_media_pool_item", "create_media_pool_item", diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index d42521200a..a44c527f13 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -15,6 +15,7 @@ log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None self.media_storage = None +self.current_project = None # OpenPype sequential rename variables self.rename_index = 0 @@ -85,47 +86,60 @@ def get_media_storage(): def get_current_project(): - # initialize project manager - get_project_manager() + """Get current project object. + """ + if not self.current_project: + self.current_project = get_project_manager().GetCurrentProject() - return self.project_manager.GetCurrentProject() + return self.current_project -def get_current_timeline(new=False, get_any=False): +def get_current_timeline(new=False): """Get current timeline object. Args: - new (bool, optional): return only new timeline. Defaults to False. - get_any (bool, optional): return any even new if no timeline available. - Defaults to False. + new (bool)[optional]: [DEPRECATED] if True it will create + new timeline if none exists + + Returns: + TODO: will need to reflect future `None` + object: resolve.Timeline + """ + project = get_current_project() + timeline = project.GetCurrentTimeline() + + # return current timeline if any + if timeline: + return timeline + + # TODO: [deprecated] and will be removed in future + if new: + return get_new_timeline() + + +def get_any_timeline(): + """Get any timeline object. + + Returns: + object | None: resolve.Timeline + """ + project = get_current_project() + timeline_count = project.GetTimelineCount() + if timeline_count > 0: + return project.GetTimelineByIndex(1) + + +def get_new_timeline(): + """Get new timeline object. Returns: object: resolve.Timeline """ - # get current project project = get_current_project() - - timeline = project.GetCurrentTimeline() - - # return current timeline only if it is not new - if timeline and not new: - return timeline - - # if get_any is True then return any timeline - if get_any: - timeline_count = project.GetTimelineCount() - if timeline_count == 0: - # if there is no timeline then create a new one - new = True - else: - return project.GetTimelineByIndex(1) - - # create new timeline if new is True - if new: - media_pool = project.GetMediaPool() - new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) - project.SetCurrentTimeline(new_timeline) - return new_timeline + media_pool = project.GetMediaPool() + new_timeline = media_pool.CreateEmptyTimeline(self.pype_timeline_name) + project.SetCurrentTimeline(new_timeline) + return new_timeline def create_bin(name: str, root: object = None) -> object: @@ -337,8 +351,13 @@ def get_current_timeline_items( track_type = track_type or "video" selecting_color = selecting_color or "Chocolate" project = get_current_project() - # make sure some timeline will be active with `any` argument - timeline = get_current_timeline(get_any=True) + + # get timeline anyhow + timeline = ( + get_current_timeline() or + get_any_timeline() or + get_new_timeline() + ) selected_clips = [] # get all tracks count filtered by track type diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 609cff60f7..e5846c2fc2 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -327,7 +327,10 @@ class ClipLoader: self.active_timeline = options["timeline"] else: # create new sequence - self.active_timeline = lib.get_current_timeline(new=True) + self.active_timeline = ( + lib.get_current_timeline() or + lib.get_new_timeline() + ) else: self.active_timeline = lib.get_current_timeline() From c61dd1b24775c6438e3ba5844f5159ef1349b66a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:18:23 +0200 Subject: [PATCH 714/918] utility scripts cosmetics only copy test and develop scripts if developer --- .../{__OpenPype__Menu__.py => OpenPype__Menu.py} | 0 openpype/hosts/resolve/utility_scripts/README.markdown | 1 - .../resolve/utility_scripts/{ => develop}/OTIO_export.py | 0 .../resolve/utility_scripts/{ => develop}/OTIO_import.py | 0 .../{ => develop}/OpenPype_sync_util_scripts.py | 0 openpype/hosts/resolve/utils.py | 9 ++++++++- 6 files changed, 8 insertions(+), 2 deletions(-) rename openpype/hosts/resolve/utility_scripts/{__OpenPype__Menu__.py => OpenPype__Menu.py} (100%) delete mode 100644 openpype/hosts/resolve/utility_scripts/README.markdown rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_export.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OTIO_import.py (100%) rename openpype/hosts/resolve/utility_scripts/{ => develop}/OpenPype_sync_util_scripts.py (100%) diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py rename to openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py diff --git a/openpype/hosts/resolve/utility_scripts/README.markdown b/openpype/hosts/resolve/utility_scripts/README.markdown deleted file mode 100644 index 8b13789179..0000000000 --- a/openpype/hosts/resolve/utility_scripts/README.markdown +++ /dev/null @@ -1 +0,0 @@ - diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_export.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_export.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_export.py diff --git a/openpype/hosts/resolve/utility_scripts/OTIO_import.py b/openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OTIO_import.py rename to openpype/hosts/resolve/utility_scripts/develop/OTIO_import.py diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py b/openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py similarity index 100% rename from openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py rename to openpype/hosts/resolve/utility_scripts/develop/OpenPype_sync_util_scripts.py diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 8e5dd9a188..9a161f4865 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -1,6 +1,6 @@ import os import shutil -from openpype.lib import Logger +from openpype.lib import Logger, is_running_from_build RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -41,6 +41,13 @@ def setup(env): # copy scripts into Resolve's utility scripts dir for directory, scripts in scripts.items(): for script in scripts: + if ( + is_running_from_build() and + script in ["tests", "develop"] + ): + # only copy those if started from build + continue + src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) log.info("Copying `{}` to `{}`...".format(src, dst)) From b47143b472eec6bb35ced0bbbb1d2b9a77c9acd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:41:36 +0200 Subject: [PATCH 715/918] collect frames to fix settings --- .../plugins/publish/collect_frames_fix.py | 21 ++++++++++++++----- .../defaults/project_settings/global.json | 8 ++++++- .../schemas/schema_global_publish.json | 20 ++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index bdd49585a5..837738eb06 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -26,11 +26,13 @@ class CollectFramesFixDef( targets = ["local"] hosts = ["nuke"] families = ["render", "prerender"] - enabled = True + + rewrite_version_enable = False def process(self, instance): attribute_values = self.get_attr_values_from_data(instance.data) frames_to_fix = attribute_values.get("frames_to_fix") + rewrite_version = attribute_values.get("rewrite_version") if frames_to_fix: @@ -71,10 +73,19 @@ class CollectFramesFixDef( @classmethod def get_attribute_defs(cls): - return [ + attributes = [ TextDef("frames_to_fix", label="Frames to fix", placeholder="5,10-15", - regex="[0-9,-]+"), - BoolDef("rewrite_version", label="Rewrite latest version", - default=False), + regex="[0-9,-]+") ] + + if cls.rewrite_version_enable: + attributes.append( + BoolDef( + "rewrite_version", + label="Rewrite latest version", + default=False + ) + ) + + return attributes diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 75f335f1de..002e547feb 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -46,6 +46,10 @@ "enabled": false, "families": [] }, + "CollectFramesFixDef": { + "enabled": true, + "rewrite_version_enable": true + }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -252,7 +256,9 @@ } }, { - "families": ["review"], + "families": [ + "review" + ], "hosts": [ "maya", "houdini" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index a7617918a3..8000e6156b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -81,6 +81,26 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFramesFixDef", + "label": "Collect Frames to Fix", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "rewrite_version_enable", + "label": "Rewrite latest version" + } + ] + }, { "type": "dict", "collapsible": true, From 31da5582fca78f9f84977700c69523e077df6e19 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 21:39:07 +0200 Subject: [PATCH 716/918] make understandable label in settings --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 8000e6156b..3164cfb62d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -97,7 +97,7 @@ { "type": "boolean", "key": "rewrite_version_enable", - "label": "Rewrite latest version" + "label": "Show 'Rewrite latest version' toggle" } ] }, From 682d8e6b0551935645724e0b632fb5736448979f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:28:45 +0800 Subject: [PATCH 717/918] custom script for setting frame range for read node --- openpype/hosts/nuke/api/lib.py | 30 ++++++ openpype/hosts/nuke/api/pipeline.py | 5 + openpype/widgets/custom_popup.py | 141 ++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 openpype/widgets/custom_popup.py diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a439142051..1aa0a95c86 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,6 +2358,36 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() + def reset_frame_range_read_nodes(self): + from openpype.widgets import custom_popup + parent = get_main_window() + dialog = custom_popup.CustomScriptDialog(parent=parent) + dialog.setWindowTitle("Frame Range for Read Node") + dialog.set_name("Frame Range: ") + dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), + nuke.root().lastFrame())) + frame = dialog.widgets["line_edit"] + selection = dialog.widgets["selection"] + dialog.on_clicked.connect( + lambda: set_frame_range(frame, selection) + ) + def set_frame_range(frame, selection): + frame_range = frame.text() + selected = selection.isChecked() + for read_node in nuke.allNodes("Read"): + if selected: + if not nuke.selectedNodes(): + return + if read_node in nuke.selectedNodes(): + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + else: + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + dialog.show() + + return False + def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d649ffae7f..8d6be76e48 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -282,6 +282,11 @@ def _install_menu(): lambda: WorkfileSettings().set_context_settings() ) + menu.addSeparator() + menu.addCommand( + "Set Frame Range(Read Node)", + lambda: WorkfileSettings().reset_frame_range_read_nodes() + ) menu.addSeparator() menu.addCommand( "Build Workfile", diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py new file mode 100644 index 0000000000..c4bb1ad43b --- /dev/null +++ b/openpype/widgets/custom_popup.py @@ -0,0 +1,141 @@ +import sys +import contextlib + +from PySide2 import QtCore, QtWidgets + + +class CustomScriptDialog(QtWidgets.QDialog): + """A Popup that moves itself to bottom right of screen on show event. + + The UI contains a message label and a red highlighted button to "show" + or perform another custom action from this pop-up. + + """ + + on_clicked = QtCore.Signal() + on_line_changed = QtCore.Signal(str) + + def __init__(self, parent=None, *args, **kwargs): + super(CustomScriptDialog, self).__init__(parent=parent, *args, **kwargs) + self.setContentsMargins(0, 0, 0, 0) + + # Layout + layout = QtWidgets.QVBoxLayout(self) + line_layout = QtWidgets.QHBoxLayout() + line_layout.setContentsMargins(10, 5, 10, 10) + selection_layout = QtWidgets.QHBoxLayout() + selection_layout.setContentsMargins(10, 5, 10, 10) + button_layout = QtWidgets.QHBoxLayout() + button_layout.setContentsMargins(10, 5, 10, 10) + + # Increase spacing slightly for readability + line_layout.setSpacing(10) + button_layout.setSpacing(8) + name = QtWidgets.QLabel("") + name.setStyleSheet(""" + QLabel { + font-size: 12px; + } + """) + line_edit = QtWidgets.QLineEdit("") + selection_name = QtWidgets.QLabel("Use Selection") + selection_name.setStyleSheet(""" + QLabel { + font-size: 12px; + } + """) + has_selection = QtWidgets.QCheckBox() + button = QtWidgets.QPushButton("Execute") + button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + cancel = QtWidgets.QPushButton("Cancel") + cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + + line_layout.addWidget(name) + line_layout.addWidget(line_edit) + selection_layout.addWidget(selection_name) + selection_layout.addWidget(has_selection) + button_layout.addWidget(button) + button_layout.addWidget(cancel) + layout.addLayout(line_layout) + layout.addLayout(selection_layout) + layout.addLayout(button_layout) + # Default size + self.resize(100, 30) + + self.widgets = { + "name": name, + "line_edit": line_edit, + "selection": has_selection, + "button": button, + "cancel": cancel + } + + # Signals + has_selection.toggled.connect(self.emit_click_with_state) + line_edit.textChanged.connect(self.on_line_edit_changed) + button.clicked.connect(self._on_clicked) + cancel.clicked.connect(self.close) + self.update_values() + # Set default title + self.setWindowTitle("Custom Popup") + + def update_values(self): + self.widgets["selection"].isChecked() + + def emit_click_with_state(self): + """Emit the on_clicked signal with the toggled state""" + checked = self.widgets["selection"].isChecked() + return checked + + def set_name(self, name): + self.widgets['name'].setText(name) + + def set_line_edit(self, line_edit): + self.widgets['line_edit'].setText(line_edit) + print(line_edit) + + def setButtonText(self, text): + self.widgets["button"].setText(text) + + def setCancelText(self, text): + self.widgets["cancel"].setText(text) + + def on_line_edit_changed(self): + line_edit = self.widgets['line_edit'].text() + self.on_line_changed.emit(line_edit) + return self.set_line_edit(line_edit) + + + def _on_clicked(self): + """Callback for when the 'show' button is clicked. + + Raises the parent (if any) + + """ + + parent = self.parent() + self.close() + + # Trigger the signal + self.on_clicked.emit() + + if parent: + parent.raise_() + + def showEvent(self, event): + + # Position popup based on contents on show event + return super(CustomScriptDialog, self).showEvent(event) + +@contextlib.contextmanager +def application(): + app = QtWidgets.QApplication(sys.argv) + yield + app.exec_() + +if __name__ == "__main__": + with application(): + dialog = CustomScriptDialog() + dialog.show() From 3ece2a8fcf73fa7d0da438ef056447fd7967c6ee Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:56:11 +0800 Subject: [PATCH 718/918] clean up & cosmetic fix --- openpype/hosts/nuke/api/lib.py | 5 ++++- openpype/widgets/custom_popup.py | 12 +++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 1aa0a95c86..94a0ff15ad 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2362,7 +2362,7 @@ class WorkfileSettings(object): from openpype.widgets import custom_popup parent = get_main_window() dialog = custom_popup.CustomScriptDialog(parent=parent) - dialog.setWindowTitle("Frame Range for Read Node") + dialog.setWindowTitle("Frame Range") dialog.set_name("Frame Range: ") dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), nuke.root().lastFrame())) @@ -2371,9 +2371,12 @@ class WorkfileSettings(object): dialog.on_clicked.connect( lambda: set_frame_range(frame, selection) ) + def set_frame_range(frame, selection): frame_range = frame.text() selected = selection.isChecked() + if not nuke.allNodes("Read"): + return for read_node in nuke.allNodes("Read"): if selected: if not nuke.selectedNodes(): diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py index c4bb1ad43b..85e31d6ce0 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/widgets/custom_popup.py @@ -16,7 +16,9 @@ class CustomScriptDialog(QtWidgets.QDialog): on_line_changed = QtCore.Signal(str) def __init__(self, parent=None, *args, **kwargs): - super(CustomScriptDialog, self).__init__(parent=parent, *args, **kwargs) + super(CustomScriptDialog, self).__init__(parent=parent, + *args, + **kwargs) self.setContentsMargins(0, 0, 0, 0) # Layout @@ -30,7 +32,7 @@ class CustomScriptDialog(QtWidgets.QDialog): # Increase spacing slightly for readability line_layout.setSpacing(10) - button_layout.setSpacing(8) + button_layout.setSpacing(10) name = QtWidgets.QLabel("") name.setStyleSheet(""" QLabel { @@ -47,7 +49,7 @@ class CustomScriptDialog(QtWidgets.QDialog): has_selection = QtWidgets.QCheckBox() button = QtWidgets.QPushButton("Execute") button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) + QtWidgets.QSizePolicy.Maximum) cancel = QtWidgets.QPushButton("Cancel") cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) @@ -62,7 +64,7 @@ class CustomScriptDialog(QtWidgets.QDialog): layout.addLayout(selection_layout) layout.addLayout(button_layout) # Default size - self.resize(100, 30) + self.resize(100, 40) self.widgets = { "name": name, @@ -107,7 +109,6 @@ class CustomScriptDialog(QtWidgets.QDialog): self.on_line_changed.emit(line_edit) return self.set_line_edit(line_edit) - def _on_clicked(self): """Callback for when the 'show' button is clicked. @@ -129,6 +130,7 @@ class CustomScriptDialog(QtWidgets.QDialog): # Position popup based on contents on show event return super(CustomScriptDialog, self).showEvent(event) + @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) From 54cccc6a6af7f4d3e04e47173b3e302dbc5d2aa0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:57:05 +0800 Subject: [PATCH 719/918] hound --- openpype/widgets/custom_popup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py index 85e31d6ce0..be4b0c32d5 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/widgets/custom_popup.py @@ -137,6 +137,7 @@ def application(): yield app.exec_() + if __name__ == "__main__": with application(): dialog = CustomScriptDialog() From 7e02416d30e4d33282a37b27331272701f276d17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 17:35:13 +0800 Subject: [PATCH 720/918] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 1073a0e19e..19c1048496 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -150,7 +150,7 @@ class RenderProducts(object): def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): """Get all the Arnold AOVs""" - aovs + aov_dict = {} amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -187,7 +187,7 @@ class RenderProducts(object): render_element = render_element.replace("\\", "/") render_dict.update({renderpass: render_element}) - return render_dirname + return render_dict def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa From 9f7f22961b6b30c256c7a60d8f16ea18058e1a62 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 10:43:54 +0100 Subject: [PATCH 721/918] Improved implementation of UMap to use UAsset base code --- .../unreal/plugins/create/create_uasset.py | 24 ++- .../unreal/plugins/create/create_umap.py | 46 ------ .../hosts/unreal/plugins/load/load_uasset.py | 28 ++-- .../hosts/unreal/plugins/load/load_umap.py | 140 ------------------ .../unreal/plugins/publish/extract_uasset.py | 15 +- .../unreal/plugins/publish/extract_umap.py | 48 ------ 6 files changed, 49 insertions(+), 252 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/create/create_umap.py delete mode 100644 openpype/hosts/unreal/plugins/load/load_umap.py delete mode 100644 openpype/hosts/unreal/plugins/publish/extract_umap.py diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index c78518e86b..f70ecc55b3 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -17,6 +17,8 @@ class CreateUAsset(UnrealAssetCreator): family = "uasset" icon = "cube" + extension = ".uasset" + def create(self, subset_name, instance_data, pre_create_data): if pre_create_data.get("use_selection"): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -37,10 +39,28 @@ class CreateUAsset(UnrealAssetCreator): f"{Path(obj).name} is not on the disk. Likely it needs to" "be saved first.") - if Path(sys_path).suffix != ".uasset": - raise CreatorError(f"{Path(sys_path).name} is not a UAsset.") + if Path(sys_path).suffix != self.extension: + raise CreatorError( + f"{Path(sys_path).name} is not a {self.label}.") super(CreateUAsset, self).create( subset_name, instance_data, pre_create_data) + + +class CreateUMap(CreateUAsset): + """Create Level.""" + + identifier = "io.ayon.creators.unreal.umap" + label = "Level" + family = "uasset" + extension = ".umap" + + def create(self, subset_name, instance_data, pre_create_data): + instance_data["families"] = ["umap"] + + super(CreateUMap, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/create/create_umap.py b/openpype/hosts/unreal/plugins/create/create_umap.py deleted file mode 100644 index 34aa8cdc00..0000000000 --- a/openpype/hosts/unreal/plugins/create/create_umap.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path - -import unreal - -from openpype.pipeline import CreatorError -from openpype.hosts.unreal.api.plugin import ( - UnrealAssetCreator, -) - - -class CreateUMap(UnrealAssetCreator): - """Create Level.""" - - identifier = "io.ayon.creators.unreal.umap" - label = "Level" - family = "uasset" - icon = "cube" - - def create(self, subset_name, instance_data, pre_create_data): - if pre_create_data.get("use_selection"): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - if len(selection) != 1: - raise CreatorError("Please select only one object.") - - obj = selection[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - - if not sys_path: - raise CreatorError( - f"{Path(obj).name} is not on the disk. Likely it needs to" - "be saved first.") - - if Path(sys_path).suffix != ".umap": - raise CreatorError(f"{Path(sys_path).name} is not a Level.") - - super(CreateUMap, self).create( - subset_name, - instance_data, - pre_create_data) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 7606bc14e4..44c87593e9 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -21,6 +21,8 @@ class UAssetLoader(plugin.Loader): icon = "cube" color = "orange" + extension = "uasset" + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -42,11 +44,7 @@ class UAssetLoader(plugin.Loader): root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) - + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( f"{root}/{asset}/{name}", suffix="" @@ -61,7 +59,7 @@ class UAssetLoader(plugin.Loader): Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") + shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -107,15 +105,15 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if not obj.get_class().get_name() == 'AyonAssetContainer': + if obj.get_class().get_name() != 'AyonAssetContainer': unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) - shutil.copy(update_filepath, f"{destination_path}/{name}.uasset") + shutil.copy( + update_filepath, f"{destination_path}/{name}.{self.extension}") - container_path = "{}/{}".format(container["namespace"], - container["objectName"]) + container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata unreal_pipeline.imprint( container_path, @@ -143,3 +141,13 @@ class UAssetLoader(plugin.Loader): if len(asset_content) == 0: unreal.EditorAssetLibrary.delete_directory(parent_path) + + +class UMapLoader(UAssetLoader): + """Load Level.""" + + families = ["uasset"] + label = "Load Level" + representations = ["umap"] + + extension = "umap" diff --git a/openpype/hosts/unreal/plugins/load/load_umap.py b/openpype/hosts/unreal/plugins/load/load_umap.py deleted file mode 100644 index f467fe6b3b..0000000000 --- a/openpype/hosts/unreal/plugins/load/load_umap.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -"""Load Level.""" -from pathlib import Path -import shutil - -from openpype.pipeline import ( - get_representation_path, - AYON_CONTAINER_ID -) -from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline -import unreal # noqa - - -class UMapLoader(plugin.Loader): - """Load Level.""" - - families = ["uasset"] - label = "Load Level" - representations = ["umap"] - icon = "cube" - color = "orange" - - def load(self, context, name, namespace, options): - """Load and containerise representation into Content Browser. - - Args: - context (dict): application context - name (str): subset name - namespace (str): in Unreal this is basically path to container. - This is not passed here, so namespace is set - by `containerise()` because only then we know - real path. - options (dict): Those would be data to be imprinted. This is not - used now, data are imprinted by `containerise()`. - - Returns: - list(str): list of container content - """ - - # Create directory for asset and Ayon container - root = "/Game/Ayon/Assets" - asset = context.get('asset').get('name') - suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - tools = unreal.AssetToolsHelpers().get_asset_tools() - asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}", suffix="" - ) - - container_name += suffix - - unreal.EditorAssetLibrary.make_directory(asset_dir) - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - shutil.copy(self.fname, f"{destination_path}/{name}.uasset") - - # Create Asset Container - unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - data = { - "schema": "ayon:container-2.0", - "id": AYON_CONTAINER_ID, - "asset": asset, - "namespace": asset_dir, - "container_name": container_name, - "asset_name": asset_name, - "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] - } - unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - return asset_content - - def update(self, container, representation): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - asset_dir = container["namespace"] - name = representation["context"]["subset"] - - destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=False, include_folder=True - ) - - for asset in asset_content: - obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': - unreal.EditorAssetLibrary.delete_asset(asset) - - update_filepath = get_representation_path(representation) - - shutil.copy(update_filepath, f"{destination_path}/{name}.umap") - - container_path = f'{container["namespace"]}/{container["objectName"]}' - # update metadata - unreal_pipeline.imprint( - container_path, - { - "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) - - asset_content = unreal.EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True - ) - - for a in asset_content: - unreal.EditorAssetLibrary.save_asset(a) - - def remove(self, container): - path = container["namespace"] - parent_path = Path(path).parent.as_posix() - - unreal.EditorAssetLibrary.delete_directory(path) - - asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False - ) - - if len(asset_content) == 0: - unreal.EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index f719df2a82..48b62faa97 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -11,16 +11,17 @@ class ExtractUAsset(publish.Extractor): label = "Extract UAsset" hosts = ["unreal"] - families = ["uasset"] + families = ["uasset", "umap"] optional = True def process(self, instance): + extension = ( + "umap" if "umap" in instance.data.get("families") else "uasset") ar = unreal.AssetRegistryHelpers.get_asset_registry() self.log.info("Performing extraction..") - staging_dir = self.staging_dir(instance) - filename = "{}.uasset".format(instance.name) + filename = f"{instance.name}.{extension}" members = instance.data.get("members", []) @@ -36,13 +37,15 @@ class ExtractUAsset(publish.Extractor): shutil.copy(sys_path, staging_dir) + self.log.info(f"instance.data: {instance.data}") + if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'uasset', - 'ext': 'uasset', - 'files': filename, + "name": extension, + "ext": extension, + "files": filename, "stagingDir": staging_dir, } instance.data["representations"].append(representation) diff --git a/openpype/hosts/unreal/plugins/publish/extract_umap.py b/openpype/hosts/unreal/plugins/publish/extract_umap.py deleted file mode 100644 index 3812834430..0000000000 --- a/openpype/hosts/unreal/plugins/publish/extract_umap.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path -import shutil - -import unreal - -from openpype.pipeline import publish - - -class ExtractUMap(publish.Extractor): - """Extract a UMap.""" - - label = "Extract Level" - hosts = ["unreal"] - families = ["uasset"] - optional = True - - def process(self, instance): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - self.log.info("Performing extraction..") - - staging_dir = self.staging_dir(instance) - filename = f"{instance.name}.umap" - - members = instance.data.get("members", []) - - if not members: - raise RuntimeError("No members found in instance.") - - # UAsset publishing supports only one member - obj = members[0] - - asset = ar.get_asset_by_object_path(obj).get_asset() - sys_path = unreal.SystemLibrary.get_system_path(asset) - filename = Path(sys_path).name - - shutil.copy(sys_path, staging_dir) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'umap', - 'ext': 'umap', - 'files': filename, - "stagingDir": staging_dir, - } - instance.data["representations"].append(representation) From 5f98c278361d9354565261174e51b43b5acaffa9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 12:00:09 +0200 Subject: [PATCH 722/918] apply settings on publish plugins can expect only project settings (#5037) --- openpype/pipeline/publish/lib.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index e87b865dce..f228709b3b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -12,7 +12,8 @@ import pyblish.api from openpype.lib import ( Logger, import_filepath, - filter_profiles + filter_profiles, + is_func_signature_supported, ) from openpype.settings import ( get_project_settings, @@ -498,12 +499,26 @@ def filter_pyblish_plugins(plugins): # iterate over plugins for plugin in plugins[:]: # Apply settings to plugins - if hasattr(plugin, "apply_settings"): + + apply_settings_func = getattr(plugin, "apply_settings", None) + if apply_settings_func is not None: # Use classmethod 'apply_settings' # - can be used to target settings from custom settings place # - skip default behavior when successful try: - plugin.apply_settings(project_settings, system_settings) + # Support to pass only project settings + # - make sure that both settings are passed, when can be + # - that covers cases when *args are in method parameters + both_supported = is_func_signature_supported( + apply_settings_func, project_settings, system_settings + ) + project_supported = is_func_signature_supported( + apply_settings_func, project_settings + ) + if not both_supported and project_supported: + plugin.apply_settings(project_settings) + else: + plugin.apply_settings(project_settings, system_settings) except Exception: log.warning( From e633cc7decbcfb8642f7d66ad17fe4219805e834 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 18:19:50 +0800 Subject: [PATCH 723/918] expected file can get the aov path --- openpype/hosts/max/api/lib_renderproducts.py | 4 +++- .../max/plugins/publish/collect_render.py | 19 ++----------------- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 19c1048496..ba1ffc3a5e 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,7 +70,7 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aov(self): + def get_aovs(self): folder = rt.maxFilePath folder = folder.replace("\\", "/") setting = self._project_settings @@ -177,6 +177,8 @@ class RenderProducts(object): render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 1: + return # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 652c2e1d2c..c4a44a5b11 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -45,23 +45,8 @@ class CollectRender(pyblish.api.InstancePlugin): } folder = folder.replace("\\", "/") - if aov_list: - if renderer in [ - "ART_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - "Redshift_Renderer", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - - render_element = RenderProducts().get_aov() - files_by_aov.update(render_element) - self.log.debug(files_by_aov) - - if renderer == "Arnold": - aovs = RenderProducts().get_aovs() - files_by_aov.update(aovs) + aovs = RenderProducts().get_aovs() + files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 905c3dbd249bce6c7d229e10466b3035aa2a6d40 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 26 May 2023 12:01:36 +0100 Subject: [PATCH 724/918] Fix problem when trying to load the same level multiple times --- .../hosts/unreal/plugins/load/load_uasset.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 44c87593e9..30f63abe39 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -50,16 +50,23 @@ class UAssetLoader(plugin.Loader): f"{root}/{asset}/{name}", suffix="" ) - container_name += suffix + unique_number = 1 + while unreal.EditorAssetLibrary.does_directory_exist( + f"{asset_dir}_{unique_number:02}" + ): + unique_number += 1 + + asset_dir = f"{asset_dir}_{unique_number:02}" + container_name = f"{container_name}_{unique_number:02}{suffix}" unreal.EditorAssetLibrary.make_directory(asset_dir) destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, f"{destination_path}/{name}.{self.extension}") + shutil.copy( + self.fname, + f"{destination_path}/{name}_{unique_number:02}.{self.extension}") # Create Asset Container unreal_pipeline.create_container( @@ -75,7 +82,7 @@ class UAssetLoader(plugin.Loader): "loader": str(self.__class__.__name__), "representation": context["representation"]["_id"], "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "family": context["representation"]["context"]["family"], } unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) @@ -94,10 +101,10 @@ class UAssetLoader(plugin.Loader): asset_dir = container["namespace"] name = representation["context"]["subset"] + unique_number = container["container_name"].split("_")[-2] + destination_path = asset_dir.replace( - "/Game", - Path(unreal.Paths.project_content_dir()).as_posix(), - 1) + "/Game", Path(unreal.Paths.project_content_dir()).as_posix(), 1) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=False, include_folder=True @@ -105,13 +112,14 @@ class UAssetLoader(plugin.Loader): for asset in asset_content: obj = ar.get_asset_by_object_path(asset).get_asset() - if obj.get_class().get_name() != 'AyonAssetContainer': + if obj.get_class().get_name() != "AyonAssetContainer": unreal.EditorAssetLibrary.delete_asset(asset) update_filepath = get_representation_path(representation) shutil.copy( - update_filepath, f"{destination_path}/{name}.{self.extension}") + update_filepath, + f"{destination_path}/{name}_{unique_number}.{self.extension}") container_path = f'{container["namespace"]}/{container["objectName"]}' # update metadata @@ -119,8 +127,9 @@ class UAssetLoader(plugin.Loader): container_path, { "representation": str(representation["_id"]), - "parent": str(representation["parent"]) - }) + "parent": str(representation["parent"]), + } + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True From 2d3ba2af0576d5a201fafa0a0957e18d0aa9d00a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:36:14 +0800 Subject: [PATCH 725/918] add _beauty to subset name --- openpype/hosts/max/api/lib_renderproducts.py | 82 +++++++++++++------ .../max/plugins/publish/collect_render.py | 8 +- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index ba1ffc3a5e..a93a1d821d 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,16 +70,26 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aovs(self): + def get_aovs(self, container): folder = rt.maxFilePath + file = rt.maxFileName folder = folder.replace("\\", "/") setting = self._project_settings + render_folder = get_default_render_folder(setting) + filename, ext = os.path.splitext(file) + + output_file = os.path.join(folder, + render_folder, + filename, + container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] + render_dict = {} if renderer in [ "ART_Renderer", "Redshift_Renderer", @@ -88,12 +98,22 @@ class RenderProducts(object): "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: - render_dict = self.get_render_elements_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": - render_dict = self.get_arnold_product_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_arnold_product_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_arnold_product( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict @@ -148,9 +168,9 @@ class RenderProducts(object): return render_dirname - def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aov_dict = {} + def get_arnold_product_name(self): + """Get all the Arnold AOVs name""" + aov_name = [] amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -161,20 +181,27 @@ class RenderProducts(object): for i in range(aov_group_num): # get the specific AOV group for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aov = str(aov.name) - aov_dict.update({aov: render_element}) + aov_name.append(aov.name) + # close the AOVs manager window amw.close() - return aov_dict + return aov_name - def get_render_elements_name(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dict = {} + def get_expected_arnold_product(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected Arnold AOVs""" + aov_list = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + aov_list.append(render_element) + return aov_list + + def get_render_elements_name(self): + """Get all the render element names. """ + render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 1: @@ -182,14 +209,21 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dict.update({renderpass: render_element}) + if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + target, renderpass = str(renderlayer_name).split(":") + render_name.append(renderpass) + return render_name - return render_dict + def get_expected_render_elements(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected render element output files. """ + render_elements = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + render_elements.append(render_element) + + return render_elements def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c4a44a5b11..1282c9b3fe 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -41,11 +41,11 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "_": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs() + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: @@ -78,8 +78,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] data = { - "subset": instance.name, "asset": asset, + "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), "imageFormat": img_format, @@ -95,5 +95,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - files = instance.data["expectedFiles"] - self.log.debug("expectedFiles: {0}".format(files)) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) From 4303b281aab9c157d109e034c68d5d8816fd4450 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:38:05 +0800 Subject: [PATCH 726/918] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/publish/collect_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a93a1d821d..b33d0c5751 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -103,7 +103,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) if renderer == "Arnold": @@ -112,7 +112,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 1282c9b3fe..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version, get_current_renderer +from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -29,8 +29,6 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] beauty_list, aov_list = RenderProducts().render_product(instance.name) full_render_list = list() if aov_list: diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 6843ae85321ae617dfea564086c5d58fa34f7daf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 26 May 2023 14:44:47 +0200 Subject: [PATCH 727/918] General: Small code cleanups (#5034) * make sure the message type is set and unset correctly * Update dummy data in readme * remove debug message from main thread callbacks * removed unused import * cleanup code in muster addon * simplified 'get_publish_instance_label' function * even better json file handling Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/modules/ftrack/tray/login_dialog.py | 2 -- openpype/modules/muster/muster.py | 14 +++++-------- openpype/pipeline/publish/lib.py | 21 ++++++-------------- openpype/tools/utils/lib.py | 1 - openpype/tools/utils/overlay_messages.py | 3 +-- tests/README.md | 18 ++++++++--------- 6 files changed, 20 insertions(+), 39 deletions(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index f374a71178..a8abdaf191 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -1,5 +1,3 @@ -import os - import requests from qtpy import QtCore, QtGui, QtWidgets diff --git a/openpype/modules/muster/muster.py b/openpype/modules/muster/muster.py index 77b9214a5a..0cdb1230c8 100644 --- a/openpype/modules/muster/muster.py +++ b/openpype/modules/muster/muster.py @@ -1,7 +1,9 @@ import os import json + import appdirs import requests + from openpype.modules import OpenPypeModule, ITrayModule @@ -110,16 +112,10 @@ class MusterModule(OpenPypeModule, ITrayModule): self.save_credentials(token) def save_credentials(self, token): - """ - Save credentials to JSON file - """ - data = { - 'token': token - } + """Save credentials to JSON file.""" - file = open(self.cred_path, 'w') - file.write(json.dumps(data)) - file.close() + with open(self.cred_path, "w") as f: + json.dump({'token': token}, f) def show_login(self): """ diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index f228709b3b..471be5ddb8 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -31,8 +31,6 @@ from .contants import ( TRANSIENT_DIR_TEMPLATE ) -_ARG_PLACEHOLDER = object() - def get_template_name_profiles( project_name, project_settings=None, logger=None @@ -885,31 +883,24 @@ def add_repre_files_for_cleanup(instance, repre): instance.context.data["cleanupFullPaths"].append(expected_file) -def get_publish_instance_label(instance, default=_ARG_PLACEHOLDER): +def get_publish_instance_label(instance): """Try to get label from pyblish instance. - First are checked 'label' and 'name' keys in instance data. If are not set - a default value is returned. Instance object is converted to string - if default value is not specific. + First are used values in instance data under 'label' and 'name' keys. Then + is used string conversion of instance object -> 'instance._name'. Todos: Maybe 'subset' key could be used too. Args: instance (pyblish.api.Instance): Pyblish instance. - default (Optional[Any]): Default value to return if any Returns: - Union[Any]: Instance label or default label. + str: Instance label. """ - label = ( + return ( instance.data.get("label") or instance.data.get("name") + or str(instance) ) - if label: - return label - - if default is _ARG_PLACEHOLDER: - return str(instance) - return default diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 950c782727..58ece7c68f 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -872,7 +872,6 @@ class WrappedCallbackItem: self.log.warning("- item is already processed") return - self.log.debug("Running callback: {}".format(str(self._callback))) try: result = self._callback(*self._args, **self._kwargs) self._result = result diff --git a/openpype/tools/utils/overlay_messages.py b/openpype/tools/utils/overlay_messages.py index 180d7eae97..4da266bcf7 100644 --- a/openpype/tools/utils/overlay_messages.py +++ b/openpype/tools/utils/overlay_messages.py @@ -127,8 +127,7 @@ class OverlayMessageWidget(QtWidgets.QFrame): if timeout: self._timeout_timer.setInterval(timeout) - if message_type: - set_style_property(self, "type", message_type) + set_style_property(self, "type", message_type) self._timeout_timer.start() diff --git a/tests/README.md b/tests/README.md index d36b6534f8..20847b2449 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,16 +15,16 @@ Structure: - openpype/modules/MODULE_NAME - structure follow directory structure in code base - fixture - sample data `(MongoDB dumps, test files etc.)` - `tests.py` - single or more pytest files for MODULE_NAME -- unit - quick unit test - - MODULE_NAME +- unit - quick unit test + - MODULE_NAME - fixture - `tests.py` - + How to run: ---------- - use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!) -- `python ${OPENPYPE_ROOT}/start.py runtests` - + By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. @@ -41,17 +41,15 @@ In some cases your tests might be so localized, that you don't care about all en In that case you might add this dummy configuration BEFORE any imports in your test file ``` import os -os.environ["AVALON_MONGO"] = "mongodb://localhost:27017" +os.environ["OPENPYPE_DEBUG"] = "1" os.environ["OPENPYPE_MONGO"] = "mongodb://localhost:27017" -os.environ["AVALON_DB"] = "avalon" os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" -os.environ["AVALON_TIMEOUT"] = '3000' -os.environ["OPENPYPE_DEBUG"] = "3" -os.environ["AVALON_CONFIG"] = "pype" +os.environ["AVALON_DB"] = "avalon" +os.environ["AVALON_TIMEOUT"] = "3000" os.environ["AVALON_ASSET"] = "Asset" os.environ["AVALON_PROJECT"] = "test_project" ``` (AVALON_ASSET and AVALON_PROJECT values should exist in your environment) This might be enough to run your test file separately. Do not commit this skeleton though. -Use only when you know what you are doing! \ No newline at end of file +Use only when you know what you are doing! From c14525f371aa8b8b2a524022f860cede764f7d0d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 23:13:17 +0800 Subject: [PATCH 728/918] fix the wrong directory for rendering --- .../max/plugins/publish/collect_render.py | 2 +- .../deadline/abstract_submit_deadline.py | 2 +- .../plugins/publish/submit_max_deadline.py | 40 +++++++++++-------- .../plugins/publish/submit_publish_job.py | 6 ++- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..c8e407bbe4 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "beauty": beauty_list + "max_beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 558a637e4b..6694f638d6 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - + self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c678c0fb6e..d2de9160fb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -132,8 +132,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- - files = instance.data.get("files") - for filepath in files: + exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -162,10 +162,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance filepath = self.scene_path - files = instance.data["files"] + files = instance.data["expectedFiles"] if not files: raise RuntimeError("No Render Elements found!") - output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" @@ -202,17 +203,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.scene_path - - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(filepath) - orig_scene = _clean_name(instance.context.data["currentFile"]) - - output_beauty = output_beauty.replace(orig_scene, new_scene) - output_beauty = output_beauty.replace("\\", "/") - plugin_data["RenderOutput"] = output_beauty + files = instance.data["expectedFiles"] + first_file = next(self._iter_expected_files(files)) + rgb_bname = os.path.basename(output_beauty) + dir = os.path.dirname(first_file) + plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] @@ -226,14 +221,25 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ]: render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): - element = element.replace(orig_scene, new_scene) - plugin_data["RenderElementOutputFilename%d" % i] = element # noqa + elem_bname = os.path.basename(element) + new_elem = f"{dir}/{elem_bname}" + plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file + @classmethod def get_attribute_defs(cls): defs = super(MaxSubmitDeadline, cls).get_attribute_defs() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..fb0608908f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) @@ -488,11 +488,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) + if aov == "max_beauty": + subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) + if aov == "max_beauty": + subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 32562e0b39500996bc25ad2173d401b83d60a48f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:53:40 +0800 Subject: [PATCH 729/918] give beauty name to RGB --- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c8e407bbe4..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "max_beauty": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fb0608908f..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -488,15 +488,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) - if aov == "max_beauty": - subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) - if aov == "max_beauty": - subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 271d017bdb368b6cbfdb95087df6da957da43f4a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:56:11 +0800 Subject: [PATCH 730/918] remove the print function, and set verify to true for payload in publishing job --- openpype/modules/deadline/abstract_submit_deadline.py | 1 - openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 6694f638d6..7938c27233 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From b7b8125d70406ef867af53f8a8afe2640e657058 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 26 May 2023 23:23:06 +0200 Subject: [PATCH 731/918] Use .scriptlib for Resolve startup launch script entry point --- .../hooks/pre_resolve_launch_last_workfile.py | 35 ++++++++++++++++ openpype/hosts/resolve/startup.py | 40 +++++++++++++++++++ .../openpype_startup.scriptlib | 22 ++++++++++ openpype/hosts/resolve/utils.py | 8 ++++ 4 files changed, 105 insertions(+) create mode 100644 openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py create mode 100644 openpype/hosts/resolve/startup.py create mode 100644 openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py new file mode 100644 index 0000000000..6db3cc28b2 --- /dev/null +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -0,0 +1,35 @@ +import os + +from openpype.lib import PreLaunchHook + + +class ResolveLaunchLastWorkfile(PreLaunchHook): + """Special hook to open last workfile for Resolve. + + Checks 'start_last_workfile', if set to False, it will not open last + workfile. This property is set explicitly in Launcher. + """ + + # Execute after workfile template copy + order = 10 + app_groups = ["resolve"] + + def execute(self): + if not self.data.get("start_last_workfile"): + self.log.info("It is set to not start last workfile on start.") + return + + last_workfile = self.data.get("last_workfile_path") + if not last_workfile: + self.log.warning("Last workfile was not collected.") + return + + if not os.path.exists(last_workfile): + self.log.info("Current context does not have any workfile yet.") + return + + # Add path to launch environment for the startup script to pick up + self.log.info(f"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch " + f"last workfile: {last_workfile}") + key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" + self.launch_context.env[key] = last_workfile diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py new file mode 100644 index 0000000000..4aeb106ef1 --- /dev/null +++ b/openpype/hosts/resolve/startup.py @@ -0,0 +1,40 @@ +import os + +# Importing this takes a little over a second and thus this means +# that we have about 1.5 seconds delay before the workfile will actually +# be opened at the minimum +import openpype.hosts.resolve.api + + +def launch_menu(): + from openpype.pipeline import install_host + print("Launching Resolve OpenPype menu..") + + # Activate resolve from openpype + install_host(openpype.hosts.resolve.api) + + openpype.hosts.resolve.api.launch_pype_menu() + + +def open_file(path): + # Avoid the need to "install" the host + openpype.hosts.resolve.api.bmdvr = resolve # noqa + openpype.hosts.resolve.api.bmdvf = fusion # noqa + openpype.hosts.resolve.api.open_file(path) + + +def main(): + # Open last workfile + workfile_path = os.environ.get("OPENPYPE_RESOLVE_OPEN_ON_LAUNCH") + if workfile_path: + open_file(workfile_path) + else: + print("No last workfile set to open. Skipping..") + + # Launch OpenPype menu + # TODO: Add a setting to enable/disable this + launch_menu() + + +if __name__ == "__main__": + main() diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib new file mode 100644 index 0000000000..9fca666d78 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -0,0 +1,22 @@ +-- Run OpenPype's Python launch script for resolve +function file_exists(name) + local f = io.open(name, "r") + return f ~= nil and io.close(f) +end + + +openpype_root = os.getenv("OPENPYPE_ROOT") +if openpype_root ~= nil then + script = openpype_root .. "/openpype/hosts/resolve/startup.py" + script = fusion:MapPath(script) + + if file_exists(script) then + -- We must use RunScript to ensure it runs in a separate + -- process to Resolve itself to avoid a deadlock for + -- certain imports of OpenPype libraries or Qt + print("Running launch script: " .. script) + fusion:RunScript(script) + else + print("Launch script not found at: " .. script) + end +end \ No newline at end of file diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 9a161f4865..e2c8c4a05e 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -50,6 +50,14 @@ def setup(env): src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) + + # TODO: Make this a less hacky workaround + if script == "openpype_startup.scriptlib": + # Handle special case for scriptlib that needs to be a folder + # up from the Comp folder in the Fusion scripts + dst = os.path.join(os.path.dirname(util_scripts_dir), + script) + log.info("Copying `{}` to `{}`...".format(src, dst)) if os.path.isdir(src): shutil.copytree( From 00e89719dd75f465411e120fbb2c37dd8e495b7a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 26 May 2023 23:23:33 +0200 Subject: [PATCH 732/918] Do not prompt save project when not in a project (e.g. on Resolve launch) --- openpype/hosts/resolve/api/workio.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 5ce73eea53..5966fa6a43 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -43,18 +43,22 @@ def open_file(filepath): """ Loading project """ + + from . import bmdvr + pm = get_project_manager() + page = bmdvr.GetCurrentPage() + if page is not None: + # Save current project only if Resolve has an active page, otherwise + # we consider Resolve being in a pre-launch state (no open UI yet) + project = pm.GetCurrentProject() + print(f"Saving current project: {project}") + pm.SaveProject() + file = os.path.basename(filepath) fname, _ = os.path.splitext(file) dname, _ = fname.split("_v") - - # deal with current project - project = pm.GetCurrentProject() - log.info(f"Test `pm`: {pm}") - pm.SaveProject() - try: - log.info(f"Test `dname`: {dname}") if not set_project_manager_to_folder_name(dname): raise # load project from input path @@ -72,6 +76,7 @@ def open_file(filepath): return False return True + def current_file(): pm = get_project_manager() current_dir = os.getenv("AVALON_WORKDIR") From eb9d8942460f3640c9aeabd63e8fdd45d2e2e955 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 27 May 2023 03:25:05 +0000 Subject: [PATCH 733/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 342bbfc85a..c24388b2ff 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.8" +__version__ = "3.15.9-nightly.1" From f8cb017e90490b80fb6f6470db685090a23e7211 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 27 May 2023 03:25:45 +0000 Subject: [PATCH 734/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4d7d06a2c8..54a4ee6ac0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 - 3.15.8-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.2-nightly.5 - 3.14.2-nightly.4 - 3.14.2-nightly.3 - - 3.14.2-nightly.2 validations: required: true - type: dropdown From 56642ac17572ca5c7c12bd97e5e717ce801518f0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:37:25 +0800 Subject: [PATCH 735/918] getting the filename from render settings and add save_scene before all the extractors running --- openpype/hosts/max/api/lib_renderproducts.py | 159 ++++++------------ .../hosts/max/plugins/create/create_render.py | 4 + .../max/plugins/publish/collect_render.py | 26 +-- .../hosts/max/plugins/publish/save_scene.py | 26 +++ .../plugins/publish/submit_max_deadline.py | 81 ++++++++- 5 files changed, 171 insertions(+), 125 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/save_scene.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index b33d0c5751..30c3c71cce 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -5,10 +5,8 @@ import os from pymxs import runtime as rt from openpype.hosts.max.api.lib import ( - get_current_renderer, - get_default_render_folder + get_current_renderer ) -from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_project_settings from openpype.pipeline import legacy_io @@ -22,66 +20,30 @@ class RenderProducts(object): legacy_io.Session["AVALON_PROJECT"] ) - def render_product(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + def get_beauty(self, container): + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) - # TODO: change the frame range follows the current render setting + + setting = self._project_settings + img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa + startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - rgba_render_list = self.beauty_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - - render_elem_list = None - - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = self.render_elements_product(output_file, - startFrame, - endFrame, - img_fmt) - - if renderer == "Arnold": - render_elem_list = self.arnold_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - return rgba_render_list, render_elem_list + render_dict = { + "beauty": self.get_expected_beauty( + output_file, startFrame, endFrame, img_fmt) + } + return render_dict def get_aovs(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa @@ -90,9 +52,9 @@ class RenderProducts(object): renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} + if renderer in [ "ART_Renderer", - "Redshift_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", @@ -105,6 +67,23 @@ class RenderProducts(object): name: self.get_expected_render_elements( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer == "Redshift_Renderer": + render_name = self.get_render_elements_name() + if render_name: + rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + if rs_AovFiles != True and img_fmt == "exr": + for name in render_name: + if name == "RsCryptomatte": + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) + else: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": render_name = self.get_arnold_product_name() @@ -114,60 +93,31 @@ class RenderProducts(object): name: self.get_expected_arnold_product( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer in [ + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3" + ]: + if img_fmt !="exr": + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict - def beauty_render_product(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, startFrame, endFrame, fmt): beauty_frame_range = [] for f in range(startFrame, endFrame): - beauty_output = f"{folder}.{f}.{fmt}" + frame = "%04d" % f + beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") beauty_frame_range.append(beauty_output) return beauty_frame_range - # TODO: Get the arnold render product - def arnold_render_product(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aovs = [] - - amw = rt.MaxtoAOps.AOVsManagerWindow() - aov_mgr = rt.renderers.current.AOVManager - # Check if there is any aov group set in AOV manager - aov_group_num = len(aov_mgr.drivers) - if aov_group_num < 1: - return - for i in range(aov_group_num): - # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aovs.append(render_element) - - # close the AOVs manager window - amw.close() - - return aovs - - def render_elements_product(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dirname = [] - - render_elem = rt.maxOps.GetCurRenderElementMgr() - render_elem_num = render_elem.NumRenderElements() - # get render elements from the renders - for i in range(render_elem_num): - renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dirname.append(render_element) - - return render_dirname - def get_arnold_product_name(self): """Get all the Arnold AOVs name""" aov_name = [] @@ -193,14 +143,15 @@ class RenderProducts(object): """Get all the expected Arnold AOVs""" aov_list = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") aov_list.append(render_element) return aov_list def get_render_elements_name(self): - """Get all the render element names. """ + """Get all the render element names for general """ render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() @@ -209,9 +160,10 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + if renderlayer_name.enabled: target, renderpass = str(renderlayer_name).split(":") render_name.append(renderpass) + return render_name def get_expected_render_elements(self, folder, name, @@ -219,7 +171,8 @@ class RenderProducts(object): """Get all the expected render element output files. """ render_elements = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") render_elements.append(render_element) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 68ae5eac72..78e9527bdf 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" +import os from openpype.hosts.max.api import plugin from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -14,6 +15,9 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt sel_obj = list(rt.selection) + file = rt.maxFileName + filename, _ = os.path.splitext(file) + instance_data["AssetName"] = filename instance = super(CreateRender, self).create( subset_name, instance_data, diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..5b3f99b2d0 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -29,19 +29,7 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - beauty_list, aov_list = RenderProducts().render_product(instance.name) - full_render_list = list() - if aov_list: - full_render_list.extend(iter(beauty_list)) - full_render_list.extend(iter(aov_list)) - - else: - full_render_list = beauty_list - - files_by_aov = { - "beauty": beauty_list - } - + files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) @@ -67,14 +55,14 @@ class CollectRender(pyblish.api.InstancePlugin): # OCIO config not support in # most of the 3dsmax renderers # so this is currently hard coded - setting = instance.context.data["project_settings"] - image_io = setting["global"]["imageio"] - instance.data["colorspaceConfig"] = image_io["ocio_config"]["filepath"][0] # noqa + # TODO: add options for redshift/vray ocio config + instance.data["colorspaceConfig"] = "" instance.data["colorspaceDisplay"] = "sRGB" - instance.data["colorspaceView"] = "ACES 1.0" + instance.data["colorspaceView"] = "ACES 1.0 SDR-video" instance.data["renderProducts"] = colorspace.ARenderProduct() + instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] - + # also need to get the render dir for coversion data = { "asset": asset, "subset": str(instance.name), @@ -84,7 +72,6 @@ class CollectRender(pyblish.api.InstancePlugin): "family": 'maxrender', "families": ['maxrender'], "source": filepath, - "files": full_render_list, "plugin": "3dsmax", "frameStart": int(rt.rendStart), "frameEnd": int(rt.rendEnd), @@ -93,3 +80,4 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) + self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py new file mode 100644 index 0000000000..93d97a3de5 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -0,0 +1,26 @@ +import pyblish.api +import os + + +class SaveCurrentScene(pyblish.api.ContextPlugin): + """Save current scene + + """ + + label = "Save current file" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["max"] + families = ["maxrender", "workfile"] + + def process(self, context): + from pymxs import runtime as rt + folder = rt.maxFilePath + file = rt.maxFileName + current = os.path.join(folder, file) + assert context.data["currentFile"] == current + + if rt.checkForSave(): + self.log.debug("Skipping file save as there " + "are no modifications..") + return + rt.saveMaxFile(current) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d2de9160fb..15aa521f43 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -78,7 +78,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.BatchName = src_filename job_info.Plugin = instance.data["plugin"] job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - + job_info.EnableAutoTimeout = True # Deadline requires integers in frame range frames = "{start}-{end}".format( start=int(instance.data["frameStart"]), @@ -207,9 +207,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" - + beauty_name = f"{dir}/{rgb_bname}" + beauty_name = beauty_name.replace("\\", "/") + plugin_data["RenderOutput"] = beauty_name + # as 3dsmax has version with different languages + plugin_data["Language"] = "ENU" renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] if renderer in [ "ART_Renderer", @@ -223,13 +227,84 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, for i, element in enumerate(render_elem_list): elem_bname = os.path.basename(element) new_elem = f"{dir}/{elem_bname}" + new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + def from_published_scene(self, replace_in_path=True): + instance = self._instance + workfile_instance = self._get_workfile_instance(instance.context) + if workfile_instance is None: + return + + # determine published path from Anatomy. + template_data = workfile_instance.data.get("anatomyData") + rep = workfile_instance.data["representations"][0] + template_data["representation"] = rep.get("name") + template_data["ext"] = rep.get("ext") + template_data["comment"] = None + + anatomy = instance.context.data['anatomy'] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) + file_path = os.path.normpath(template_filled) + + self.log.info("Using published scene for render {}".format(file_path)) + + if not os.path.exists(file_path): + self.log.error("published scene does not exist!") + raise + + if not replace_in_path: + return file_path + + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + def _clean_name(path): + return os.path.splitext(os.path.basename(path))[0] + + new_scene = _clean_name(file_path) + orig_scene = _clean_name(instance.data["AssetName"]) + expected_files = instance.data.get("expectedFiles") + + if isinstance(expected_files[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in expected_files[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( + str(f).replace(orig_scene, new_scene) + ) + new_exp[aov] = replaced_files + # [] might be too much here, TODO + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in expected_files: + new_exp.append( + str(f).replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = new_exp + + metadata_folder = instance.data.get("publishRenderMetadataFolder") + if metadata_folder: + metadata_folder = metadata_folder.replace(orig_scene, + new_scene) + instance.data["publishRenderMetadataFolder"] = metadata_folder + + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + + return file_path + @staticmethod def _iter_expected_files(exp): if isinstance(exp[0], dict): From f85051863ccfbd2f54e0f9198c1b3123f08391b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:44:22 +0800 Subject: [PATCH 736/918] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 15 +++++++++------ .../hosts/max/plugins/publish/collect_render.py | 1 - .../plugins/publish/submit_max_deadline.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 30c3c71cce..68090dfefd 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -65,7 +65,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() @@ -76,13 +77,15 @@ class RenderProducts(object): if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Arnold": @@ -95,15 +98,15 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - ]: + "V_Ray_GPU_6_Hotfix_3"]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) # noqa }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 5b3f99b2d0..9137f8c854 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -80,4 +80,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 15aa521f43..4682cc4487 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -207,7 +207,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" + beauty_name = f"{dir}/{rgb_bname}" beauty_name = beauty_name.replace("\\", "/") plugin_data["RenderOutput"] = beauty_name # as 3dsmax has version with different languages From b89bb229a0e7bf4c8d8308776d3d288a65bdd387 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:48:09 +0800 Subject: [PATCH 737/918] hound --- openpype/hosts/max/api/lib_renderproducts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 68090dfefd..21c41446a9 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,7 +72,7 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles != True and img_fmt == "exr": + if rs_AovFiles == False and img_fmt == "exr": for name in render_name: if name == "RsCryptomatte": render_dict.update({ @@ -98,7 +98,8 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3"]: + "V_Ray_GPU_6_Hotfix_3" + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From f030ca17d8a3a7d979f01f3b56b11732d6dcfb85 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:50:36 +0800 Subject: [PATCH 738/918] hound --- openpype/hosts/max/api/lib_renderproducts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 21c41446a9..6bd7e5b7b0 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,14 +72,14 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles == False and img_fmt == "exr": + if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": render_dict.update({ - name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) - }) + name: self.get_expected_render_elements( + output_file, name, startFrame, + endFrame, img_fmt) + }) else: for name in render_name: render_dict.update({ @@ -99,7 +99,7 @@ class RenderProducts(object): if renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" - ]: + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From 44c0f1cf8a52a5eb1b45d43310f2451aa966be01 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:51:26 +0800 Subject: [PATCH 739/918] hound --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 6bd7e5b7b0..2fbb7e8ff3 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -100,7 +100,7 @@ class RenderProducts(object): "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: - if img_fmt !="exr": + if img_fmt != "exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: From 0fccdaf5f5d46e775863193ff7f3a44e3338eda9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 15:54:55 +0800 Subject: [PATCH 740/918] use expected files instead of files --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 4682cc4487..3fde667dfe 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -197,10 +197,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, else: plugin_data["DisableMultipass"] = 1 - files = instance.data.get("files") + files = instance.data.get("expectedFiles") if not files: raise RuntimeError("No render elements found") - old_output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) files = instance.data["expectedFiles"] From 7820d92dc9fefa090e773e720d3a122989025b5c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 09:37:26 +0100 Subject: [PATCH 741/918] Add pools as last attributes --- .../maya/plugins/create/create_render.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 387b7321b9..4681175808 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -181,16 +181,34 @@ class CreateRender(plugin.Creator): primary_pool = pool_setting["primary_pool"] sorted_pools = self._set_default_pool(list(pools), primary_pool) - cmds.addAttr(self.instance, longName="primaryPool", - attributeType="enum", - enumName=":".join(sorted_pools)) + cmds.addAttr( + self.instance, + longName="primaryPool", + attributeType="enum", + enumName=":".join(sorted_pools) + ) + cmds.setAttr( + "{}.primaryPool".format(self.instance), + 0, + keyable=False, + channelBox=True + ) pools = ["-"] + pools secondary_pool = pool_setting["secondary_pool"] sorted_pools = self._set_default_pool(list(pools), secondary_pool) - cmds.addAttr("{}.secondaryPool".format(self.instance), - attributeType="enum", - enumName=":".join(sorted_pools)) + cmds.addAttr( + self.instance, + longName="secondaryPool", + attributeType="enum", + enumName=":".join(sorted_pools) + ) + cmds.setAttr( + "{}.secondaryPool".format(self.instance), + 0, + keyable=False, + channelBox=True + ) def _create_render_settings(self): """Create instance settings.""" @@ -260,6 +278,12 @@ class CreateRender(plugin.Creator): default_priority) self.data["tile_priority"] = tile_priority + strict_error_checking = maya_submit_dl.get("strict_error_checking", + True) + self.data["strict_error_checking"] = strict_error_checking + + # Pool attributes should be last since they will be recreated when + # the deadline server changes. pool_setting = (self._project_settings["deadline"] ["publish"] ["CollectDeadlinePools"]) @@ -272,9 +296,6 @@ class CreateRender(plugin.Creator): secondary_pool = pool_setting["secondary_pool"] self.data["secondaryPool"] = self._set_default_pool(pool_names, secondary_pool) - strict_error_checking = maya_submit_dl.get("strict_error_checking", - True) - self.data["strict_error_checking"] = strict_error_checking if muster_enabled: self.log.info(">>> Loading Muster credentials ...") From 1fe89ebbb6096245fdebd59a0f16b150cb33e529 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 09:38:04 +0100 Subject: [PATCH 742/918] Fix getting server settings. --- .../maya/plugins/publish/collect_render.py | 2 +- .../collect_deadline_server_from_instance.py | 41 ++++++++++++++----- .../collect_default_deadline_server.py | 3 +- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 7c47f17acb..babd494758 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -336,7 +336,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): context.data["system_settings"]["modules"]["deadline"] ) if deadline_settings["enabled"]: - data["deadlineUrl"] = render_instance.data.get("deadlineUrl") + data["deadlineUrl"] = render_instance.data["deadlineUrl"] if self.sync_workfile_version: data["version"] = context.data["version"] diff --git a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index 9981bead3e..2de6073e29 100644 --- a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -5,23 +5,26 @@ This is resolving index of server lists stored in `deadlineServers` instance attribute or using default server if that attribute doesn't exists. """ +from maya import cmds + import pyblish.api class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): """Collect Deadline Webservice URL from instance.""" - order = pyblish.api.CollectorOrder + 0.415 + # Run before collect_render. + order = pyblish.api.CollectorOrder + 0.005 label = "Deadline Webservice from the Instance" families = ["rendering", "renderlayer"] + hosts = ["maya"] def process(self, instance): instance.data["deadlineUrl"] = self._collect_deadline_url(instance) self.log.info( "Using {} for submission.".format(instance.data["deadlineUrl"])) - @staticmethod - def _collect_deadline_url(render_instance): + def _collect_deadline_url(self, render_instance): # type: (pyblish.api.Instance) -> str """Get Deadline Webservice URL from render instance. @@ -49,8 +52,16 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): default_server = render_instance.context.data["defaultDeadline"] instance_server = render_instance.data.get("deadlineServers") if not instance_server: + self.log.debug("Using default server.") return default_server + # Get instance server as sting. + if isinstance(instance_server, int): + instance_server = cmds.getAttr( + "{}.deadlineServers".format(render_instance.data["objset"]), + asString=True + ) + default_servers = deadline_settings["deadline_urls"] project_servers = ( render_instance.context.data @@ -58,15 +69,23 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): ["deadline"] ["deadline_servers"] ) - deadline_servers = { + if not project_servers: + self.log.debug("Not project servers found. Using default servers.") + return default_servers[instance_server] + + project_enabled_servers = { k: default_servers[k] for k in project_servers if k in default_servers } - # This is Maya specific and may not reflect real selection of deadline - # url as dictionary keys in Python 2 are not ordered - return deadline_servers[ - list(deadline_servers.keys())[ - int(render_instance.data.get("deadlineServers")) - ] - ] + + msg = ( + "\"{}\" server on instance is not enabled in project settings." + " Enabled project servers:\n{}".format( + instance_server, project_enabled_servers + ) + ) + assert instance_server in project_enabled_servers, msg + + self.log.debug("Using project approved server.") + return project_enabled_servers[instance_server] diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py index cb2b0cf156..1a0d615dc3 100644 --- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -17,7 +17,8 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): `CollectDeadlineServerFromInstance`. """ - order = pyblish.api.CollectorOrder + 0.410 + # Run before collect_deadline_server_instance. + order = pyblish.api.CollectorOrder + 0.0025 label = "Default Deadline Webservice" pass_mongo_url = False From 66070377104142edce133201389e896774d1f3f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 29 May 2023 11:25:14 +0200 Subject: [PATCH 743/918] Publisher: Call explicitly prepared tab methods (#5044) * call explicitly prepared tab methods * add an overlay message --- openpype/tools/publisher/window.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index fc90e66f21..6ab444109e 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -676,7 +676,15 @@ class PublisherWindow(QtWidgets.QDialog): self._tabs_widget.set_current_tab(identifier) def set_current_tab(self, tab): - self._set_current_tab(tab) + if tab == "create": + self._go_to_create_tab() + elif tab == "publish": + self._go_to_publish_tab() + elif tab == "report": + self._go_to_report_tab() + elif tab == "details": + self._go_to_details_tab() + if not self._window_is_visible: self.set_tab_on_reset(tab) @@ -686,6 +694,12 @@ class PublisherWindow(QtWidgets.QDialog): def _go_to_create_tab(self): if self._create_tab.isEnabled(): self._set_current_tab("create") + return + + self._overlay_object.add_message( + "Can't switch to Create tab because publishing is paused.", + message_type="info" + ) def _go_to_publish_tab(self): self._set_current_tab("publish") From 71b3242abd277995482a410720a4a84a13818a3a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 11:41:10 +0100 Subject: [PATCH 744/918] Missing deadlineUrl on instances metadata. --- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..22370dea14 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -1089,6 +1089,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): deadline_publish_job_id = \ self._submit_deadline_post_job(instance, render_job, instances) + # Inject deadline url to instances. + for inst in instances: + inst["deadlineUrl"] = self.deadline_url + # publish job file publish_job = { "asset": asset, From 2388cf53cb10f0989b57d86ff0963670556e7ebb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 11:44:05 +0100 Subject: [PATCH 745/918] Support same attribute names on different node types. --- .../publish/validate_rendersettings.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index ebf7b3138d..0ca7c6f5a7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -274,16 +274,18 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): # go through definitions and test if such node.attribute exists. # if so, compare its value from the one required. - for attribute, data in cls.get_nodes(instance, renderer).items(): + for data in cls.get_nodes(instance, renderer): for node in data["nodes"]: try: render_value = cmds.getAttr( - "{}.{}".format(node, attribute) + "{}.{}".format(node, data["attribute"]) ) except RuntimeError: invalid = True cls.log.error( - "Cannot get value of {}.{}".format(node, attribute) + "Cannot get value of {}.{}".format( + node, data["attribute"] + ) ) else: if render_value not in data["values"]: @@ -291,7 +293,10 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): cls.log.error( "Invalid value {} set on {}.{}. Expecting " "{}".format( - render_value, node, attribute, data["values"] + render_value, + node, + data["attribute"], + data["values"] ) ) @@ -305,7 +310,7 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): "{}_render_attributes".format(renderer) ) or [] ) - result = {} + result = [] for attr, values in OrderedDict(validation_settings).items(): values = [convert_to_int_or_float(v) for v in values if v] @@ -335,7 +340,13 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): ) continue - result[attribute_name] = {"nodes": nodes, "values": values} + result.append( + { + "attribute": attribute_name, + "nodes": nodes, + "values": values + } + ) return result @@ -350,11 +361,11 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): "{aov_separator}", instance.data.get("aovSeparator", "_") ) - for attribute, data in cls.get_nodes(instance, renderer).items(): + for data in cls.get_nodes(instance, renderer): if not data["values"]: continue for node in data["nodes"]: - lib.set_attribute(attribute, data["values"][0], node) + lib.set_attribute(data["attribute"], data["values"][0], node) with lib.renderlayer(layer_node): default = lib.RENDER_ATTRS['default'] From 83d79b9eb80bc1ffc90f941b4d607e20134065c0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 11:48:08 +0100 Subject: [PATCH 746/918] Repair RenderPass token when merging AOVs. --- .../maya/plugins/publish/validate_rendersettings.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py index ebf7b3138d..a5d5ab0c9e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rendersettings.py +++ b/openpype/hosts/maya/plugins/publish/validate_rendersettings.py @@ -364,6 +364,17 @@ class ValidateRenderSettings(pyblish.api.InstancePlugin): cmds.setAttr("defaultRenderGlobals.animation", True) # Repair prefix + if renderer == "arnold": + multipart = cmds.getAttr("defaultArnoldDriver.mergeAOVs") + if multipart: + separator_variations = [ + "_", + "_", + "", + ] + for variant in separator_variations: + default_prefix = default_prefix.replace(variant, "") + if renderer != "renderman": node = render_attrs["node"] prefix_attr = render_attrs["prefix"] From a4fd3c7c6dae31509fc37621878aa4600933f2ae Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:50:56 +0100 Subject: [PATCH 747/918] Minor refactor --- .../hosts/unreal/plugins/load/load_camera.py | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index c4fe9df70b..af7a594b41 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -89,10 +89,7 @@ class CameraLoader(plugin.Loader): hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() @@ -106,23 +103,15 @@ class CameraLoader(plugin.Loader): # Get highest number to make a unique name folders = [a for a in asset_content if a[-1] == "/" and f"{name}_" in a] - f_numbers = [] - for f in folders: - # Get number from folder name. Splits the string by "_" and - # removes the last element (which is a "/"). - f_numbers.append(int(f.split("_")[-1][:-1])) + # Get number from folder name. Splits the string by "_" and + # removes the last element (which is a "/"). + f_numbers = [int(f.split("_")[-1][:-1]) for f in folders] f_numbers.sort() - if not f_numbers: - unique_number = 1 - else: - unique_number = f_numbers[-1] + 1 + unique_number = f_numbers[-1] + 1 if f_numbers else 1 asset_dir, container_name = tools.create_unique_asset_name( f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") - asset_path = Path(asset_dir) - asset_path_parent = str(asset_path.parent.as_posix()) - container_name += suffix EditorAssetLibrary.make_directory(asset_dir) @@ -163,17 +152,17 @@ class CameraLoader(plugin.Loader): asset).get_class().get_name() == 'LevelSequence' ] - if not existing_sequences: + if existing_sequences: + for seq in existing_sequences: + sequences.append(seq.get_asset()) + frame_ranges.append(( + seq.get_asset().get_playback_start(), + seq.get_asset().get_playback_end())) + else: sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) - else: - for e in existing_sequences: - sequences.append(e.get_asset()) - frame_ranges.append(( - e.get_asset().get_playback_start(), - e.get_asset().get_playback_end())) EditorAssetLibrary.make_directory(asset_dir) @@ -252,8 +241,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - imprint( - "{}/{}".format(asset_dir, container_name), data) + imprint(f"{asset_dir}/{container_name}", data) EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) @@ -415,8 +403,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) + imprint(f"{asset_dir}/{container.get('container_name')}", data) EditorLevelLibrary.save_current_level() @@ -514,10 +501,8 @@ class CameraLoader(plugin.Loader): break sequences.append(ss.get_sequence()) # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 if visibility_track: sections = visibility_track.get_sections() From 79a0210c35eda89c355afd0497d2ae14fa9bcb45 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:56:03 +0100 Subject: [PATCH 748/918] Save whole hierarchy when loading camera or layout --- openpype/hosts/unreal/plugins/load/load_camera.py | 5 +++-- openpype/hosts/unreal/plugins/load/load_layout.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index af7a594b41..3ed7b055e3 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -246,8 +246,9 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) + # Save all assets in the hierarchy asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True + hierarchy_dir_list[0], recursive=True, include_folder=False ) for a in asset_content: @@ -408,7 +409,7 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + f"{root}/{ms_asset}", recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5a3953f82e..51ca0383e0 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -642,8 +642,10 @@ class LayoutLoader(plugin.Loader): imprint( "{}/{}".format(asset_dir, container_name), data) + save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) @@ -664,11 +666,12 @@ class LayoutLoader(plugin.Loader): asset_dir = container.get('namespace') context = representation.get("context") + hierarchy = context.get('hierarchy').split("/") + sequence = None master_level = None if create_sequences: - hierarchy = context.get('hierarchy').split("/") h_dir = f"{root}/{hierarchy[0]}" h_asset = hierarchy[0] master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" @@ -726,8 +729,10 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() + save_dir = f"{root}/{hierarchy[0]}" if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) From ff2c494705f79d21b75dcfc77fb2c1a49b23bae8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:10:23 +0100 Subject: [PATCH 749/918] Set view range in sequencer when creating sequences --- openpype/hosts/unreal/api/pipeline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 5030e8ee86..72816c9b81 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -623,11 +623,18 @@ def generate_sequence(h, h_dir): min_frame = min(start_frames) max_frame = max(end_frames) + fps = asset_data.get('data').get("fps") + sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + unreal.FrameRate(fps, 1.0)) sequence.set_playback_start(min_frame) sequence.set_playback_end(max_frame) + sequence.set_work_range_start(min_frame / fps) + sequence.set_work_range_end(max_frame / fps) + sequence.set_view_range_start(min_frame / fps) + sequence.set_view_range_end(max_frame / fps) + tracks = sequence.get_master_tracks() track = None for t in tracks: From 9d9eebfe09499474dac3504d7c4b252100e8c57d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:25:49 +0800 Subject: [PATCH 750/918] using current file to render and add validator for deadline publish in max hosts --- openpype/hosts/max/api/lib_renderproducts.py | 4 +- .../hosts/max/plugins/create/create_render.py | 14 ++++ .../max/plugins/publish/collect_render.py | 9 ++- .../hosts/max/plugins/publish/save_scene.py | 5 -- .../publish/validate_deadline_publish.py | 41 ++++++++++ .../plugins/publish/submit_max_deadline.py | 76 ++----------------- 6 files changed, 72 insertions(+), 77 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_deadline_publish.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 2fbb7e8ff3..7e016f6f15 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container): + def get_aovs(self, container, instance): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + rs_AovFiles = instance.data.get("separateAovFiles") if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 78e9527bdf..3d5ed781a4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,6 +2,7 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin +from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -18,6 +19,9 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename + instance_data["separateAovFiles"] = ( + pre_create_data.get("separateAovFiles")) + instance = super(CreateRender, self).create( subset_name, instance_data, @@ -40,3 +44,13 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRender, self).get_pre_create_attr_defs() + + return attrs + [ + BoolDef("separateAovFiles", + label="Separate Aov Files", + default=False), + + ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 9137f8c854..14d23c42fc 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -31,12 +31,14 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name) + aovs = RenderProducts().get_aovs(instance.name, instance) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() + instance.data["files"] = list() instance.data["expectedFiles"].append(files_by_aov) + instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() project_name = context.data["projectName"] @@ -62,6 +64,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] # also need to get the render dir for coversion data = { "asset": asset, @@ -71,6 +75,7 @@ class CollectRender(pyblish.api.InstancePlugin): "imageFormat": img_format, "family": 'maxrender', "families": ['maxrender'], + "renderer": renderer, "source": filepath, "plugin": "3dsmax", "frameStart": int(rt.rendStart), diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py index 93d97a3de5..a40788ab41 100644 --- a/openpype/hosts/max/plugins/publish/save_scene.py +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -18,9 +18,4 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): file = rt.maxFileName current = os.path.join(folder, file) assert context.data["currentFile"] == current - - if rt.checkForSave(): - self.log.debug("Skipping file save as there " - "are no modifications..") - return rt.saveMaxFile(current) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py new file mode 100644 index 0000000000..f516e09337 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -0,0 +1,41 @@ +import os +import pyblish.api +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.hosts.max.api.lib_rendersettings import RenderSettings + + +class ValidateDeadlinePublish(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Render File Directory is + not the same in every submission + """ + + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + label = "Render Output for Deadline" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + file = rt.maxFileName + filename, ext = os.path.splitext(file) + if filename not in rt.rendOutputFilename: + raise PublishValidationError( + "Directory of RenderOutput doesn't " + "have with the current Max SceneName " + "please repair to rename the folder!" + ) + + @classmethod + def repair(cls, instance): + container= instance.data.get("instance_node") + RenderSettings().render_output(container) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 3fde667dfe..d7ba7107a3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -133,6 +133,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -204,8 +205,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - files = instance.data["expectedFiles"] - first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) beauty_name = f"{dir}/{rgb_bname}" @@ -231,6 +230,9 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + if renderer == "Redshift_Renderer": + plugin_data["redshift_SeparateAovFiles"] = instance.data.get( + "separateAovFiles", False) self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) @@ -239,72 +241,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - workfile_instance = self._get_workfile_instance(instance.context) - if workfile_instance is None: - return - - # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data["representations"][0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data['anatomy'] - template_obj = anatomy.templates_obj["publish"]["path"] - template_filled = template_obj.format_strict(template_data) - file_path = os.path.normpath(template_filled) - - self.log.info("Using published scene for render {}".format(file_path)) - - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") - raise - - if not replace_in_path: - return file_path - - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(file_path) - orig_scene = _clean_name(instance.data["AssetName"]) - expected_files = instance.data.get("expectedFiles") - - if isinstance(expected_files[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in expected_files[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - str(f).replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - # [] might be too much here, TODO - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in expected_files: - new_exp.append( - str(f).replace(orig_scene, new_scene) - ) - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - return file_path + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") + replace_in_path = False + return replace_in_path @staticmethod def _iter_expected_files(exp): From dbb175204adf16e763d2c2295d6ad675988453ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:29:35 +0800 Subject: [PATCH 751/918] hound --- openpype/hosts/max/plugins/publish/validate_deadline_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index f516e09337..c5bc979043 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -37,5 +37,5 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - container= instance.data.get("instance_node") + container = instance.data.get("instance_node") RenderSettings().render_output(container) From eb5b5bf492c69d3369d944ff019f36a6e306b3bd Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:59:54 +0100 Subject: [PATCH 752/918] Set back sequencer and viewport when updating layout or camera --- .../hosts/unreal/plugins/load/load_camera.py | 23 ++++++++++++-- .../hosts/unreal/plugins/load/load_layout.py | 31 ++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 3ed7b055e3..59ea14697d 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -3,9 +3,12 @@ from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, @@ -259,6 +262,13 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + asset_dir = container.get('namespace') EditorLevelLibrary.save_current_level() @@ -416,6 +426,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): asset_dir = container.get('namespace') path = Path(asset_dir) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 51ca0383e0..86b2e1456c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -5,13 +5,16 @@ import collections from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils -from unreal import AssetToolsHelpers -from unreal import FBXImportType -from unreal import MovieSceneLevelVisibilityTrack -from unreal import MovieSceneSubTrack +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + AssetToolsHelpers, + FBXImportType, + MovieSceneLevelVisibilityTrack, + MovieSceneSubTrack, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( @@ -661,6 +664,13 @@ class LayoutLoader(plugin.Loader): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -742,6 +752,13 @@ class LayoutLoader(plugin.Loader): elif prev_level: EditorLevelLibrary.load_level(prev_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): """ Delete the layout. First, check if the assets loaded with the layout From e506d88ed3d113f608759e883dcdf17dcb6f5ccc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:33:40 +0800 Subject: [PATCH 753/918] style fix --- openpype/hosts/max/plugins/create/create_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 3d5ed781a4..6c581cdcf4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -47,10 +47,8 @@ class CreateRender(plugin.MaxCreator): def get_pre_create_attr_defs(self): attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ BoolDef("separateAovFiles", label="Separate Aov Files", - default=False), - + default=False) ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d7ba7107a3..b6a30e36b7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -232,7 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if renderer == "Redshift_Renderer": plugin_data["redshift_SeparateAovFiles"] = instance.data.get( - "separateAovFiles", False) + "separateAovFiles") self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) From 5f763a4e8e65d0aeb8e4515e69ec768f6f2c5f4d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:36:38 +0800 Subject: [PATCH 754/918] dont add separate aov as instance data --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/create/create_render.py | 10 ---------- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 7e016f6f15..a6427bf7c5 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container, instance): + def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = instance.data.get("separateAovFiles") + rs_AovFiles = rt.RedShift_Renderer().separateAovFiles if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 6c581cdcf4..be5dece05b 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -19,8 +19,6 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename - instance_data["separateAovFiles"] = ( - pre_create_data.get("separateAovFiles")) instance = super(CreateRender, self).create( subset_name, @@ -44,11 +42,3 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) - - def get_pre_create_attr_defs(self): - attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ - BoolDef("separateAovFiles", - label="Separate Aov Files", - default=False) - ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 14d23c42fc..77ab5f654d 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -31,7 +31,7 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name, instance) + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: From 982186ffa08618f8d590e894356595f886af0f57 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:38:17 +0800 Subject: [PATCH 755/918] remove unused import --- openpype/hosts/max/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index be5dece05b..5ad895b86e 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,7 +2,6 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin -from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings From bafc085ec19e624c163d6a862b9fcc6e0edab983 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 22:56:47 +0800 Subject: [PATCH 756/918] updating validator's comment --- .../max/plugins/publish/validate_deadline_publish.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index c5bc979043..b2f0e863f4 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -30,12 +30,14 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, filename, ext = os.path.splitext(file) if filename not in rt.rendOutputFilename: raise PublishValidationError( - "Directory of RenderOutput doesn't " - "have with the current Max SceneName " - "please repair to rename the folder!" + "Render output folder " + "doesn't match the max scene name! " + "Use Repair action to " + "fix the folder file path.." ) @classmethod def repair(cls, instance): container = instance.data.get("instance_node") RenderSettings().render_output(container) + cls.log.debug("Reset the render output folder...") From a4e9eaf3c38aaa4bb31784067d35d54656ae8333 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 29 May 2023 23:24:48 +0200 Subject: [PATCH 757/918] Update openpype/hosts/max/plugins/load/load_redshift_proxy.py Co-authored-by: Roy Nieterau --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 9451e5299b..31692f6367 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -10,7 +10,6 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): - """Load rs files with Redshift Proxy""" label = "Load Redshift Proxy" From 279b3dc767fc870a8632abc625f1e2dbf3706add Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 12:09:53 +0200 Subject: [PATCH 758/918] Set explicit startup script path --- .../resolve/hooks/pre_resolve_launch_last_workfile.py | 11 +++++++++++ .../utility_scripts/openpype_startup.scriptlib | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py index 6db3cc28b2..2ad4352b82 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -1,6 +1,7 @@ import os from openpype.lib import PreLaunchHook +import openpype.hosts.resolve class ResolveLaunchLastWorkfile(PreLaunchHook): @@ -33,3 +34,13 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): f"last workfile: {last_workfile}") key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" self.launch_context.env[key] = last_workfile + + # Set the openpype prelaunch startup script path for easy access + # in the LUA .scriptlib code + op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__) + script_path = os.path.join(op_resolve_root, "startup.py") + key = "OPENPYPE_RESOLVE_STARTUP_SCRIPT" + self.launch_context.env[key] = script_path + self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: " + f"{script_path}") + diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib index 9fca666d78..ec9b30a18d 100644 --- a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -5,10 +5,9 @@ function file_exists(name) end -openpype_root = os.getenv("OPENPYPE_ROOT") -if openpype_root ~= nil then - script = openpype_root .. "/openpype/hosts/resolve/startup.py" - script = fusion:MapPath(script) +openpype_startup_script = os.getenv("OPENPYPE_RESOLVE_STARTUP_SCRIPT") +if openpype_startup_script ~= nil then + script = fusion:MapPath(openpype_startup_script) if file_exists(script) then -- We must use RunScript to ensure it runs in a separate From 990737623bcfab7c5f7502e3cb4a0f2156f0891c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 12:20:17 +0200 Subject: [PATCH 759/918] Cosmetics --- openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py index 2ad4352b82..0e27ddb8c3 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -43,4 +43,3 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): self.launch_context.env[key] = script_path self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: " f"{script_path}") - From 307eca8c28aeb1afeb68ba1e93e63e993c21a084 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 13:24:17 +0200 Subject: [PATCH 760/918] Cleanup Resolve startup script + add setting for launch menu on start --- openpype/hosts/resolve/startup.py | 46 ++++++++++++++----- .../defaults/project_settings/resolve.json | 1 + .../schema_project_resolve.json | 5 ++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py index 4aeb106ef1..79a64e0fbf 100644 --- a/openpype/hosts/resolve/startup.py +++ b/openpype/hosts/resolve/startup.py @@ -1,26 +1,44 @@ +"""This script is used as a startup script in Resolve through a .scriptlib file + +It triggers directly after the launch of Resolve and it's recommended to keep +it optimized for fast performance since the Resolve UI is actually interactive +while this is running. As such, there's nothing ensuring the user isn't +continuing manually before any of the logic here runs. As such we also try +to delay any imports as much as possible. + +This code runs in a separate process to the main Resolve process. + +""" import os -# Importing this takes a little over a second and thus this means -# that we have about 1.5 seconds delay before the workfile will actually -# be opened at the minimum import openpype.hosts.resolve.api -def launch_menu(): - from openpype.pipeline import install_host - print("Launching Resolve OpenPype menu..") +def ensure_installed_host(): + """Install resolve host with openpype and return the registered host. + + This function can be called multiple times without triggering an + additional install. + """ + from openpype.pipeline import install_host, registered_host + host = registered_host() + if host: + return host - # Activate resolve from openpype install_host(openpype.hosts.resolve.api) + return registered_host() + +def launch_menu(): + print("Launching Resolve OpenPype menu..") + ensure_installed_host() openpype.hosts.resolve.api.launch_pype_menu() def open_file(path): # Avoid the need to "install" the host - openpype.hosts.resolve.api.bmdvr = resolve # noqa - openpype.hosts.resolve.api.bmdvf = fusion # noqa - openpype.hosts.resolve.api.open_file(path) + host = ensure_installed_host() + host.open_file(path) def main(): @@ -32,8 +50,12 @@ def main(): print("No last workfile set to open. Skipping..") # Launch OpenPype menu - # TODO: Add a setting to enable/disable this - launch_menu() + from openpype.settings import get_project_settings + from openpype.pipeline.context_tools import get_current_project_name + project_name = get_current_project_name() + settings = get_project_settings(project_name) + if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True): + launch_menu() if __name__ == "__main__": diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 264f3bd902..56efa78e89 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -1,4 +1,5 @@ { + "launch_openpype_menu_on_start": false, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index b326f22394..6f98bdd3bd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -5,6 +5,11 @@ "label": "DaVinci Resolve", "is_file": true, "children": [ + { + "type": "boolean", + "key": "launch_openpype_menu_on_start", + "label": "Launch OpenPype menu on start of Resolve" + }, { "key": "imageio", "type": "dict", From 5fbae39a745b0016b2ada9d054b696aef2007fee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 May 2023 13:32:27 +0200 Subject: [PATCH 761/918] Ftrack: Role names are not case sensitive in ftrack event server status action (#5058) * statuser is not case sensitive about role names * safer role check --- .../ftrack/lib/ftrack_action_handler.py | 23 +++++++++++++++---- .../ftrack/scripts/sub_event_status.py | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 07b3a780a2..1be4353b26 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -234,6 +234,10 @@ class BaseAction(BaseHandler): if not settings_roles: return default + user_roles = { + role_name.lower() + for role_name in user_roles + } for role_name in settings_roles: if role_name.lower() in user_roles: return True @@ -264,8 +268,15 @@ class BaseAction(BaseHandler): return user_entity @classmethod - def get_user_roles_from_event(cls, session, event): - """Query user entity from event.""" + def get_user_roles_from_event(cls, session, event, lower=True): + """Get user roles based on data in event. + + Args: + session (ftrack_api.Session): Prepared ftrack session. + event (ftrack_api.event.Event): Event which is processed. + lower (Optional[bool]): Lower the role names. Default 'True'. + """ + not_set = object() user_roles = event["data"].get("user_roles", not_set) @@ -273,7 +284,10 @@ class BaseAction(BaseHandler): user_roles = [] user_entity = cls.get_user_entity_from_event(session, event) for role in user_entity["user_security_roles"]: - user_roles.append(role["security_role"]["name"].lower()) + role_name = role["security_role"]["name"] + if lower: + role_name = role_name.lower() + user_roles.append(role_name) event["data"]["user_roles"] = user_roles return user_roles @@ -322,7 +336,8 @@ class BaseAction(BaseHandler): if not settings.get(self.settings_enabled_key, True): return False - user_role_list = self.get_user_roles_from_event(session, event) + user_role_list = self.get_user_roles_from_event( + session, event, lower=False) if not self.roles_check(settings.get("role_list"), user_role_list): return False return True diff --git a/openpype/modules/ftrack/scripts/sub_event_status.py b/openpype/modules/ftrack/scripts/sub_event_status.py index dc5836e7f2..c6c2e9e1f6 100644 --- a/openpype/modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/ftrack/scripts/sub_event_status.py @@ -296,9 +296,9 @@ def server_activity_validate_user(event): if not user_ent: return False - role_list = ["Pypeclub", "Administrator"] + role_list = {"pypeclub", "administrator"} for role in user_ent["user_security_roles"]: - if role["security_role"]["name"] in role_list: + if role["security_role"]["name"].lower() in role_list: return True return False From 4bc61d1c89f1c3adfbbc7ba80fa2fff5f156eec0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 13:33:58 +0200 Subject: [PATCH 762/918] Fix border widget --- .../publisher/widgets/border_label_widget.py | 125 +++++++++++++----- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 5617e159cd..1381d74eb3 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors @@ -14,32 +15,44 @@ class _VLineWidget(QtWidgets.QWidget): It is expected that parent widget will set width. """ - def __init__(self, color, left, parent): + def __init__(self, color, line_size, left, parent): super(_VLineWidget, self).__init__(parent) self._color = color self._left = left + self._line_size = line_size + + def set_line_size(self, line_size): + self._line_size = line_size def paintEvent(self, event): if not self.isVisible(): return - if self._left: - pos_x = 0 - else: - pos_x = self.width() + pos_x = self._line_size * 0.5 + if not self._left: + pos_x = self.width() - pos_x + painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawLine(pos_x, 0, pos_x, self.height()) + painter.drawRect( + QtCore.QRectF( + pos_x, + -self._line_size, + pos_x + (self.width() * 2), + self.height() + (self._line_size * 2) + ) + ) painter.end() @@ -56,34 +69,46 @@ class _HBottomLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, parent): + def __init__(self, color, line_size, parent): super(_HBottomLineWidget, self).__init__(parent) self._color = color self._radius = 0 + self._line_size = line_size def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - rect = QtCore.QRect( - 0, -self._radius, self.width(), self.height() + self._radius + x_offset = self._line_size * 0.5 + rect = QtCore.QRectF( + x_offset, + -self._radius, + self.width() - (2 * x_offset), + (self.height() + self._radius) - x_offset ) painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -102,30 +127,38 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, left_side, parent): + + def __init__(self, color, line_size, left_side, parent): super(_HTopCornerLineWidget, self).__init__(parent) self._left_side = left_side + self._line_size = line_size self._color = color self._radius = 0 def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - pos_y = self.height() / 2 - + pos_y = self.height() * 0.5 + x_offset = self._line_size * 0.5 if self._left_side: - rect = QtCore.QRect( - 0, pos_y, self.width() + self._radius, self.height() + rect = QtCore.QRectF( + x_offset, + pos_y, + self.width() + self._radius + x_offset, + self.height() ) else: - rect = QtCore.QRect( - -self._radius, + rect = QtCore.QRectF( + (-self._radius), pos_y, - self.width() + self._radius, + (self.width() + self._radius) - x_offset, self.height() ) @@ -138,10 +171,13 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -163,8 +199,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): if color_value: color = color_value.get_qcolor() - top_left_w = _HTopCornerLineWidget(color, True, self) - top_right_w = _HTopCornerLineWidget(color, False, self) + line_size = 1 + + top_left_w = _HTopCornerLineWidget(color, line_size, True, self) + top_right_w = _HTopCornerLineWidget(color, line_size, False, self) label_widget = QtWidgets.QLabel(label, self) @@ -175,10 +213,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): top_layout.addWidget(label_widget, 0) top_layout.addWidget(top_right_w, 1) - left_w = _VLineWidget(color, True, self) - right_w = _VLineWidget(color, False, self) + left_w = _VLineWidget(color, line_size, True, self) + right_w = _VLineWidget(color, line_size, False, self) - bottom_w = _HBottomLineWidget(color, self) + bottom_w = _HBottomLineWidget(color, line_size, self) center_layout = QtWidgets.QHBoxLayout() center_layout.setContentsMargins(5, 5, 5, 5) @@ -201,6 +239,7 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._widget = None self._radius = 0 + self._line_size = line_size self._top_left_w = top_left_w self._top_right_w = top_right_w @@ -216,14 +255,38 @@ class BorderedLabelWidget(QtWidgets.QFrame): value, value, value, value ) + def set_line_size(self, line_size): + if self._line_size == line_size: + return + self._line_size = line_size + for widget in ( + self._top_left_w, + self._top_right_w, + self._left_w, + self._right_w, + self._bottom_w + ): + widget.set_line_size(line_size) + self._recalculate_sizes() + def showEvent(self, event): super(BorderedLabelWidget, self).showEvent(event) + self._recalculate_sizes() + def _recalculate_sizes(self): height = self._label_widget.height() - radius = (height + (height % 2)) / 2 + radius = int((height + (height % 2)) / 2) self._radius = radius - side_width = 1 + radius + radius_size = self._line_size + 1 + if radius_size < radius: + radius_size = radius + + if radius: + side_width = self._line_size + radius + else: + side_width = self._line_size + 1 + # Don't use fixed width/height as that would set also set # the other size (When fixed width is set then is also set # fixed height). @@ -231,8 +294,8 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._left_w.setMaximumWidth(side_width) self._right_w.setMinimumWidth(side_width) self._right_w.setMaximumWidth(side_width) - self._bottom_w.setMinimumHeight(radius) - self._bottom_w.setMaximumHeight(radius) + self._bottom_w.setMinimumHeight(radius_size) + self._bottom_w.setMaximumHeight(radius_size) self._bottom_w.set_radius(radius) self._top_right_w.set_radius(radius) self._top_left_w.set_radius(radius) From 26b99db61e4ab17033bfaa5b0f9099a3b6561275 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 15:18:53 +0200 Subject: [PATCH 763/918] removed unused import --- openpype/tools/publisher/widgets/border_label_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 1381d74eb3..e5693368b1 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors From 2e58cd2e1a1a1dd23e0bc59bc1cbc414fa6d1ec9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:44:26 +0800 Subject: [PATCH 764/918] move the custom popup menu to nuke/startup and add the frame setting to Openpype tool menu --- openpype/hosts/nuke/api/lib.py | 32 --------- openpype/hosts/nuke/api/pipeline.py | 5 -- .../nuke/startup}/custom_popup.py | 71 ++++++++++++++----- .../defaults/project_settings/nuke.json | 7 ++ 4 files changed, 61 insertions(+), 54 deletions(-) rename openpype/{widgets => hosts/nuke/startup}/custom_popup.py (65%) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 94a0ff15ad..59a63d1373 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,38 +2358,6 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() - def reset_frame_range_read_nodes(self): - from openpype.widgets import custom_popup - parent = get_main_window() - dialog = custom_popup.CustomScriptDialog(parent=parent) - dialog.setWindowTitle("Frame Range") - dialog.set_name("Frame Range: ") - dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), - nuke.root().lastFrame())) - frame = dialog.widgets["line_edit"] - selection = dialog.widgets["selection"] - dialog.on_clicked.connect( - lambda: set_frame_range(frame, selection) - ) - - def set_frame_range(frame, selection): - frame_range = frame.text() - selected = selection.isChecked() - if not nuke.allNodes("Read"): - return - for read_node in nuke.allNodes("Read"): - if selected: - if not nuke.selectedNodes(): - return - if read_node in nuke.selectedNodes(): - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - else: - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - dialog.show() - - return False def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 33e25d3c81..75b0f80d21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -286,11 +286,6 @@ def _install_menu(): lambda: WorkfileSettings().set_context_settings() ) - menu.addSeparator() - menu.addCommand( - "Set Frame Range(Read Node)", - lambda: WorkfileSettings().reset_frame_range_read_nodes() - ) menu.addSeparator() menu.addCommand( "Build Workfile", diff --git a/openpype/widgets/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py similarity index 65% rename from openpype/widgets/custom_popup.py rename to openpype/hosts/nuke/startup/custom_popup.py index be4b0c32d5..c85577133c 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -1,9 +1,26 @@ import sys import contextlib - +import re +import nuke from PySide2 import QtCore, QtWidgets +def get_main_window(): + """Acquire Nuke's main window""" + main_window = None + if main_window is None: + + top_widgets = QtWidgets.QApplication.topLevelWidgets() + name = "Foundry::UI::DockMainWindow" + for widget in top_widgets: + if ( + widget.inherits("QMainWindow") + and widget.metaObject().className() == name + ): + main_window = widget + break + return main_window + class CustomScriptDialog(QtWidgets.QDialog): """A Popup that moves itself to bottom right of screen on show event. @@ -14,6 +31,9 @@ class CustomScriptDialog(QtWidgets.QDialog): on_clicked = QtCore.Signal() on_line_changed = QtCore.Signal(str) + context = None + + def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, @@ -23,23 +43,25 @@ class CustomScriptDialog(QtWidgets.QDialog): # Layout layout = QtWidgets.QVBoxLayout(self) - line_layout = QtWidgets.QHBoxLayout() - line_layout.setContentsMargins(10, 5, 10, 10) + frame_layout = QtWidgets.QHBoxLayout() + frame_layout.setContentsMargins(10, 5, 10, 10) selection_layout = QtWidgets.QHBoxLayout() selection_layout.setContentsMargins(10, 5, 10, 10) button_layout = QtWidgets.QHBoxLayout() button_layout.setContentsMargins(10, 5, 10, 10) # Increase spacing slightly for readability - line_layout.setSpacing(10) + frame_layout.setSpacing(10) button_layout.setSpacing(10) - name = QtWidgets.QLabel("") + name = QtWidgets.QLabel("Frame Range: ") name.setStyleSheet(""" QLabel { font-size: 12px; } """) - line_edit = QtWidgets.QLineEdit("") + line_edit = QtWidgets.QLineEdit( + "%s-%s" % (nuke.root().firstFrame(), + nuke.root().lastFrame())) selection_name = QtWidgets.QLabel("Use Selection") selection_name.setStyleSheet(""" QLabel { @@ -54,13 +76,13 @@ class CustomScriptDialog(QtWidgets.QDialog): cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) - line_layout.addWidget(name) - line_layout.addWidget(line_edit) + frame_layout.addWidget(name) + frame_layout.addWidget(line_edit) selection_layout.addWidget(selection_name) selection_layout.addWidget(has_selection) button_layout.addWidget(button) button_layout.addWidget(cancel) - layout.addLayout(line_layout) + layout.addLayout(frame_layout) layout.addLayout(selection_layout) layout.addLayout(button_layout) # Default size @@ -73,7 +95,6 @@ class CustomScriptDialog(QtWidgets.QDialog): "button": button, "cancel": cancel } - # Signals has_selection.toggled.connect(self.emit_click_with_state) line_edit.textChanged.connect(self.on_line_edit_changed) @@ -115,18 +136,34 @@ class CustomScriptDialog(QtWidgets.QDialog): Raises the parent (if any) """ + frame_range = self.widgets['line_edit'].text() + selected = self.widgets["selection"].isChecked() + pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" + match = re.match(pattern, frame_range) + frame_start = int(match.group("start")) + frame_end = int(match.group("end")) + if not nuke.allNodes("Read"): + return + for read_node in nuke.allNodes("Read"): + if selected: + if not nuke.selectedNodes(): + return + if read_node in nuke.selectedNodes(): + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + read_node["first"].setValue(frame_start) + read_node["last"].setValue(frame_end) + else: + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + read_node["first"].setValue(frame_start) + read_node["last"].setValue(frame_end) - parent = self.parent() self.close() - # Trigger the signal - self.on_clicked.emit() - - if parent: - parent.raise_() + return False def showEvent(self, event): - # Position popup based on contents on show event return super(CustomScriptDialog, self).showEvent(event) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index f01bdf7d50..287d13e5c9 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -222,6 +222,13 @@ "title": "OpenPype Docs", "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", "tooltip": "Open the OpenPype Nuke user doc page" + }, + { + "type": "action", + "sourcetype": "python", + "title": "Set Frame Range(Read Node)", + "command": "from openpype.hosts.nuke.startup import custom_popup;from openpype.hosts.nuke.startup.custom_popup import get_main_window;custom_popup.CustomScriptDialog(parent=get_main_window()).show();", + "tooltip": "Set Frame Range for Read Node(s)" } ] }, From 3556b58fdc593eef0c22eb3d6b357e0c0ac0be7c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:46:25 +0800 Subject: [PATCH 765/918] hound fix --- openpype/hosts/nuke/api/lib.py | 1 - openpype/hosts/nuke/startup/custom_popup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 59a63d1373..a439142051 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,7 +2358,6 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() - def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index c85577133c..dfbd590e03 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -21,6 +21,7 @@ def get_main_window(): break return main_window + class CustomScriptDialog(QtWidgets.QDialog): """A Popup that moves itself to bottom right of screen on show event. @@ -34,7 +35,6 @@ class CustomScriptDialog(QtWidgets.QDialog): context = None - def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, *args, From dddfeecceb4c711e1d8b848c8454d529992e6182 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:47:03 +0800 Subject: [PATCH 766/918] hound fix --- openpype/hosts/nuke/startup/custom_popup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index dfbd590e03..d400ed913c 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -34,7 +34,6 @@ class CustomScriptDialog(QtWidgets.QDialog): on_line_changed = QtCore.Signal(str) context = None - def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, *args, From 8f0821ab327db90a6fba5d95594bdcc461fb33e3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 22:12:47 +0800 Subject: [PATCH 767/918] the dialog closes as usual by clicking execute button when there is no nuke nodes or no nuke nodes by selection --- openpype/hosts/nuke/startup/custom_popup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index d400ed913c..57d79f99ff 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -142,10 +142,12 @@ class CustomScriptDialog(QtWidgets.QDialog): frame_start = int(match.group("start")) frame_end = int(match.group("end")) if not nuke.allNodes("Read"): + self.close() return for read_node in nuke.allNodes("Read"): if selected: if not nuke.selectedNodes(): + self.close() return if read_node in nuke.selectedNodes(): read_node["frame_mode"].setValue("start_at") From de72f26f9451f48608229cc85868c1545a201afd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 May 2023 17:09:54 +0200 Subject: [PATCH 768/918] adding check also against class attribute --- openpype/plugins/publish/collect_frames_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index 837738eb06..ca1ccc19fd 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -66,7 +66,7 @@ class CollectFramesFixDef( self.log.debug("last_version_published_files::{}".format( instance.data["last_version_published_files"])) - if rewrite_version: + if self.rewrite_version_enable and rewrite_version: instance.data["version"] = version["name"] # limits triggering version validator instance.data.pop("latestVersion") From 04e6f5f4bb844b11dbb93475f5d023c2125651ff Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 30 May 2023 17:16:18 +0200 Subject: [PATCH 769/918] :bug: fix support for separate AOVs and some style issues --- openpype/hosts/max/api/lib_renderproducts.py | 76 +++++++++---------- .../max/plugins/publish/collect_render.py | 10 ++- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a6427bf7c5..81057db733 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -3,22 +3,20 @@ # arnold # https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html import os + from pymxs import runtime as rt -from openpype.hosts.max.api.lib import ( - get_current_renderer -) -from openpype.settings import get_project_settings + +from openpype.hosts.max.api.lib import get_current_renderer from openpype.pipeline import legacy_io +from openpype.settings import get_project_settings class RenderProducts(object): def __init__(self, project_settings=None): - self._project_settings = project_settings - if not self._project_settings: - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + self._project_settings = project_settings or get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -29,14 +27,14 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 - render_dict = { + return { "beauty": self.get_expected_beauty( - output_file, startFrame, endFrame, img_fmt) + output_file, start_frame, end_frame, img_fmt + ) } - return render_dict def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -47,8 +45,8 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} @@ -65,38 +63,40 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Redshift_Renderer": + elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.RedShift_Renderer().separateAovFiles - if img_fmt == "exr" and not rs_AovFiles: + rs_aov_files = rt.Execute("renderers.current.separateAovFiles") + # this doesn't work, always returns False + # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles + if img_fmt == "exr" and not rs_aov_files: for name in render_name: if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Arnold": + elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, start_frame, end_frame, img_fmt) }) - if renderer in [ + elif renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: @@ -106,15 +106,15 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) # noqa + output_file, name, start_frame, + end_frame, img_fmt) # noqa }) return render_dict - def get_expected_beauty(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, start_frame, end_frame, fmt): beauty_frame_range = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") @@ -134,19 +134,17 @@ class RenderProducts(object): return for i in range(aov_group_num): # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - aov_name.append(aov.name) - + aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list) # close the AOVs manager window amw.close() return aov_name def get_expected_arnold_product(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected Arnold AOVs""" aov_list = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") @@ -171,10 +169,10 @@ class RenderProducts(object): return render_name def get_expected_render_elements(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected render element output files. """ render_elements = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 77ab5f654d..db5c84fad9 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -66,7 +66,7 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] - # also need to get the render dir for coversion + # also need to get the render dir for conversion data = { "asset": asset, "subset": str(instance.name), @@ -84,4 +84,12 @@ class CollectRender(pyblish.api.InstancePlugin): "farm": True } instance.data.update(data) + + # TODO: this should be unified with maya and its "multipart" flag + # on instance. + if renderer == "Redshift_Renderer": + instance.data.update( + {"separateAovFiles": rt.Execute( + "renderers.current.separateAovFiles")}) + self.log.info("data: {0}".format(data)) From 4d76c2520f254991da405dc463418d9bb6e2cfc4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 May 2023 17:19:00 +0200 Subject: [PATCH 770/918] cleanup --- .../plugins/publish/collect_frames_fix.py | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index ca1ccc19fd..86e727b053 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -35,41 +35,47 @@ class CollectFramesFixDef( rewrite_version = attribute_values.get("rewrite_version") - if frames_to_fix: - instance.data["frames_to_fix"] = frames_to_fix + if not frames_to_fix: + return - subset_name = instance.data["subset"] - asset_name = instance.data["asset"] + instance.data["frames_to_fix"] = frames_to_fix - project_entity = instance.data["projectEntity"] - project_name = project_entity["name"] + subset_name = instance.data["subset"] + asset_name = instance.data["asset"] - version = get_last_version_by_subset_name(project_name, - subset_name, - asset_name=asset_name) - if not version: - self.log.warning("No last version found, " - "re-render not possible") - return + project_entity = instance.data["projectEntity"] + project_name = project_entity["name"] - representations = get_representations(project_name, - version_ids=[version["_id"]]) - published_files = [] - for repre in representations: - if repre["context"]["family"] not in self.families: - continue + version = get_last_version_by_subset_name( + project_name, + subset_name, + asset_name=asset_name + ) + if not version: + self.log.warning( + "No last version found, re-render not possible" + ) + return - for file_info in repre.get("files"): - published_files.append(file_info["path"]) + representations = get_representations( + project_name, version_ids=[version["_id"]] + ) + published_files = [] + for repre in representations: + if repre["context"]["family"] not in self.families: + continue - instance.data["last_version_published_files"] = published_files - self.log.debug("last_version_published_files::{}".format( - instance.data["last_version_published_files"])) + for file_info in repre.get("files"): + published_files.append(file_info["path"]) - if self.rewrite_version_enable and rewrite_version: - instance.data["version"] = version["name"] - # limits triggering version validator - instance.data.pop("latestVersion") + instance.data["last_version_published_files"] = published_files + self.log.debug("last_version_published_files::{}".format( + instance.data["last_version_published_files"])) + + if self.rewrite_version_enable and rewrite_version: + instance.data["version"] = version["name"] + # limits triggering version validator + instance.data.pop("latestVersion") @classmethod def get_attribute_defs(cls): From be523bc5ec967efce3181668a932eb644a5e8f59 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 May 2023 16:49:49 +0100 Subject: [PATCH 771/918] Use temp folder to copy commandlet project --- openpype/hosts/unreal/ue_workers.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index e7a690ac9c..2b7e1375e6 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -6,6 +6,8 @@ import subprocess from distutils import dir_util from pathlib import Path from typing import List, Union +import tempfile +from distutils.dir_util import copy_tree import openpype.hosts.unreal.lib as ue_lib @@ -90,9 +92,20 @@ class UEProjectGenerationWorker(QtCore.QObject): ("Generating a new UE project ... 1 out of " f"{stage_count}")) + # Need to copy the commandlet project to a temporary folder where + # users don't need admin rights to write to. + cmdlet_tmp = tempfile.TemporaryDirectory() + cmdlet_filename = cmdlet_project.name + cmdlet_dir = cmdlet_project.parent.as_posix() + cmdlet_tmp_name = Path(cmdlet_tmp.name) + cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename) + copy_tree( + cmdlet_dir, + cmdlet_tmp_name.as_posix()) + commandlet_cmd = [ f"{ue_editor_exe.as_posix()}", - f"{cmdlet_project.as_posix()}", + f"{cmdlet_tmp_file.as_posix()}", "-run=AyonGenerateProject", f"{project_file.resolve().as_posix()}", ] @@ -111,6 +124,8 @@ class UEProjectGenerationWorker(QtCore.QObject): gen_process.stdout.close() return_code = gen_process.wait() + cmdlet_tmp.cleanup() + if return_code and return_code != 0: msg = ( f"Failed to generate {self.project_name} " From a215e4f00665f779f1056f5a128b3e2674e3c68f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 03:26:47 +0000 Subject: [PATCH 772/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c24388b2ff..5c7105e7e0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.1" +__version__ = "3.15.9-nightly.2" From e8b47be8d562e8bb1edd77dd4aebf6e75f6fc720 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 03:27:31 +0000 Subject: [PATCH 773/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 54a4ee6ac0..0036e121b7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.2 - 3.14.2-nightly.5 - 3.14.2-nightly.4 - - 3.14.2-nightly.3 validations: required: true - type: dropdown From e6d10fa3358b61eae7c9461da2119af72adf9be6 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 31 May 2023 10:37:21 +0100 Subject: [PATCH 774/918] Update settings_project_global.md (#5045) --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index c17f707830..7bd24a5773 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -63,7 +63,7 @@ Example here describes use case for creation of new color coded review of png im ![global_oiio_transcode](assets/global_oiio_transcode.png) Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. -![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png)n +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode2.png) ## Profile filters From e22d1bf78b4e9d4aeeb7c0295c8bb92b1c5595d9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 10:45:47 +0100 Subject: [PATCH 775/918] Set dev mode off by default --- openpype/settings/defaults/project_settings/unreal.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 737a17d289..92bdb468ba 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -15,6 +15,6 @@ "preroll_frames": 0, "render_format": "png", "project_setup": { - "dev_mode": true + "dev_mode": false } } From c2b753326fb6cb6460a43e401fb5731cdee1fabb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 11:59:21 +0100 Subject: [PATCH 776/918] Check if the Unreal app name follows the right format --- openpype/hosts/unreal/addon.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 1119b5c16c..fddceb00a8 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,5 +1,7 @@ import os +import re from openpype.modules import IHostAddon, OpenPypeModule +from openpype.widgets.message_window import Window UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -19,6 +21,18 @@ class UnrealAddon(OpenPypeModule, IHostAddon): from .lib import get_compatible_integration + pattern = re.compile(r'^\d+-\d+$') + + if not pattern.match(app.name): + Window( + parent=None, + title="Unreal application name format", + message="Unreal application name must be in format '5-0' or '5-1'", + level="critical") + raise ValueError( + "Unreal application name must be in format '5-0' or '5-1'" + ) + ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" From 142ae35d9b81b5325c462b67cc870c959a801524 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 12:13:00 +0100 Subject: [PATCH 777/918] Hound fixes --- openpype/hosts/unreal/addon.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index fddceb00a8..16f6fcf27c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,14 +24,13 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): + msg = "Unreal application name must be in format '5-0' or '5-1'" Window( parent=None, title="Unreal application name format", - message="Unreal application name must be in format '5-0' or '5-1'", + message=msg, level="critical") - raise ValueError( - "Unreal application name must be in format '5-0' or '5-1'" - ) + raise ValueError(msg) ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( From 11728bae0e4f1a539d5959ec3cfe9fe523fe356a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:02:05 +0800 Subject: [PATCH 778/918] roy's comment and uses import instead of from..import --- openpype/hosts/nuke/startup/custom_popup.py | 17 ++++------------- .../defaults/project_settings/nuke.json | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index 57d79f99ff..7f60fdc70b 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -23,10 +23,7 @@ def get_main_window(): class CustomScriptDialog(QtWidgets.QDialog): - """A Popup that moves itself to bottom right of screen on show event. - - The UI contains a message label and a red highlighted button to "show" - or perform another custom action from this pop-up. + """A Custom Popup For Nuke Read Node """ @@ -95,7 +92,7 @@ class CustomScriptDialog(QtWidgets.QDialog): "cancel": cancel } # Signals - has_selection.toggled.connect(self.emit_click_with_state) + has_selection.toggled.connect(self.on_checked_changed) line_edit.textChanged.connect(self.on_line_edit_changed) button.clicked.connect(self._on_clicked) cancel.clicked.connect(self.close) @@ -104,10 +101,9 @@ class CustomScriptDialog(QtWidgets.QDialog): self.setWindowTitle("Custom Popup") def update_values(self): - self.widgets["selection"].isChecked() + return self.widgets["selection"].isChecked() - def emit_click_with_state(self): - """Emit the on_clicked signal with the toggled state""" + def on_checked_changed(self): checked = self.widgets["selection"].isChecked() return checked @@ -164,11 +160,6 @@ class CustomScriptDialog(QtWidgets.QDialog): return False - def showEvent(self, event): - # Position popup based on contents on show event - return super(CustomScriptDialog, self).showEvent(event) - - @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 287d13e5c9..c116540a99 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range(Read Node)", - "command": "from openpype.hosts.nuke.startup import custom_popup;from openpype.hosts.nuke.startup.custom_popup import get_main_window;custom_popup.CustomScriptDialog(parent=get_main_window()).show();", + "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 803dd565751bb65a6ef5b97e7c6f5447f4534717 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:03:18 +0800 Subject: [PATCH 779/918] hound fix --- openpype/hosts/nuke/startup/custom_popup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index 7f60fdc70b..76d5a25596 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -160,6 +160,7 @@ class CustomScriptDialog(QtWidgets.QDialog): return False + @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) From 183b2866d9e1f1f7b611fa6860c1edc315798b66 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:14:57 +0800 Subject: [PATCH 780/918] style fix --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c116540a99..35e5b1975c 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -226,7 +226,7 @@ { "type": "action", "sourcetype": "python", - "title": "Set Frame Range(Read Node)", + "title": "Set Frame Range (Read Node)", "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", "tooltip": "Set Frame Range for Read Node(s)" } From 0635c39a37094bf4f78898a9a179fbf657baf021 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 14:32:24 +0100 Subject: [PATCH 781/918] Improved error message --- openpype/hosts/unreal/addon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 16f6fcf27c..ed23950b35 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,7 +24,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): - msg = "Unreal application name must be in format '5-0' or '5-1'" + msg = ( + "Unreal application key in the settings must be in format" + "'5-0' or '5-1'" + ) Window( parent=None, title="Unreal application name format", From 396486067bd4c33f0ac6ef75f0676e05b9b6c985 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 13:43:50 +0000 Subject: [PATCH 782/918] [Automated] Release --- CHANGELOG.md | 335 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 337 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a33904735b..ec6544e659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,341 @@ # Changelog +## [3.15.9](https://github.com/ynput/OpenPype/tree/3.15.9) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.8...3.15.9) + +### **🆕 New features** + + +
+Blender: Implemented Loading of Alembic Camera #4990 + +Implemented loading of Alembic cameras in Blender. + + +___ + +
+ + +
+Unreal: Implemented Creator, Loader and Extractor for Levels #5008 + +Creator, Loader and Extractor for Unreal Levels have been implemented. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Blender: Added setting for base unit scale #4987 + +A setting for the base unit scale has been added for Blender.The unit scale is automatically applied when opening a file or creating a new one. + + +___ + +
+ + +
+Unreal: Changed naming and path of Camera Levels #5010 + +The levels created for the camera in Unreal now include `_camera` in the name, to be better identifiable, and are placed in the camera folder. + + +___ + +
+ + +
+Settings: Added option to nest settings templates #5022 + +It is possible to nest settings templates in another templates. + + +___ + +
+ + +
+Enhancement/publisher: Remove "hit play to continue" label on continue #5029 + +Remove "hit play to continue" message on continue so that it doesn't show anymore when play was clicked. + + +___ + +
+ + +
+Ftrack: Limit number of ftrack events to query at once #5033 + +Limit the amount of ftrack events received from mongo at once to 100. + + +___ + +
+ + +
+General: Small code cleanups #5034 + +Small code cleanup and updates. + + +___ + +
+ + +
+Global: collect frames to fix with settings #5036 + +Settings for `Collect Frames to Fix` will allow disable per project the plugin. Also `Rewriting latest version` attribute is hiddable from settings. + + +___ + +
+ + +
+General: Publish plugin apply settings can expect only project settings #5037 + +Only project settings are passed to optional `apply_settings` method, if the method expects only one argument. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Load Assembly fix invalid imports #4859 + +Refactors imports so they are now correct. + + +___ + +
+ + +
+Maya: Skipping rendersetup for members. #4973 + +When publishing a `rendersetup`, the objectset is and should be empty. + + +___ + +
+ + +
+Maya: Validate Rig Output IDs #5016 + +Absolute names of node were not used, so plugin did not fetch the nodes properly.Also missed pymel command. + + +___ + +
+ + +
+Deadline: escape rootless path in publish job #4910 + +If the publish path on Deadline job contains spaces or other characters, command was failing because the path wasn't properly escaped. This is fixing it. + + +___ + +
+ + +
+General: Company name and URL changed #4974 + +The current records were obsolete in inno_setup, changed to the up-to-date. +___ + +
+ + +
+Unreal: Fix usage of 'get_full_path' function #5014 + +This PR changes all the occurrences of `get_full_path` functions to alternatives to get the path of the objects. + + +___ + +
+ + +
+Unreal: Fix sequence frames validator to use correct data #5021 + +Fix sequence frames validator to use clipIn and clipOut data instead of frameStart and frameEnd. + + +___ + +
+ + +
+Unreal: Fix render instances collection to use correct data #5023 + +Fix render instances collection to use `frameStart` and `frameEnd` from the Project Manager, instead of the sequence's ones. + + +___ + +
+ + +
+Resolve: loader is opening even if no timeline in project #5025 + +Loader is opening now even no timeline is available in a project. + + +___ + +
+ + +
+nuke: callback for dirmapping is on demand #5030 + +Nuke was slowed down on processing due this callback. Since it is disabled by default it made sense to add it only on demand. + + +___ + +
+ + +
+Publisher: UI works with instances without label #5032 + +Publisher UI does not crash if instance don't have filled 'label' key in instance data. + + +___ + +
+ + +
+Publisher: Call explicitly prepared tab methods #5044 + +It is not possible to go to Create tab during publishing from OpenPype menu. + + +___ + +
+ + +
+Ftrack: Role names are not case sensitive in ftrack event server status action #5058 + +Event server status action is not case sensitive for role names of user. + + +___ + +
+ + +
+Publisher: Fix border widget #5063 + +Fixed border lines in Publisher UI to be painted correctly with correct indentation and size. + + +___ + +
+ + +
+Unreal: Fix Commandlet Project and Permissions #5066 + +Fix problem when creating an Unreal Project when Commandlet Project is in a protected location. + + +___ + +
+ + +
+Unreal: Added verification for Unreal app name format #5070 + +The Unreal app name is used to determine the Unreal version folder, so it is necessary that if follows the format `x-x`, where `x` is any integer. This PR adds a verification that the app name follows that format. + + +___ + +
+ +### **📃 Documentation** + + +
+Docs: Display wrong image in ExtractOIIOTranscode #5045 + +Wrong image display in `https://openpype.io/docs/project_settings/settings_project_global#extract-oiio-transcode`. + + +___ + +
+ +### **Merged pull requests** + + +
+Drop-down menu to list all families in create placeholder #4928 + +Currently in the create placeholder window, we need to write the family manually. This replace the text field by an enum field with all families for the current software. + + +___ + +
+ + +
+add sync to specific projects or listen only #4919 + +Extend kitsu sync service with additional arguments to sync specific projects. + + +___ + +
+ + + + ## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8) diff --git a/openpype/version.py b/openpype/version.py index 5c7105e7e0..dd23138dee 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.2" +__version__ = "3.15.9" diff --git a/pyproject.toml b/pyproject.toml index a72a3d66d7..633899d3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.8" # OpenPype +version = "3.15.9" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 776dcb5a6fcf65eec08f95655c870d7e43ec4ce9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 13:44:50 +0000 Subject: [PATCH 783/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0036e121b7..aa5b8decdc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9 - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.1 - 3.14.2 - 3.14.2-nightly.5 - - 3.14.2-nightly.4 validations: required: true - type: dropdown From ee41b877e666db4984d7525b05187b56d1e7b2b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 31 May 2023 16:10:17 +0200 Subject: [PATCH 784/918] refactor file rules logic to separate host activation This is implementing logic introduced here https://github.com/ynput/OpenPype/pull/4700#discussion_r1193612003 --- openpype/pipeline/colorspace.py | 12 ++++++------ .../defaults/project_settings/aftereffects.json | 2 +- .../settings/defaults/project_settings/blender.json | 2 +- .../defaults/project_settings/celaction.json | 2 +- .../settings/defaults/project_settings/flame.json | 2 +- .../settings/defaults/project_settings/fusion.json | 2 +- .../settings/defaults/project_settings/harmony.json | 2 +- .../settings/defaults/project_settings/hiero.json | 2 +- .../settings/defaults/project_settings/houdini.json | 2 +- openpype/settings/defaults/project_settings/max.json | 2 +- .../settings/defaults/project_settings/maya.json | 2 +- .../settings/defaults/project_settings/nuke.json | 2 +- .../defaults/project_settings/photoshop.json | 2 +- .../settings/defaults/project_settings/resolve.json | 2 +- .../defaults/project_settings/substancepainter.json | 2 +- .../defaults/project_settings/traypublisher.json | 2 +- .../settings/defaults/project_settings/tvpaint.json | 2 +- .../settings/defaults/project_settings/unreal.json | 2 +- .../defaults/project_settings/webpublisher.json | 2 +- .../schemas/template_imageio_file_rules.json | 4 ++-- 20 files changed, 26 insertions(+), 26 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 5af313c570..d4011d32c9 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -483,22 +483,22 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): frules_global = imageio_global["file_rules"] activate_global_rules = frules_global.get( "activate_global_file_rules", False) + global_rules = frules_global["rules"] if not activate_global_rules: log.info( "Colorspace global file rules are disabled." ) - return {} + global_rules = {} # host is optional, some might not have any settings frules_host = imageio_host.get("file_rules", {}) # compile file rules dictionary - override_global_rules = frules_host.get("override_global_rules") - if override_global_rules: - return frules_host["rules"] - else: - return frules_global["rules"] + activate_host_rules = frules_host.get("activate_host_rules") + + # return host rules if activated or global rules + return frules_host["rules"] if activate_host_rules else global_rules def get_remapped_colorspace_to_native( diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 1a312c27df..9be8a6e7d5 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 7cdbb0e6fb..eae5b239c8 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -11,7 +11,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/celaction.json b/openpype/settings/defaults/project_settings/celaction.json index 0e8b465118..af56a36649 100644 --- a/openpype/settings/defaults/project_settings/celaction.json +++ b/openpype/settings/defaults/project_settings/celaction.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index 19773727ca..5b4b62c140 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -9,7 +9,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} }, "project": { diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 822ec422df..0ee7d6127d 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index e6fb00a700..02f51d1d2b 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index 01eb15bfbc..9c83733b09 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} }, "workfile": { diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 2b7192ff99..a53f1ff202 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index db203f7f46..bfb1aa4aeb 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 14d4408138..19c3da13e6 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -416,7 +416,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} }, "workfile": { diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 5262694484..cdfc236d5c 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -15,7 +15,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} }, "viewer": { diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index ffcf87d8a5..71f94f5bfc 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -9,7 +9,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index f2d3727be1..da47ae2553 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -9,7 +9,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/substancepainter.json b/openpype/settings/defaults/project_settings/substancepainter.json index 4a1b86f3f4..4adeff98ef 100644 --- a/openpype/settings/defaults/project_settings/substancepainter.json +++ b/openpype/settings/defaults/project_settings/substancepainter.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": true, + "activate_host_rules": true, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 6f22f8a6ec..3a42c93515 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 3c930b84eb..1f4f468656 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 5adf1cce60..20e55c74f0 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/defaults/project_settings/webpublisher.json b/openpype/settings/defaults/project_settings/webpublisher.json index 17d61ef028..e451bcfc17 100644 --- a/openpype/settings/defaults/project_settings/webpublisher.json +++ b/openpype/settings/defaults/project_settings/webpublisher.json @@ -6,7 +6,7 @@ "filepath": [] }, "file_rules": { - "override_global_rules": false, + "activate_host_rules": false, "rules": {} } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json b/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json index 829fd02489..5c6c696578 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/template_imageio_file_rules.json @@ -7,8 +7,8 @@ "children": [ { "type": "boolean", - "key": "override_global_rules", - "label": "Override global File Rules" + "key": "activate_host_rules", + "label": "Activate Host File Rules" }, { "key": "rules", From 3955c466c3a0f01e20bf95bfa594b561884517e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 May 2023 17:17:35 +0200 Subject: [PATCH 785/918] fix apply settings on hiero loader (#5073) --- openpype/hosts/hiero/plugins/load/load_clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index 77844d2448..c9bebfa8b2 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -41,8 +41,8 @@ class LoadClip(phiero.SequenceLoader): clip_name_template = "{asset}_{subset}_{representation}" + @classmethod def apply_settings(cls, project_settings, system_settings): - plugin_type_settings = ( project_settings .get("hiero", {}) From a30ba51508f03c1adbac8c434432a80184d0665b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:11:30 +0800 Subject: [PATCH 786/918] use nukescript's panel --- openpype/hosts/nuke/startup/custom_popup.py | 174 ------------------ .../startup/ops_frame_setting_for_read.py | 46 +++++ .../defaults/project_settings/nuke.json | 2 +- 3 files changed, 47 insertions(+), 175 deletions(-) delete mode 100644 openpype/hosts/nuke/startup/custom_popup.py create mode 100644 openpype/hosts/nuke/startup/ops_frame_setting_for_read.py diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py deleted file mode 100644 index 76d5a25596..0000000000 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ /dev/null @@ -1,174 +0,0 @@ -import sys -import contextlib -import re -import nuke -from PySide2 import QtCore, QtWidgets - - -def get_main_window(): - """Acquire Nuke's main window""" - main_window = None - if main_window is None: - - top_widgets = QtWidgets.QApplication.topLevelWidgets() - name = "Foundry::UI::DockMainWindow" - for widget in top_widgets: - if ( - widget.inherits("QMainWindow") - and widget.metaObject().className() == name - ): - main_window = widget - break - return main_window - - -class CustomScriptDialog(QtWidgets.QDialog): - """A Custom Popup For Nuke Read Node - - """ - - on_clicked = QtCore.Signal() - on_line_changed = QtCore.Signal(str) - context = None - - def __init__(self, parent=None, *args, **kwargs): - super(CustomScriptDialog, self).__init__(parent=parent, - *args, - **kwargs) - self.setContentsMargins(0, 0, 0, 0) - - # Layout - layout = QtWidgets.QVBoxLayout(self) - frame_layout = QtWidgets.QHBoxLayout() - frame_layout.setContentsMargins(10, 5, 10, 10) - selection_layout = QtWidgets.QHBoxLayout() - selection_layout.setContentsMargins(10, 5, 10, 10) - button_layout = QtWidgets.QHBoxLayout() - button_layout.setContentsMargins(10, 5, 10, 10) - - # Increase spacing slightly for readability - frame_layout.setSpacing(10) - button_layout.setSpacing(10) - name = QtWidgets.QLabel("Frame Range: ") - name.setStyleSheet(""" - QLabel { - font-size: 12px; - } - """) - line_edit = QtWidgets.QLineEdit( - "%s-%s" % (nuke.root().firstFrame(), - nuke.root().lastFrame())) - selection_name = QtWidgets.QLabel("Use Selection") - selection_name.setStyleSheet(""" - QLabel { - font-size: 12px; - } - """) - has_selection = QtWidgets.QCheckBox() - button = QtWidgets.QPushButton("Execute") - button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - cancel = QtWidgets.QPushButton("Cancel") - cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - - frame_layout.addWidget(name) - frame_layout.addWidget(line_edit) - selection_layout.addWidget(selection_name) - selection_layout.addWidget(has_selection) - button_layout.addWidget(button) - button_layout.addWidget(cancel) - layout.addLayout(frame_layout) - layout.addLayout(selection_layout) - layout.addLayout(button_layout) - # Default size - self.resize(100, 40) - - self.widgets = { - "name": name, - "line_edit": line_edit, - "selection": has_selection, - "button": button, - "cancel": cancel - } - # Signals - has_selection.toggled.connect(self.on_checked_changed) - line_edit.textChanged.connect(self.on_line_edit_changed) - button.clicked.connect(self._on_clicked) - cancel.clicked.connect(self.close) - self.update_values() - # Set default title - self.setWindowTitle("Custom Popup") - - def update_values(self): - return self.widgets["selection"].isChecked() - - def on_checked_changed(self): - checked = self.widgets["selection"].isChecked() - return checked - - def set_name(self, name): - self.widgets['name'].setText(name) - - def set_line_edit(self, line_edit): - self.widgets['line_edit'].setText(line_edit) - print(line_edit) - - def setButtonText(self, text): - self.widgets["button"].setText(text) - - def setCancelText(self, text): - self.widgets["cancel"].setText(text) - - def on_line_edit_changed(self): - line_edit = self.widgets['line_edit'].text() - self.on_line_changed.emit(line_edit) - return self.set_line_edit(line_edit) - - def _on_clicked(self): - """Callback for when the 'show' button is clicked. - - Raises the parent (if any) - - """ - frame_range = self.widgets['line_edit'].text() - selected = self.widgets["selection"].isChecked() - pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" - match = re.match(pattern, frame_range) - frame_start = int(match.group("start")) - frame_end = int(match.group("end")) - if not nuke.allNodes("Read"): - self.close() - return - for read_node in nuke.allNodes("Read"): - if selected: - if not nuke.selectedNodes(): - self.close() - return - if read_node in nuke.selectedNodes(): - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - read_node["first"].setValue(frame_start) - read_node["last"].setValue(frame_end) - else: - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - read_node["first"].setValue(frame_start) - read_node["last"].setValue(frame_end) - - self.close() - - return False - - -@contextlib.contextmanager -def application(): - app = QtWidgets.QApplication(sys.argv) - yield - app.exec_() - - -if __name__ == "__main__": - with application(): - dialog = CustomScriptDialog() - dialog.show() diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py new file mode 100644 index 0000000000..153effc7ad --- /dev/null +++ b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py @@ -0,0 +1,46 @@ +import nuke +import nukescripts +import re + + +class FrameSettingsPanel(nukescripts.PythonPanel): + def __init__(self, node): + nukescripts.PythonPanel.__init__(self, 'Frame Range') + self.read_node = node + # CREATE KNOBS + self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % + (nuke.root().firstFrame(), nuke.root().lastFrame())) + self.selected = nuke.Boolean_Knob("selection") + # ADD KNOBS + self.addKnob(self.selected) + self.addKnob(self.range) + self.selected.setValue(False) + + def knobChanged(self, knob): + frame_range = self.range.value() + pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" + match = re.match(pattern, frame_range) + frame_start = int(match.group("start")) + frame_end = int(match.group("end")) + if not self.read_node: + return + for r in self.read_node: + if self.onchecked(): + if not nuke.selectedNodes(): + return + if r in nuke.selectedNodes(): + r["frame_mode"].setValue("start_at") + r["frame"].setValue(frame_range) + r["first"].setValue(frame_start) + r["last"].setValue(frame_end) + else: + r["frame_mode"].setValue("start_at") + r["frame"].setValue(frame_range) + r["first"].setValue(frame_start) + r["last"].setValue(frame_end) + + def onchecked(self): + if self.selected.value(): + return True + else: + return False diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 35e5b1975c..cb06ad0a3b 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range (Read Node)", - "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", + "command": "import ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 0655a6222bf3c69f4b4b3e9008c6c7803ffd3f94 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:12:22 +0800 Subject: [PATCH 787/918] hound fix --- openpype/hosts/nuke/startup/ops_frame_setting_for_read.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py index 153effc7ad..bf98ef83f6 100644 --- a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py +++ b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py @@ -9,11 +9,14 @@ class FrameSettingsPanel(nukescripts.PythonPanel): self.read_node = node # CREATE KNOBS self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % - (nuke.root().firstFrame(), nuke.root().lastFrame())) + (nuke.root().firstFrame(), + nuke.root().lastFrame())) self.selected = nuke.Boolean_Knob("selection") + self.info = nuke.Help_Knob("Instruction") # ADD KNOBS self.addKnob(self.selected) self.addKnob(self.range) + self.addKnob(self.info) self.selected.setValue(False) def knobChanged(self, knob): From aeaad018c1af7b30c9d1377d27ed91bec65e91cd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 31 May 2023 18:12:48 +0200 Subject: [PATCH 788/918] :rotating_light: make peace with the hound :dog: --- openpype/hosts/max/api/lib_renderproducts.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 81057db733..94b0aeb913 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -15,14 +15,12 @@ class RenderProducts(object): def __init__(self, project_settings=None): self._project_settings = project_settings or get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + legacy_io.Session["AVALON_PROJECT"]) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(render_dir, - container) + output_file = os.path.join(render_dir, container) setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa From 021ea8d6380e439ccddcdbf3ac78542b4329a7e1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:30:19 +0800 Subject: [PATCH 789/918] update the command --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index cb06ad0a3b..a0caa40396 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range (Read Node)", - "command": "import ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", + "command": "import openpype.hosts.nuke.startup.ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 717a2bc81c418a0d77cc7fd730bcb1d4ec18ae90 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Thu, 1 Jun 2023 01:04:10 +0300 Subject: [PATCH 790/918] update letterbox docs --- website/docs/project_settings/settings_project_global.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 7bd24a5773..5ddf247d98 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -170,12 +170,10 @@ A profile may generate multiple outputs from a single input. Each output must de - **`Letter Box`** - **Enabled** - Enable letter boxes - - **Ratio** - Ratio of letter boxes - - **Type** - **Letterbox** (horizontal bars) or **Pillarbox** (vertical bars) + - **Ratio** - Ratio of letter boxes. Ratio type is calculated from output image dimensions. If letterbox ratio > image ratio, _letterbox_ is applied. Otherwise _pillarbox_ will be rendered. - **Fill color** - Fill color of boxes (RGBA: 0-255) - **Line Thickness** - Line thickness on the edge of box (set to `0` to turn off) - - **Fill color** - Line color on the edge of box (RGBA: 0-255) - - **Example** + - **Line color** - Line color on the edge of box (RGBA: 0-255) ![global_extract_review_letter_box_settings](assets/global_extract_review_letter_box_settings.png) ![global_extract_review_letter_box](assets/global_extract_review_letter_box.png) From b048a4a40ea11661be95a61e5cf58076f0b404fe Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Thu, 1 Jun 2023 01:05:02 +0300 Subject: [PATCH 791/918] update letterbox screenshot --- ...bal_extract_review_letter_box_settings.png | Bin 6926 -> 27150 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png b/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png index 80e00702e6193dbac34e4976d78133c2ae413d0c..76dd9b372a03142c6849b6ce5347cd780a2415fe 100644 GIT binary patch literal 27150 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 From 5379679f869db99bf169263ca43bb6f068df9dbf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:26 +0000 Subject: [PATCH 792/918] update README.md [skip ci] --- README.md | 75 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 514ffb62c0..8757e3db92 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-) OpenPype ==== @@ -303,41 +303,44 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Milan Kolar

💻 📖 🚇 💼 🖋 🔍 🚧 📆 👀 🧑‍🏫 💬

Jakub Ježek

💻 📖 🚇 🖋 👀 🚧 🧑‍🏫 📆 💬

Ondřej Samohel

💻 📖 🚇 🖋 👀 🚧 🧑‍🏫 📆 💬

Jakub Trllo

💻 📖 🚇 👀 🚧 💬

Petr Kalis

💻 📖 🚇 👀 🚧 💬

64qam

💻 👀 📖 🚇 📆 🚧 🖋 📓

Roy Nieterau

💻 📖 👀 🧑‍🏫 💬

Toke Jepsen

💻 📖 👀 🧑‍🏫 💬

Jiri Sindelar

💻 👀 📖 🖋 📓

Simone Barbieri

💻 📖

karimmozilla

💻

Allan I. A.

💻

murphy

💻 👀 📓 📖 📆

Wijnand Koreman

💻

Bo Zhou

💻

Clément Hector

💻 👀

David Lai

💻 👀

Derek

💻 📖

Gábor Marinov

💻 📖

icyvapor

💻 📖

Jérôme LORRAIN

💻

David Morris-Oliveros

💻

BenoitConnan

💻

Malthaldar

💻

Sven Neve

💻

zafrs

💻

Félix David

💻 📖
Milan Kolar
Milan Kolar

💻 📖 🚇 💼 🖋 🔍 🚧 📆 👀 🧑‍🏫 💬
Jakub Ježek
Jakub Ježek

💻 📖 🚇 🖋 👀 🚧 🧑‍🏫 📆 💬
Ondřej Samohel
Ondřej Samohel

💻 📖 🚇 🖋 👀 🚧 🧑‍🏫 📆 💬
Jakub Trllo
Jakub Trllo

💻 📖 🚇 👀 🚧 💬
Petr Kalis
Petr Kalis

💻 📖 🚇 👀 🚧 💬
64qam
64qam

💻 👀 📖 🚇 📆 🚧 🖋 📓
Roy Nieterau
Roy Nieterau

💻 📖 👀 🧑‍🏫 💬
Toke Jepsen
Toke Jepsen

💻 📖 👀 🧑‍🏫 💬
Jiri Sindelar
Jiri Sindelar

💻 👀 📖 🖋 📓
Simone Barbieri
Simone Barbieri

💻 📖
karimmozilla
karimmozilla

💻
Allan I. A.
Allan I. A.

💻
murphy
murphy

💻 👀 📓 📖 📆
Wijnand Koreman
Wijnand Koreman

💻
Bo Zhou
Bo Zhou

💻
Clément Hector
Clément Hector

💻 👀
David Lai
David Lai

💻 👀
Derek
Derek

💻 📖
Gábor Marinov
Gábor Marinov

💻 📖
icyvapor
icyvapor

💻 📖
Jérôme LORRAIN
Jérôme LORRAIN

💻
David Morris-Oliveros
David Morris-Oliveros

💻
BenoitConnan
BenoitConnan

💻
Malthaldar
Malthaldar

💻
Sven Neve
Sven Neve

💻
zafrs
zafrs

💻
Félix David
Félix David

💻 📖
Alexey Bogomolov
Alexey Bogomolov

💻
From 28e9da8918b86d6ef30f2ffb549483edef3e7a24 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:27 +0000 Subject: [PATCH 793/918] update .all-contributorsrc [skip ci] --- .all-contributorsrc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b30f3b2499..60812cdb3c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,6 +1,6 @@ { "projectName": "OpenPype", - "projectOwner": "pypeclub", + "projectOwner": "ynput", "repoType": "github", "repoHost": "https://github.com", "files": [ @@ -319,8 +319,18 @@ "code", "doc" ] + }, + { + "login": "movalex", + "name": "Alexey Bogomolov", + "avatar_url": "https://avatars.githubusercontent.com/u/11698866?v=4", + "profile": "http://abogomolov.com", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, - "skipCi": true + "skipCi": true, + "commitType": "docs" } From eeaa79125ce111272e94e3f5c7f2d6c7f3b154f3 Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:06:08 +0100 Subject: [PATCH 794/918] refactor: use actual pymxs implementation --- .../hosts/max/plugins/load/load_model_fbx.py | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 01e6acae12..61101c482d 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -1,8 +1,5 @@ import os -from openpype.pipeline import ( - load, - get_representation_path -) +from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -24,10 +21,7 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Preserveinstances", True) - rt.importFile( - filepath, - rt.name("noPrompt"), - using=rt.FBXIMP) + rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP) container = rt.getNodeByName(f"{name}") if not container: @@ -38,7 +32,8 @@ class FbxModelLoader(load.LoaderPlugin): selection.Parent = container return containerise( - name, [container], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__ + ) def update(self, container, representation): from pymxs import runtime as rt @@ -46,24 +41,21 @@ class FbxModelLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) rt.select(node.Children) - fbx_reimport_cmd = ( - f""" -FBXImporterSetParam "Animation" false -FBXImporterSetParam "Cameras" false -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true -importFile @"{path}" #noPrompt using:FBXIMP - """) - rt.execute(fbx_reimport_cmd) + rt.FBXImporterSetParam("Animation", False) + rt.FBXImporterSetParam("Cameras", False) + rt.FBXImporterSetParam("AxisConversionMethod", True) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) with maintained_selection(): rt.select(node) - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) def switch(self, container, representation): self.update(container, representation) From e51967a6596efc2d0464728a4df9ac19d232995b Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:09:50 +0100 Subject: [PATCH 795/918] refactor: use correct pymxs --- .../hosts/max/plugins/load/load_pointcache.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index b3e12adc7b..5fb9772f87 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -5,9 +5,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. """ import os -from openpype.pipeline import ( - load, get_representation_path -) +from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib @@ -15,9 +13,7 @@ from openpype.hosts.max.api import lib class AbcLoader(load.LoaderPlugin): """Alembic loader.""" - families = ["camera", - "animation", - "pointcache"] + families = ["camera", "animation", "pointcache"] label = "Load Alembic" representations = ["abc"] order = -10 @@ -30,21 +26,17 @@ class AbcLoader(load.LoaderPlugin): file_path = os.path.normpath(self.fname) abc_before = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } - abc_export_cmd = (f""" -AlembicImport.ImportToRoot = false - -importFile @"{file_path}" #noPrompt - """) - - self.log.debug(f"Executing command: {abc_export_cmd}") - rt.execute(abc_export_cmd) + rt.AlembicImport.ImportToRoot = False + rt.importFile(file_path, rt.name("noPrompt")) abc_after = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } @@ -57,7 +49,8 @@ importFile @"{file_path}" #noPrompt abc_container = abc_containers.pop() return containerise( - name, [abc_container], context, loader=self.__class__.__name__) + name, [abc_container], context, loader=self.__class__.__name__ + ) def update(self, container, representation): from pymxs import runtime as rt @@ -69,9 +62,10 @@ importFile @"{file_path}" #noPrompt for alembic_object in alembic_objects: alembic_object.source = path - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) def switch(self, container, representation): self.update(container, representation) From 31b331811cf20c4a8b750d34d11a463aaf28d7ff Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:15:05 +0100 Subject: [PATCH 796/918] refactor: use proper pymxs --- openpype/hosts/max/plugins/load/load_model.py | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 95ee014e07..febcaed8be 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -1,8 +1,5 @@ - import os -from openpype.pipeline import ( - load, get_representation_path -) +from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -24,24 +21,20 @@ class ModelAbcLoader(load.LoaderPlugin): file_path = os.path.normpath(self.fname) abc_before = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } - abc_import_cmd = (f""" -AlembicImport.ImportToRoot = false -AlembicImport.CustomAttributes = true -AlembicImport.UVs = true -AlembicImport.VertexColors = true - -importFile @"{file_path}" #noPrompt - """) - - self.log.debug(f"Executing command: {abc_import_cmd}") - rt.execute(abc_import_cmd) + rt.AlembicImport.ImportToRoot = False + rt.AlembicImport.CustomAttributes = True + rt.AlembicImport.UVs = True + rt.AlembicImport.VertexColors = True + rt.importFile(filepath, rt.name("noPrompt")) abc_after = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } @@ -54,10 +47,12 @@ importFile @"{file_path}" #noPrompt abc_container = abc_containers.pop() return containerise( - name, [abc_container], context, loader=self.__class__.__name__) + name, [abc_container], context, loader=self.__class__.__name__ + ) def update(self, container, representation): from pymxs import runtime as rt + path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) rt.select(node.Children) @@ -76,9 +71,10 @@ importFile @"{file_path}" #noPrompt with maintained_selection(): rt.select(node) - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) def switch(self, container, representation): self.update(container, representation) From d4a807194ebb0971a629f608f3fc2ecf84723394 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:24:34 +0200 Subject: [PATCH 797/918] Resolve: Make sure scripts dir exists (#5078) * make sure scripts dir exists * use exist_ok in makedirs --- openpype/hosts/resolve/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 9a161f4865..1213fd9e7a 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -29,6 +29,9 @@ def setup(env): log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths)) log.info("Utility Scripts: `{}`".format(scripts)) + # Make sure scripts dir exists + os.makedirs(util_scripts_dir, exist_ok=True) + # make sure no script file is in folder for script in os.listdir(util_scripts_dir): path = os.path.join(util_scripts_dir, script) From 3fdbcd3247ad5bbd13230d17bdbec90f65b3985c Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:31:50 +0100 Subject: [PATCH 798/918] fix: incorrect var name --- openpype/hosts/max/plugins/load/load_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index febcaed8be..5f1ae3378e 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -30,7 +30,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.AlembicImport.CustomAttributes = True rt.AlembicImport.UVs = True rt.AlembicImport.VertexColors = True - rt.importFile(filepath, rt.name("noPrompt")) + rt.importFile(file_path, rt.name("noPrompt")) abc_after = { c From 711dd888e960fb3cdb5ce246fbcefb920ca1b217 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jun 2023 12:37:39 +0200 Subject: [PATCH 799/918] updating testing data --- tests/unit/openpype/pipeline/publish/test_publish_plugins.py | 2 +- tests/unit/openpype/pipeline/test_colorspace.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/openpype/pipeline/publish/test_publish_plugins.py b/tests/unit/openpype/pipeline/publish/test_publish_plugins.py index bbeab2cc90..aace8cf7e3 100644 --- a/tests/unit/openpype/pipeline/publish/test_publish_plugins.py +++ b/tests/unit/openpype/pipeline/publish/test_publish_plugins.py @@ -37,7 +37,7 @@ class TestPipelinePublishPlugins(TestPipeline): # files are the same as those used in `test_pipeline_colorspace` TEST_FILES = [ ( - "1YinxOToVyAd3-jAMFgVf7EWQa2x8Ma-O", + "1Lf-mFxev7xiwZCWfImlRcw7Fj8XgNQMh", "test_pipeline_colorspace.zip", "" ) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index d0981723ad..c22acee2d4 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -31,7 +31,7 @@ class TestPipelineColorspace(TestPipeline): TEST_FILES = [ ( - "1YinxOToVyAd3-jAMFgVf7EWQa2x8Ma-O", + "1Lf-mFxev7xiwZCWfImlRcw7Fj8XgNQMh", "test_pipeline_colorspace.zip", "" ) From aab6e19b5ed0f4f76335cea49342f098ed548319 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 1 Jun 2023 15:48:52 +0200 Subject: [PATCH 800/918] skip roots validation for documents only variant of the functions --- openpype/lib/project_backpack.py | 42 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/openpype/lib/project_backpack.py b/openpype/lib/project_backpack.py index 07107ec011..674eaa3b91 100644 --- a/openpype/lib/project_backpack.py +++ b/openpype/lib/project_backpack.py @@ -113,26 +113,29 @@ def pack_project( project_name )) - roots = project_doc["config"]["roots"] - # Determine root directory of project - source_root = None - source_root_name = None - for root_name, root_value in roots.items(): - if source_root is not None: - raise ValueError( - "Packaging is supported only for single root projects" - ) - source_root = root_value - source_root_name = root_name + root_path = None + source_root = {} + project_source_path = None + if not only_documents: + roots = project_doc["config"]["roots"] + # Determine root directory of project + source_root_name = None + for root_name, root_value in roots.items(): + if source_root is not None: + raise ValueError( + "Packaging is supported only for single root projects" + ) + source_root = root_value + source_root_name = root_name - root_path = source_root[platform.system().lower()] - print("Using root \"{}\" with path \"{}\"".format( - source_root_name, root_path - )) + root_path = source_root[platform.system().lower()] + print("Using root \"{}\" with path \"{}\"".format( + source_root_name, root_path + )) - project_source_path = os.path.join(root_path, project_name) - if not os.path.exists(project_source_path): - raise ValueError("Didn't find source of project files") + project_source_path = os.path.join(root_path, project_name) + if not os.path.exists(project_source_path): + raise ValueError("Didn't find source of project files") # Determine zip filepath where data will be stored if not destination_dir: @@ -273,8 +276,7 @@ def unpack_project( low_platform = platform.system().lower() project_name = metadata["project_name"] - source_root = metadata["root"] - root_path = source_root[low_platform] + root_path = metadata["root"].get(low_platform) # Drop existing collection replace_project_documents(project_name, docs, database_name) From abc266e9e5868277b47c2dabdd1697ec13d1a024 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jun 2023 16:14:29 +0200 Subject: [PATCH 801/918] refactor the script to be frame number rather then frame range --- .../startup/frame_setting_for_read_nodes.py | 47 ++++++++++++++++++ .../startup/ops_frame_setting_for_read.py | 49 ------------------- .../defaults/project_settings/nuke.json | 6 +-- 3 files changed, 50 insertions(+), 52 deletions(-) create mode 100644 openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py delete mode 100644 openpype/hosts/nuke/startup/ops_frame_setting_for_read.py diff --git a/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py new file mode 100644 index 0000000000..f0cbabe20f --- /dev/null +++ b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py @@ -0,0 +1,47 @@ +""" OpenPype custom script for resetting read nodes start frame values """ + +import nuke +import nukescripts + + +class FrameSettingsPanel(nukescripts.PythonPanel): + """ Frame Settings Panel """ + def __init__(self): + nukescripts.PythonPanel.__init__(self, "Set Frame Start (Read Node)") + + # create knobs + self.frame = nuke.Int_Knob( + 'frame', 'Frame Number') + self.selected = nuke.Boolean_Knob("selection") + # add knobs to panel + self.addKnob(self.selected) + self.addKnob(self.frame) + + # set values + self.selected.setValue(False) + self.frame.setValue(nuke.root().firstFrame()) + + def process(self): + """ Process the panel values. """ + # get values + frame = self.frame.value() + if self.selected.value(): + # selected nodes processing + if not nuke.selectedNodes(): + return + for rn_ in nuke.selectedNodes(): + if rn_.Class() != "Read": + continue + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + else: + # all nodes processing + for rn_ in nuke.allNodes(filter="Read"): + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + + +def main(): + p_ = FrameSettingsPanel() + if p_.showModalDialog(): + print(p_.process()) diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py deleted file mode 100644 index bf98ef83f6..0000000000 --- a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py +++ /dev/null @@ -1,49 +0,0 @@ -import nuke -import nukescripts -import re - - -class FrameSettingsPanel(nukescripts.PythonPanel): - def __init__(self, node): - nukescripts.PythonPanel.__init__(self, 'Frame Range') - self.read_node = node - # CREATE KNOBS - self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % - (nuke.root().firstFrame(), - nuke.root().lastFrame())) - self.selected = nuke.Boolean_Knob("selection") - self.info = nuke.Help_Knob("Instruction") - # ADD KNOBS - self.addKnob(self.selected) - self.addKnob(self.range) - self.addKnob(self.info) - self.selected.setValue(False) - - def knobChanged(self, knob): - frame_range = self.range.value() - pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" - match = re.match(pattern, frame_range) - frame_start = int(match.group("start")) - frame_end = int(match.group("end")) - if not self.read_node: - return - for r in self.read_node: - if self.onchecked(): - if not nuke.selectedNodes(): - return - if r in nuke.selectedNodes(): - r["frame_mode"].setValue("start_at") - r["frame"].setValue(frame_range) - r["first"].setValue(frame_start) - r["last"].setValue(frame_end) - else: - r["frame_mode"].setValue("start_at") - r["frame"].setValue(frame_range) - r["first"].setValue(frame_start) - r["last"].setValue(frame_end) - - def onchecked(self): - if self.selected.value(): - return True - else: - return False diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index a0caa40396..3f8be4c872 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -226,9 +226,9 @@ { "type": "action", "sourcetype": "python", - "title": "Set Frame Range (Read Node)", - "command": "import openpype.hosts.nuke.startup.ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", - "tooltip": "Set Frame Range for Read Node(s)" + "title": "Set Frame Start (Read Node)", + "command": "from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();", + "tooltip": "Set frame start for read node(s)" } ] }, From 8356dfac7e1150cbe31cc8a63f26cee0c0fe1dd0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jun 2023 16:41:13 +0200 Subject: [PATCH 802/918] py2 compatibility --- openpype/hosts/nuke/startup/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 openpype/hosts/nuke/startup/__init__.py diff --git a/openpype/hosts/nuke/startup/__init__.py b/openpype/hosts/nuke/startup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From d0886e43fe8efc8b675d9da5ccc9c37c459408d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 1 Jun 2023 16:42:42 +0200 Subject: [PATCH 803/918] fix doc --- website/docs/dev_blender.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/dev_blender.md b/website/docs/dev_blender.md index 228447fb64..bed0e4a09d 100644 --- a/website/docs/dev_blender.md +++ b/website/docs/dev_blender.md @@ -9,14 +9,14 @@ toc_max_heading_level: 4 In case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows: ```python -from openpype.hosts.blender.hooks.pre_add_run_python_script_arg import AddPythonScriptToLaunchArgs +from openpype.hosts.blender.hooks import pre_add_run_python_script_arg from openpype.lib import PreLaunchHook class MyHook(PreLaunchHook): """Add python script to be executed before Blender launch.""" - order = AddPythonScriptToLaunchArgs.order - 1 + order = pre_add_run_python_script_arg.AddPythonScriptToLaunchArgs.order - 1 app_groups = [ "blender", ] From bb74019d3e6acd86c536cfc88e43dc78e2ea6652 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 31 May 2023 14:15:21 +0200 Subject: [PATCH 804/918] removing info knob from nuke creators also remove node if instance is removed --- openpype/hosts/nuke/api/lib.py | 2 -- openpype/hosts/nuke/api/pipeline.py | 1 + openpype/hosts/nuke/api/plugin.py | 16 ---------------- .../hosts/nuke/plugins/create/create_backdrop.py | 2 -- .../hosts/nuke/plugins/create/create_camera.py | 2 -- .../hosts/nuke/plugins/create/create_gizmo.py | 2 -- .../hosts/nuke/plugins/create/create_model.py | 2 -- .../hosts/nuke/plugins/create/create_source.py | 2 +- .../nuke/plugins/create/create_write_image.py | 1 - .../plugins/create/create_write_prerender.py | 1 - .../nuke/plugins/create/create_write_render.py | 1 - 11 files changed, 2 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a439142051..4a57bc3165 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1403,8 +1403,6 @@ def create_write_node( # adding write to read button add_button_clear_rendered(GN, os.path.dirname(fpath)) - GN.addKnob(nuke.Text_Knob('', '')) - # set tile color tile_color = next( iter( diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 75b0f80d21..88f7144542 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -564,6 +564,7 @@ def remove_instance(instance): instance_node = instance.transient_data["node"] instance_knob = instance_node.knobs()[INSTANCE_DATA_KNOB] instance_node.removeKnob(instance_knob) + nuke.delete(instance_node) def select_instance(instance): diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 3566cb64c1..7035da2bb5 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -75,20 +75,6 @@ class NukeCreator(NewCreator): for pass_key in keys: creator_attrs[pass_key] = pre_create_data[pass_key] - def add_info_knob(self, node): - if "OP_info" in node.knobs().keys(): - return - - # add info text - info_knob = nuke.Text_Knob("OP_info", "") - info_knob.setValue(""" - -

This node is maintained by OpenPype Publisher.

-

To remove it use Publisher gui.

-
- """) - node.addKnob(info_knob) - def check_existing_subset(self, subset_name): """Make sure subset name is unique. @@ -153,8 +139,6 @@ class NukeCreator(NewCreator): created_node = nuke.createNode(node_type) created_node["name"].setValue(node_name) - self.add_info_knob(created_node) - for key, values in node_knobs.items(): if key in created_node.knobs(): created_node["key"].setValue(values) diff --git a/openpype/hosts/nuke/plugins/create/create_backdrop.py b/openpype/hosts/nuke/plugins/create/create_backdrop.py index ff415626be..52959bbef2 100644 --- a/openpype/hosts/nuke/plugins/create/create_backdrop.py +++ b/openpype/hosts/nuke/plugins/create/create_backdrop.py @@ -36,8 +36,6 @@ class CreateBackdrop(NukeCreator): created_node["note_font_size"].setValue(24) created_node["label"].setValue("[{}]".format(node_name)) - self.add_info_knob(created_node) - return created_node def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/nuke/plugins/create/create_camera.py b/openpype/hosts/nuke/plugins/create/create_camera.py index 5553645af6..b84280b11b 100644 --- a/openpype/hosts/nuke/plugins/create/create_camera.py +++ b/openpype/hosts/nuke/plugins/create/create_camera.py @@ -39,8 +39,6 @@ class CreateCamera(NukeCreator): created_node["name"].setValue(node_name) - self.add_info_knob(created_node) - return created_node def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/nuke/plugins/create/create_gizmo.py b/openpype/hosts/nuke/plugins/create/create_gizmo.py index e3ce70dd59..cbe2f635c9 100644 --- a/openpype/hosts/nuke/plugins/create/create_gizmo.py +++ b/openpype/hosts/nuke/plugins/create/create_gizmo.py @@ -40,8 +40,6 @@ class CreateGizmo(NukeCreator): created_node["name"].setValue(node_name) - self.add_info_knob(created_node) - return created_node def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/nuke/plugins/create/create_model.py b/openpype/hosts/nuke/plugins/create/create_model.py index 08a53abca2..a94c9f0313 100644 --- a/openpype/hosts/nuke/plugins/create/create_model.py +++ b/openpype/hosts/nuke/plugins/create/create_model.py @@ -40,8 +40,6 @@ class CreateModel(NukeCreator): created_node["name"].setValue(node_name) - self.add_info_knob(created_node) - return created_node def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/nuke/plugins/create/create_source.py b/openpype/hosts/nuke/plugins/create/create_source.py index 57504b5d53..8419c3ef33 100644 --- a/openpype/hosts/nuke/plugins/create/create_source.py +++ b/openpype/hosts/nuke/plugins/create/create_source.py @@ -32,7 +32,7 @@ class CreateSource(NukeCreator): read_node["tile_color"].setValue( int(self.node_color, 16)) read_node["name"].setValue(node_name) - self.add_info_knob(read_node) + return read_node def create(self, subset_name, instance_data, pre_create_data): diff --git a/openpype/hosts/nuke/plugins/create/create_write_image.py b/openpype/hosts/nuke/plugins/create/create_write_image.py index b74cea5dae..0c8adfb75c 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_image.py +++ b/openpype/hosts/nuke/plugins/create/create_write_image.py @@ -86,7 +86,6 @@ class CreateWriteImage(napi.NukeWriteCreator): "frame": nuke.frame() } ) - self.add_info_knob(created_node) self._add_frame_range_limit(created_node, instance_data) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 387768b1dd..f46dd2d6d5 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -74,7 +74,6 @@ class CreateWritePrerender(napi.NukeWriteCreator): "height": height } ) - self.add_info_knob(created_node) self._add_frame_range_limit(created_node) diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 09257f662e..c24405873a 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -66,7 +66,6 @@ class CreateWriteRender(napi.NukeWriteCreator): "height": height } ) - self.add_info_knob(created_node) self.integrate_links(created_node, outputs=False) From 47e5d3646046041e1cf55e55e51e0a5818360add Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:01:25 +0200 Subject: [PATCH 805/918] :recycle: incorporating comments --- .../houdini/plugins/publish/collect_arnold_rop.py | 15 +++++---------- .../houdini/plugins/publish/collect_karma_rop.py | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 2fd419ef9b..946eed3301 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -1,16 +1,12 @@ -import re import os +import re import hou import pyblish.api +from openpype.hosts.houdini.api import colorspace from openpype.hosts.houdini.api.lib import ( - evalParmNoFrame, - get_color_management_preferences -) -from openpype.hosts.houdini.api import ( - colorspace -) + evalParmNoFrame, get_color_management_preferences) class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): @@ -52,7 +48,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): } num_aovs = rop.evalParm("ar_aovs") - for index in range(num_aovs): + for index in range(1, num_aovs + 1): i = index + 1 # Skip disabled AOVs @@ -97,8 +93,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): # When AOV is explicitly defined in prefix we just swap it out # directly with the AOV suffix to embed it. # Note: ${AOV} seems to be evaluated in the parameter as %AOV% - has_aov_in_prefix = "%AOV%" in prefix - if has_aov_in_prefix: + if "%AOV%" in prefix: # It seems that when some special separator characters are present # before the %AOV% token that Redshift will secretly remove it if # there is no suffix for the current product, for example: diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index b87bb06767..a41e19d93f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -75,7 +75,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) - product_name = prefix_base + "." + suffix + ext + product_name = "{}.{}{}".format(prefix_base, suffix,ext) return product_name From 6de4ceabd38d84e4b410e5ad2cb795b5e34c1fa0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 23:13:34 +0800 Subject: [PATCH 806/918] custom settings for write node without publish and register --- .../nuke/startup/ops_write_node_no_publish.py | 67 +++++++++++++++++++ .../defaults/project_settings/nuke.json | 7 ++ 2 files changed, 74 insertions(+) create mode 100644 openpype/hosts/nuke/startup/ops_write_node_no_publish.py diff --git a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py new file mode 100644 index 0000000000..74be6c8de9 --- /dev/null +++ b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py @@ -0,0 +1,67 @@ +import os +import nuke +from pathlib import Path +from openpype.client import get_asset_by_name, get_project +from openpype.pipeline import Anatomy, legacy_io +from openpype.pipeline.template_data import get_template_data +from openpype.hosts.nuke.api.lib import ( + get_imageio_node_setting, + set_node_knobs_from_settings) + + +def main(): + project_name = legacy_io.Session["AVALON_PROJECT"] + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + # fetch asset docs + asset_doc = get_asset_by_name(project_name, asset_name) + + # get task type to fill the timer tag + # template = "{root[work]}/{project[name]}/{hierarchy}/{asset}" + anatomy = Anatomy(project_name) + project_doc = get_project(project_name) + template_data = get_template_data(project_doc, asset_doc) + template_data["root"] = anatomy.roots + template_data["task"] = {"name":task_name} + + padding = int( + anatomy.templates["render"]["frame_padding"] + ) + version_int = 0 + version_int += 1 + if version_int: + version_int += 1 + + node_settings = get_imageio_node_setting( + "Write", "CreateWriteRender", subset=None) + + ext = None + for knob in node_settings["knobs"]: + if knob["name"] == "file_type": + ext = knob["value"] + data = { + "asset": asset_name, + "task": task_name, + "subset": "non_publish_render", + "frame": "#" * padding, + "ext": ext + } + + write_selected_nodes = [ + s for s in nuke.selectedNodes() if s.Class() == "Write"] + + for i in range(len(write_selected_nodes)): + data.update({"version": i}) + data.update(template_data) + + anatomy_filled = anatomy.format(data) + folder = anatomy_filled["work"]["folder"] + render_folder = os.path.join(folder, "render_no_publish") + filename = anatomy_filled["render"]["file"] + file_path = os.path.join(render_folder, filename) + file_path = file_path.replace("\\", "/") + + knobs = node_settings["knobs"] + for w in write_selected_nodes: + w["file"].setValue(file_path) + set_node_knobs_from_settings(w, knobs) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index f01bdf7d50..ae37a14494 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -222,6 +222,13 @@ "title": "OpenPype Docs", "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", "tooltip": "Open the OpenPype Nuke user doc page" + }, + { + "type": "action", + "sourcetype": "python", + "title": "Set non publish output for Write Node", + "command": "from openpype.hosts.nuke.startup.ops_write_node_no_publish import main;main();", + "tooltip": "Open the OpenPype Nuke user doc page" } ] }, From 3c64fd3b748318cc7bb788d6b7bb2441c7caec29 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 23:17:55 +0800 Subject: [PATCH 807/918] hound fix --- openpype/hosts/nuke/startup/ops_write_node_no_publish.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py index 74be6c8de9..cc4c6ccd8f 100644 --- a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py +++ b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py @@ -1,6 +1,5 @@ import os import nuke -from pathlib import Path from openpype.client import get_asset_by_name, get_project from openpype.pipeline import Anatomy, legacy_io from openpype.pipeline.template_data import get_template_data @@ -48,11 +47,11 @@ def main(): } write_selected_nodes = [ - s for s in nuke.selectedNodes() if s.Class() == "Write"] + s for s in nuke.selectedNodes() if s.Class() == "Write"] for i in range(len(write_selected_nodes)): - data.update({"version": i}) - data.update(template_data) + data.update({"version": i}) + data.update(template_data) anatomy_filled = anatomy.format(data) folder = anatomy_filled["work"]["folder"] From 5da6e4b8d0cfcaacf17cbe2f1b37b7d2e271cdb6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 23:18:28 +0800 Subject: [PATCH 808/918] hound fix --- openpype/hosts/nuke/startup/ops_write_node_no_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py index cc4c6ccd8f..a56074e535 100644 --- a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py +++ b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py @@ -21,7 +21,7 @@ def main(): project_doc = get_project(project_name) template_data = get_template_data(project_doc, asset_doc) template_data["root"] = anatomy.roots - template_data["task"] = {"name":task_name} + template_data["task"] = {"name": task_name} padding = int( anatomy.templates["render"]["frame_padding"] From 7b454a92ceaa70ccab5990663055ae63ada38940 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:24:56 +0200 Subject: [PATCH 809/918] :bug: show arnold render settings --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 9634bf1bd9..3bb736995e 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -33,11 +33,6 @@ class CreateArnoldRop(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) - # Hide Properties Tab on Arnold ROP since that's used - # for rendering instead of .ass Archive Export - parm_template_group = instance_node.parmTemplateGroup() - parm_template_group.hideFolder("Properties", True) - instance_node.setParmTemplateGroup(parm_template_group) ext = pre_create_data.get("image_format") From 1818d061df127549bc18a70ba52692bd04983897 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:52:03 +0200 Subject: [PATCH 810/918] :rotating_light: fix hound --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 3bb736995e..bddf26dbd5 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -33,7 +33,6 @@ class CreateArnoldRop(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) - ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( From 15ce81e267903862ba9ad3cc440ef7cc72da889b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:56:56 +0200 Subject: [PATCH 811/918] :rotating_light: fix linter errors --- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index a41e19d93f..eabb1128d8 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -75,7 +75,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) - product_name = "{}.{}{}".format(prefix_base, suffix,ext) + product_name = "{}.{}{}".format(prefix_base, suffix, ext) return product_name From 4eaeb5682c643b9bc9842f042875b16bd069c84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 2 Jun 2023 00:17:37 +0200 Subject: [PATCH 812/918] Remove default windowFlags as it makes the publisher window not show minimize/maximize hints --- openpype/tools/publisher/window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 6ab444109e..006098cb37 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -66,8 +66,7 @@ class PublisherWindow(QtWidgets.QDialog): on_top_flag = QtCore.Qt.Dialog self.setWindowFlags( - self.windowFlags() - | QtCore.Qt.WindowTitleHint + QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowCloseButtonHint From 11db326cc52ce24849e851e2e0d73040c764f942 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 2 Jun 2023 13:27:57 +0800 Subject: [PATCH 813/918] clean up unused code --- openpype/hosts/nuke/startup/ops_write_node_no_publish.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py index a56074e535..12a69ff378 100644 --- a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py +++ b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py @@ -26,10 +26,6 @@ def main(): padding = int( anatomy.templates["render"]["frame_padding"] ) - version_int = 0 - version_int += 1 - if version_int: - version_int += 1 node_settings = get_imageio_node_setting( "Write", "CreateWriteRender", subset=None) From 27ae0d9f206ce1b36cb243ce409b7ceafe805bea Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 2 Jun 2023 17:11:16 +0800 Subject: [PATCH 814/918] refactor custom write node script --- .../hosts/nuke/startup/custom_write_node.py | 81 +++++++++++++++++++ .../nuke/startup/ops_write_node_no_publish.py | 62 -------------- .../defaults/project_settings/nuke.json | 2 +- 3 files changed, 82 insertions(+), 63 deletions(-) create mode 100644 openpype/hosts/nuke/startup/custom_write_node.py delete mode 100644 openpype/hosts/nuke/startup/ops_write_node_no_publish.py diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py new file mode 100644 index 0000000000..98751c2c7b --- /dev/null +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -0,0 +1,81 @@ +import os +import nuke +from pathlib import Path +from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings + + +frame_padding = 5 +temp_rendering_path_template = ( + "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}") + +knobs_setting = { + "knobs": [ + { + "type": "text", + "name": "file_type", + "value": "exr" + }, + { + "type": "text", + "name": "datatype", + "value": "16 bit half" + }, + { + "type": "text", + "name": "compression", + "value": "Zip (1 scanline)" + }, + { + "type": "bool", + "name": "autocrop", + "value": True + }, + { + "type": "color_gui", + "name": "tile_color", + "value": [ + 186, + 35, + 35, + 255 + ] + }, + { + "type": "text", + "name": "channels", + "value": "rgb" + }, + { + "type": "text", + "name": "colorspace", + "value": "linear" + }, + { + "type": "bool", + "name": "create_directories", + "value": True + } + ] +} + + +def main(): + write_selected_nodes = [ + s for s in nuke.selectedNodes() if s.Class() == "Write"] + + ext = None + knobs = knobs_setting["knobs"] + for knob in knobs: + if knob["name"] == "file_type": + ext = knob["value"] + for w in write_selected_nodes: + data = { + "work": os.getenv("AVALON_WORKDIR"), + "subset": w["name"].value(), + "frame": "#" * frame_padding, + "ext": ext + } + file_path = temp_rendering_path_template.format(**data) + file_path = file_path.replace("\\", "/") + w["file"].setValue(file_path) + set_node_knobs_from_settings(w, knobs) diff --git a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py b/openpype/hosts/nuke/startup/ops_write_node_no_publish.py deleted file mode 100644 index 12a69ff378..0000000000 --- a/openpype/hosts/nuke/startup/ops_write_node_no_publish.py +++ /dev/null @@ -1,62 +0,0 @@ -import os -import nuke -from openpype.client import get_asset_by_name, get_project -from openpype.pipeline import Anatomy, legacy_io -from openpype.pipeline.template_data import get_template_data -from openpype.hosts.nuke.api.lib import ( - get_imageio_node_setting, - set_node_knobs_from_settings) - - -def main(): - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - # fetch asset docs - asset_doc = get_asset_by_name(project_name, asset_name) - - # get task type to fill the timer tag - # template = "{root[work]}/{project[name]}/{hierarchy}/{asset}" - anatomy = Anatomy(project_name) - project_doc = get_project(project_name) - template_data = get_template_data(project_doc, asset_doc) - template_data["root"] = anatomy.roots - template_data["task"] = {"name": task_name} - - padding = int( - anatomy.templates["render"]["frame_padding"] - ) - - node_settings = get_imageio_node_setting( - "Write", "CreateWriteRender", subset=None) - - ext = None - for knob in node_settings["knobs"]: - if knob["name"] == "file_type": - ext = knob["value"] - data = { - "asset": asset_name, - "task": task_name, - "subset": "non_publish_render", - "frame": "#" * padding, - "ext": ext - } - - write_selected_nodes = [ - s for s in nuke.selectedNodes() if s.Class() == "Write"] - - for i in range(len(write_selected_nodes)): - data.update({"version": i}) - data.update(template_data) - - anatomy_filled = anatomy.format(data) - folder = anatomy_filled["work"]["folder"] - render_folder = os.path.join(folder, "render_no_publish") - filename = anatomy_filled["render"]["file"] - file_path = os.path.join(render_folder, filename) - file_path = file_path.replace("\\", "/") - - knobs = node_settings["knobs"] - for w in write_selected_nodes: - w["file"].setValue(file_path) - set_node_knobs_from_settings(w, knobs) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index ae37a14494..c2610591aa 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set non publish output for Write Node", - "command": "from openpype.hosts.nuke.startup.ops_write_node_no_publish import main;main();", + "command": "from openpype.hosts.nuke.startup.custom_write_node import main;main();", "tooltip": "Open the OpenPype Nuke user doc page" } ] From 22251f4958d1073ff7ba488ac5d2b3a988d284be Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 2 Jun 2023 17:13:08 +0800 Subject: [PATCH 815/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 98751c2c7b..9538c4f4bc 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -1,6 +1,5 @@ import os import nuke -from pathlib import Path from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings @@ -70,10 +69,10 @@ def main(): ext = knob["value"] for w in write_selected_nodes: data = { - "work": os.getenv("AVALON_WORKDIR"), - "subset": w["name"].value(), - "frame": "#" * frame_padding, - "ext": ext + "work": os.getenv("AVALON_WORKDIR"), + "subset": w["name"].value(), + "frame": "#" * frame_padding, + "ext": ext } file_path = temp_rendering_path_template.format(**data) file_path = file_path.replace("\\", "/") From 7075d5c4452dc0ea1c9f5addf3b96a75aef0ae55 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 2 Jun 2023 17:18:13 +0800 Subject: [PATCH 816/918] add some comment --- .../hooks/pre_add_run_python_script_arg.py | 55 ++++++++++++++++ openpype/hosts/max/api/lib.py | 9 ++- .../plugins/create/create_redshift_proxy.py | 18 ++++++ .../max/plugins/load/load_redshift_proxy.py | 63 +++++++++++++++++++ .../plugins/publish/extract_redshift_proxy.py | 62 ++++++++++++++++++ .../validate_renderer_redshift_proxy.py | 54 ++++++++++++++++ openpype/hosts/nuke/startup/__init__.py | 0 .../hosts/nuke/startup/custom_write_node.py | 1 + .../startup/frame_setting_for_read_nodes.py | 47 ++++++++++++++ openpype/hosts/resolve/api/workio.py | 19 +++--- .../hooks/pre_resolve_launch_last_workfile.py | 45 +++++++++++++ openpype/hosts/resolve/startup.py | 62 ++++++++++++++++++ .../openpype_startup.scriptlib | 21 +++++++ openpype/hosts/resolve/utils.py | 11 ++++ .../plugins/publish/collect_frames_fix.py | 62 +++++++++--------- .../defaults/project_settings/nuke.json | 7 +++ .../defaults/project_settings/resolve.json | 1 + .../schema_project_resolve.json | 5 ++ website/docs/artist_hosts_3dsmax.md | 14 ++--- website/docs/dev_blender.md | 61 ++++++++++++++++++ website/sidebars.js | 1 + 21 files changed, 573 insertions(+), 45 deletions(-) create mode 100644 openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py create mode 100644 openpype/hosts/max/plugins/create/create_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/load/load_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/publish/extract_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py create mode 100644 openpype/hosts/nuke/startup/__init__.py create mode 100644 openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py create mode 100644 openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py create mode 100644 openpype/hosts/resolve/startup.py create mode 100644 openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib create mode 100644 website/docs/dev_blender.md diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py new file mode 100644 index 0000000000..559e9ae0ce --- /dev/null +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -0,0 +1,55 @@ +from pathlib import Path + +from openpype.lib import PreLaunchHook + + +class AddPythonScriptToLaunchArgs(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + # Append after file argument + order = 15 + app_groups = [ + "blender", + ] + + def execute(self): + if not self.launch_context.data.get("python_scripts"): + return + + # Add path to workfile to arguments + for python_script_path in self.launch_context.data["python_scripts"]: + self.log.info( + f"Adding python script {python_script_path} to launch" + ) + # Test script path exists + python_script_path = Path(python_script_path) + if not python_script_path.exists(): + self.log.warning( + f"Python script {python_script_path} doesn't exist. " + "Skipped..." + ) + continue + + if "--" in self.launch_context.launch_args: + # Insert before separator + separator_index = self.launch_context.launch_args.index("--") + self.launch_context.launch_args.insert( + separator_index, + "-P", + ) + self.launch_context.launch_args.insert( + separator_index + 1, + python_script_path.as_posix(), + ) + else: + self.launch_context.launch_args.extend( + ["-P", python_script_path.as_posix()] + ) + + # Ensure separator + if "--" not in self.launch_context.launch_args: + self.launch_context.launch_args.append("--") + + self.launch_context.launch_args.extend( + [*self.launch_context.data.get("script_args", [])] + ) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index d9213863b1..e2af0720ec 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -128,7 +128,14 @@ def get_all_children(parent, node_type=None): def get_current_renderer(): - """get current renderer""" + """ + Notes: + Get current renderer for Max + + Returns: + "{Current Renderer}:{Current Renderer}" + e.g. "Redshift_Renderer:Redshift_Renderer" + """ return rt.renderers.production diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py new file mode 100644 index 0000000000..698ea82b69 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating camera.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateRedshiftProxy(plugin.MaxCreator): + identifier = "io.openpype.creators.max.redshiftproxy" + label = "Redshift Proxy" + family = "redshiftproxy" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + + _ = super(CreateRedshiftProxy, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py new file mode 100644 index 0000000000..31692f6367 --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -0,0 +1,63 @@ +import os +import clique + +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class RedshiftProxyLoader(load.LoaderPlugin): + """Load rs files with Redshift Proxy""" + + label = "Load Redshift Proxy" + families = ["redshiftproxy"] + representations = ["rs"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = self.filepath_from_context(context) + rs_proxy = rt.RedshiftProxy() + rs_proxy.file = filepath + files_in_folder = os.listdir(os.path.dirname(filepath)) + collections, remainder = clique.assemble(files_in_folder) + if collections: + rs_proxy.is_sequence = True + + container = rt.container() + container.name = name + rs_proxy.Parent = container + + asset = rt.getNodeByName(name) + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + for children in node.Children: + children_node = rt.getNodeByName(children.name) + for proxy in children_node.Children: + proxy.file = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py new file mode 100644 index 0000000000..3b44099609 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -0,0 +1,62 @@ +import os +import pyblish.api +from openpype.pipeline import publish +from pymxs import runtime as rt +from openpype.hosts.max.api import maintained_selection + + +class ExtractRedshiftProxy(publish.Extractor): + """ + Extract Redshift Proxy with rsProxy + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract RedShift Proxy" + hosts = ["max"] + families = ["redshiftproxy"] + + def process(self, instance): + container = instance.data["instance_node"] + start = int(instance.context.data.get("frameStart")) + end = int(instance.context.data.get("frameEnd")) + + self.log.info("Extracting Redshift Proxy...") + stagingdir = self.staging_dir(instance) + rs_filename = "{name}.rs".format(**instance.data) + rs_filepath = os.path.join(stagingdir, rs_filename) + rs_filepath = rs_filepath.replace("\\", "/") + + rs_filenames = self.get_rsfiles(instance, start, end) + + with maintained_selection(): + # select and export + con = rt.getNodeByName(container) + rt.select(con.Children) + # Redshift rsProxy command + # rsProxy fp selected compress connectivity startFrame endFrame + # camera warnExisting transformPivotToOrigin + rt.rsProxy(rs_filepath, 1, 0, 0, start, end, 0, 1, 1) + + self.log.info("Performing Extraction ...") + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'rs', + 'ext': 'rs', + 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + stagingdir)) + + def get_rsfiles(self, instance, startFrame, endFrame): + rs_filenames = [] + rs_name = instance.data["name"] + for frame in range(startFrame, endFrame + 1): + rs_filename = "%s.%04d.rs" % (rs_name, frame) + rs_filenames.append(rs_filename) + + return rs_filenames diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py new file mode 100644 index 0000000000..bc82f82f3b --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt +from openpype.pipeline.publish import RepairAction +from openpype.hosts.max.api.lib import get_current_renderer + + +class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): + """ + Validates Redshift as the current renderer for creating + Redshift Proxy + """ + + order = pyblish.api.ValidatorOrder + families = ["redshiftproxy"] + hosts = ["max"] + label = "Redshift Renderer" + actions = [RepairAction] + + def process(self, instance): + invalid = self.get_redshift_renderer(instance) + if invalid: + raise PublishValidationError("Please install Redshift for 3dsMax" + " before using the Redshift proxy instance") # noqa + invalid = self.get_current_renderer(instance) + if invalid: + raise PublishValidationError("The Redshift proxy extraction" + "discontinued since the current renderer is not Redshift") # noqa + + def get_redshift_renderer(self, instance): + invalid = list() + max_renderers_list = str(rt.RendererClass.classes) + if "Redshift_Renderer" not in max_renderers_list: + invalid.append(max_renderers_list) + + return invalid + + def get_current_renderer(self, instance): + invalid = list() + renderer_class = get_current_renderer() + current_renderer = str(renderer_class).split(":")[0] + if current_renderer != "Redshift_Renderer": + invalid.append(current_renderer) + + return invalid + + @classmethod + def repair(cls, instance): + for Renderer in rt.RendererClass.classes: + renderer = Renderer() + if "Redshift_Renderer" in str(renderer): + rt.renderers.production = renderer + break diff --git a/openpype/hosts/nuke/startup/__init__.py b/openpype/hosts/nuke/startup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 9538c4f4bc..eaf9cf86f7 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -68,6 +68,7 @@ def main(): if knob["name"] == "file_type": ext = knob["value"] for w in write_selected_nodes: + # data for mapping the path data = { "work": os.getenv("AVALON_WORKDIR"), "subset": w["name"].value(), diff --git a/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py new file mode 100644 index 0000000000..f0cbabe20f --- /dev/null +++ b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py @@ -0,0 +1,47 @@ +""" OpenPype custom script for resetting read nodes start frame values """ + +import nuke +import nukescripts + + +class FrameSettingsPanel(nukescripts.PythonPanel): + """ Frame Settings Panel """ + def __init__(self): + nukescripts.PythonPanel.__init__(self, "Set Frame Start (Read Node)") + + # create knobs + self.frame = nuke.Int_Knob( + 'frame', 'Frame Number') + self.selected = nuke.Boolean_Knob("selection") + # add knobs to panel + self.addKnob(self.selected) + self.addKnob(self.frame) + + # set values + self.selected.setValue(False) + self.frame.setValue(nuke.root().firstFrame()) + + def process(self): + """ Process the panel values. """ + # get values + frame = self.frame.value() + if self.selected.value(): + # selected nodes processing + if not nuke.selectedNodes(): + return + for rn_ in nuke.selectedNodes(): + if rn_.Class() != "Read": + continue + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + else: + # all nodes processing + for rn_ in nuke.allNodes(filter="Read"): + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + + +def main(): + p_ = FrameSettingsPanel() + if p_.showModalDialog(): + print(p_.process()) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 5ce73eea53..5966fa6a43 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -43,18 +43,22 @@ def open_file(filepath): """ Loading project """ + + from . import bmdvr + pm = get_project_manager() + page = bmdvr.GetCurrentPage() + if page is not None: + # Save current project only if Resolve has an active page, otherwise + # we consider Resolve being in a pre-launch state (no open UI yet) + project = pm.GetCurrentProject() + print(f"Saving current project: {project}") + pm.SaveProject() + file = os.path.basename(filepath) fname, _ = os.path.splitext(file) dname, _ = fname.split("_v") - - # deal with current project - project = pm.GetCurrentProject() - log.info(f"Test `pm`: {pm}") - pm.SaveProject() - try: - log.info(f"Test `dname`: {dname}") if not set_project_manager_to_folder_name(dname): raise # load project from input path @@ -72,6 +76,7 @@ def open_file(filepath): return False return True + def current_file(): pm = get_project_manager() current_dir = os.getenv("AVALON_WORKDIR") diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py new file mode 100644 index 0000000000..0e27ddb8c3 --- /dev/null +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -0,0 +1,45 @@ +import os + +from openpype.lib import PreLaunchHook +import openpype.hosts.resolve + + +class ResolveLaunchLastWorkfile(PreLaunchHook): + """Special hook to open last workfile for Resolve. + + Checks 'start_last_workfile', if set to False, it will not open last + workfile. This property is set explicitly in Launcher. + """ + + # Execute after workfile template copy + order = 10 + app_groups = ["resolve"] + + def execute(self): + if not self.data.get("start_last_workfile"): + self.log.info("It is set to not start last workfile on start.") + return + + last_workfile = self.data.get("last_workfile_path") + if not last_workfile: + self.log.warning("Last workfile was not collected.") + return + + if not os.path.exists(last_workfile): + self.log.info("Current context does not have any workfile yet.") + return + + # Add path to launch environment for the startup script to pick up + self.log.info(f"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch " + f"last workfile: {last_workfile}") + key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" + self.launch_context.env[key] = last_workfile + + # Set the openpype prelaunch startup script path for easy access + # in the LUA .scriptlib code + op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__) + script_path = os.path.join(op_resolve_root, "startup.py") + key = "OPENPYPE_RESOLVE_STARTUP_SCRIPT" + self.launch_context.env[key] = script_path + self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: " + f"{script_path}") diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py new file mode 100644 index 0000000000..79a64e0fbf --- /dev/null +++ b/openpype/hosts/resolve/startup.py @@ -0,0 +1,62 @@ +"""This script is used as a startup script in Resolve through a .scriptlib file + +It triggers directly after the launch of Resolve and it's recommended to keep +it optimized for fast performance since the Resolve UI is actually interactive +while this is running. As such, there's nothing ensuring the user isn't +continuing manually before any of the logic here runs. As such we also try +to delay any imports as much as possible. + +This code runs in a separate process to the main Resolve process. + +""" +import os + +import openpype.hosts.resolve.api + + +def ensure_installed_host(): + """Install resolve host with openpype and return the registered host. + + This function can be called multiple times without triggering an + additional install. + """ + from openpype.pipeline import install_host, registered_host + host = registered_host() + if host: + return host + + install_host(openpype.hosts.resolve.api) + return registered_host() + + +def launch_menu(): + print("Launching Resolve OpenPype menu..") + ensure_installed_host() + openpype.hosts.resolve.api.launch_pype_menu() + + +def open_file(path): + # Avoid the need to "install" the host + host = ensure_installed_host() + host.open_file(path) + + +def main(): + # Open last workfile + workfile_path = os.environ.get("OPENPYPE_RESOLVE_OPEN_ON_LAUNCH") + if workfile_path: + open_file(workfile_path) + else: + print("No last workfile set to open. Skipping..") + + # Launch OpenPype menu + from openpype.settings import get_project_settings + from openpype.pipeline.context_tools import get_current_project_name + project_name = get_current_project_name() + settings = get_project_settings(project_name) + if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True): + launch_menu() + + +if __name__ == "__main__": + main() diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib new file mode 100644 index 0000000000..324c82d6b7 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -0,0 +1,21 @@ +-- Run OpenPype's Python launch script for resolve +function file_exists(name) + local f = io.open(name, "r") + return f ~= nil and io.close(f) +end + + +openpype_startup_script = os.getenv("OPENPYPE_RESOLVE_STARTUP_SCRIPT") +if openpype_startup_script ~= nil then + script = fusion:MapPath(openpype_startup_script) + + if file_exists(script) then + -- We must use RunScript to ensure it runs in a separate + -- process to Resolve itself to avoid a deadlock for + -- certain imports of OpenPype libraries or Qt + print("Running launch script: " .. script) + fusion:RunScript(script) + else + print("Launch script not found at: " .. script) + end +end diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 9a161f4865..5e3003862f 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -29,6 +29,9 @@ def setup(env): log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths)) log.info("Utility Scripts: `{}`".format(scripts)) + # Make sure scripts dir exists + os.makedirs(util_scripts_dir, exist_ok=True) + # make sure no script file is in folder for script in os.listdir(util_scripts_dir): path = os.path.join(util_scripts_dir, script) @@ -50,6 +53,14 @@ def setup(env): src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) + + # TODO: Make this a less hacky workaround + if script == "openpype_startup.scriptlib": + # Handle special case for scriptlib that needs to be a folder + # up from the Comp folder in the Fusion scripts + dst = os.path.join(os.path.dirname(util_scripts_dir), + script) + log.info("Copying `{}` to `{}`...".format(src, dst)) if os.path.isdir(src): shutil.copytree( diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index 837738eb06..86e727b053 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -35,41 +35,47 @@ class CollectFramesFixDef( rewrite_version = attribute_values.get("rewrite_version") - if frames_to_fix: - instance.data["frames_to_fix"] = frames_to_fix + if not frames_to_fix: + return - subset_name = instance.data["subset"] - asset_name = instance.data["asset"] + instance.data["frames_to_fix"] = frames_to_fix - project_entity = instance.data["projectEntity"] - project_name = project_entity["name"] + subset_name = instance.data["subset"] + asset_name = instance.data["asset"] - version = get_last_version_by_subset_name(project_name, - subset_name, - asset_name=asset_name) - if not version: - self.log.warning("No last version found, " - "re-render not possible") - return + project_entity = instance.data["projectEntity"] + project_name = project_entity["name"] - representations = get_representations(project_name, - version_ids=[version["_id"]]) - published_files = [] - for repre in representations: - if repre["context"]["family"] not in self.families: - continue + version = get_last_version_by_subset_name( + project_name, + subset_name, + asset_name=asset_name + ) + if not version: + self.log.warning( + "No last version found, re-render not possible" + ) + return - for file_info in repre.get("files"): - published_files.append(file_info["path"]) + representations = get_representations( + project_name, version_ids=[version["_id"]] + ) + published_files = [] + for repre in representations: + if repre["context"]["family"] not in self.families: + continue - instance.data["last_version_published_files"] = published_files - self.log.debug("last_version_published_files::{}".format( - instance.data["last_version_published_files"])) + for file_info in repre.get("files"): + published_files.append(file_info["path"]) - if rewrite_version: - instance.data["version"] = version["name"] - # limits triggering version validator - instance.data.pop("latestVersion") + instance.data["last_version_published_files"] = published_files + self.log.debug("last_version_published_files::{}".format( + instance.data["last_version_published_files"])) + + if self.rewrite_version_enable and rewrite_version: + instance.data["version"] = version["name"] + # limits triggering version validator + instance.data.pop("latestVersion") @classmethod def get_attribute_defs(cls): diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c2610591aa..791e95a9f3 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -223,6 +223,13 @@ "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", "tooltip": "Open the OpenPype Nuke user doc page" }, + { + "type": "action", + "sourcetype": "python", + "title": "Set Frame Start (Read Node)", + "command": "from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();", + "tooltip": "Set frame start for read node(s)" + }, { "type": "action", "sourcetype": "python", diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 264f3bd902..56efa78e89 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -1,4 +1,5 @@ { + "launch_openpype_menu_on_start": false, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index b326f22394..6f98bdd3bd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -5,6 +5,11 @@ "label": "DaVinci Resolve", "is_file": true, "children": [ + { + "type": "boolean", + "key": "launch_openpype_menu_on_start", + "label": "Launch OpenPype menu on start of Resolve" + }, { "key": "imageio", "type": "dict", diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index 12c1f40181..fffab8ca5d 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -30,7 +30,7 @@ By clicking the icon ```OpenPype Menu``` rolls out. Choose ```OpenPype Menu > Launcher``` to open the ```Launcher``` window. -When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** +When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** and finally **run 3dsmax by its icon** in the tools. ![Menu OpenPype](assets/3dsmax_tray_OP.png) @@ -65,13 +65,13 @@ If not any workfile present simply hit ```Save As``` and keep ```Subversion``` e ![Save As Dialog](assets/3dsmax_SavingFirstFile_OP.png) -OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like +OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like ```workfileName_v001``` ```workfileName_v002``` - etc. + etc. Basically meaning user is free of guessing what is the correct naming and other necessities to keep everything in order and managed. @@ -105,13 +105,13 @@ Before proceeding further please check [Glossary](artist_concepts.md) and [What ### Intro -Current OpenPype integration (ver 3.15.0) supports only ```PointCache``` and ```Camera``` families now. +Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Camera```, ```Geometry``` and ```Redshift Proxy``` families now. **Pointcache** family being basically any geometry outputted as Alembic cache (.abc) format **Camera** family being 3dsmax Camera object with/without animation outputted as native .max, FBX, Alembic format - +**Redshift Proxy** family being Redshift Proxy object with/without animation outputted as rs format(Redshift Proxy's very own format) --- :::note Work in progress @@ -119,7 +119,3 @@ This part of documentation is still work in progress. ::: ## ...to be added - - - - diff --git a/website/docs/dev_blender.md b/website/docs/dev_blender.md new file mode 100644 index 0000000000..bed0e4a09d --- /dev/null +++ b/website/docs/dev_blender.md @@ -0,0 +1,61 @@ +--- +id: dev_blender +title: Blender integration +sidebar_label: Blender integration +toc_max_heading_level: 4 +--- + +## Run python script at launch +In case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows: + +```python +from openpype.hosts.blender.hooks import pre_add_run_python_script_arg +from openpype.lib import PreLaunchHook + + +class MyHook(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + order = pre_add_run_python_script_arg.AddPythonScriptToLaunchArgs.order - 1 + app_groups = [ + "blender", + ] + + def execute(self): + self.launch_context.data.setdefault("python_scripts", []).append( + "/path/to/my_script.py" + ) +``` + +You can write a bare python script, as you could run into the [Text Editor](https://docs.blender.org/manual/en/latest/editors/text_editor.html). + +### Python script with arguments +#### Adding arguments +In case you need to pass arguments to your script, you can append them to `self.launch_context.data["script_args"]`: + +```python +self.launch_context.data.setdefault("script_args", []).append( + "--my-arg", + "value", + ) +``` + +#### Parsing arguments +You can parse arguments in your script using [argparse](https://docs.python.org/3/library/argparse.html) as follows: + +```python +import argparse + +parser = argparse.ArgumentParser( + description="Parsing arguments for my_script.py" +) +parser.add_argument( + "--my-arg", + nargs="?", + help="My argument", +) +args, unknown = arg_parser.parse_known_args( + sys.argv[sys.argv.index("--") + 1 :] +) +print(args.my_arg) +``` diff --git a/website/sidebars.js b/website/sidebars.js index 4874782197..267cc7f6d7 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -180,6 +180,7 @@ module.exports = { ] }, "dev_deadline", + "dev_blender", "dev_colorspace" ] }; From 59b9745deb40e89f8d86251abadf0d614e65ef11 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 2 Jun 2023 17:30:06 +0800 Subject: [PATCH 817/918] Jakub's comment --- openpype/hosts/nuke/startup/custom_write_node.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index eaf9cf86f7..d9313231d8 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -44,11 +44,6 @@ knobs_setting = { "name": "channels", "value": "rgb" }, - { - "type": "text", - "name": "colorspace", - "value": "linear" - }, { "type": "bool", "name": "create_directories", From e64779b3450e66f43bf87a43efb97a177d94360c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 Jun 2023 15:07:55 +0200 Subject: [PATCH 818/918] fix restart arguments in tray (#5085) --- openpype/tools/tray/pype_tray.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 2f3b5251f9..fdc0a8094d 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -633,10 +633,10 @@ class TrayManager: # Create a copy of sys.argv additional_args = list(sys.argv) - # Check last argument from `get_openpype_execute_args` - # - when running from code it is the same as first from sys.argv - if args[-1] == additional_args[0]: - additional_args.pop(0) + # Remove first argument from 'sys.argv' + # - when running from code the first argument is 'start.py' + # - when running from build the first argument is executable + additional_args.pop(0) cleanup_additional_args = False if use_expected_version: @@ -663,7 +663,6 @@ class TrayManager: additional_args = _additional_args args.extend(additional_args) - run_detached_process(args, env=envs) self.exit() From 69297d0b687693f0b29e751a4e38b562c336e969 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 2 Jun 2023 14:40:20 +0100 Subject: [PATCH 819/918] cmds.ls returns list --- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index f4a4a44344..74ca27ff3c 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -33,7 +33,7 @@ def preserve_modelpanel_cameras(container, log=None): panel_cameras = {} for panel in cmds.getPanel(type="modelPanel"): cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True), - long=True) + long=True)[0] # Often but not always maya returns the transform from the # modelPanel as opposed to the camera shape, so we convert it From 51a9d6b73ce9d6f7760a8e8265219cb1db2f8b8a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 2 Jun 2023 14:46:30 +0100 Subject: [PATCH 820/918] Improve error feedback when no renderable cameras exist --- .../maya/plugins/publish/collect_arnold_scene_source.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index 0845f653b1..eda7efa244 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -35,6 +35,11 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): # camera. cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] + if not renderable: + raise ValueError( + "No renderable cameraes found, which is required for " + "publishing ASS." + ) camera = renderable[0] for node in instance.data["contentMembers"]: camera_shapes = cmds.listRelatives( From aa7dceb79c59f7aafb1909de8ee0c5af56465c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 2 Jun 2023 18:04:22 +0200 Subject: [PATCH 821/918] Add 'user' details on workfile manager details pane tab for Unix platform --- openpype/tools/workfiles/window.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 31ecf50d3b..34f7f24b02 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -1,6 +1,7 @@ import os import datetime import copy +import platform from qtpy import QtCore, QtWidgets, QtGui from openpype.client import ( @@ -94,6 +95,18 @@ class SidePanelWidget(QtWidgets.QWidget): self._on_note_change() self.save_clicked.emit() + def get_user_name(self, file): + """Get user name from file path""" + # Only run on Unix because pwd module is not available on Windows. + # NOTE: we tried adding "win32security" module but it was not working + # on all hosts so we decided to just support Linux until migration + # to Ayon + if platform.system() != "Windows": + import pwd + + filestat = os.stat(file) + return pwd.getpwuid(filestat.st_uid).pw_name + def set_context(self, asset_id, task_name, filepath, workfile_doc): # Check if asset, task and file are selected # NOTE workfile document is not requirement @@ -134,7 +147,9 @@ class SidePanelWidget(QtWidgets.QWidget): "Created:", creation_time.strftime(datetime_format), "Modified:", - modification_time.strftime(datetime_format) + modification_time.strftime(datetime_format), + "User:", + self.get_user_name(filepath), ) self._details_input.appendHtml("
".join(lines)) From 9c4e1ad4b1cd7938e007a65e7e50f87224e56763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 2 Jun 2023 18:08:28 +0200 Subject: [PATCH 822/918] Only add User details if platform isn't windows --- openpype/tools/workfiles/window.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 34f7f24b02..afaf7b9967 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -148,9 +148,12 @@ class SidePanelWidget(QtWidgets.QWidget): creation_time.strftime(datetime_format), "Modified:", modification_time.strftime(datetime_format), - "User:", - self.get_user_name(filepath), ) + if platform.system() != "Windows": + lines += ( + "User:", + self.get_user_name(filepath), + ) self._details_input.appendHtml("
".join(lines)) def get_workfile_data(self): From 4b6059339e251218e4c5817551d44c3d28e7c056 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 3 Jun 2023 03:24:55 +0000 Subject: [PATCH 823/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index dd23138dee..b55ca42244 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9" +__version__ = "3.15.10-nightly.1" From 5b662ecd20e65893bbf4dfe6121e5d9b5c60aa29 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Jun 2023 03:25:35 +0000 Subject: [PATCH 824/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index aa5b8decdc..3406ca8b65 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.10-nightly.1 - 3.15.9 - 3.15.9-nightly.2 - 3.15.9-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.2 - 3.14.3-nightly.1 - 3.14.2 - - 3.14.2-nightly.5 validations: required: true - type: dropdown From d37ccb4718e3fa9299d098844ddb0a92c1a09552 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 5 Jun 2023 10:26:42 +0200 Subject: [PATCH 825/918] :recycle: resolve few conversations --- .../houdini/plugins/publish/collect_arnold_rop.py | 15 ++++++--------- .../publish/submit_houdini_render_deadline.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 946eed3301..614785487f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -49,16 +49,14 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): num_aovs = rop.evalParm("ar_aovs") for index in range(1, num_aovs + 1): - i = index + 1 - # Skip disabled AOVs - if not rop.evalParm("ar_enable_aov%s" % i): + if not rop.evalParm("ar_enable_aovP{}".format(index)): continue - if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): - label = rop.evalParm("ar_aov_exr_layer_name%s" % i) + if rop.evalParm("ar_aov_exr_enable_layer_name{}".format(index)): + label = rop.evalParm("ar_aov_exr_layer_name{}".format(index)) else: - label = evalParmNoFrame(rop, "ar_aov_label%s" % i) + label = evalParmNoFrame(rop, "ar_aov_label{}".format(index)) aov_product = self.get_render_product_name(default_prefix, suffix=label) @@ -67,10 +65,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): aov_product) for product in render_products: - self.log.debug("Found render product: %s" % product) + self.log.debug("Found render product: {}".format(product)) - filenames = list(render_products) - instance.data["files"] = filenames + instance.data["files"] = list(render_products) instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 6a62ee0ea8..254914a850 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -55,7 +55,7 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): filepath = context.data["currentFile"] filename = os.path.basename(filepath) - job_info.Name = "%s - %s" % (filename, instance.name) + job_info.Name = "{} - {}".format(filename, instance.name) job_info.BatchName = filename job_info.Plugin = "Houdini" job_info.UserName = context.data.get( From d46cee554d929d434859f38daf0660021dff53dc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 5 Jun 2023 17:03:23 +0800 Subject: [PATCH 826/918] fix the bug of not being able to use repair action --- .../maya/plugins/publish/validate_arnold_scene_source_cbid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index e27723e104..8ce76c8d04 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -70,5 +70,5 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - for content_node, proxy_node in cls.get_invalid_couples(cls, instance): - lib.set_id(proxy_node, lib.get_id(content_node), overwrite=False) + for content_node, proxy_node in cls.get_invalid_couples(instance): + lib.set_id(proxy_node, lib.get_id(content_node), overwrite=True) From c12e6995dea9e7e2b0929f4790affbe0e8ca62d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 5 Jun 2023 14:49:58 +0200 Subject: [PATCH 827/918] Update openpype/tools/workfiles/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/workfiles/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index afaf7b9967..907598e9df 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -149,10 +149,11 @@ class SidePanelWidget(QtWidgets.QWidget): "Modified:", modification_time.strftime(datetime_format), ) - if platform.system() != "Windows": + username = self.get_user_name(filepath) + if username: lines += ( "User:", - self.get_user_name(filepath), + username, ) self._details_input.appendHtml("
".join(lines)) From 7c70ea968dc604f27cb4f0519fcef5eb297df3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 5 Jun 2023 15:07:35 +0200 Subject: [PATCH 828/918] Update openpype/tools/workfiles/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/workfiles/window.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 907598e9df..5bf6df35ca 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -101,11 +101,12 @@ class SidePanelWidget(QtWidgets.QWidget): # NOTE: we tried adding "win32security" module but it was not working # on all hosts so we decided to just support Linux until migration # to Ayon - if platform.system() != "Windows": - import pwd + if platform.system().lower() == "window": + return None + import pwd - filestat = os.stat(file) - return pwd.getpwuid(filestat.st_uid).pw_name + filestat = os.stat(file) + return pwd.getpwuid(filestat.st_uid).pw_name def set_context(self, asset_id, task_name, filepath, workfile_doc): # Check if asset, task and file are selected From bde15953595e57c30ea8b39becbdb4b81b9e537d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 5 Jun 2023 15:17:10 +0200 Subject: [PATCH 829/918] Update openpype/tools/workfiles/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/workfiles/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 5bf6df35ca..53f8894665 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -101,7 +101,7 @@ class SidePanelWidget(QtWidgets.QWidget): # NOTE: we tried adding "win32security" module but it was not working # on all hosts so we decided to just support Linux until migration # to Ayon - if platform.system().lower() == "window": + if platform.system().lower() == "windows": return None import pwd From a6d5b23fa765dd298b1183a4c0c0a843c5b10b62 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 5 Jun 2023 15:13:37 +0100 Subject: [PATCH 830/918] Update openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/collect_arnold_scene_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index eda7efa244..d72a428624 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -37,7 +37,7 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] if not renderable: raise ValueError( - "No renderable cameraes found, which is required for " + "No renderable cameras found, which is required for " "publishing ASS." ) camera = renderable[0] From 791dd3ee6eabb4a4cb5ce77c4c366b61c5e92a3b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 5 Jun 2023 15:21:39 +0100 Subject: [PATCH 831/918] Debug logging instead of error --- .../publish/collect_arnold_scene_source.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index d72a428624..b7fa9bb6f9 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -35,18 +35,16 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): # camera. cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] - if not renderable: - raise ValueError( - "No renderable cameras found, which is required for " - "publishing ASS." - ) - camera = renderable[0] - for node in instance.data["contentMembers"]: - camera_shapes = cmds.listRelatives( - node, shapes=True, type="camera" - ) - if camera_shapes: - camera = node - instance.data["camera"] = camera + if renderable: + camera = renderable[0] + for node in instance.data["contentMembers"]: + camera_shapes = cmds.listRelatives( + node, shapes=True, type="camera" + ) + if camera_shapes: + camera = node + instance.data["camera"] = camera + else: + self.log.debug("No renderable cameraes found.") self.log.debug("data: {}".format(instance.data)) From 4fa079c312bafb037e163c53e7e98e4a80d796ae Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 5 Jun 2023 15:42:06 +0100 Subject: [PATCH 832/918] Update openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- .../hosts/maya/plugins/publish/collect_arnold_scene_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index b7fa9bb6f9..f160a3a0c5 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -45,6 +45,6 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): camera = node instance.data["camera"] = camera else: - self.log.debug("No renderable cameraes found.") + self.log.debug("No renderable cameras found.") self.log.debug("data: {}".format(instance.data)) From 99fbc6dc3537115734b23b718cd04b050723495f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 6 Jun 2023 21:43:22 +0800 Subject: [PATCH 833/918] refactor the redshiftproxy family --- openpype/hosts/max/plugins/create/create_redshift_proxy.py | 7 ------- .../hosts/max/plugins/publish/extract_redshift_proxy.py | 5 ++--- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 698ea82b69..6eb59f0a73 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -9,10 +9,3 @@ class CreateRedshiftProxy(plugin.MaxCreator): label = "Redshift Proxy" family = "redshiftproxy" icon = "gear" - - def create(self, subset_name, instance_data, pre_create_data): - - _ = super(CreateRedshiftProxy, self).create( - subset_name, - instance_data, - pre_create_data) # type: CreatedInstance diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 3b44099609..8f76eef5ec 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection +from openpype.hosts.max.api import get_all_children, maintained_selection class ExtractRedshiftProxy(publish.Extractor): @@ -30,8 +30,7 @@ class ExtractRedshiftProxy(publish.Extractor): with maintained_selection(): # select and export - con = rt.getNodeByName(container) - rt.select(con.Children) + rt.select(get_all_children(rt.getNodeByName(container))) # Redshift rsProxy command # rsProxy fp selected compress connectivity startFrame endFrame # camera warnExisting transformPivotToOrigin From a4e2b18636aa77fbc538fb2493169ce9b19261c1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 6 Jun 2023 21:47:33 +0800 Subject: [PATCH 834/918] remove duplicated imported function --- openpype/hosts/max/plugins/load/load_model.py | 1 - openpype/hosts/max/plugins/load/load_model_fbx.py | 1 - openpype/hosts/max/plugins/load/load_pointcache.py | 2 -- 3 files changed, 4 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 0ec94ab074..58c6d3c889 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -3,7 +3,6 @@ from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection -from openpype.pipeline import get_representation_path, load class ModelAbcLoader(load.LoaderPlugin): diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index ee7d04d5eb..663f79f9f5 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -3,7 +3,6 @@ from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection -from openpype.pipeline import get_representation_path, load class FbxModelLoader(load.LoaderPlugin): diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 8a51e86000..cadbe7cac2 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -6,10 +6,8 @@ Because of limited api, alembics can be only loaded, but not easily updated. """ import os from openpype.pipeline import load, get_representation_path - from openpype.hosts.max.api import lib, maintained_selection from openpype.hosts.max.api.pipeline import containerise -from openpype.pipeline import get_representation_path, load class AbcLoader(load.LoaderPlugin): From a98ef68549d090a738615f1cefdbf3cbeac22c95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 6 Jun 2023 16:49:34 +0200 Subject: [PATCH 835/918] fixing publisher parent --- openpype/hosts/nuke/api/pipeline.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 88f7144542..e7c1c0ba0e 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -240,12 +240,14 @@ def _install_menu(): menu.addCommand( "Create...", lambda: host_tools.show_publisher( + parent=main_window, tab="create" ) ) menu.addCommand( "Publish...", lambda: host_tools.show_publisher( + parent=main_window, tab="publish" ) ) From bb56d5dc16f73f15c502281516361385cc4d06d8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 6 Jun 2023 16:49:48 +0200 Subject: [PATCH 836/918] fixing nuke settings default --- .../settings/defaults/project_settings/nuke.json | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 3f8be4c872..8eca824a6a 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -148,7 +148,7 @@ }, { "plugins": [ - "CreateWriteStill" + "CreateWriteImage" ], "nukeNodeClass": "Write", "knobs": [ @@ -556,15 +556,7 @@ "load": { "LoadImage": { "enabled": true, - "_representations": [ - "exr", - "dpx", - "jpg", - "jpeg", - "png", - "psd", - "tiff" - ], + "_representations": [], "node_name_template": "{class_name}_{ext}" }, "LoadClip": { From 69dc5d85f9fe02a2d8de261ed52eacc87287e7c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 6 Jun 2023 16:59:42 +0200 Subject: [PATCH 837/918] OP-6145 - make prerender check safer In Nuke is correctly `prerendere.farm` in families, which wasn't handled here. Eventually this query should be simplified only to `prerender.farm`, but this way it is safer for now. --- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 87b4ca64f4..590acf86c2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -825,7 +825,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): ).format(source)) family = "render" - if "prerender" in instance.data["families"]: + if ("prerender" in instance.data["families"] or + "prerender.farm" in instance.data["families"]): family = "prerender" families = [family] From fbb6afe5c370101918b77e54ec8aeeec6ab9409a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 6 Jun 2023 17:17:42 +0200 Subject: [PATCH 838/918] adding psd to IMAGE_EXTENSIONS constant --- openpype/lib/transcoding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 57968b3700..de6495900e 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -51,7 +51,7 @@ IMAGE_EXTENSIONS = { ".jng", ".jpeg", ".jpeg-ls", ".jpeg", ".2000", ".jpg", ".xr", ".jpeg", ".xt", ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd", ".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", - ".pictor", ".png", ".psb", ".psp", ".qtvr", ".ras", + ".pictor", ".png", ".psd", ".psb", ".psp", ".qtvr", ".ras", ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep", ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf", ".xpm", ".xwd" From 754b48ebe27000105bbb3bf5f9cbd51112b0a9f2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 6 Jun 2023 23:43:59 +0800 Subject: [PATCH 839/918] resolve the conflict --- .../hosts/resolve/utility_scripts/openpype_startup.scriptlib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib index 324c82d6b7..ec9b30a18d 100644 --- a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -18,4 +18,4 @@ if openpype_startup_script ~= nil then else print("Launch script not found at: " .. script) end -end +end \ No newline at end of file From 29c0b8a12a4dcf1ded4c492d63d950766c94b22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 6 Jun 2023 18:51:44 +0200 Subject: [PATCH 840/918] Sort actions by label if it exists instead of name (#5106) --- openpype/tools/launcher/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 3aa6c5d8cb..63ffcc9365 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -273,7 +273,7 @@ class ActionModel(QtGui.QStandardItemModel): # Sort by order and name return sorted( compatible, - key=lambda action: (action.order, action.name) + key=lambda action: (action.order, lib.get_action_label(action)) ) def update_force_not_open_workfile_settings(self, is_checked, action_id): From e0d95d1348a127d16be69411a916d410dd015824 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 7 Jun 2023 03:27:59 +0000 Subject: [PATCH 841/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index b55ca42244..868664c601 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.10-nightly.1" +__version__ = "3.15.10-nightly.2" From 65e5aa40cf87cde18d4ab99598c43486d85ab4b2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 7 Jun 2023 03:28:46 +0000 Subject: [PATCH 842/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3406ca8b65..e614d2fa65 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.10-nightly.2 - 3.15.10-nightly.1 - 3.15.9 - 3.15.9-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.3 - 3.14.3-nightly.2 - 3.14.3-nightly.1 - - 3.14.2 validations: required: true - type: dropdown From b3cab6a88b1fd2c8e3eb3606bfc4d5f8cafa10cb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 7 Jun 2023 11:34:59 +0200 Subject: [PATCH 843/918] adding parent to publisher only if nuke higher then 14 --- openpype/hosts/nuke/api/__init__.py | 4 +++- openpype/hosts/nuke/api/lib.py | 11 +++++++++++ openpype/hosts/nuke/api/pipeline.py | 12 +++++++++--- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index 1af5ff365d..55c4b8c808 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -43,7 +43,8 @@ from .lib import ( get_node_data, set_node_data, update_node_data, - create_write_node + create_write_node, + get_app_version_info ) from .utils import ( colorspace_exists_on_node, @@ -90,6 +91,7 @@ __all__ = ( "set_node_data", "update_node_data", "create_write_node", + "get_app_version_info", "colorspace_exists_on_node", "get_colorspace_list" diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 4a57bc3165..bf66b9e1a9 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3116,3 +3116,14 @@ def get_viewer_config_from_string(input_string): ).format(input_string)) return (display, viewer) + + +def get_app_version_info(): + """ Return dict of Nuke's version semantic info""" + dot_split = nuke.NUKE_VERSION_STRING.split(".") + v_spit = dot_split[1].split("v") + return { + "major": int(dot_split[0]), + "minor": int(v_spit[0]), + "patch": int(v_spit[1]) + } diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index e7c1c0ba0e..998c03b3dd 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -43,7 +43,8 @@ from .lib import ( add_scripts_menu, add_scripts_gizmo, get_node_data, - set_node_data + set_node_data, + get_app_version_info ) from .workfile_template_builder import ( NukePlaceholderLoadPlugin, @@ -218,6 +219,7 @@ def _install_menu(): main_window = get_main_window() menubar = nuke.menu("Nuke") menu = menubar.addMenu(MENU_LABEL) + app_version = get_app_version_info() if not ASSIST: label = "{0}, {1}".format( @@ -237,17 +239,21 @@ def _install_menu(): menu.addSeparator() if not ASSIST: + # only add parent if nuke version is 14 or higher + # known issue with no solution yet menu.addCommand( "Create...", lambda: host_tools.show_publisher( - parent=main_window, + parent=main_window if app_version["major"] >= 14 else None, tab="create" ) ) + # only add parent if nuke version is 14 or higher + # known issue with no solution yet menu.addCommand( "Publish...", lambda: host_tools.show_publisher( - parent=main_window, + parent=main_window if app_version["major"] >= 14 else None, tab="publish" ) ) From 4c4eeb29bed247982edad6d402aaf5903996be01 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 8 Jun 2023 00:04:41 +0800 Subject: [PATCH 844/918] connect custom write node script to the OP setting --- .../hosts/nuke/startup/custom_write_node.py | 99 +++++++++++++++---- 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index d9313231d8..f2244b84c5 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -1,6 +1,11 @@ +""" OpenPype custom script for setting up write nodes for non-publish """ import os import nuke -from openpype.hosts.nuke.api.lib import set_node_knobs_from_settings +import nukescripts +from openpype.hosts.nuke.api.lib import ( + set_node_knobs_from_settings, + get_nuke_imageio_settings +) frame_padding = 5 @@ -53,24 +58,76 @@ knobs_setting = { } -def main(): - write_selected_nodes = [ - s for s in nuke.selectedNodes() if s.Class() == "Write"] +class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): + """ Write Node's Knobs Settings Panel """ + def __init__(self): + nukescripts.PythonPanel.__init__(self, "Set Knobs Value(Write Node)") - ext = None - knobs = knobs_setting["knobs"] - for knob in knobs: - if knob["name"] == "file_type": - ext = knob["value"] - for w in write_selected_nodes: - # data for mapping the path - data = { - "work": os.getenv("AVALON_WORKDIR"), - "subset": w["name"].value(), - "frame": "#" * frame_padding, - "ext": ext - } - file_path = temp_rendering_path_template.format(**data) - file_path = file_path.replace("\\", "/") - w["file"].setValue(file_path) - set_node_knobs_from_settings(w, knobs) + knobs_value = self.get_node_knobs_override() + # create knobs + + self.typeKnob = nuke.Enumeration_Knob( + 'override_subsets', 'override subsets', knobs_value) + # add knobs to panel + self.addKnob(self.typeKnob) + + def process(self): + """ Process the panel values. """ + write_selected_nodes = [ + s for s in nuke.selectedNodes() if s.Class() == "Write"] + + node_knobs = self.typeKnob.value() + ext = None + knobs = None + if node_knobs: + knobs = self.get_node_knobs_setting(node_knobs) + if not knobs: + nuke.message("No knobs value found in subset group..\nDefault setting will be used..") # noqa + knobs = knobs_setting["knobs"] + else: + knobs = knobs_setting["knobs"] + + for knob in knobs: + if knob["name"] == "file_type": + ext = knob["value"] + for w in write_selected_nodes: + # data for mapping the path + data = { + "work": os.getenv("AVALON_WORKDIR"), + "subset": w["name"].value(), + "frame": "#" * frame_padding, + "ext": ext + } + file_path = temp_rendering_path_template.format(**data) + file_path = file_path.replace("\\", "/") + w["file"].setValue(file_path) + set_node_knobs_from_settings(w, knobs) + + def get_node_knobs_setting(self, value): + settings = [ + node for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] + ] + if not settings: + return + for i, setting in enumerate(settings): + if value in settings[i]["subsets"]: + return settings[i]["knobs"] + + def get_node_knobs_override(self): + knobs_value = [] + settings = [ + node for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] + ] + if not settings: + return + + for setting in settings: + if setting["nukeNodeClass"] == "Write" and setting["subsets"]: + for knob in setting["subsets"]: + knobs_value.append(knob) + return knobs_value + +def main(): + p_ = WriteNodeKnobSettingPanel() + if p_.showModalDialog(): + print(p_.process()) From 5dd066804bbc42e5fc1f1cc722e6bf69fc2643b4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 8 Jun 2023 00:07:12 +0800 Subject: [PATCH 845/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index f2244b84c5..66095409a6 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -105,18 +105,20 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): def get_node_knobs_setting(self, value): settings = [ - node for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] + node + for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] ] if not settings: return - for i, setting in enumerate(settings): + for i, _ in enumerate(settings): if value in settings[i]["subsets"]: return settings[i]["knobs"] def get_node_knobs_override(self): knobs_value = [] settings = [ - node for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] + node + for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] ] if not settings: return @@ -127,6 +129,7 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): knobs_value.append(knob) return knobs_value + def main(): p_ = WriteNodeKnobSettingPanel() if p_.showModalDialog(): From f423ebcfcbeadacc12a8af5ff5a5e32f13c1a1e2 Mon Sep 17 00:00:00 2001 From: Oscar Domingo Date: Thu, 8 Jun 2023 10:40:46 +0100 Subject: [PATCH 846/918] Keep `publisher.create_widget` variant when creating subsets Whenever a person is creating a subset to publish, the "creator" widget resets (where you choose the variant, product, etc.) so if the person is publishing several images of the a variant which is not the default one, they have to keep selecting the correct one after every "create". This commit resets the original variant upon successful creation of a subset for publishing. --- openpype/tools/publisher/widgets/create_widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/widgets/create_widget.py b/openpype/tools/publisher/widgets/create_widget.py index 30980af03d..b7605b1188 100644 --- a/openpype/tools/publisher/widgets/create_widget.py +++ b/openpype/tools/publisher/widgets/create_widget.py @@ -828,6 +828,7 @@ class CreateWidget(QtWidgets.QWidget): if success: self._set_creator(self._selected_creator) + self.variant_input.setText(variant) self._controller.emit_card_message("Creation finished...") self._last_thumbnail_path = None self._thumbnail_widget.set_current_thumbnails() From b24b4a9d5f6123f7fa096aba81bdc71bff726282 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 8 Jun 2023 11:54:41 +0200 Subject: [PATCH 847/918] Loader: Hide inactive versions in UI (#5100) * Function 'get_last_versions' have active filter * filter in active versions in loader --- openpype/client/entities.py | 20 +++++++++++++++----- openpype/tools/loader/model.py | 1 + openpype/tools/utils/delegates.py | 12 ++++++++---- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 8004dc3019..adbdd7a47c 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -855,12 +855,13 @@ def get_output_link_versions(project_name, version_id, fields=None): return conn.find(query_filter, _prepare_fields(fields)) -def get_last_versions(project_name, subset_ids, fields=None): +def get_last_versions(project_name, subset_ids, active=None, fields=None): """Latest versions for entered subset_ids. Args: project_name (str): Name of project where to look for queried entities. subset_ids (Iterable[Union[str, ObjectId]]): List of subset ids. + active (Optional[bool]): If True only active versions are returned. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. @@ -899,12 +900,21 @@ def get_last_versions(project_name, subset_ids, fields=None): if name_needed: group_item["name"] = {"$last": "$name"} + aggregate_filter = { + "type": "version", + "parent": {"$in": subset_ids} + } + if active is False: + aggregate_filter["data.active"] = active + elif active is True: + aggregate_filter["$or"] = [ + {"data.active": {"$exists": 0}}, + {"data.active": active}, + ] + aggregation_pipeline = [ # Find all versions of those subsets - {"$match": { - "type": "version", - "parent": {"$in": subset_ids} - }}, + {"$match": aggregate_filter}, # Sorting versions all together {"$sort": {"name": 1}}, # Group them by "parent", but only take the last diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index e5d8400031..e58e02f89a 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -446,6 +446,7 @@ class SubsetsModel(BaseRepresentationModel, TreeModel): last_versions_by_subset_id = get_last_versions( project_name, subset_ids, + active=True, fields=["_id", "parent", "name", "type", "data", "schema"] ) diff --git a/openpype/tools/utils/delegates.py b/openpype/tools/utils/delegates.py index fa69113ef1..c71c87f9b0 100644 --- a/openpype/tools/utils/delegates.py +++ b/openpype/tools/utils/delegates.py @@ -123,10 +123,14 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): project_name = self.dbcon.active_project() # Add all available versions to the editor parent_id = item["version_document"]["parent"] - version_docs = list(sorted( - get_versions(project_name, subset_ids=[parent_id]), - key=lambda item: item["name"] - )) + version_docs = [ + version_doc + for version_doc in sorted( + get_versions(project_name, subset_ids=[parent_id]), + key=lambda item: item["name"] + ) + if version_doc["data"].get("active", True) + ] hero_versions = list( get_hero_versions( From 29a66eddf014ec68ef4b80b8894a96ea03d2ab5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 8 Jun 2023 15:12:44 +0200 Subject: [PATCH 848/918] :recycle: Move from deprecated interface (#5117) `INewPublisher` is deprecated, using `IPublishHost` instead --- openpype/hosts/max/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 50fe30b299..03b85a4066 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -6,7 +6,7 @@ from operator import attrgetter import json -from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost import pyblish.api from openpype.pipeline import ( register_creator_plugin_path, @@ -28,7 +28,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") -class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): +class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "max" menu = None From d231558d90447d441ad135bb629836642d93ee06 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:35:35 +0200 Subject: [PATCH 849/918] fix video streams collection (#5120) --- .../plugins/create/create_editorial.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/create/create_editorial.py b/openpype/hosts/traypublisher/plugins/create/create_editorial.py index 0630dfb3da..8640500b18 100644 --- a/openpype/hosts/traypublisher/plugins/create/create_editorial.py +++ b/openpype/hosts/traypublisher/plugins/create/create_editorial.py @@ -487,7 +487,22 @@ or updating already created. Publishing will create OTIO file. ) # get video stream data - video_stream = media_data["streams"][0] + video_streams = [] + audio_streams = [] + for stream in media_data["streams"]: + codec_type = stream.get("codec_type") + if codec_type == "audio": + audio_streams.append(stream) + + elif codec_type == "video": + video_streams.append(stream) + + if not video_streams: + raise ValueError( + "Could not find video stream in source file." + ) + + video_stream = video_streams[0] return_data = { "video": True, "start_frame": 0, @@ -500,12 +515,7 @@ or updating already created. Publishing will create OTIO file. } # get audio streams data - audio_stream = [ - stream for stream in media_data["streams"] - if stream["codec_type"] == "audio" - ] - - if audio_stream: + if audio_streams: return_data["audio"] = True except Exception as exc: From d345b3e1b178c45cdce4bca315a9d5ce0e69a88f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 14:29:32 +0800 Subject: [PATCH 850/918] select the object from the node references for extractions instead of selecting children of the container --- openpype/hosts/max/plugins/publish/collect_members.py | 1 + openpype/hosts/max/plugins/publish/extract_camera_abc.py | 5 +++-- openpype/hosts/max/plugins/publish/extract_camera_fbx.py | 5 +++-- openpype/hosts/max/plugins/publish/extract_max_scene_raw.py | 3 +-- openpype/hosts/max/plugins/publish/extract_model.py | 5 +++-- openpype/hosts/max/plugins/publish/extract_model_fbx.py | 5 +++-- openpype/hosts/max/plugins/publish/extract_model_obj.py | 5 +++-- openpype/hosts/max/plugins/publish/extract_pointcache.py | 5 +++-- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 5 +++-- 9 files changed, 23 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py index 54020d7dae..0acb4f408d 100644 --- a/openpype/hosts/max/plugins/publish/collect_members.py +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -19,3 +19,4 @@ class CollectMembers(pyblish.api.InstancePlugin): member.node for member in container.openPypeData.all_handles ] + self.log.debug("{}".format(instance.data["members"])) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index c526de8960..b42732e70d 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -3,7 +3,7 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.hosts.max.api import get_all_children, maintained_selection +from openpype.hosts.max.api import maintained_selection from openpype.pipeline import OptionalPyblishPluginMixin, publish @@ -41,7 +41,8 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): with maintained_selection(): # select and export - rt.Select(get_all_children(rt.GetNodeByName(container))) + node_list = instance.data["members"] + rt.Select(node_list) rt.ExportFile( path, rt.Name("noPrompt"), diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 0c8a82dcaa..06ac3da093 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -3,7 +3,7 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.hosts.max.api import get_all_children, maintained_selection +from openpype.hosts.max.api import maintained_selection from openpype.pipeline import OptionalPyblishPluginMixin, publish @@ -36,7 +36,8 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): with maintained_selection(): # select and export - rt.Select(get_all_children(rt.GetNodeByName(container))) + node_list = instance.data["members"] + rt.Select(node_list) rt.ExportFile( filepath, rt.Name("noPrompt"), diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index f0c2aff7f3..de5db9ab56 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -2,7 +2,6 @@ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import get_all_children class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): @@ -33,7 +32,7 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): if "representations" not in instance.data: instance.data["representations"] = [] - nodes = get_all_children(rt.getNodeByName(container)) + nodes = instance.data["members"] rt.saveNodes(nodes, max_path, quiet=True) self.log.info("Performing Extraction ...") diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 4c7c98e2cc..c7ecf7efc9 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection, get_all_children +from openpype.hosts.max.api import maintained_selection class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): @@ -40,7 +40,8 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) + node_list = instance.data["members"] + rt.Select(node_list) rt.exportFile( filepath, rt.name("noPrompt"), diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index 815438d378..56c2cadd94 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection, get_all_children +from openpype.hosts.max.api import maintained_selection class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): @@ -40,7 +40,8 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) + node_list = instance.data["members"] + rt.Select(node_list) rt.exportFile( filepath, rt.name("noPrompt"), diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index ed3d68c990..4fde65cf22 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection, get_all_children +from openpype.hosts.max.api import maintained_selection class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): @@ -31,7 +31,8 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) + node_list = instance.data["members"] + rt.Select(node_list) rt.exportFile( filepath, rt.name("noPrompt"), diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 8658cecb1b..6d1e8d03b4 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -41,7 +41,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import maintained_selection, get_all_children +from openpype.hosts.max.api import maintained_selection class ExtractAlembic(publish.Extractor): @@ -72,7 +72,8 @@ class ExtractAlembic(publish.Extractor): with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) + node_list = instance.data["members"] + rt.Select(node_list) rt.exportFile( path, rt.name("noPrompt"), diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 8f76eef5ec..ab569ecbcb 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -2,7 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import get_all_children, maintained_selection +from openpype.hosts.max.api import maintained_selection class ExtractRedshiftProxy(publish.Extractor): @@ -30,7 +30,8 @@ class ExtractRedshiftProxy(publish.Extractor): with maintained_selection(): # select and export - rt.select(get_all_children(rt.getNodeByName(container))) + node_list = instance.data["members"] + rt.Select(node_list) # Redshift rsProxy command # rsProxy fp selected compress connectivity startFrame endFrame # camera warnExisting transformPivotToOrigin From 8958585da07a7ba07fb2d84e8d26efee6f20d68c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 15:22:51 +0800 Subject: [PATCH 851/918] oscar's comment --- .../hosts/nuke/startup/custom_write_node.py | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 66095409a6..708f40db27 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -63,7 +63,7 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): def __init__(self): nukescripts.PythonPanel.__init__(self, "Set Knobs Value(Write Node)") - knobs_value = self.get_node_knobs_override() + knobs_value, _ = self.get_node_knobs_setting() # create knobs self.typeKnob = nuke.Enumeration_Knob( @@ -78,56 +78,51 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): node_knobs = self.typeKnob.value() ext = None - knobs = None + knobs = knobs_setting["knobs"] + if node_knobs: - knobs = self.get_node_knobs_setting(node_knobs) - if not knobs: + _, node_knobs_settings = self.get_node_knobs_setting(node_knobs) + if not node_knobs_settings: nuke.message("No knobs value found in subset group..\nDefault setting will be used..") # noqa - knobs = knobs_setting["knobs"] - else: - knobs = knobs_setting["knobs"] + else: + knobs = node_knobs_settings for knob in knobs: if knob["name"] == "file_type": ext = knob["value"] - for w in write_selected_nodes: + for write_node in write_selected_nodes: # data for mapping the path data = { "work": os.getenv("AVALON_WORKDIR"), - "subset": w["name"].value(), + "subset": write_node["name"].value(), "frame": "#" * frame_padding, "ext": ext } file_path = temp_rendering_path_template.format(**data) file_path = file_path.replace("\\", "/") - w["file"].setValue(file_path) - set_node_knobs_from_settings(w, knobs) + write_node["file"].setValue(file_path) + set_node_knobs_from_settings(write_node, knobs) - def get_node_knobs_setting(self, value): + def get_node_knobs_setting(self, value=None): + knobs_value = [] + knobs_nodes = [] settings = [ node for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] ] if not settings: return + for i, _ in enumerate(settings): if value in settings[i]["subsets"]: - return settings[i]["knobs"] - - def get_node_knobs_override(self): - knobs_value = [] - settings = [ - node - for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] - ] - if not settings: - return + knobs_nodes = settings[i]["knobs"] for setting in settings: if setting["nukeNodeClass"] == "Write" and setting["subsets"]: - for knob in setting["subsets"]: - knobs_value.append(knob) - return knobs_value + for knob in setting["subsets"]: + knobs_value.append(knob) + + return knobs_value, knobs_nodes def main(): From 4b0b06ea4f7201dd274866544de51a750ed30395 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 15:23:48 +0800 Subject: [PATCH 852/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 708f40db27..d229b6b268 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -119,8 +119,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): for setting in settings: if setting["nukeNodeClass"] == "Write" and setting["subsets"]: - for knob in setting["subsets"]: - knobs_value.append(knob) + for knob in setting["subsets"]: + knobs_value.append(knob) return knobs_value, knobs_nodes From fbf4605417493ed5c0cf4646a9b0645ffc9f7c46 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 12:40:05 +0200 Subject: [PATCH 853/918] removing get_app_version_info --- openpype/hosts/nuke/api/__init__.py | 4 +--- openpype/hosts/nuke/api/lib.py | 11 ----------- openpype/hosts/nuke/api/pipeline.py | 12 +++++++----- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index 55c4b8c808..1af5ff365d 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -43,8 +43,7 @@ from .lib import ( get_node_data, set_node_data, update_node_data, - create_write_node, - get_app_version_info + create_write_node ) from .utils import ( colorspace_exists_on_node, @@ -91,7 +90,6 @@ __all__ = ( "set_node_data", "update_node_data", "create_write_node", - "get_app_version_info", "colorspace_exists_on_node", "get_colorspace_list" diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index bf66b9e1a9..4a57bc3165 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3116,14 +3116,3 @@ def get_viewer_config_from_string(input_string): ).format(input_string)) return (display, viewer) - - -def get_app_version_info(): - """ Return dict of Nuke's version semantic info""" - dot_split = nuke.NUKE_VERSION_STRING.split(".") - v_spit = dot_split[1].split("v") - return { - "major": int(dot_split[0]), - "minor": int(v_spit[0]), - "patch": int(v_spit[1]) - } diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 998c03b3dd..8406a251e9 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -43,8 +43,7 @@ from .lib import ( add_scripts_menu, add_scripts_gizmo, get_node_data, - set_node_data, - get_app_version_info + set_node_data ) from .workfile_template_builder import ( NukePlaceholderLoadPlugin, @@ -219,7 +218,6 @@ def _install_menu(): main_window = get_main_window() menubar = nuke.menu("Nuke") menu = menubar.addMenu(MENU_LABEL) - app_version = get_app_version_info() if not ASSIST: label = "{0}, {1}".format( @@ -244,7 +242,9 @@ def _install_menu(): menu.addCommand( "Create...", lambda: host_tools.show_publisher( - parent=main_window if app_version["major"] >= 14 else None, + parent=( + main_window if nuke.NUKE_VERSION_RELEASE >= 14 else None + ), tab="create" ) ) @@ -253,7 +253,9 @@ def _install_menu(): menu.addCommand( "Publish...", lambda: host_tools.show_publisher( - parent=main_window if app_version["major"] >= 14 else None, + parent=( + main_window if nuke.NUKE_VERSION_RELEASE >= 14 else None + ), tab="publish" ) ) From a80b1b0f766ad127927575a4e91b7bb48fdb446d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 13:59:27 +0200 Subject: [PATCH 854/918] fixing still to image family conversion --- openpype/hosts/nuke/plugins/publish/extract_render_local.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index e5feda4cd8..e2cf2addc5 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -23,7 +23,7 @@ class NukeRenderLocal(publish.Extractor, order = pyblish.api.ExtractorOrder label = "Render Local" hosts = ["nuke"] - families = ["render.local", "prerender.local", "still.local"] + families = ["render.local", "prerender.local", "image.local"] def process(self, instance): child_nodes = ( @@ -136,9 +136,9 @@ class NukeRenderLocal(publish.Extractor, families.remove('prerender.local') families.insert(0, "prerender") instance.data["anatomyData"]["family"] = "prerender" - elif "still.local" in families: + elif "image.local" in families: instance.data['family'] = 'image' - families.remove('still.local') + families.remove('image.local') instance.data["anatomyData"]["family"] = "image" instance.data["families"] = families From 7c3229db704c8b12529f68e2f6d053a76ae85f51 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 20:19:04 +0800 Subject: [PATCH 855/918] Jakub's comment --- .../hosts/nuke/startup/custom_write_node.py | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index d229b6b268..3edc74ceac 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -2,6 +2,7 @@ import os import nuke import nukescripts +from openpype.pipeline import Anatomy from openpype.hosts.nuke.api.lib import ( set_node_knobs_from_settings, get_nuke_imageio_settings @@ -66,30 +67,43 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): knobs_value, _ = self.get_node_knobs_setting() # create knobs - self.typeKnob = nuke.Enumeration_Knob( - 'override_subsets', 'override subsets', knobs_value) + self.selected_preset_name = nuke.Enumeration_Knob( + 'preset_selector', 'presets', knobs_value) # add knobs to panel - self.addKnob(self.typeKnob) + self.addKnob(self.selected_preset_name ) def process(self): """ Process the panel values. """ write_selected_nodes = [ s for s in nuke.selectedNodes() if s.Class() == "Write"] - node_knobs = self.typeKnob.value() + node_knobs = self.selected_preset_name.value() ext = None knobs = knobs_setting["knobs"] - - if node_knobs: - _, node_knobs_settings = self.get_node_knobs_setting(node_knobs) + knobs_value, node_knobs_settings = self.get_node_knobs_setting(node_knobs) + if node_knobs and knobs_value: if not node_knobs_settings: nuke.message("No knobs value found in subset group..\nDefault setting will be used..") # noqa else: knobs = node_knobs_settings - for knob in knobs: - if knob["name"] == "file_type": + ext_knob_list = [knob for knob in knobs if knob["name"]== "file_type"] + if not ext_knob_list: + for knob in knobs_setting["knobs"]: ext = knob["value"] + nuke.message("No file type found in the subset's knobs.Default file_type value will be used..") + else: + for knob in ext_knob_list: + ext = knob["value"] + + + anatomy = Anatomy() + + frame_padding = int( + anatomy.templates["render"].get( + "frame_padding" + ) + ) for write_node in write_selected_nodes: # data for mapping the path data = { From 399562d950ab277967ded5c02ed52fbd66771436 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 20:20:44 +0800 Subject: [PATCH 856/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 3edc74ceac..d708b277a6 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -67,10 +67,10 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): knobs_value, _ = self.get_node_knobs_setting() # create knobs - self.selected_preset_name = nuke.Enumeration_Knob( + self.selected_preset_name = nuke.Enumeration_Knob( 'preset_selector', 'presets', knobs_value) # add knobs to panel - self.addKnob(self.selected_preset_name ) + self.addKnob(self.selected_preset_name) def process(self): """ Process the panel values. """ @@ -80,18 +80,20 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): node_knobs = self.selected_preset_name.value() ext = None knobs = knobs_setting["knobs"] - knobs_value, node_knobs_settings = self.get_node_knobs_setting(node_knobs) + knobs_value, node_knobs_settings = ( + self.get_node_knobs_setting(node_knobs) + ) if node_knobs and knobs_value: if not node_knobs_settings: nuke.message("No knobs value found in subset group..\nDefault setting will be used..") # noqa else: knobs = node_knobs_settings - ext_knob_list = [knob for knob in knobs if knob["name"]== "file_type"] + ext_knob_list = [knob for knob in knobs if knob["name"] == "file_type"] if not ext_knob_list: for knob in knobs_setting["knobs"]: ext = knob["value"] - nuke.message("No file type found in the subset's knobs.Default file_type value will be used..") + nuke.message("No file type found in the subset's knobs.\nDefault file_type value will be used..") # noqa else: for knob in ext_knob_list: ext = knob["value"] From 1a1afee31957f1c5f12957a313b7c4438de4317c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 20:21:57 +0800 Subject: [PATCH 857/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index d708b277a6..3e2a6fa652 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -82,7 +82,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): knobs = knobs_setting["knobs"] knobs_value, node_knobs_settings = ( self.get_node_knobs_setting(node_knobs) - ) + ) + if node_knobs and knobs_value: if not node_knobs_settings: nuke.message("No knobs value found in subset group..\nDefault setting will be used..") # noqa @@ -98,7 +99,6 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): for knob in ext_knob_list: ext = knob["value"] - anatomy = Anatomy() frame_padding = int( From feff57de37708f70e30e602ce0fe0edda3e2a3e0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 20:30:50 +0800 Subject: [PATCH 858/918] tell the user no file type in knobs --- openpype/hosts/nuke/startup/custom_write_node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 3e2a6fa652..08b11ee0fc 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -92,9 +92,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): ext_knob_list = [knob for knob in knobs if knob["name"] == "file_type"] if not ext_knob_list: - for knob in knobs_setting["knobs"]: - ext = knob["value"] nuke.message("No file type found in the subset's knobs.\nDefault file_type value will be used..") # noqa + break else: for knob in ext_knob_list: ext = knob["value"] From 704620287c479880ad0205772db96112ca8e8dfb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 20:31:41 +0800 Subject: [PATCH 859/918] tell the user no file type in knobs and ask them to add one --- openpype/hosts/nuke/startup/custom_write_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 08b11ee0fc..2b326d2a64 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -92,8 +92,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): ext_knob_list = [knob for knob in knobs if knob["name"] == "file_type"] if not ext_knob_list: - nuke.message("No file type found in the subset's knobs.\nDefault file_type value will be used..") # noqa - break + nuke.message("No file type found in the subset's knobs.\nPlease add one...") # noqa + return else: for knob in ext_knob_list: ext = knob["value"] From e1b2b723d7bbf2846e1bf5577dc3ed8790187c27 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 20:33:06 +0800 Subject: [PATCH 860/918] tell the user no file type in knobs and ask them to add one --- openpype/hosts/nuke/startup/custom_write_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 2b326d2a64..a221a26424 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -92,7 +92,7 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): ext_knob_list = [knob for knob in knobs if knob["name"] == "file_type"] if not ext_knob_list: - nuke.message("No file type found in the subset's knobs.\nPlease add one...") # noqa + nuke.message("ERROR: No file type found in the subset's knobs.\nPlease add one to complete setting up the node") # noqa return else: for knob in ext_knob_list: From 13efd0eceb3cbeb41e1acb909667b173b363e353 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 9 Jun 2023 16:16:43 +0200 Subject: [PATCH 861/918] :bug: fix 3dsmax host name 3dsmax host name is just `max` --- openpype/hooks/pre_ocio_hook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index eac7d2696f..7af0f4ceef 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -14,7 +14,7 @@ class OCIOEnvHook(PreLaunchHook): "fusion", "blender", "aftereffects", - "3dsmax", + "max", "houdini", "maya", "nuke", From 4d40857ed80757a762ade8c49359c14490eb32f5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 16:24:42 +0200 Subject: [PATCH 862/918] removing redundant hook --- openpype/hooks/pre_host_set_ocio.py | 37 ----------------------------- 1 file changed, 37 deletions(-) delete mode 100644 openpype/hooks/pre_host_set_ocio.py diff --git a/openpype/hooks/pre_host_set_ocio.py b/openpype/hooks/pre_host_set_ocio.py deleted file mode 100644 index 3620d88db6..0000000000 --- a/openpype/hooks/pre_host_set_ocio.py +++ /dev/null @@ -1,37 +0,0 @@ -from openpype.lib import PreLaunchHook - -from openpype.pipeline.colorspace import get_imageio_config -from openpype.pipeline.template_data import get_template_data - - -class PreLaunchHostSetOCIO(PreLaunchHook): - """Set OCIO environment for the host""" - - order = 0 - app_groups = ["substancepainter"] - - def execute(self): - """Hook entry method.""" - - anatomy_data = get_template_data( - project_doc=self.data["project_doc"], - asset_doc=self.data["asset_doc"], - task_name=self.data["task_name"], - host_name=self.host_name, - system_settings=self.data["system_settings"] - ) - - ocio_config = get_imageio_config( - project_name=self.data["project_doc"]["name"], - host_name=self.host_name, - project_settings=self.data["project_settings"], - anatomy_data=anatomy_data, - anatomy=self.data["anatomy"] - ) - - if ocio_config: - ocio_path = ocio_config["path"] - self.log.info(f"Setting OCIO config path: {ocio_path}") - self.launch_context.env["OCIO"] = ocio_path - else: - self.log.debug("OCIO not set or enabled") From 4595813ea6031e7147a3daa2c825fd6b70dd0b02 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 16:25:33 +0200 Subject: [PATCH 863/918] merge from redundant hook --- openpype/hooks/pre_ocio_hook.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 7af0f4ceef..8f462665bc 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -11,6 +11,7 @@ class OCIOEnvHook(PreLaunchHook): order = 0 hosts = [ + "substancepainter", "fusion", "blender", "aftereffects", @@ -48,3 +49,5 @@ class OCIOEnvHook(PreLaunchHook): f"Setting OCIO environment to config path: {ocio_path}") self.launch_context.env["OCIO"] = ocio_path + else: + self.log.debug("OCIO not set or enabled") From f0d7876db70b09e5763906d91f9660b7019552e8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 16:26:22 +0200 Subject: [PATCH 864/918] backward compatibility - for hosts which had activated ocio_config in overrides - those are group settings. --- openpype/pipeline/colorspace.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index d4011d32c9..7e6efabf20 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -357,8 +357,12 @@ def get_imageio_config( imageio_global, imageio_host = _get_imageio_settings( project_settings, host_name) - activate_color_management = imageio_global.get( - "activate_global_color_management", False) + activate_color_management = ( + imageio_global.get("activate_global_color_management", False) + # for already saved overrides from previous version + # TODO: remove this in future - backward compatibility + or imageio_host.get("ocio_config").get("enabled") + ) if not activate_color_management: # if global settings are disabled return empty dict because @@ -391,7 +395,12 @@ def get_imageio_config( # depending on override flag # TODO: in future rewrite this to be more explicit config_data = None - override_global_config = config_host.get("override_global_config") + override_global_config = ( + config_host.get("override_global_config") + # for already saved overrides from previous version + # TODO: remove this in future - backward compatibility + or config_host.get("enabled") + ) if override_global_config: config_data = _get_config_data( config_host["filepath"], formatting_data From a71fed7a566579740eb34d4066fa420ccceaf109 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 16:27:06 +0200 Subject: [PATCH 865/918] nuke: backward compatibility for older project settings --- openpype/hosts/nuke/api/lib.py | 67 ++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 912bc5963b..777f4454dc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -39,6 +39,7 @@ from openpype.settings import ( from openpype.modules import ModulesManager from openpype.pipeline.template_data import get_template_data_with_names from openpype.pipeline import ( + get_current_project_name, discover_legacy_creator_plugins, legacy_io, Anatomy, @@ -2008,37 +2009,65 @@ class WorkfileSettings(object): imageio_host (dict): host colorspace configurations ''' + config_data = get_imageio_config( + project_name=get_current_project_name(), + host_name="nuke" + ) + workfile_settings = imageio_host["workfile"] - # first set OCIO - if self._root_node["colorManagement"].value() \ - not in str(workfile_settings["colorManagement"]): - self._root_node["colorManagement"].setValue( - str(workfile_settings["colorManagement"])) + if not config_data: + # TODO: backward compatibility for old projects - remove later + # perhaps old project overrides is having it set to older version + # with use of `customOCIOConfigPath` + if workfile_settings.get("customOCIOConfigPath"): + unresolved_path = workfile_settings["customOCIOConfigPath"] + ocio_paths = unresolved_path[platform.system().lower()] - # we dont need the key anymore - workfile_settings.pop("colorManagement") + resolved_path = None + for ocio_p in ocio_paths: + resolved_path = str(ocio_p).format(**os.environ) + if not os.path.exists(resolved_path): + continue + if resolved_path: + # set values to root + self._root_node["colorManagement"].setValue("OCIO") + self._root_node["OCIO_config"].setValue("custom") + self._root_node["customOCIOConfigPath"].setValue( + resolved_path) + else: + # no ocio config found and no custom path used + if self._root_node["colorManagement"].value() \ + not in str(workfile_settings["colorManagement"]): + self._root_node["colorManagement"].setValue( + str(workfile_settings["colorManagement"])) - # second set ocio version - if self._root_node["OCIO_config"].value() \ - not in str(workfile_settings["OCIO_config"]): - self._root_node["OCIO_config"].setValue( - str(workfile_settings["OCIO_config"])) + # second set ocio version + if self._root_node["OCIO_config"].value() \ + not in str(workfile_settings["OCIO_config"]): + self._root_node["OCIO_config"].setValue( + str(workfile_settings["OCIO_config"])) - # we dont need the key anymore - workfile_settings.pop("OCIO_config") + else: + # set values to root + self._root_node["colorManagement"].setValue("OCIO") + + # we dont need the key anymore + workfile_settings.pop("customOCIOConfigPath") + workfile_settings.pop("colorManagement") + workfile_settings.pop("OCIO_config") # then set the rest - for knob, value in workfile_settings.items(): + for knob, value_ in workfile_settings.items(): # skip unfilled ocio config path # it will be dict in value - if isinstance(value, dict): + if isinstance(value_, dict): continue - if self._root_node[knob].value() not in value: - self._root_node[knob].setValue(str(value)) + if self._root_node[knob].value() not in value_: + self._root_node[knob].setValue(str(value_)) log.debug("nuke.root()['{}'] changed to: {}".format( - knob, value)) + knob, value_)) def set_writes_colorspace(self): ''' Adds correct colorspace to write node dict From 44bf1dcc8a1111f0b8d10319c0dce48fe24fd733 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 16:33:47 +0200 Subject: [PATCH 866/918] backward combability for file rules --- openpype/pipeline/colorspace.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 7e6efabf20..899b14148b 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -490,8 +490,11 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): # get file rules from global and host_name frules_global = imageio_global["file_rules"] - activate_global_rules = frules_global.get( - "activate_global_file_rules", False) + activate_global_rules = ( + frules_global.get("activate_global_file_rules", False) + # TODO: remove this in future - backward compatibility + or frules_global.get("enabled") + ) global_rules = frules_global["rules"] if not activate_global_rules: @@ -504,7 +507,11 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): frules_host = imageio_host.get("file_rules", {}) # compile file rules dictionary - activate_host_rules = frules_host.get("activate_host_rules") + activate_host_rules = ( + frules_host.get("activate_host_rules") + # TODO: remove this in future - backward compatibility + or frules_host.get("enabled") + ) # return host rules if activated or global rules return frules_host["rules"] if activate_host_rules else global_rules From aaf8756e18008552b89dacaff19ba1d4b0de12f4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 Jun 2023 16:57:14 +0200 Subject: [PATCH 867/918] hiero: backward compatibility --- openpype/hosts/hiero/api/lib.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 0d4368529f..fa874f9e9d 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -23,11 +23,17 @@ except ImportError: from openpype.client import get_project from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io, Anatomy +from openpype.pipeline import ( + get_current_project_name, legacy_io, Anatomy +) from openpype.pipeline.load import filter_containers from openpype.lib import Logger from . import tags +from openpype.pipeline.colorspace import ( + get_imageio_config +) + class DeprecatedWarning(DeprecationWarning): pass @@ -1047,6 +1053,18 @@ def apply_colorspace_project(): imageio = get_project_settings(project_name)["hiero"]["imageio"] presets = imageio.get("workfile") + # backward compatibility layer + # TODO: remove this after some time + config_data = get_imageio_config( + project_name=get_current_project_name(), + host_name="hiero" + ) + + if config_data: + presets.update({ + "ocioConfigName": "custom" + }) + # save the workfile as subversion "comment:_colorspaceChange" split_current_file = os.path.splitext(current_file) copy_current_file = current_file From 7be1d97118e2c2b3cb159b2fe192caa233872002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 9 Jun 2023 17:01:05 +0200 Subject: [PATCH 868/918] Update openpype/hosts/flame/hooks/pre_flame_setup.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fabià Serra Arrizabalaga --- openpype/hosts/flame/hooks/pre_flame_setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 1f01499333..83110bb6b5 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -47,7 +47,11 @@ class FlamePrelaunch(PreLaunchHook): imageio_flame = project_settings["flame"]["imageio"] - # get host imageio settings enabled key if exists + # Check whether 'enabled' key from host imageio settings exists + # so we can tell if host is using the new colormanagement framework. + # If the 'enabled' isn't found we want 'colormanaged' set to True + # because prior to the key existing we always did colormanagement for + # Flame colormanaged = imageio_flame.get("enabled") # if key was not found, set to True # ensuring backward compatibility From cd7317410be99bdd1d109e5227bd8c5301f84c21 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 9 Jun 2023 15:28:20 +0000 Subject: [PATCH 869/918] [Automated] Release --- CHANGELOG.md | 393 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 395 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec6544e659..882620f26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,399 @@ # Changelog +## [3.15.10](https://github.com/ynput/OpenPype/tree/3.15.10) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.9...3.15.10) + +### **🆕 New features** + + +
+ImageIO: Adding ImageIO activation toggle to all hosts #4700 + +Colorspace management can now be enabled at the project level, although it is disabled by default. Once enabled, all hosts will use the OCIO config file defined in the settings. If settings are disabled, the system switches to DCC's native color space management, and we do not store colorspace information at the representative level. + + +___ + +
+ + +
+Redshift Proxy Support in 3dsMax #4625 + +Redshift Proxy Support for 3dsMax. +- [x] Creator +- [x] Loader +- [x] Extractor +- [x] Validator +- [x] Add documentation + + +___ + +
+ + +
+Houdini farm publishing and rendering #4825 + +Deadline Farm publishing and Rendering for Houdini +- [x] Mantra +- [x] Karma(including usd renders) +- [x] Arnold +- [x] Elaborate Redshift ROP for deadline submission +- [x] fix the existing bug in Redshift ROP +- [x] Vray +- [x] add docs + + +___ + +
+ + +
+Feature: Blender hook to execute python scripts at launch #4905 + +Hook to allow hooks to add path to a python script that will be executed when Blender starts. + + +___ + +
+ + +
+Feature: Resolve: Open last workfile on launch through .scriptlib #5047 + +Added implementation to Resolve integration to open last workfile on launch. + + +___ + +
+ + +
+General: Remove default windowFlags from publisher #5089 + +The default windowFlags is making the publisher window (in Linux at least) only show the close button and it's frustrating as many times you just want to minimize the window and get back to the validation after. Removing that line I get what I'd expect.**Before:****After:** + + +___ + +
+ + +
+General: Show user who created the workfile on the details pane of workfile manager #5093 + +New PR for https://github.com/ynput/OpenPype/pull/5087, which was closed after merging `next-minor` branch and then realizing we don't need to target it as it was decided it's not required to support windows. More info on that PR discussion.Small addition to add name of the `user` who created the workfile on the details pane of the workfile manager: + + +___ + +
+ + +
+Loader: Hide inactive versions in UI #5100 + +Hide versions with `active` set to `False` in Loader UI. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: Repair RenderPass token when merging AOVs. #5055 + +Validator was flagging that `` was in the image prefix, but did not repair the issue. + + +___ + +
+ + +
+Maya: Improve error feedback when no renderable cameras exist for ASS family. #5092 + +When collecting cameras for `ass` family, this improves the error message when no cameras are renderable. + + +___ + +
+ + +
+Nuke: Custom script to set frame range of read nodes #5039 + +Adding option to set frame range specifically for the read nodes in Openpype Panel. User can set up their preferred frame range with the frame range dialog, which can be showed after clicking `Set Frame Range (Read Node)` in Openpype Tools + + +___ + +
+ + +
+Update extract review letterbox docs #5074 + +Update Extract Review - Letter Box section in Docs. Letterbox type description is removed. + + +___ + +
+ + +
+Project pack: Documents only skips roots validation #5082 + +Single roots validation is skipped if only documents are extracted. + + +___ + +
+ + +
+Nuke: custom settings for write node without publish #5084 + +Set Render Output and other settings to write nodes for non-publish purposes. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Deadline servers #5052 + +Fix working with multiple Deadline servers in Maya. +- Pools (primary and secondary) attributes were not recreated correctly. +- Order of collector plugins were wrong, so collected data was not injected into render instances. +- Server attribute was not converted to string so comparing with settings was incorrect. +- Improve debug logging for where the webservice url is getting fetched from. + + +___ + +
+ + +
+Maya: Fix Load Reference. #5091 + +Fix bug introduced with https://github.com/ynput/OpenPype/pull/4751 where `cmds.ls` returns a list. + + +___ + +
+ + +
+3dsmax: Publishing Deadline jobs from RedShift #4960 + +Fix the bug of being uable to publish deadline jobs from RedshiftUse Current File instead of Published Scene for just Redshift. +- add save scene before rendering to ensure the scene is saved after the modification. +- add separated aov files option to allow users to choose to have aovs in render output +- add validator for render publish to aovid overriding the previous renders + + +___ + +
+ + +
+Houdini: Fix missing frame range for pointcache and camera exports #5026 + +Fix missing frame range for pointcache and camera exports on published version. + + +___ + +
+ + +
+Global: collect_frame_fix plugin fix and cleanup #5064 + +Previous implementation https://github.com/ynput/OpenPype/pull/5036 was broken this is fixing the issue where attribute is found in instance data although the settings were disabled for the plugin. + + +___ + +
+ + +
+Hiero: Fix apply settings Clip Load #5073 + +Changed `apply_settings` to classmethod which fixes the issue with settings. + + +___ + +
+ + +
+Resolve: Make sure scripts dir exists #5078 + +Make sure the scripts directory exists before looping over it's content. + + +___ + +
+ + +
+removing info knob from nuke creators #5083 + +- removing instance node if removed via publisher +- removing info knob since it is not needed any more (was there only for the transition phase) + + +___ + +
+ + +
+Tray: Fix restart arguments on update #5085 + +Fix arguments on restart. + + +___ + +
+ + +
+Maya: bug fix on repair action in Arnold Scene Source CBID Validator #5096 + +Fix the bug of not being able to use repair action in Arnold Scene Source CBID Validator + + +___ + +
+ + +
+Nuke: batch of small fixes #5103 + +- default settings for `imageio.requiredNodes` **CreateWriteImage** +- default settings for **LoadImage** representations +- **Create** and **Publish** menu items with `parent=main_window` (version > 14) + + +___ + +
+ + +
+Deadline: make prerender check safer #5104 + +Prerender wasn't correctly recognized and was replaced with just 'render' family.In Nuke it is correctly `prerender.farm` in families, which wasn't handled here. It resulted into using `render` in templates even if `render` and `prerender` templates were split. + + +___ + +
+ + +
+General: Sort launcher actions alphabetically #5106 + +The launcher actions weren't being sorted by its label but its name (which on the case of the apps it's the version number) and thus the order wasn't consistent and we kept getting a different order on every launch. From my debugging session, this was the result of what the `actions` variable held after the `filter_compatible_actions` function before these changes: +``` +(Pdb) for p in actions: print(p.order, p.name) +0 14-02 +0 14-02 +0 14-02 +0 14-02 +0 14-02 +0 19-5-493 +0 2023 +0 3-41 +0 6-01 +```This caused already a couple bugs from our artists thinking they had launched Nuke X and instead launched Nuke and telling us their Nuke was missing nodes**Before:****After:** + + +___ + +
+ + +
+TrayPublisher: Editorial video stream discovery #5120 + +Editorial create plugin in traypublisher does not expect that first stream in input is video. + + +___ + +
+ +### **🔀 Refactored code** + + +
+3dsmax: Move from deprecated interface #5117 + +`INewPublisher` interface is deprecated, this PR is changing the use to `IPublishHost` instead. + + +___ + +
+ +### **Merged pull requests** + + +
+add movalex as a contributor for code #5076 + +Adds @movalex as a contributor for code. + +This was requested by mkolar [in this comment](https://github.com/ynput/OpenPype/pull/4916#issuecomment-1571498425) + +[skip ci] +___ + +
+ + +
+3dsmax: refactor load plugins #5079 + + +___ + +
+ + + + ## [3.15.9](https://github.com/ynput/OpenPype/tree/3.15.9) diff --git a/openpype/version.py b/openpype/version.py index 868664c601..cf4e1efc66 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.10-nightly.2" +__version__ = "3.15.10" diff --git a/pyproject.toml b/pyproject.toml index 633899d3a0..56c130982c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.9" # OpenPype +version = "3.15.10" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 333249b4d8ee32116851c6aabad25a9bc29cac16 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 9 Jun 2023 15:29:28 +0000 Subject: [PATCH 870/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index e614d2fa65..aa5a99f66e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.10 - 3.15.10-nightly.2 - 3.15.10-nightly.1 - 3.15.9 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.4 - 3.14.3-nightly.3 - 3.14.3-nightly.2 - - 3.14.3-nightly.1 validations: required: true - type: dropdown From 54294abb49e1e1b215aad6d07c3fa50000ccf52a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 10 Jun 2023 03:25:04 +0000 Subject: [PATCH 871/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index cf4e1efc66..300f5546ae 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.10" +__version__ = "3.15.11-nightly.1" From 6f0c5083d310af4291ce14e87dde80dff479fa5d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Jun 2023 03:25:49 +0000 Subject: [PATCH 872/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index aa5a99f66e..0b1d0f5087 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.11-nightly.1 - 3.15.10 - 3.15.10-nightly.2 - 3.15.10-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.5 - 3.14.3-nightly.4 - 3.14.3-nightly.3 - - 3.14.3-nightly.2 validations: required: true - type: dropdown From e9d60c96f50e5a278aa602e80a6a7b677a8decf9 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jun 2023 12:44:12 +0200 Subject: [PATCH 873/918] :bug: handle cancelled dialog --- openpype/hosts/max/api/plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index f52e78de70..5eb4eaf391 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -35,6 +35,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( current_selection = selectByName title:"Select Objects to add to the Container" buttontext:"Add" + if current_selection == undefined then return False temp_arr = #() i_node_arr = #() for c in current_selection do @@ -52,8 +53,10 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( current_selection = selectByName title:"Select Objects to remove from the Container" buttontext:"Remove" + if current_selection == undefined then return False temp_arr = #() i_node_arr = #() + for c in current_selection do ( node_ref = NodeTransformMonitor node:c From 74efe431765272ff640b6478b15ac06e03a5e0a4 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Tue, 13 Jun 2023 10:03:05 +0300 Subject: [PATCH 874/918] hide macos dock icon on build --- tools/build.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/build.sh b/tools/build.sh index 753a9c55b8..e828cc149e 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -196,6 +196,8 @@ if [ "$disable_submodule_update" == 1 ]; then echo -e "${BIGreen}>>>${RST} Fixing libs ..." mv "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/dependencies/cx_Freeze" "$openpype_root/build/OpenPype $openpype_version.app/Contents/MacOS/lib/" || { echo -e "${BIRed}!!!>${RST} ${BIYellow}Can't move cx_Freeze libs${RST}"; return 1; } + # force hide icon from Dock + defaults write "$openpype_root/build/OpenPype $openpype_version.app/Contents/Info" LSUIElement 1 # fix code signing issue echo -e "${BIGreen}>>>${RST} Fixing code signatures ...\c" From 0a0c374ee1fc874b4f5521a72cd5273b00082ec7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 13 Jun 2023 18:26:31 +0800 Subject: [PATCH 875/918] removing the node references successfully in the parameter --- openpype/hosts/max/api/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 5eb4eaf391..6bf3756e72 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -72,13 +72,13 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" DeleteItem list_node.items idx ) ) - all_handles = join i_node_arr all_handles - list_node.items = join temp_arr list_node.items + all_handles = i_node_arr + list_node.items = temp_arr ) on OPparams open do ( - if all_handles.count != 0 do + if all_handles.count != 0 then ( temp_arr = #() for x in all_handles do From 15dd1e13b6e37dce8dbc8d1f5d0262ea3639f412 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 13 Jun 2023 19:00:37 +0800 Subject: [PATCH 876/918] removing the node references successfully in the parameter --- openpype/hosts/max/api/plugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 6bf3756e72..3fa4fa55b1 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -56,6 +56,8 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" if current_selection == undefined then return False temp_arr = #() i_node_arr = #() + new_temp_arr = #() + new_i_node_arr = #() for c in current_selection do ( @@ -65,15 +67,17 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" if idx do ( DeleteItem all_nodes idx + for i in all_nodes do append new_i_node_arr i ) idx = finditem list_node.items handle_name if idx do ( DeleteItem list_node.items idx + for i in list_node.items do append new_temp_arr i ) ) - all_handles = i_node_arr - list_node.items = temp_arr + all_handles = new_i_node_arr + list_node.items = new_temp_arr ) on OPparams open do From fc4c4668d0dca05cd40c4b671a38f8ea8cd78d5b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 13 Jun 2023 20:50:24 +0800 Subject: [PATCH 877/918] removing the node references successfully in the parameter --- openpype/hosts/max/api/plugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 3fa4fa55b1..be40a765fb 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -56,7 +56,6 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" if current_selection == undefined then return False temp_arr = #() i_node_arr = #() - new_temp_arr = #() new_i_node_arr = #() for c in current_selection do @@ -73,11 +72,10 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" if idx do ( DeleteItem list_node.items idx - for i in list_node.items do append new_temp_arr i ) ) all_handles = new_i_node_arr - list_node.items = new_temp_arr + list_node.items = temp_arr ) on OPparams open do From 1c77dffc3d45e6ec7a8876b2f19eac572eb04cab Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 14 Jun 2023 03:25:23 +0000 Subject: [PATCH 878/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 300f5546ae..c44b1d29fb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.11-nightly.1" +__version__ = "3.15.11-nightly.2" From fa9d348b321abfc4efa94065925055212dd8e8b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 Jun 2023 03:26:11 +0000 Subject: [PATCH 879/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0b1d0f5087..2339ec878f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.11-nightly.2 - 3.15.11-nightly.1 - 3.15.10 - 3.15.10-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.6 - 3.14.3-nightly.5 - 3.14.3-nightly.4 - - 3.14.3-nightly.3 validations: required: true - type: dropdown From bfde7cba51e99fc2de2cb15b831e9c4910c21680 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 14 Jun 2023 14:02:01 +0800 Subject: [PATCH 880/918] fix the bug of not deleting instance with delete button lasted in Openpype attribute --- openpype/hosts/max/api/plugin.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index be40a765fb..3427c85d98 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -57,25 +57,34 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" temp_arr = #() i_node_arr = #() new_i_node_arr = #() + new_temp_arr = #() for c in current_selection do ( - node_ref = NodeTransformMonitor node:c + node_ref = NodeTransformMonitor node:c as string handle_name = node_to_name c - idx = finditem all_handles node_ref + tmp_all_handles = #() + for i in all_handles do + ( + tmp = i as string + append tmp_all_handles tmp + ) + print all_handles + print node_ref + idx = finditem tmp_all_handles node_ref if idx do ( - DeleteItem all_nodes idx - for i in all_nodes do append new_i_node_arr i + new_i_node_arr = DeleteItem all_handles idx + ) idx = finditem list_node.items handle_name if idx do ( - DeleteItem list_node.items idx + new_temp_arr = DeleteItem list_node.items idx ) ) - all_handles = new_i_node_arr - list_node.items = temp_arr + all_handles = join i_node_arr new_i_node_arr + list_node.items = join temp_arr new_temp_arr ) on OPparams open do From cb92481676bd1f9f9720e2489e04eaf3b73a8065 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 14 Jun 2023 14:02:29 +0800 Subject: [PATCH 881/918] fix the docstring --- openpype/hosts/max/plugins/publish/collect_members.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_members.py b/openpype/hosts/max/plugins/publish/collect_members.py index 0acb4f408d..812d82ff26 100644 --- a/openpype/hosts/max/plugins/publish/collect_members.py +++ b/openpype/hosts/max/plugins/publish/collect_members.py @@ -5,7 +5,7 @@ from pymxs import runtime as rt class CollectMembers(pyblish.api.InstancePlugin): - """Collect Render for Deadline.""" + """Collect Set Members.""" order = pyblish.api.CollectorOrder + 0.01 label = "Collect Instance Members" From b736c8c632157facc24cf9ac36bccfff8851d09a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 14 Jun 2023 10:06:45 +0200 Subject: [PATCH 882/918] :recycle: remove debug prints --- openpype/hosts/max/api/plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 3427c85d98..4fc852e2fe 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -69,8 +69,6 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" tmp = i as string append tmp_all_handles tmp ) - print all_handles - print node_ref idx = finditem tmp_all_handles node_ref if idx do ( From 37e17f3ba03d6b0c101492d9d9a7da41be5fedc9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 14:52:41 +0800 Subject: [PATCH 883/918] bug fix the standin being not loaded when they are first loaded --- .../hosts/maya/plugins/load/load_arnold_standin.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 7c3a732389..dd2e8d0885 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -1,5 +1,6 @@ import os import clique +import time import maya.cmds as cmds @@ -35,9 +36,14 @@ class ArnoldStandinLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, options): - - # Make sure to load arnold before importing `mtoa.ui.arnoldmenu` - cmds.loadPlugin("mtoa", quiet=True) + # Make sure user has loaded arnold before importing `mtoa.ui.arnoldmenu` + # and getting attribute from defaultArnoldRenderOption.operator + # Otherwises standins will not be loaded successfully for + # every first time using this loader after the build + if not cmds.pluginInfo("mtoa", query=True, loaded=True): + raise RuntimeError("Plugin 'mtoa' must be loaded" + " before using this loader") + # cmds.loadPlugin("mtoa", quiet=True) import mtoa.ui.arnoldmenu version = context['version'] From 9ba54ecace3a5554c737f83f489154a99d597a49 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 15:09:40 +0800 Subject: [PATCH 884/918] hound fix --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index dd2e8d0885..f41afefcf4 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -1,6 +1,5 @@ import os import clique -import time import maya.cmds as cmds @@ -36,7 +35,8 @@ class ArnoldStandinLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, options): - # Make sure user has loaded arnold before importing `mtoa.ui.arnoldmenu` + # Make sure user has loaded arnold + # before importing `mtoa.ui.arnoldmenu` # and getting attribute from defaultArnoldRenderOption.operator # Otherwises standins will not be loaded successfully for # every first time using this loader after the build From 59b7e265dab01adbd393f9aa934a699d64502aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Hector?= Date: Thu, 15 Jun 2023 10:13:49 +0200 Subject: [PATCH 885/918] add label to matching family (#5128) * add label to matching family * Update openpype/tools/creator/model.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/creator/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/creator/model.py b/openpype/tools/creator/model.py index 7bb2757a11..6e905d0b56 100644 --- a/openpype/tools/creator/model.py +++ b/openpype/tools/creator/model.py @@ -53,6 +53,9 @@ class CreatorsModel(QtGui.QStandardItemModel): index = self.index(row, 0) item_id = index.data(ITEM_ID_ROLE) creator_plugin = self._creators_by_id.get(item_id) - if creator_plugin and creator_plugin.family == family: + if creator_plugin and ( + creator_plugin.label.lower() == family.lower() + or creator_plugin.family.lower() == family.lower() + ): indexes.append(index) return indexes From 22296aeb6e8ae078cde738cea8f3c38d47c3a76b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 15 Jun 2023 14:32:53 +0200 Subject: [PATCH 886/918] Pack project: Raise exception with reasonable message (#5145) * raise exception with reasonable message * raise errors at better places --- openpype/lib/project_backpack.py | 11 +++++++++++ openpype/pype_commands.py | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/openpype/lib/project_backpack.py b/openpype/lib/project_backpack.py index 674eaa3b91..55a96664d8 100644 --- a/openpype/lib/project_backpack.py +++ b/openpype/lib/project_backpack.py @@ -113,6 +113,12 @@ def pack_project( project_name )) + if only_documents and not destination_dir: + raise ValueError(( + "Destination directory must be defined" + " when only documents should be packed." + )) + root_path = None source_root = {} project_source_path = None @@ -141,6 +147,11 @@ def pack_project( if not destination_dir: destination_dir = root_path + if not destination_dir: + raise ValueError( + "Project {} does not have any roots.".format(project_name) + ) + destination_dir = os.path.normpath(destination_dir) if not os.path.exists(destination_dir): os.makedirs(destination_dir) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 6a24cb0ebc..56a0fe60cd 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -356,6 +356,13 @@ class PypeCommands: def pack_project(self, project_name, dirpath, database_only): from openpype.lib.project_backpack import pack_project + if database_only and not dirpath: + raise ValueError(( + "Destination dir must be defined when using --dbonly." + " Use '--dirpath {output dir path}' flag" + " to specify directory." + )) + pack_project(project_name, dirpath, database_only) def unpack_project(self, zip_filepath, new_root, database_only): From 359d6856442f8d9f3d992ec3af335d8b9aa300f2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Jun 2023 14:35:47 +0200 Subject: [PATCH 887/918] Draft to allow "inventory" actions to be supplied by a Module or Addon. --- openpype/modules/README.md | 3 ++- openpype/modules/base.py | 22 +++++++++++++++++++--- openpype/modules/interfaces.py | 23 +++++++++++++++++++---- openpype/pipeline/context_tools.py | 5 +++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index 86afdb9d91..ce3f99b338 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -138,7 +138,8 @@ class ClockifyModule( "publish": [], "create": [], "load": [], - "actions": [] + "actions": [], + "inventory": [] } ``` diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 732525b6eb..fb9b4e1096 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -740,15 +740,16 @@ class ModulesManager: Unknown keys are logged out. Returns: - dict: Output is dictionary with keys "publish", "create", "load" - and "actions" each containing list of paths. + dict: Output is dictionary with keys "publish", "create", "load", + "actions" and "inventory" each containing list of paths. """ # Output structure output = { "publish": [], "create": [], "load": [], - "actions": [] + "actions": [], + "inventory": [] } unknown_keys_by_module = {} for module in self.get_enabled_modules(): @@ -853,6 +854,21 @@ class ModulesManager: host_name ) + def collect_inventory_action_paths(self, host_name): + """Helper to collect load plugin paths from modules. + + Args: + host_name (str): For which host are load plugins meant. + + Returns: + list: List of pyblish plugin paths. + """ + + return self._collect_plugin_paths( + "get_inventory_action_paths", + host_name + ) + def get_host_module(self, host_name): """Find host module by host name. diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 8c9a6ee1dd..40a42e9290 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -33,8 +33,8 @@ class OpenPypeInterface: class IPluginPaths(OpenPypeInterface): """Module has plugin paths to return. - Expected result is dictionary with keys "publish", "create", "load" or - "actions" and values as list or string. + Expected result is dictionary with keys "publish", "create", "load", + "actions" or "inventory" and values as list or string. { "publish": ["path/to/publish_plugins"] } @@ -109,6 +109,21 @@ class IPluginPaths(OpenPypeInterface): return self._get_plugin_paths_by_type("publish") + def get_inventory_action_paths(self, host_name): + """Receive inventory action paths. + + Give addons ability to add inventory action plugin paths. + + Notes: + Default implementation uses 'get_plugin_paths' and always return + all publish plugin paths. + + Args: + host_name (str): For which host are the plugins meant. + """ + + return self._get_plugin_paths_by_type("inventory") + class ILaunchHookPaths(OpenPypeInterface): """Module has launch hook paths to return. @@ -397,8 +412,8 @@ class ITrayService(ITrayModule): class ISettingsChangeListener(OpenPypeInterface): """Module has plugin paths to return. - Expected result is dictionary with keys "publish", "create", "load" or - "actions" and values as list or string. + Expected result is dictionary with keys "publish", "create", "load", + "actions" or "inventory" and values as list or string. { "publish": ["path/to/publish_plugins"] } diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index ada78b989d..97a5c1ba69 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -181,6 +181,11 @@ def install_openpype_plugins(project_name=None, host_name=None): for path in load_plugin_paths: register_loader_plugin_path(path) + inventory_action_paths = modules_manager.collect_inventory_action_paths( + host_name) + for path in inventory_action_paths: + register_inventory_action_path(path) + if project_name is None: project_name = os.environ.get("AVALON_PROJECT") From b01424111aa579f95a77b3d0b39bace1e02a8a03 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 21:25:41 +0800 Subject: [PATCH 888/918] roy's comment --- .../maya/plugins/load/load_arnold_standin.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index f41afefcf4..a729e0bb06 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -23,6 +23,20 @@ def is_sequence(files): return sequence +def post_process(): + import maya.utils + from qtpy import QtWidgets + + # I'm not sure this would work, but it'd be the simplest trick to try + cmds.refresh(force=True) + + # I suspect this might work + maya.utils.processIdleEvents() + + # I suspect this one might work too but it's might be a hard to track whether it solves all cases (and whether the events were already submitted to Qt at that time this command starts to run) So I'd always try to avoid this when possible. + QtWidgets.QApplication.instance().processEvents() + + class ArnoldStandinLoader(load.LoaderPlugin): """Load as Arnold standin""" @@ -40,10 +54,13 @@ class ArnoldStandinLoader(load.LoaderPlugin): # and getting attribute from defaultArnoldRenderOption.operator # Otherwises standins will not be loaded successfully for # every first time using this loader after the build + """ if not cmds.pluginInfo("mtoa", query=True, loaded=True): raise RuntimeError("Plugin 'mtoa' must be loaded" " before using this loader") - # cmds.loadPlugin("mtoa", quiet=True) + """ + cmds.loadPlugin("mtoa", quiet=True) + post_process() import mtoa.ui.arnoldmenu version = context['version'] From 5bb476cfa439d168c4e5668c30bc7945f38713a7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 21:27:38 +0800 Subject: [PATCH 889/918] hound fix --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index a729e0bb06..9016359c2f 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -27,13 +27,8 @@ def post_process(): import maya.utils from qtpy import QtWidgets - # I'm not sure this would work, but it'd be the simplest trick to try cmds.refresh(force=True) - - # I suspect this might work maya.utils.processIdleEvents() - - # I suspect this one might work too but it's might be a hard to track whether it solves all cases (and whether the events were already submitted to Qt at that time this command starts to run) So I'd always try to avoid this when possible. QtWidgets.QApplication.instance().processEvents() From d077491617d2c1f8b95b7c86568a88c16ff290b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 21:28:56 +0800 Subject: [PATCH 890/918] add docstring --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 9016359c2f..f45a070c85 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -24,6 +24,10 @@ def is_sequence(files): def post_process(): + """ + Make sure mtoa script finished loading + before the loader doing any action + """ import maya.utils from qtpy import QtWidgets From 0fe552a01485e52512139a3aa92eb62a513f6885 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 15 Jun 2023 15:35:06 +0200 Subject: [PATCH 891/918] Update invalid docstring --- openpype/modules/interfaces.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 40a42e9290..0d73bc35a3 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -410,13 +410,11 @@ class ITrayService(ITrayModule): class ISettingsChangeListener(OpenPypeInterface): - """Module has plugin paths to return. + """Module tries to listen to settings changes. + + Only settings changes in the current process are propagated. + Changes made in other processes or machines won't trigger the callbacks. - Expected result is dictionary with keys "publish", "create", "load", - "actions" or "inventory" and values as list or string. - { - "publish": ["path/to/publish_plugins"] - } """ @abstractmethod From 6087b27c32d46c0e8c67cd133f031305eb1ae54f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 15 Jun 2023 16:05:59 +0200 Subject: [PATCH 892/918] Ftrack: Task status during publishing (#5123) * ftrack status is not set in deadline plugin during celaction integration * implemented new logic to set task status during publishing * added missing settings for new plugins * integrate hierarchy ftrack is filling status names for newly created tasks * resaved default settings * change label in settings * Remove leftover docstring Co-authored-by: Roy Nieterau * Use smaller differentiation in order to keep plugin in integration range * formatting changes --------- Co-authored-by: Roy Nieterau --- .../publish/submit_celaction_deadline.py | 1 - .../plugins/publish/integrate_ftrack_api.py | 41 -- .../publish/integrate_ftrack_farm_status.py | 150 ------ .../publish/integrate_ftrack_status.py | 433 ++++++++++++++++++ .../publish/integrate_hierarchy_ftrack.py | 26 +- .../defaults/project_settings/ftrack.json | 24 +- .../schema_project_ftrack.json | 140 +++++- 7 files changed, 609 insertions(+), 206 deletions(-) delete mode 100644 openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py create mode 100644 openpype/modules/ftrack/plugins/publish/integrate_ftrack_status.py diff --git a/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py b/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py index bcf0850768..ee28612b44 100644 --- a/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_celaction_deadline.py @@ -59,7 +59,6 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): render_path).replace("\\", "/") instance.data["publishJobState"] = "Suspended" - instance.context.data['ftrackStatus'] = "Render" # adding 2d render specific family for version identification in Loader instance.data["families"] = ["render2d"] diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py index cec48ef54f..deb8b414f0 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py @@ -109,8 +109,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): for status in asset_version_statuses } - self._set_task_status(instance, project_entity, task_entity, session) - # Prepare AssetTypes asset_types_by_short = self._ensure_asset_types_exists( session, component_list @@ -180,45 +178,6 @@ class IntegrateFtrackApi(pyblish.api.InstancePlugin): if asset_version not in instance.data[asset_versions_key]: instance.data[asset_versions_key].append(asset_version) - def _set_task_status(self, instance, project_entity, task_entity, session): - if not project_entity: - self.log.info("Task status won't be set, project is not known.") - return - - if not task_entity: - self.log.info("Task status won't be set, task is not known.") - return - - status_name = instance.context.data.get("ftrackStatus") - if not status_name: - self.log.info("Ftrack status name is not set.") - return - - self.log.debug( - "Ftrack status name will be (maybe) set to \"{}\"".format( - status_name - ) - ) - - project_schema = project_entity["project_schema"] - task_statuses = project_schema.get_statuses( - "Task", task_entity["type_id"] - ) - task_statuses_by_low_name = { - status["name"].lower(): status for status in task_statuses - } - status = task_statuses_by_low_name.get(status_name.lower()) - if not status: - self.log.warning(( - "Task status \"{}\" won't be set," - " status is now allowed on task type \"{}\"." - ).format(status_name, task_entity["type"]["name"])) - return - - self.log.info("Setting task status to \"{}\"".format(status_name)) - task_entity["status"] = status - session.commit() - def _fill_component_locations(self, session, component_list): components_by_location_name = collections.defaultdict(list) components_by_location_id = collections.defaultdict(list) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py deleted file mode 100644 index ab5738c33f..0000000000 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ /dev/null @@ -1,150 +0,0 @@ -import pyblish.api -from openpype.lib import filter_profiles - - -class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): - """Change task status when should be published on farm. - - Instance which has set "farm" key in data to 'True' is considered as will - be rendered on farm thus it's status should be changed. - """ - - order = pyblish.api.IntegratorOrder + 0.48 - label = "Integrate Ftrack Farm Status" - - farm_status_profiles = [] - - def process(self, context): - # Quick end - if not self.farm_status_profiles: - project_name = context.data["projectName"] - self.log.info(( - "Status profiles are not filled for project \"{}\". Skipping" - ).format(project_name)) - return - - filtered_instances = self.filter_instances(context) - instances_with_status_names = self.get_instances_with_statuse_names( - context, filtered_instances - ) - if instances_with_status_names: - self.fill_statuses(context, instances_with_status_names) - - def filter_instances(self, context): - filtered_instances = [] - for instance in context: - # Skip disabled instances - if instance.data.get("publish") is False: - continue - subset_name = instance.data["subset"] - msg_start = "Skipping instance {}.".format(subset_name) - if not instance.data.get("farm"): - self.log.debug( - "{} Won't be rendered on farm.".format(msg_start) - ) - continue - - task_entity = instance.data.get("ftrackTask") - if not task_entity: - self.log.debug( - "{} Does not have filled task".format(msg_start) - ) - continue - - filtered_instances.append(instance) - return filtered_instances - - def get_instances_with_statuse_names(self, context, instances): - instances_with_status_names = [] - for instance in instances: - family = instance.data["family"] - subset_name = instance.data["subset"] - task_entity = instance.data["ftrackTask"] - host_name = context.data["hostName"] - task_name = task_entity["name"] - task_type = task_entity["type"]["name"] - status_profile = filter_profiles( - self.farm_status_profiles, - { - "hosts": host_name, - "task_types": task_type, - "task_names": task_name, - "families": family, - "subsets": subset_name, - }, - logger=self.log - ) - if not status_profile: - # There already is log in 'filter_profiles' - continue - - status_name = status_profile["status_name"] - if status_name: - instances_with_status_names.append((instance, status_name)) - return instances_with_status_names - - def fill_statuses(self, context, instances_with_status_names): - # Prepare available task statuses on the project - project_name = context.data["projectName"] - session = context.data["ftrackSession"] - project_entity = session.query(( - "select project_schema from Project where full_name is \"{}\"" - ).format(project_name)).one() - project_schema = project_entity["project_schema"] - - task_type_ids = set() - for item in instances_with_status_names: - instance, _ = item - task_entity = instance.data["ftrackTask"] - task_type_ids.add(task_entity["type"]["id"]) - - task_statuses_by_type_id = { - task_type_id: project_schema.get_statuses("Task", task_type_id) - for task_type_id in task_type_ids - } - - # Keep track if anything has changed - skipped_status_names = set() - status_changed = False - for item in instances_with_status_names: - instance, status_name = item - task_entity = instance.data["ftrackTask"] - task_statuses = task_statuses_by_type_id[task_entity["type"]["id"]] - status_name_low = status_name.lower() - - status_id = None - status_name = None - # Skip if status name was already tried to be found - for status in task_statuses: - if status["name"].lower() == status_name_low: - status_id = status["id"] - status_name = status["name"] - break - - if status_id is None: - if status_name_low not in skipped_status_names: - skipped_status_names.add(status_name_low) - joined_status_names = ", ".join({ - '"{}"'.format(status["name"]) - for status in task_statuses - }) - self.log.warning(( - "Status \"{}\" is not available on project \"{}\"." - " Available statuses are {}" - ).format(status_name, project_name, joined_status_names)) - continue - - # Change task status id - if status_id != task_entity["status_id"]: - task_entity["status_id"] = status_id - status_changed = True - path = "/".join([ - item["name"] - for item in task_entity["link"] - ]) - self.log.debug("Set status \"{}\" to \"{}\"".format( - status_name, path - )) - - if status_changed: - session.commit() diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_status.py new file mode 100644 index 0000000000..e862dba7fc --- /dev/null +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_status.py @@ -0,0 +1,433 @@ +import copy + +import pyblish.api +from openpype.lib import filter_profiles + + +def create_chunks(iterable, chunk_size=None): + """Separate iterable into multiple chunks by size. + + Args: + iterable(list|tuple|set): Object that will be separated into chunks. + chunk_size(int): Size of one chunk. Default value is 200. + + Returns: + list: Chunked items. + """ + chunks = [] + + tupled_iterable = tuple(iterable) + if not tupled_iterable: + return chunks + iterable_size = len(tupled_iterable) + if chunk_size is None: + chunk_size = 200 + + if chunk_size < 1: + chunk_size = 1 + + for idx in range(0, iterable_size, chunk_size): + chunks.append(tupled_iterable[idx:idx + chunk_size]) + return chunks + + +class CollectFtrackTaskStatuses(pyblish.api.ContextPlugin): + """Collect available task statuses on the project. + + This is preparation for integration of task statuses. + + Requirements: + ftrackSession (ftrack_api.Session): Prepared ftrack session. + + Provides: + ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available + task statuses on project by task type id. + ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task + statuses by task id. Status on task can be set only once. + Value should be a name of status. + """ + + # After 'CollectFtrackApi' + order = pyblish.api.CollectorOrder + 0.4992 + label = "Collect Ftrack Task Statuses" + settings_category = "ftrack" + + def process(self, context): + ftrack_session = context.data("ftrackSession") + if ftrack_session is None: + self.log.info("Ftrack session is not created.") + return + + # Prepare available task statuses on the project + project_name = context.data["projectName"] + project_entity = ftrack_session.query(( + "select project_schema from Project where full_name is \"{}\"" + ).format(project_name)).one() + project_schema = project_entity["project_schema"] + + task_type_ids = { + task_type["id"] + for task_type in ftrack_session.query("select id from Type").all() + } + task_statuses_by_type_id = { + task_type_id: project_schema.get_statuses("Task", task_type_id) + for task_type_id in task_type_ids + } + context.data["ftrackTaskStatuses"] = task_statuses_by_type_id + context.data["ftrackStatusByTaskId"] = {} + self.log.info("Collected ftrack task statuses.") + + +class IntegrateFtrackStatusBase(pyblish.api.InstancePlugin): + """Base plugin for status collection. + + Requirements: + projectName (str): Name of the project. + hostName (str): Name of the host. + ftrackSession (ftrack_api.Session): Prepared ftrack session. + ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available + task statuses on project by task type id. + ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task + statuses by task id. Status on task can be set only once. + Value should be a name of status. + """ + + active = False + settings_key = None + status_profiles = [] + + @classmethod + def apply_settings(cls, project_settings): + settings_key = cls.settings_key + if settings_key is None: + settings_key = cls.__name__ + + try: + settings = project_settings["ftrack"]["publish"][settings_key] + except KeyError: + return + + for key, value in settings.items(): + setattr(cls, key, value) + + def process(self, instance): + context = instance.context + # No profiles -> skip + profiles = self.get_status_profiles() + if not profiles: + project_name = context.data["projectName"] + self.log.info(( + "Status profiles are not filled for project \"{}\". Skipping" + ).format(project_name)) + return + + # Task statuses were not collected -> skip + task_statuses_by_type_id = context.data.get("ftrackTaskStatuses") + if not task_statuses_by_type_id: + self.log.info( + "Ftrack task statuses are not collected. Skipping.") + return + + self.prepare_status_names(context, instance, profiles) + + def get_status_profiles(self): + """List of profiles to determine status name. + + Example profile item: + { + "host_names": ["nuke"], + "task_types": ["Compositing"], + "task_names": ["Comp"], + "families": ["render"], + "subset_names": ["renderComp"], + "status_name": "Rendering", + } + + Returns: + list[dict[str, Any]]: List of profiles. + """ + + return self.status_profiles + + def prepare_status_names(self, context, instance, profiles): + if not self.is_valid_instance(context, instance): + return + + filter_data = self.get_profile_filter_data(context, instance) + status_profile = filter_profiles( + profiles, + filter_data, + logger=self.log + ) + if not status_profile: + return + + status_name = status_profile["status_name"] + if status_name: + self.fill_status(context, instance, status_name) + + def get_profile_filter_data(self, context, instance): + task_entity = instance.data["ftrackTask"] + return { + "host_names": context.data["hostName"], + "task_types": task_entity["type"]["name"], + "task_names": task_entity["name"], + "families": instance.data["family"], + "subset_names": instance.data["subset"], + } + + def is_valid_instance(self, context, instance): + """Filter instances that should be processed. + + Ignore instances that are not enabled for publishing or don't have + filled task. Also skip instances with tasks that already have defined + status. + + Plugin should do more filtering which is custom for plugin logic. + + Args: + context (pyblish.api.Context): Pyblish context. + instance (pyblish.api.Instance): Instance to process. + + Returns: + list[pyblish.api.Instance]: List of instances that should be + processed. + """ + + ftrack_status_by_task_id = context.data["ftrackStatusByTaskId"] + # Skip disabled instances + if instance.data.get("publish") is False: + return False + + task_entity = instance.data.get("ftrackTask") + if not task_entity: + self.log.debug( + "Skipping instance Does not have filled task".format( + instance.data["subset"])) + return False + + task_id = task_entity["id"] + if task_id in ftrack_status_by_task_id: + self.log.debug("Status for task {} was already defined".format( + task_entity["name"] + )) + return False + + return True + + def fill_status(self, context, instance, status_name): + """Fill status for instance task. + + If task already had set status, it will be skipped. + + Args: + context (pyblish.api.Context): Pyblish context. + instance (pyblish.api.Instance): Pyblish instance. + status_name (str): Name of status to set. + """ + + task_entity = instance.data["ftrackTask"] + task_id = task_entity["id"] + ftrack_status_by_task_id = context.data["ftrackStatusByTaskId"] + if task_id in ftrack_status_by_task_id: + self.log.debug("Status for task {} was already defined".format( + task_entity["name"] + )) + return + + ftrack_status_by_task_id[task_id] = status_name + self.log.info(( + "Task {} will be set to \"{}\" status." + ).format(task_entity["name"], status_name)) + + +class IntegrateFtrackFarmStatus(IntegrateFtrackStatusBase): + """Collect task status names for instances that are sent to farm. + + Instance which has set "farm" key in data to 'True' is considered as will + be rendered on farm thus it's status should be changed. + + Requirements: + projectName (str): Name of the project. + hostName (str): Name of the host. + ftrackSession (ftrack_api.Session): Prepared ftrack session. + ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available + task statuses on project by task type id. + ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task + statuses by task id. Status on task can be set only once. + Value should be a name of status. + """ + + order = pyblish.api.IntegratorOrder + 0.48 + label = "Ftrack Task Status To Farm Status" + active = True + + farm_status_profiles = [] + status_profiles = None + + def is_valid_instance(self, context, instance): + if not instance.data.get("farm"): + self.log.debug("{} Won't be rendered on farm.".format( + instance.data["subset"] + )) + return False + return super(IntegrateFtrackFarmStatus, self).is_valid_instance( + context, instance) + + def get_status_profiles(self): + if self.status_profiles is None: + profiles = copy.deepcopy(self.farm_status_profiles) + for profile in profiles: + profile["host_names"] = profile.pop("hosts") + profile["subset_names"] = profile.pop("subsets") + self.status_profiles = profiles + return self.status_profiles + + +class IntegrateFtrackLocalStatus(IntegrateFtrackStatusBase): + """Collect task status names for instances that are published locally. + + Instance which has set "farm" key in data to 'True' is considered as will + be rendered on farm thus it's status should be changed. + + Requirements: + projectName (str): Name of the project. + hostName (str): Name of the host. + ftrackSession (ftrack_api.Session): Prepared ftrack session. + ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available + task statuses on project by task type id. + ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task + statuses by task id. Status on task can be set only once. + Value should be a name of status. + """ + + order = IntegrateFtrackFarmStatus.order + 0.001 + label = "Ftrack Task Status Local Publish" + active = True + targets = ["local"] + settings_key = "ftrack_task_status_local_publish" + + def is_valid_instance(self, context, instance): + if instance.data.get("farm"): + self.log.debug("{} Will be rendered on farm.".format( + instance.data["subset"] + )) + return False + return super(IntegrateFtrackLocalStatus, self).is_valid_instance( + context, instance) + + +class IntegrateFtrackOnFarmStatus(IntegrateFtrackStatusBase): + """Collect task status names for instances that are published on farm. + + Requirements: + projectName (str): Name of the project. + hostName (str): Name of the host. + ftrackSession (ftrack_api.Session): Prepared ftrack session. + ftrackTaskStatuses (dict[str, list[Any]]): Dictionary of available + task statuses on project by task type id. + ftrackStatusByTaskId (dict[str, str]): Empty dictionary of task + statuses by task id. Status on task can be set only once. + Value should be a name of status. + """ + + order = IntegrateFtrackLocalStatus.order + 0.001 + label = "Ftrack Task Status On Farm Status" + active = True + targets = ["farm"] + settings_key = "ftrack_task_status_on_farm_publish" + + +class IntegrateFtrackTaskStatus(pyblish.api.ContextPlugin): + # Use order of Integrate Ftrack Api plugin and offset it before or after + base_order = pyblish.api.IntegratorOrder + 0.499 + # By default is after Integrate Ftrack Api + order = base_order + 0.0001 + label = "Integrate Ftrack Task Status" + + @classmethod + def apply_settings(cls, project_settings): + """Apply project settings to plugin. + + Args: + project_settings (dict[str, Any]): Project settings. + """ + + settings = ( + project_settings["ftrack"]["publish"]["IntegrateFtrackTaskStatus"] + ) + diff = 0.001 + if not settings["after_version_statuses"]: + diff = -diff + cls.order = cls.base_order + diff + + def process(self, context): + task_statuses_by_type_id = context.data.get("ftrackTaskStatuses") + if not task_statuses_by_type_id: + self.log.info("Ftrack task statuses are not collected. Skipping.") + return + + status_by_task_id = self._get_status_by_task_id(context) + if not status_by_task_id: + self.log.info("No statuses to set. Skipping.") + return + + ftrack_session = context.data["ftrackSession"] + + task_entities = self._get_task_entities( + ftrack_session, status_by_task_id) + + for task_entity in task_entities: + task_path = "/".join([ + item["name"] for item in task_entity["link"] + ]) + task_id = task_entity["id"] + type_id = task_entity["type_id"] + new_status = None + status_name = status_by_task_id[task_id] + self.log.debug( + "Status to set {} on task {}.".format(status_name, task_path)) + status_name_low = status_name.lower() + available_statuses = task_statuses_by_type_id[type_id] + for status in available_statuses: + if status["name"].lower() == status_name_low: + new_status = status + break + + if new_status is None: + joined_statuses = ", ".join([ + "'{}'".format(status["name"]) + for status in available_statuses + ]) + self.log.debug(( + "Status '{}' was not found in available statuses: {}." + ).format(status_name, joined_statuses)) + continue + + if task_entity["status_id"] != new_status["id"]: + task_entity["status_id"] = new_status["id"] + + self.log.debug("Changing status of task '{}' to '{}'".format( + task_path, status_name + )) + ftrack_session.commit() + + def _get_status_by_task_id(self, context): + status_by_task_id = context.data["ftrackStatusByTaskId"] + return { + task_id: status_name + for task_id, status_name in status_by_task_id.items() + if status_name + } + + def _get_task_entities(self, ftrack_session, status_by_task_id): + task_entities = [] + for chunk_ids in create_chunks(status_by_task_id.keys()): + joined_ids = ",".join( + ['"{}"'.format(task_id) for task_id in chunk_ids] + ) + task_entities.extend(ftrack_session.query(( + "select id, type_id, status_id, link from Task" + " where id in ({})" + ).format(joined_ids)).all()) + return task_entities diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index 6daaea5f18..a1aa7c0daa 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -63,7 +63,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): """ order = pyblish.api.IntegratorOrder - 0.04 - label = 'Integrate Hierarchy To Ftrack' + label = "Integrate Hierarchy To Ftrack" families = ["shot"] hosts = [ "hiero", @@ -94,14 +94,13 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): "Project \"{}\" was not found on ftrack.".format(project_name) ) - self.context = context self.session = session self.ft_project = project self.task_types = self.get_all_task_types(project) self.task_statuses = self.get_task_statuses(project) # import ftrack hierarchy - self.import_to_ftrack(project_name, hierarchy_context) + self.import_to_ftrack(context, project_name, hierarchy_context) def query_ftrack_entitites(self, session, ft_project): project_id = ft_project["id"] @@ -227,7 +226,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return output - def import_to_ftrack(self, project_name, hierarchy_context): + def import_to_ftrack(self, context, project_name, hierarchy_context): # Prequery hiearchical custom attributes hier_attrs = get_pype_attr(self.session)[1] hier_attr_by_key = { @@ -258,7 +257,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.session, matching_entities, hier_attrs) # Get ftrack api module (as they are different per python version) - ftrack_api = self.context.data["ftrackPythonModule"] + ftrack_api = context.data["ftrackPythonModule"] # Use queue of hierarchy items to process import_queue = collections.deque() @@ -292,7 +291,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): # CUSTOM ATTRIBUTES custom_attributes = entity_data.get('custom_attributes', {}) instances = [] - for instance in self.context: + for instance in context: instance_asset_name = instance.data.get("asset") if ( instance_asset_name @@ -369,6 +368,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): if task_name: instances_by_task_name[task_name.lower()].append(instance) + ftrack_status_by_task_id = context.data["ftrackStatusByTaskId"] tasks = entity_data.get('tasks', []) existing_tasks = [] tasks_to_create = [] @@ -389,11 +389,11 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): for task_name, task_type in tasks_to_create: task_entity = self.create_task( - name=task_name, - task_type=task_type, - parent=entity + task_name, + task_type, + entity, + ftrack_status_by_task_id ) - for instance in instances_by_task_name[task_name.lower()]: instance.data["ftrackTask"] = task_entity @@ -481,7 +481,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): for status in task_workflow_statuses } - def create_task(self, name, task_type, parent): + def create_task(self, name, task_type, parent, ftrack_status_by_task_id): filter_data = { "task_names": name, "task_types": task_type @@ -491,12 +491,14 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): filter_data ) status_id = None + status_name = None if profile: status_name = profile["status_name"] status_name_low = status_name.lower() for _status_id, status in self.task_statuses.items(): if status["name"].lower() == status_name_low: status_id = _status_id + status_name = status["name"] break if status_id is None: @@ -523,6 +525,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.session._configure_locations() six.reraise(tp, value, tb) + if status_id is not None: + ftrack_status_by_task_id[task["id"]] = None return task def _get_active_assets(self, context): diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 4ca4a35d1f..b87c45666d 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -493,7 +493,29 @@ "upload_reviewable_with_origin_name": false }, "IntegrateFtrackFarmStatus": { - "farm_status_profiles": [] + "farm_status_profiles": [ + { + "hosts": [ + "celaction" + ], + "task_types": [], + "task_names": [], + "families": [ + "render" + ], + "subsets": [], + "status_name": "Render" + } + ] + }, + "ftrack_task_status_local_publish": { + "status_profiles": [] + }, + "ftrack_task_status_on_farm_publish": { + "status_profiles": [] + }, + "IntegrateFtrackTaskStatus": { + "after_version_statuses": true } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 7050721742..157a8d297e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1058,7 +1058,7 @@ { "type": "dict", "key": "IntegrateFtrackFarmStatus", - "label": "Integrate Ftrack Farm Status", + "label": "Ftrack Status To Farm", "children": [ { "type": "label", @@ -1068,7 +1068,7 @@ "type": "list", "collapsible": true, "key": "farm_status_profiles", - "label": "Farm status profiles", + "label": "Profiles", "use_label_wrap": true, "object_type": { "type": "dict", @@ -1114,6 +1114,142 @@ } } ] + }, + { + "type": "dict", + "key": "ftrack_task_status_local_publish", + "label": "Ftrack Status Local Integration", + "children": [ + { + "type": "label", + "label": "Change status of task when is integrated locally" + }, + { + "type": "list", + "collapsible": true, + "key": "status_profiles", + "label": "Profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "host_names", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subset_names", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "status_name", + "label": "Status name", + "type": "text" + } + ] + } + } + ] + }, + { + "type": "dict", + "key": "ftrack_task_status_on_farm_publish", + "label": "Ftrack Status On Farm", + "children": [ + { + "type": "label", + "label": "Change status of task when it's subset is integrated on farm" + }, + { + "type": "list", + "collapsible": true, + "key": "status_profiles", + "label": "Profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "host_names", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subset_names", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "status_name", + "label": "Status name", + "type": "text" + } + ] + } + } + ] + }, + { + "type": "dict", + "key": "IntegrateFtrackTaskStatus", + "label": "Integrate Ftrack Task Status", + "children": [ + { + "type": "label", + "label": "Apply collected task statuses. This plugin can run before or after version integration. Some status automations may conflict with status changes on versions because of wrong order." + }, + { + "type": "boolean", + "key": "after_version_statuses", + "label": "After version integration" + } + ] } ] } From 2f95aab31efdd90dbdf0b899059d87416190e329 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 15 Jun 2023 22:31:55 +0800 Subject: [PATCH 893/918] clean up the code --- .../maya/plugins/load/load_arnold_standin.py | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index f45a070c85..1a582647cc 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -2,6 +2,7 @@ import os import clique import maya.cmds as cmds +import maya.utils from openpype.settings import get_project_settings from openpype.pipeline import ( @@ -23,19 +24,6 @@ def is_sequence(files): return sequence -def post_process(): - """ - Make sure mtoa script finished loading - before the loader doing any action - """ - import maya.utils - from qtpy import QtWidgets - - cmds.refresh(force=True) - maya.utils.processIdleEvents() - QtWidgets.QApplication.instance().processEvents() - - class ArnoldStandinLoader(load.LoaderPlugin): """Load as Arnold standin""" @@ -48,18 +36,15 @@ class ArnoldStandinLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, options): - # Make sure user has loaded arnold - # before importing `mtoa.ui.arnoldmenu` - # and getting attribute from defaultArnoldRenderOption.operator - # Otherwises standins will not be loaded successfully for - # every first time using this loader after the build - """ if not cmds.pluginInfo("mtoa", query=True, loaded=True): - raise RuntimeError("Plugin 'mtoa' must be loaded" - " before using this loader") - """ - cmds.loadPlugin("mtoa", quiet=True) - post_process() + # Allow mtoa plugin load to process all its events + # because otherwise `defaultArnoldRenderOptions.operator` + # does not exist yet and some connections to the standin + # can't be correctly generated on create resulting in an error + cmds.loadPlugin("mtoa") + cmds.refresh(force=True) + maya.utils.processIdleEvents() + import mtoa.ui.arnoldmenu version = context['version'] From f9a64192b37d5c474d2be7803d6f0242415e7539 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 16 Jun 2023 10:50:38 +0200 Subject: [PATCH 894/918] fix match check of save sequence (#5148) --- openpype/tools/publisher/window.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 006098cb37..2bda0c1cfe 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -453,7 +453,11 @@ class PublisherWindow(QtWidgets.QDialog): return save_match = event.matches(QtGui.QKeySequence.Save) - if save_match == QtGui.QKeySequence.ExactMatch: + # PySide2 and PySide6 support + if not isinstance(save_match, bool): + save_match = save_match == QtGui.QKeySequence.ExactMatch + + if save_match: if not self._controller.publish_has_started: self._save_changes(True) event.accept() From 7ecb03bb75cc66b411301d62b79df5a0bd198bfb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:17:08 +0200 Subject: [PATCH 895/918] ImageIO: Minor fixes (#5147) * define variable 'resolved_path' in right scope * fixed missing 'input' variable * make checks for keys more explicit and safe proof * fixed caching of remapped colorspaces * trying to fix indentation issue * use safe keys pop --- openpype/hosts/nuke/api/lib.py | 8 +-- openpype/pipeline/colorspace.py | 107 ++++++++++++++++---------------- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 777f4454dc..c05182ce97 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2020,11 +2020,11 @@ class WorkfileSettings(object): # TODO: backward compatibility for old projects - remove later # perhaps old project overrides is having it set to older version # with use of `customOCIOConfigPath` + resolved_path = None if workfile_settings.get("customOCIOConfigPath"): unresolved_path = workfile_settings["customOCIOConfigPath"] ocio_paths = unresolved_path[platform.system().lower()] - resolved_path = None for ocio_p in ocio_paths: resolved_path = str(ocio_p).format(**os.environ) if not os.path.exists(resolved_path): @@ -2054,9 +2054,9 @@ class WorkfileSettings(object): self._root_node["colorManagement"].setValue("OCIO") # we dont need the key anymore - workfile_settings.pop("customOCIOConfigPath") - workfile_settings.pop("colorManagement") - workfile_settings.pop("OCIO_config") + workfile_settings.pop("customOCIOConfigPath", None) + workfile_settings.pop("colorManagement", None) + workfile_settings.pop("OCIO_config", None) # then set the rest for knob, value_ in workfile_settings.items(): diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 899b14148b..1999ad3bed 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -312,7 +312,8 @@ def get_views_data_subprocess(config_path): def get_imageio_config( - project_name, host_name, + project_name, + host_name, project_settings=None, anatomy_data=None, anatomy=None @@ -325,12 +326,9 @@ def get_imageio_config( Args: project_name (str): project name host_name (str): host name - project_settings (dict, optional): project settings. - Defaults to None. - anatomy_data (dict, optional): anatomy formatting data. - Defaults to None. - anatomy (lib.Anatomy, optional): Anatomy object. - Defaults to None. + project_settings (Optional[dict]): Project settings. + anatomy_data (Optional[dict]): anatomy formatting data. + anatomy (Optional[Anatomy]): Anatomy object. Returns: dict: config path data or empty dict @@ -345,37 +343,36 @@ def get_imageio_config( formatting_data = deepcopy(anatomy_data) - # add project roots to anatomy data + # Add project roots to anatomy data formatting_data["root"] = anatomy.roots formatting_data["platform"] = platform.system().lower() - # get colorspace settings - # check if global settings group is having activate_global_color_management - # key at all. If it does't then default it to False - # this is for backward compatibility only - # TODO: in future rewrite this to be more explicit + # Get colorspace settings imageio_global, imageio_host = _get_imageio_settings( project_settings, host_name) - activate_color_management = ( - imageio_global.get("activate_global_color_management", False) - # for already saved overrides from previous version - # TODO: remove this in future - backward compatibility - or imageio_host.get("ocio_config").get("enabled") - ) + # Host 'ocio_config' is optional + host_ocio_config = imageio_host.get("ocio_config") or {} + + # Global color management must be enabled to be able to use host settings + activate_color_management = imageio_global.get( + "activate_global_color_management") + # TODO: remove this in future - backward compatibility + # For already saved overrides from previous version look for 'enabled' + # on host settings. + if activate_color_management is None: + activate_color_management = host_ocio_config.get("enabled", False) if not activate_color_management: # if global settings are disabled return empty dict because # it is expected that no colorspace management is needed - log.info( - "Colorspace management is disabled globally." - ) + log.info("Colorspace management is disabled globally.") return {} - # check if host settings group is having activate_host_color_management - # if it does not have activation key then default it to True so it uses - # global settings - # this is for backward compatibility + # Check if host settings group is having 'activate_host_color_management' + # - if it does not have activation key then default it to True so it uses + # global settings + # This is for backward compatibility. # TODO: in future rewrite this to be more explicit activate_host_color_management = imageio_host.get( "activate_host_color_management", True) @@ -389,21 +386,18 @@ def get_imageio_config( ) return {} - config_host = imageio_host.get("ocio_config", {}) - - # get config path from either global or host_name + # get config path from either global or host settings # depending on override flag # TODO: in future rewrite this to be more explicit - config_data = None - override_global_config = ( - config_host.get("override_global_config") + override_global_config = host_ocio_config.get("override_global_config") + if override_global_config is None: # for already saved overrides from previous version # TODO: remove this in future - backward compatibility - or config_host.get("enabled") - ) + override_global_config = host_ocio_config.get("enabled") + if override_global_config: config_data = _get_config_data( - config_host["filepath"], formatting_data + host_ocio_config["filepath"], formatting_data ) else: # get config path from global @@ -507,34 +501,35 @@ def get_imageio_file_rules(project_name, host_name, project_settings=None): frules_host = imageio_host.get("file_rules", {}) # compile file rules dictionary - activate_host_rules = ( - frules_host.get("activate_host_rules") + activate_host_rules = frules_host.get("activate_host_rules") + if activate_host_rules is None: # TODO: remove this in future - backward compatibility - or frules_host.get("enabled") - ) + activate_host_rules = frules_host.get("enabled", False) # return host rules if activated or global rules return frules_host["rules"] if activate_host_rules else global_rules def get_remapped_colorspace_to_native( - ocio_colorspace_name, host_name, imageio_host_settings): + ocio_colorspace_name, host_name, imageio_host_settings +): """Return native colorspace name. Args: ocio_colorspace_name (str | None): ocio colorspace name + host_name (str): Host name. + imageio_host_settings (dict[str, Any]): ImageIO host settings. Returns: - str: native colorspace name defined in remapping or None + Union[str, None]: native colorspace name defined in remapping or None """ - if not CashedData.remapping.get(host_name, {}).get("to_native"): + CashedData.remapping.setdefault(host_name, {}) + if CashedData.remapping[host_name].get("to_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name] = { - "to_native": { - rule["ocio_name"]: input["host_native_name"] - for rule in remapping_rules - } + CashedData.remapping[host_name]["to_native"] = { + rule["ocio_name"]: rule["host_native_name"] + for rule in remapping_rules } return CashedData.remapping[host_name]["to_native"].get( @@ -542,23 +537,25 @@ def get_remapped_colorspace_to_native( def get_remapped_colorspace_from_native( - host_native_colorspace_name, host_name, imageio_host_settings): + host_native_colorspace_name, host_name, imageio_host_settings +): """Return ocio colorspace name remapped from host native used name. Args: host_native_colorspace_name (str): host native colorspace name + host_name (str): Host name. + imageio_host_settings (dict[str, Any]): ImageIO host settings. Returns: - str: ocio colorspace name defined in remapping or None + Union[str, None]: Ocio colorspace name defined in remapping or None. """ - if not CashedData.remapping.get(host_name, {}).get("from_native"): + CashedData.remapping.setdefault(host_name, {}) + if CashedData.remapping[host_name].get("from_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name] = { - "from_native": { - input["host_native_name"]: rule["ocio_name"] - for rule in remapping_rules - } + CashedData.remapping[host_name]["from_native"] = { + rule["host_native_name"]: rule["ocio_name"] + for rule in remapping_rules } return CashedData.remapping[host_name]["from_native"].get( From c388ee94636e7eac952d7cff5c067fc03173ecb4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 16 Jun 2023 22:08:21 +0800 Subject: [PATCH 896/918] add collector to tray publisher for getting frame range data --- .../publish/collect_anatomy_frame_range.py | 33 +++++++++++++++++++ .../project_settings/traypublisher.json | 4 +++ .../schema_project_traypublisher.json | 4 +++ 3 files changed, 41 insertions(+) create mode 100644 openpype/plugins/publish/collect_anatomy_frame_range.py diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py new file mode 100644 index 0000000000..71a5dcfeb0 --- /dev/null +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -0,0 +1,33 @@ +import pyblish.api + + +class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): + """Collect Frame Range specific Anatomy data. + + Plugin is running for all instances on context even not active instances. + """ + + order = pyblish.api.CollectorOrder + 0.491 + label = "Collect Anatomy Frame Range" + hosts = ["traypublisher"] + + def process(self, instance): + self.log.info("Collecting Anatomy frame range.") + asset_doc = instance.data.get("assetEntity") + if not asset_doc: + self.log.info("Missing required data..") + return + + asset_data = asset_doc["data"] + for key in ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd" + ): + if key not in instance.data and key in asset_data: + value = asset_data[key] + instance.data[key] = value + + self.log.info("Anatomy frame range collection finished.") diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 3a42c93515..6b8bdcfcc5 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -318,6 +318,10 @@ } }, "publish": { + "CollectAnatomyFrameRange": { + "enabled": true, + "active": true + }, "ValidateFrameRange": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 3703d82856..44442a07d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -343,6 +343,10 @@ "type": "schema_template", "name": "template_validate_plugin", "template_data": [ + { + "key": "CollectAnatomyFrameRange", + "label": "Collect Anatomy frame range" + }, { "key": "ValidateFrameRange", "label": "Validate frame range" From ec8c70db272ae51bb7118d6c7f359dff953efc6d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 16 Jun 2023 22:14:06 +0800 Subject: [PATCH 897/918] delete unrelated code --- .../publish/collect_anatomy_frame_range.py | 33 +++++++++++++++++++ .../project_settings/traypublisher.json | 4 +++ .../schema_project_traypublisher.json | 4 +++ 3 files changed, 41 insertions(+) create mode 100644 openpype/plugins/publish/collect_anatomy_frame_range.py diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py new file mode 100644 index 0000000000..71a5dcfeb0 --- /dev/null +++ b/openpype/plugins/publish/collect_anatomy_frame_range.py @@ -0,0 +1,33 @@ +import pyblish.api + + +class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): + """Collect Frame Range specific Anatomy data. + + Plugin is running for all instances on context even not active instances. + """ + + order = pyblish.api.CollectorOrder + 0.491 + label = "Collect Anatomy Frame Range" + hosts = ["traypublisher"] + + def process(self, instance): + self.log.info("Collecting Anatomy frame range.") + asset_doc = instance.data.get("assetEntity") + if not asset_doc: + self.log.info("Missing required data..") + return + + asset_data = asset_doc["data"] + for key in ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd" + ): + if key not in instance.data and key in asset_data: + value = asset_data[key] + instance.data[key] = value + + self.log.info("Anatomy frame range collection finished.") diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 3a42c93515..6b8bdcfcc5 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -318,6 +318,10 @@ } }, "publish": { + "CollectAnatomyFrameRange": { + "enabled": true, + "active": true + }, "ValidateFrameRange": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 3703d82856..44442a07d4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -343,6 +343,10 @@ "type": "schema_template", "name": "template_validate_plugin", "template_data": [ + { + "key": "CollectAnatomyFrameRange", + "label": "Collect Anatomy frame range" + }, { "key": "ValidateFrameRange", "label": "Validate frame range" From 00eab724a4dec7df938013349cdd5ecaa9fc5645 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 16 Jun 2023 22:15:58 +0800 Subject: [PATCH 898/918] delete unrelated code --- .../publish/collect_anatomy_frame_range.py | 33 ------------------- .../project_settings/traypublisher.json | 4 --- .../schema_project_traypublisher.json | 4 --- 3 files changed, 41 deletions(-) delete mode 100644 openpype/plugins/publish/collect_anatomy_frame_range.py diff --git a/openpype/plugins/publish/collect_anatomy_frame_range.py b/openpype/plugins/publish/collect_anatomy_frame_range.py deleted file mode 100644 index 71a5dcfeb0..0000000000 --- a/openpype/plugins/publish/collect_anatomy_frame_range.py +++ /dev/null @@ -1,33 +0,0 @@ -import pyblish.api - - -class CollectAnatomyFrameRange(pyblish.api.InstancePlugin): - """Collect Frame Range specific Anatomy data. - - Plugin is running for all instances on context even not active instances. - """ - - order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Anatomy Frame Range" - hosts = ["traypublisher"] - - def process(self, instance): - self.log.info("Collecting Anatomy frame range.") - asset_doc = instance.data.get("assetEntity") - if not asset_doc: - self.log.info("Missing required data..") - return - - asset_data = asset_doc["data"] - for key in ( - "fps", - "frameStart", - "frameEnd", - "handleStart", - "handleEnd" - ): - if key not in instance.data and key in asset_data: - value = asset_data[key] - instance.data[key] = value - - self.log.info("Anatomy frame range collection finished.") diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 6b8bdcfcc5..3a42c93515 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -318,10 +318,6 @@ } }, "publish": { - "CollectAnatomyFrameRange": { - "enabled": true, - "active": true - }, "ValidateFrameRange": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 44442a07d4..3703d82856 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -343,10 +343,6 @@ "type": "schema_template", "name": "template_validate_plugin", "template_data": [ - { - "key": "CollectAnatomyFrameRange", - "label": "Collect Anatomy frame range" - }, { "key": "ValidateFrameRange", "label": "Validate frame range" From 3d41ee6591f554b1b2ad25a208ad1ae8525868a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 16 Jun 2023 16:26:04 +0200 Subject: [PATCH 899/918] TrayPublisher & StandalonePublisher: Specify version (#5142) * modified simple creator plugin to be able handle version control * added 'allow_version_control' to simple creators * don't remove 'create_context' from pyblish context during publishing * implemented validator for existing version override * actually fill version on collected instances * version can be again changed from standalone publisher * added comment to collector * make sure the version is set always to int * removed unused import * disable validator if is disabled * fix filtered instances loop --- .../plugins/publish/collect_context.py | 9 +- openpype/hosts/traypublisher/api/plugin.py | 182 +++++++++++++++++- .../publish/collect_simple_instances.py | 24 +++ .../help/validate_existing_version.xml | 16 ++ .../publish/validate_existing_version.py | 57 ++++++ openpype/pipeline/create/context.py | 13 ++ .../publish/collect_from_create_context.py | 2 +- .../project_settings/traypublisher.json | 16 ++ .../schema_project_traypublisher.json | 10 + .../widgets/widget_family.py | 3 +- 10 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml create mode 100644 openpype/hosts/traypublisher/plugins/publish/validate_existing_version.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index 96aaae23dc..8fa53f5f48 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -222,7 +222,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": in_data["family"], - # "version": in_data.get("version", 1), "frameStart": in_data.get("representations", [None])[0].get( "frameStart", None ), @@ -232,6 +231,14 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): "families": instance_families } ) + # Fill version only if 'use_next_available_version' is disabled + # and version is filled in instance data + version = in_data.get("version") + use_next_available_version = in_data.get( + "use_next_available_version", True) + if not use_next_available_version and version is not None: + instance.data["version"] = version + self.log.info("collected instance: {}".format(pformat(instance.data))) self.log.info("parsing data: {}".format(pformat(in_data))) diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 75930f0f31..36e041a32c 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,4 +1,14 @@ -from openpype.lib.attribute_definitions import FileDef +from openpype.client import ( + get_assets, + get_subsets, + get_last_versions, +) +from openpype.lib.attribute_definitions import ( + FileDef, + BoolDef, + NumberDef, + UISeparatorDef, +) from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS from openpype.pipeline.create import ( Creator, @@ -94,6 +104,7 @@ class TrayPublishCreator(Creator): class SettingsCreator(TrayPublishCreator): create_allow_context_change = True create_allow_thumbnail = True + allow_version_control = False extensions = [] @@ -101,8 +112,18 @@ class SettingsCreator(TrayPublishCreator): # Pass precreate data to creator attributes thumbnail_path = pre_create_data.pop(PRE_CREATE_THUMBNAIL_KEY, None) + # Fill 'version_to_use' if version control is enabled + if self.allow_version_control: + asset_name = data["asset"] + subset_docs_by_asset_id = self._prepare_next_versions( + [asset_name], [subset_name]) + version = subset_docs_by_asset_id[asset_name].get(subset_name) + pre_create_data["version_to_use"] = version + data["_previous_last_version"] = version + data["creator_attributes"] = pre_create_data data["settings_creator"] = True + # Create new instance new_instance = CreatedInstance(self.family, subset_name, data, self) @@ -111,7 +132,158 @@ class SettingsCreator(TrayPublishCreator): if thumbnail_path: self.set_instance_thumbnail_path(new_instance.id, thumbnail_path) + def _prepare_next_versions(self, asset_names, subset_names): + """Prepare next versions for given asset and subset names. + + Todos: + Expect combination of subset names by asset name to avoid + unnecessary server calls for unused subsets. + + Args: + asset_names (Iterable[str]): Asset names. + subset_names (Iterable[str]): Subset names. + + Returns: + dict[str, dict[str, int]]: Last versions by asset + and subset names. + """ + + # Prepare all versions for all combinations to '1' + subset_docs_by_asset_id = { + asset_name: { + subset_name: 1 + for subset_name in subset_names + } + for asset_name in asset_names + } + if not asset_names or not subset_names: + return subset_docs_by_asset_id + + asset_docs = get_assets( + self.project_name, + asset_names=asset_names, + fields=["_id", "name"] + ) + asset_names_by_id = { + asset_doc["_id"]: asset_doc["name"] + for asset_doc in asset_docs + } + subset_docs = list(get_subsets( + self.project_name, + asset_ids=asset_names_by_id.keys(), + subset_names=subset_names, + fields=["_id", "name", "parent"] + )) + + subset_ids = {subset_doc["_id"] for subset_doc in subset_docs} + last_versions = get_last_versions( + self.project_name, + subset_ids, + fields=["name", "parent"]) + + for subset_doc in subset_docs: + asset_id = subset_doc["parent"] + asset_name = asset_names_by_id[asset_id] + subset_name = subset_doc["name"] + subset_id = subset_doc["_id"] + last_version = last_versions.get(subset_id) + version = 0 + if last_version is not None: + version = last_version["name"] + subset_docs_by_asset_id[asset_name][subset_name] += version + return subset_docs_by_asset_id + + def _fill_next_versions(self, instances_data): + """Fill next version for instances. + + Instances have also stored previous next version to be able to + recognize if user did enter different version. If version was + not changed by user, or user set it to '0' the next version will be + updated by current database state. + """ + + filtered_instance_data = [] + for instance in instances_data: + previous_last_version = instance.get("_previous_last_version") + creator_attributes = instance["creator_attributes"] + use_next_version = creator_attributes.get( + "use_next_version", True) + version = creator_attributes.get("version_to_use", 0) + if ( + use_next_version + or version == 0 + or version == previous_last_version + ): + filtered_instance_data.append(instance) + + asset_names = { + instance["asset"] + for instance in filtered_instance_data} + subset_names = { + instance["subset"] + for instance in filtered_instance_data} + subset_docs_by_asset_id = self._prepare_next_versions( + asset_names, subset_names + ) + for instance in filtered_instance_data: + asset_name = instance["asset"] + subset_name = instance["subset"] + version = subset_docs_by_asset_id[asset_name][subset_name] + instance["creator_attributes"]["version_to_use"] = version + instance["_previous_last_version"] = version + + def collect_instances(self): + """Collect instances from host. + + Overriden to be able to manage version control attributes. If version + control is disabled, the attributes will be removed from instances, + and next versions are filled if is version control enabled. + """ + + instances_by_identifier = cache_and_get_instances( + self, SHARED_DATA_KEY, list_instances + ) + instances = instances_by_identifier[self.identifier] + if not instances: + return + + if self.allow_version_control: + self._fill_next_versions(instances) + + for instance_data in instances: + # Make sure that there are not data related to version control + # if plugin does not support it + if not self.allow_version_control: + instance_data.pop("_previous_last_version", None) + creator_attributes = instance_data["creator_attributes"] + creator_attributes.pop("version_to_use", None) + creator_attributes.pop("use_next_version", None) + + instance = CreatedInstance.from_existing(instance_data, self) + self._add_instance_to_context(instance) + def get_instance_attr_defs(self): + defs = self.get_pre_create_attr_defs() + if self.allow_version_control: + defs += [ + UISeparatorDef(), + BoolDef( + "use_next_version", + default=True, + label="Use next version", + ), + NumberDef( + "version_to_use", + default=1, + minimum=0, + maximum=999, + label="Version to use", + ) + ] + return defs + + def get_pre_create_attr_defs(self): + # Use same attributes as for instance attributes return [ FileDef( "representation_files", @@ -132,10 +304,6 @@ class SettingsCreator(TrayPublishCreator): ) ] - def get_pre_create_attr_defs(self): - # Use same attributes as for instance attrobites - return self.get_instance_attr_defs() - @classmethod def from_settings(cls, item_data): identifier = item_data["identifier"] @@ -155,6 +323,8 @@ class SettingsCreator(TrayPublishCreator): "extensions": item_data["extensions"], "allow_sequences": item_data["allow_sequences"], "allow_multiple_items": item_data["allow_multiple_items"], - "default_variants": item_data["default_variants"] + "allow_version_control": item_data.get( + "allow_version_control", False), + "default_variants": item_data["default_variants"], } ) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index c081216481..3fa3c3b8c8 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -47,6 +47,8 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): "Created temp staging directory for instance {}. {}" ).format(instance_label, tmp_folder)) + self._fill_version(instance, instance_label) + # Store filepaths for validation of their existence source_filepaths = [] # Make sure there are no representations with same name @@ -93,6 +95,28 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): ) ) + def _fill_version(self, instance, instance_label): + """Fill instance version under which will be instance integrated. + + Instance must have set 'use_next_version' to 'False' + and 'version_to_use' to version to use. + + Args: + instance (pyblish.api.Instance): Instance to fill version for. + instance_label (str): Label of instance to fill version for. + """ + + creator_attributes = instance.data["creator_attributes"] + use_next_version = creator_attributes.get("use_next_version", True) + # If 'version_to_use' is '0' it means that next version should be used + version_to_use = creator_attributes.get("version_to_use", 0) + if use_next_version or not version_to_use: + return + instance.data["version"] = version_to_use + self.log.debug( + "Version for instance \"{}\" was set to \"{}\"".format( + instance_label, version_to_use)) + def _create_main_representations( self, instance, diff --git a/openpype/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml b/openpype/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml new file mode 100644 index 0000000000..8a3b8f4d7d --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml @@ -0,0 +1,16 @@ + + + +Version already exists + +## Version already exists + +Version {version} you have set on instance '{subset_name}' under '{asset_name}' already exists. This validation is enabled by default to prevent accidental override of existing versions. + +### How to repair? +- Click on 'Repair' action -> this will change version to next available. +- Disable validation on the instance if you are sure you want to override the version. +- Reset publishing and manually change the version number. + + + diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_existing_version.py b/openpype/hosts/traypublisher/plugins/publish/validate_existing_version.py new file mode 100644 index 0000000000..1fb27acdeb --- /dev/null +++ b/openpype/hosts/traypublisher/plugins/publish/validate_existing_version.py @@ -0,0 +1,57 @@ +import pyblish.api + +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishXmlValidationError, + OptionalPyblishPluginMixin, + RepairAction, +) + + +class ValidateExistingVersion( + OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin +): + label = "Validate Existing Version" + order = ValidateContentsOrder + + hosts = ["traypublisher"] + + actions = [RepairAction] + + settings_category = "traypublisher" + optional = True + + def process(self, instance): + if not self.is_active(instance.data): + return + + version = instance.data.get("version") + if version is None: + return + + last_version = instance.data.get("latestVersion") + if last_version is None or last_version < version: + return + + subset_name = instance.data["subset"] + msg = "Version {} already exists for subset {}.".format( + version, subset_name) + + formatting_data = { + "subset_name": subset_name, + "asset_name": instance.data["asset"], + "version": version + } + raise PublishXmlValidationError( + self, msg, formatting_data=formatting_data) + + @classmethod + def repair(cls, instance): + create_context = instance.context.data["create_context"] + created_instance = create_context.get_instance_by_id( + instance.data["instance_id"]) + creator_attributes = created_instance["creator_attributes"] + # Disable version override + creator_attributes["use_next_version"] = True + create_context.save_changes() diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 2fc0669732..332e271b0d 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1441,6 +1441,19 @@ class CreateContext: """Access to global publish attributes.""" return self._publish_attributes + def get_instance_by_id(self, instance_id): + """Receive instance by id. + + Args: + instance_id (str): Instance id. + + Returns: + Union[CreatedInstance, None]: Instance or None if instance with + given id is not available. + """ + + return self._instances_by_id.get(instance_id) + def get_sorted_creators(self, identifiers=None): """Sorted creators by 'order' attribute. diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 4888476fff..8806a13ca0 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -16,7 +16,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.5 def process(self, context): - create_context = context.data.pop("create_context", None) + create_context = context.data.get("create_context") if not create_context: host = registered_host() if isinstance(host, IPublishHost): diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 3a42c93515..4c2c2f1391 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -23,6 +23,7 @@ "detailed_description": "Workfiles are full scenes from any application that are directly edited by artists. They represent a state of work on a task at a given point and are usually not directly referenced into other scenes.", "allow_sequences": false, "allow_multiple_items": false, + "allow_version_control": false, "extensions": [ ".ma", ".mb", @@ -57,6 +58,7 @@ "detailed_description": "Models should only contain geometry data, without any extras like cameras, locators or bones.\n\nKeep in mind that models published from tray publisher are not validated for correctness. ", "allow_sequences": false, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [ ".ma", ".mb", @@ -82,6 +84,7 @@ "detailed_description": "Alembic or bgeo cache of animated data", "allow_sequences": true, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [ ".abc", ".bgeo", @@ -105,6 +108,7 @@ "detailed_description": "Any type of image seqeuence coming from outside of the studio. Usually camera footage, but could also be animatics used for reference.", "allow_sequences": true, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [ ".exr", ".png", @@ -127,6 +131,7 @@ "detailed_description": "Sequence or single file renders", "allow_sequences": true, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [ ".exr", ".png", @@ -150,6 +155,7 @@ "detailed_description": "Ideally this should be only camera itself with baked animation, however, it can technically also include helper geometry.", "allow_sequences": false, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [ ".abc", ".ma", @@ -174,6 +180,7 @@ "detailed_description": "Any image data can be published as image family. References, textures, concept art, matte paints. This is a fallback 2d family for everything that doesn't fit more specific family.", "allow_sequences": false, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [ ".exr", ".jpg", @@ -197,6 +204,7 @@ "detailed_description": "Hierarchical data structure for the efficient storage and manipulation of sparse volumetric data discretized on three-dimensional grids", "allow_sequences": true, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [ ".vdb" ] @@ -215,6 +223,7 @@ "detailed_description": "Script exported from matchmoving application to be later processed into a tracked camera with additional data", "allow_sequences": false, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [] }, { @@ -227,6 +236,7 @@ "detailed_description": "CG rigged character or prop. Rig should be clean of any extra data and directly loadable into it's respective application\t", "allow_sequences": false, "allow_multiple_items": false, + "allow_version_control": false, "extensions": [ ".ma", ".blend", @@ -244,6 +254,7 @@ "detailed_description": "Texture files with Unreal Engine naming conventions", "allow_sequences": false, "allow_multiple_items": true, + "allow_version_control": false, "extensions": [] } ], @@ -322,6 +333,11 @@ "enabled": true, "optional": true, "active": true + }, + "ValidateExistingVersion": { + "enabled": true, + "optional": true, + "active": true } } } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 3703d82856..e75e2887db 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -85,6 +85,12 @@ "label": "Allow multiple items", "type": "boolean" }, + { + "type": "boolean", + "key": "allow_version_control", + "label": "Allow version control", + "default": false + }, { "type": "list", "key": "extensions", @@ -346,6 +352,10 @@ { "key": "ValidateFrameRange", "label": "Validate frame range" + }, + { + "key": "ValidateExistingVersion", + "label": "Validate Existing Version" } ] } diff --git a/openpype/tools/standalonepublish/widgets/widget_family.py b/openpype/tools/standalonepublish/widgets/widget_family.py index 11c5ec33b7..8c18a93a00 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family.py +++ b/openpype/tools/standalonepublish/widgets/widget_family.py @@ -128,7 +128,8 @@ class FamilyWidget(QtWidgets.QWidget): 'family_preset_key': key, 'family': family, 'subset': self.input_result.text(), - 'version': self.version_spinbox.value() + 'version': self.version_spinbox.value(), + 'use_next_available_version': self.version_checkbox.isChecked(), } return data From 7b19762d5dda46513b38724e8e19cad1c5f70ca0 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 17 Jun 2023 03:25:31 +0000 Subject: [PATCH 900/918] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c44b1d29fb..9c5a60964b 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.11-nightly.2" +__version__ = "3.15.11-nightly.3" From e3e09e7df9e0c066e5cc77fa4be9631bd910109f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 17 Jun 2023 03:26:12 +0000 Subject: [PATCH 901/918] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 2339ec878f..2fd2780e55 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.11-nightly.3 - 3.15.11-nightly.2 - 3.15.11-nightly.1 - 3.15.10 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.7 - 3.14.3-nightly.6 - 3.14.3-nightly.5 - - 3.14.3-nightly.4 validations: required: true - type: dropdown From a2525bf9bb3040d3efc5bb222aab6cc2d9794547 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 15:40:44 +0800 Subject: [PATCH 902/918] use getLastMergedNodes() in max_scene loader --- openpype/hosts/max/plugins/load/load_max_scene.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 4d9367b16f..e3fb34f5bc 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -22,12 +22,8 @@ class MaxSceneLoader(load.LoaderPlugin): path = os.path.normpath(self.fname) # import the max scene by using "merge file" path = path.replace('\\', '/') - - merge_before = set(rt.RootNode.Children) rt.MergeMaxFile(path) - - merge_after = set(rt.RootNode.Children) - max_objects = merge_after.difference(merge_before) + max_objects = rt.getLastMergedNodes() max_container = rt.Container(name=f"{name}") for max_object in max_objects: max_object.Parent = max_container @@ -40,15 +36,14 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node_name = container["instance_node"] - instance_name, _ = node_name.split("_") - merge_before = set(rt.RootNode.Children) + rt.MergeMaxFile(path, rt.Name("noRedraw"), rt.Name("deleteOldDups"), rt.Name("useSceneMtlDups")) - merge_after = set(rt.EootNode.Children) - max_objects = merge_after.difference(merge_before) - container_node = rt.GetNodeByName(instance_name) + + max_objects = rt.getLastMergedNodes() + container_node = rt.GetNodeByName(node_name) for max_object in max_objects: max_object.Parent = container_node From 3631cc5f4048edc710f51122d6c91a79e33231db Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:18:55 +0200 Subject: [PATCH 903/918] fix single root packing (#5154) --- openpype/lib/project_backpack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/lib/project_backpack.py b/openpype/lib/project_backpack.py index 55a96664d8..91a5b76e35 100644 --- a/openpype/lib/project_backpack.py +++ b/openpype/lib/project_backpack.py @@ -125,6 +125,7 @@ def pack_project( if not only_documents: roots = project_doc["config"]["roots"] # Determine root directory of project + source_root = None source_root_name = None for root_name, root_value in roots.items(): if source_root is not None: From a4c63c12cf0bfb61a4e6005304d40609290132ca Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Mon, 19 Jun 2023 14:15:34 +0300 Subject: [PATCH 904/918] Add height, width and fps setup to project manager (#5075) * add width, height and fps setup * add corresponding ui tweaks * update docstring * remove unnecessary fallbacks * remove print * hound * remove whitespace * revert operations change * wip commit project update with new data * formatting * update the project data correctly * Update openpype/tools/project_manager/project_manager/widgets.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * show default settings, use spinbox to validate values add pixel aspec, frame start, frame end * formatting * get default anatomy settings properly * check if singlestep is set Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * not used Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * mindless code copying is evil, removed unnecesary parts Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/tools/project_manager/project_manager/widgets.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Update openpype/tools/project_manager/project_manager/widgets.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * remove unused import * use integer or float instead of text Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * import PixmapLabel from 'utils' * fix spinbox field length for macos * set aspect decimals to 2 * remove set size policy * set field growth policy for macos * add newline --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/client/operations.py | 9 +- .../project_manager/widgets.py | 161 ++++++++++++------ 2 files changed, 117 insertions(+), 53 deletions(-) diff --git a/openpype/client/operations.py b/openpype/client/operations.py index ef48f2a1c4..e8c9d28636 100644 --- a/openpype/client/operations.py +++ b/openpype/client/operations.py @@ -220,7 +220,6 @@ def new_representation_doc( "parent": version_id, "name": name, "data": data, - # Imprint shortcut to context for performance reasons. "context": context } @@ -708,7 +707,11 @@ class OperationsSession(object): return operation -def create_project(project_name, project_code, library_project=False): +def create_project( + project_name, + project_code, + library_project=False, +): """Create project using OpenPype settings. This project creation function is not validating project document on @@ -752,7 +755,7 @@ def create_project(project_name, project_code, library_project=False): "name": project_name, "data": { "code": project_code, - "library_project": library_project + "library_project": library_project, }, "schema": CURRENT_PROJECT_SCHEMA } diff --git a/openpype/tools/project_manager/project_manager/widgets.py b/openpype/tools/project_manager/project_manager/widgets.py index 06ae06e4d2..3154f777df 100644 --- a/openpype/tools/project_manager/project_manager/widgets.py +++ b/openpype/tools/project_manager/project_manager/widgets.py @@ -1,4 +1,5 @@ import re +import platform from openpype.client import get_projects, create_project from .constants import ( @@ -8,13 +9,16 @@ from .constants import ( from openpype.client.operations import ( PROJECT_NAME_ALLOWED_SYMBOLS, PROJECT_NAME_REGEX, + OperationsSession, ) from openpype.style import load_stylesheet from openpype.pipeline import AvalonMongoDB from openpype.tools.utils import ( PlaceholderLineEdit, - get_warning_pixmap + get_warning_pixmap, + PixmapLabel, ) +from openpype.settings.lib import get_default_anatomy_settings from qtpy import QtWidgets, QtCore, QtGui @@ -35,7 +39,7 @@ class NameTextEdit(QtWidgets.QLineEdit): sub_regex = "[^{}]+".format(NAME_ALLOWED_SYMBOLS) new_before_text = re.sub(sub_regex, "", before_text) new_after_text = re.sub(sub_regex, "", after_text) - idx -= (len(before_text) - len(new_before_text)) + idx -= len(before_text) - len(new_before_text) self.setText(new_before_text + new_after_text) self.setCursorPosition(idx) @@ -141,13 +145,40 @@ class CreateProjectDialog(QtWidgets.QDialog): inputs_widget = QtWidgets.QWidget(self) project_name_input = QtWidgets.QLineEdit(inputs_widget) project_code_input = QtWidgets.QLineEdit(inputs_widget) + project_width_input = NumScrollWidget(0, 9999999) + project_height_input = NumScrollWidget(0, 9999999) + project_fps_input = FloatScrollWidget(1, 9999999, decimals=3, step=1) + project_aspect_input = FloatScrollWidget( + 0, 9999999, decimals=2, step=0.1 + ) + project_frame_start_input = NumScrollWidget(-9999999, 9999999) + project_frame_end_input = NumScrollWidget(-9999999, 9999999) + + default_project_data = self.get_default_attributes() + project_width_input.setValue(default_project_data["resolutionWidth"]) + project_height_input.setValue(default_project_data["resolutionHeight"]) + project_fps_input.setValue(default_project_data["fps"]) + project_aspect_input.setValue(default_project_data["pixelAspect"]) + project_frame_start_input.setValue(default_project_data["frameStart"]) + project_frame_end_input.setValue(default_project_data["frameEnd"]) + library_project_input = QtWidgets.QCheckBox(inputs_widget) inputs_layout = QtWidgets.QFormLayout(inputs_widget) + if platform.system() == "Darwin": + inputs_layout.setFieldGrowthPolicy( + QtWidgets.QFormLayout.AllNonFixedFieldsGrow + ) inputs_layout.setContentsMargins(0, 0, 0, 0) inputs_layout.addRow("Project name:", project_name_input) inputs_layout.addRow("Project code:", project_code_input) inputs_layout.addRow("Library project:", library_project_input) + inputs_layout.addRow("Width:", project_width_input) + inputs_layout.addRow("Height:", project_height_input) + inputs_layout.addRow("FPS:", project_fps_input) + inputs_layout.addRow("Aspect:", project_aspect_input) + inputs_layout.addRow("Frame Start:", project_frame_start_input) + inputs_layout.addRow("Frame End:", project_frame_end_input) project_name_label = QtWidgets.QLabel(self) project_code_label = QtWidgets.QLabel(self) @@ -183,6 +214,12 @@ class CreateProjectDialog(QtWidgets.QDialog): self.project_name_input = project_name_input self.project_code_input = project_code_input self.library_project_input = library_project_input + self.project_width_input = project_width_input + self.project_height_input = project_height_input + self.project_fps_input = project_fps_input + self.project_aspect_input = project_aspect_input + self.project_frame_start_input = project_frame_start_input + self.project_frame_end_input = project_frame_end_input self.ok_btn = ok_btn @@ -190,6 +227,10 @@ class CreateProjectDialog(QtWidgets.QDialog): def project_name(self): return self.project_name_input.text() + def get_default_attributes(self): + settings = get_default_anatomy_settings() + return settings["attributes"] + def _on_project_name_change(self, value): if self._project_code_value is None: self._ignore_code_change = True @@ -215,12 +256,12 @@ class CreateProjectDialog(QtWidgets.QDialog): is_valid = False elif value in self.invalid_project_names: - message = "Project name \"{}\" already exist".format(value) + message = 'Project name "{}" already exist'.format(value) is_valid = False elif not PROJECT_NAME_REGEX.match(value): message = ( - "Project name \"{}\" contain not supported symbols" + 'Project name "{}" contain not supported symbols' ).format(value) is_valid = False @@ -237,12 +278,12 @@ class CreateProjectDialog(QtWidgets.QDialog): is_valid = False elif value in self.invalid_project_names: - message = "Project code \"{}\" already exist".format(value) + message = 'Project code "{}" already exist'.format(value) is_valid = False elif not PROJECT_NAME_REGEX.match(value): message = ( - "Project code \"{}\" contain not supported symbols" + 'Project code "{}" contain not supported symbols' ).format(value) is_valid = False @@ -264,9 +305,35 @@ class CreateProjectDialog(QtWidgets.QDialog): project_name = self.project_name_input.text() project_code = self.project_code_input.text() - library_project = self.library_project_input.isChecked() - create_project(project_name, project_code, library_project) + project_width = self.project_width_input.value() + project_height = self.project_height_input.value() + project_fps = self.project_fps_input.value() + project_aspect = self.project_aspect_input.value() + project_frame_start = self.project_frame_start_input.value() + project_frame_end = self.project_frame_end_input.value() + library_project = self.library_project_input.isChecked() + project_doc = create_project( + project_name, + project_code, + library_project, + ) + update_data = { + "data.resolutionWidth": project_width, + "data.resolutionHeight": project_height, + "data.fps": project_fps, + "data.pixelAspect": project_aspect, + "data.frameStart": project_frame_start, + "data.frameEnd": project_frame_end, + } + session = OperationsSession() + session.update_entity( + project_name, + project_doc["type"], + project_doc["_id"], + update_data, + ) + session.commit() self.done(1) def _get_existing_projects(self): @@ -288,45 +355,15 @@ class CreateProjectDialog(QtWidgets.QDialog): return project_names, project_codes -# TODO PixmapLabel should be moved to 'utils' in other future PR so should be -# imported from there -class PixmapLabel(QtWidgets.QLabel): - """Label resizing image to height of font.""" - def __init__(self, pixmap, parent): - super(PixmapLabel, self).__init__(parent) - self._empty_pixmap = QtGui.QPixmap(0, 0) - self._source_pixmap = pixmap - - def set_source_pixmap(self, pixmap): - """Change source image.""" - self._source_pixmap = pixmap - self._set_resized_pix() - +class ProjectManagerPixmapLabel(PixmapLabel): def _get_pix_size(self): size = self.fontMetrics().height() * 4 return size, size - def _set_resized_pix(self): - if self._source_pixmap is None: - self.setPixmap(self._empty_pixmap) - return - width, height = self._get_pix_size() - self.setPixmap( - self._source_pixmap.scaled( - width, - height, - QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation - ) - ) - - def resizeEvent(self, event): - self._set_resized_pix() - super(PixmapLabel, self).resizeEvent(event) - class ConfirmProjectDeletion(QtWidgets.QDialog): """Dialog which confirms deletion of a project.""" + def __init__(self, project_name, parent): super(ConfirmProjectDeletion, self).__init__(parent) @@ -335,23 +372,26 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): top_widget = QtWidgets.QWidget(self) warning_pixmap = get_warning_pixmap() - warning_icon_label = PixmapLabel(warning_pixmap, top_widget) + warning_icon_label = ProjectManagerPixmapLabel( + warning_pixmap, top_widget + ) message_label = QtWidgets.QLabel(top_widget) message_label.setWordWrap(True) message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - message_label.setText(( - "WARNING: This cannot be undone.

" - "Project \"{}\" with all related data will be" - " permanently removed from the database. (This action won't remove" - " any files on disk.)" - ).format(project_name)) + message_label.setText( + ( + "WARNING: This cannot be undone.

" + 'Project "{}" with all related data will be' + " permanently removed from the database." + " (This action won't remove any files on disk.)" + ).format(project_name) + ) top_layout = QtWidgets.QHBoxLayout(top_widget) top_layout.setContentsMargins(0, 0, 0, 0) top_layout.addWidget( - warning_icon_label, 0, - QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter + warning_icon_label, 0, QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter ) top_layout.addWidget(message_label, 1) @@ -359,7 +399,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): confirm_input = PlaceholderLineEdit(self) confirm_input.setPlaceholderText( - "Type \"{}\" to confirm...".format(project_name) + 'Type "{}" to confirm...'.format(project_name) ) cancel_btn = QtWidgets.QPushButton("Cancel", self) @@ -429,6 +469,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog): class SpinBoxScrollFixed(QtWidgets.QSpinBox): """QSpinBox which only allow edits change with scroll wheel when active""" + def __init__(self, *args, **kwargs): super(SpinBoxScrollFixed, self).__init__(*args, **kwargs) self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -442,6 +483,7 @@ class SpinBoxScrollFixed(QtWidgets.QSpinBox): class DoubleSpinBoxScrollFixed(QtWidgets.QDoubleSpinBox): """QDoubleSpinBox which only allow edits with scroll wheel when active""" + def __init__(self, *args, **kwargs): super(DoubleSpinBoxScrollFixed, self).__init__(*args, **kwargs) self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -451,3 +493,22 @@ class DoubleSpinBoxScrollFixed(QtWidgets.QDoubleSpinBox): event.ignore() else: super(DoubleSpinBoxScrollFixed, self).wheelEvent(event) + + +class NumScrollWidget(SpinBoxScrollFixed): + def __init__(self, minimum, maximum): + super(NumScrollWidget, self).__init__() + self.setMaximum(maximum) + self.setMinimum(minimum) + self.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) + + +class FloatScrollWidget(DoubleSpinBoxScrollFixed): + def __init__(self, minimum, maximum, decimals, step=None): + super(FloatScrollWidget, self).__init__() + self.setMaximum(maximum) + self.setMinimum(minimum) + self.setDecimals(decimals) + if step is not None: + self.setSingleStep(step) + self.setButtonSymbols(QtWidgets.QSpinBox.NoButtons) From 3314c5f282539f6688ee24ee583e53e95c6c8e04 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 21:57:30 +0800 Subject: [PATCH 905/918] use createOptions() for defaultArnoldRenderOptions --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index 1a582647cc..d71b40e877 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -42,8 +42,10 @@ class ArnoldStandinLoader(load.LoaderPlugin): # does not exist yet and some connections to the standin # can't be correctly generated on create resulting in an error cmds.loadPlugin("mtoa") - cmds.refresh(force=True) - maya.utils.processIdleEvents() + # create defaultArnoldRenderOptions for + # `defaultArnoldRenderOptions.operator`` + from mtoa.core import createOptions + createOptions() import mtoa.ui.arnoldmenu From 92c6cee333d08bb685574f896d33abc253a6d220 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 21:58:31 +0800 Subject: [PATCH 906/918] hound fix --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index d71b40e877..a12ecf8f9f 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -2,7 +2,6 @@ import os import clique import maya.cmds as cmds -import maya.utils from openpype.settings import get_project_settings from openpype.pipeline import ( From 3020ccd90abdf8528052c07eb437e4bd065721f1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 23:00:44 +0800 Subject: [PATCH 907/918] update docstring --- .../hosts/maya/plugins/load/load_arnold_standin.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index a12ecf8f9f..a085f8d575 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -36,13 +36,11 @@ class ArnoldStandinLoader(load.LoaderPlugin): def load(self, context, name, namespace, options): if not cmds.pluginInfo("mtoa", query=True, loaded=True): - # Allow mtoa plugin load to process all its events - # because otherwise `defaultArnoldRenderOptions.operator` - # does not exist yet and some connections to the standin - # can't be correctly generated on create resulting in an error cmds.loadPlugin("mtoa") - # create defaultArnoldRenderOptions for - # `defaultArnoldRenderOptions.operator`` + # create defaultArnoldRenderOptions before creating aiStandin + # which tried to connect it. Since we load the plugin and directly + # create aiStandin without the defaultArnoldRenderOptions, + # here needs to create the render options for aiStandin creation. from mtoa.core import createOptions createOptions() From 4ffab0bb678855d2b963d63cb982c614099cf447 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 23:20:08 +0800 Subject: [PATCH 908/918] update docstring --- openpype/hosts/maya/plugins/load/load_arnold_standin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_arnold_standin.py b/openpype/hosts/maya/plugins/load/load_arnold_standin.py index a085f8d575..38a7adfd7d 100644 --- a/openpype/hosts/maya/plugins/load/load_arnold_standin.py +++ b/openpype/hosts/maya/plugins/load/load_arnold_standin.py @@ -37,10 +37,10 @@ class ArnoldStandinLoader(load.LoaderPlugin): def load(self, context, name, namespace, options): if not cmds.pluginInfo("mtoa", query=True, loaded=True): cmds.loadPlugin("mtoa") - # create defaultArnoldRenderOptions before creating aiStandin - # which tried to connect it. Since we load the plugin and directly + # Create defaultArnoldRenderOptions before creating aiStandin + # which tries to connect it. Since we load the plugin and directly # create aiStandin without the defaultArnoldRenderOptions, - # here needs to create the render options for aiStandin creation. + # we need to create the render options for aiStandin creation. from mtoa.core import createOptions createOptions() From b365fc7534f14dc90ba183eca1e38b650eb7a836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 19 Jun 2023 18:37:33 +0200 Subject: [PATCH 909/918] Update openpype/hosts/max/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/max/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 4fc852e2fe..4c1dbb2810 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -110,7 +110,7 @@ class MaxCreatorBase(object): @staticmethod def cache_subsets(shared_data): - if shared_data.get("max_cached_subsets"): + if shared_data.get("max_cached_subsets") is not None: return shared_data shared_data["max_cached_subsets"] = {} From bc33407f28dcd99a22d6239031f52dc375ebeb41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 19 Jun 2023 18:39:54 +0200 Subject: [PATCH 910/918] Update openpype/hosts/max/plugins/publish/extract_pointcloud.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/max/plugins/publish/extract_pointcloud.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcloud.py b/openpype/hosts/max/plugins/publish/extract_pointcloud.py index 618f9856fd..583bbb6dbd 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcloud.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcloud.py @@ -138,9 +138,9 @@ class ExtractPointCloud(publish.Extractor): sub_anim = rt.GetSubAnim(obj, anim_name) boolean = rt.IsProperty(sub_anim, "Export_Particles") if boolean: - event_name = sub_anim.Name - opt = f"${member.Name}.{event_name}.export_particles" - opt_list.append(opt) + event_name = sub_anim.Name + opt = f"${member.Name}.{event_name}.export_particles" + opt_list.append(opt) return opt_list From 203e29c45117668a4ed0281dc42e3893ae897d3f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 19 Jun 2023 19:03:21 +0200 Subject: [PATCH 911/918] :rewind: in unrelated file --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 42cacdc93c..10cca3eb3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,4 +28,4 @@ omit = /tests directory = ./coverage [tool:pytest] -norecursedirs = repos/* openpype/modules/ftrack/* +norecursedirs = repos/* openpype/modules/ftrack/* \ No newline at end of file From 9b4641de65a1d20a66d4f78b30a9c1b8448a4d2e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 17:56:16 +0800 Subject: [PATCH 912/918] rename the setting panel --- openpype/hosts/nuke/startup/custom_write_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index a221a26424..ef3923f40a 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -62,7 +62,7 @@ knobs_setting = { class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): """ Write Node's Knobs Settings Panel """ def __init__(self): - nukescripts.PythonPanel.__init__(self, "Set Knobs Value(Write Node)") + nukescripts.PythonPanel.__init__(self, "Set Presets(Write Node)") knobs_value, _ = self.get_node_knobs_setting() # create knobs From f0bf7d8c665b23dbd19ae2cda7da47ef233dd741 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 17:57:45 +0800 Subject: [PATCH 913/918] remove hardcoded frame padding --- openpype/hosts/nuke/startup/custom_write_node.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index ef3923f40a..1db235b71a 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -9,7 +9,6 @@ from openpype.hosts.nuke.api.lib import ( ) -frame_padding = 5 temp_rendering_path_template = ( "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}") @@ -62,7 +61,7 @@ knobs_setting = { class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): """ Write Node's Knobs Settings Panel """ def __init__(self): - nukescripts.PythonPanel.__init__(self, "Set Presets(Write Node)") + nukescripts.PythonPanel.__init__(self, "Set Preset(Write Node)") knobs_value, _ = self.get_node_knobs_setting() # create knobs From 1ae4cbcdad521fcbda19cf868973b0f0ab470322 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 17:59:33 +0800 Subject: [PATCH 914/918] use preset name --- openpype/hosts/nuke/startup/custom_write_node.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 1db235b71a..9628876d0a 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -61,13 +61,13 @@ knobs_setting = { class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): """ Write Node's Knobs Settings Panel """ def __init__(self): - nukescripts.PythonPanel.__init__(self, "Set Preset(Write Node)") + nukescripts.PythonPanel.__init__(self,"Set Knobs Value(Write Node)") - knobs_value, _ = self.get_node_knobs_setting() + preset_name, _ = self.get_node_knobs_setting() # create knobs self.selected_preset_name = nuke.Enumeration_Knob( - 'preset_selector', 'presets', knobs_value) + 'preset_selector', 'presets', preset_name) # add knobs to panel self.addKnob(self.selected_preset_name) @@ -79,11 +79,11 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): node_knobs = self.selected_preset_name.value() ext = None knobs = knobs_setting["knobs"] - knobs_value, node_knobs_settings = ( + preset_name, node_knobs_settings = ( self.get_node_knobs_setting(node_knobs) ) - if node_knobs and knobs_value: + if node_knobs and preset_name: if not node_knobs_settings: nuke.message("No knobs value found in subset group..\nDefault setting will be used..") # noqa else: @@ -118,7 +118,7 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): set_node_knobs_from_settings(write_node, knobs) def get_node_knobs_setting(self, value=None): - knobs_value = [] + preset_name = [] knobs_nodes = [] settings = [ node @@ -134,9 +134,9 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): for setting in settings: if setting["nukeNodeClass"] == "Write" and setting["subsets"]: for knob in setting["subsets"]: - knobs_value.append(knob) + preset_name.append(knob) - return knobs_value, knobs_nodes + return preset_name, knobs_nodes def main(): From 02db5c6792d7e5a89c6243f73e5b11947ee66688 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 18:00:11 +0800 Subject: [PATCH 915/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 9628876d0a..ff4a3a41eb 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -61,7 +61,7 @@ knobs_setting = { class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): """ Write Node's Knobs Settings Panel """ def __init__(self): - nukescripts.PythonPanel.__init__(self,"Set Knobs Value(Write Node)") + nukescripts.PythonPanel.__init__(self, "Set Knobs Value(Write Node)") preset_name, _ = self.get_node_knobs_setting() # create knobs From c920b0ac5accb84982e0bf61d903c9ddc06b701a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 19:22:28 +0800 Subject: [PATCH 916/918] rename the variable and clean up the code --- .../hosts/nuke/startup/custom_write_node.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index ff4a3a41eb..007971fc27 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -74,24 +74,29 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): def process(self): """ Process the panel values. """ write_selected_nodes = [ - s for s in nuke.selectedNodes() if s.Class() == "Write"] + selected_nodes for selected_nodes in nuke.selectedNodes() + if selected_nodes.Class() == "Write"] - node_knobs = self.selected_preset_name.value() + selected_preset = self.selected_preset_name.value() ext = None knobs = knobs_setting["knobs"] - preset_name, node_knobs_settings = ( - self.get_node_knobs_setting(node_knobs) + preset_name, node_knobs_presets = ( + self.get_node_knobs_setting(selected_preset) ) - if node_knobs and preset_name: - if not node_knobs_settings: - nuke.message("No knobs value found in subset group..\nDefault setting will be used..") # noqa + if selected_preset and preset_name: + if not node_knobs_presets: + nuke.message( + "No knobs value found in subset group.." + "\nDefault setting will be used..") else: - knobs = node_knobs_settings + knobs = node_knobs_presets ext_knob_list = [knob for knob in knobs if knob["name"] == "file_type"] if not ext_knob_list: - nuke.message("ERROR: No file type found in the subset's knobs.\nPlease add one to complete setting up the node") # noqa + nuke.message( + "ERROR: No file type found in the subset's knobs." + "\nPlease add one to complete setting up the node") return else: for knob in ext_knob_list: @@ -117,24 +122,24 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): write_node["file"].setValue(file_path) set_node_knobs_from_settings(write_node, knobs) - def get_node_knobs_setting(self, value=None): + def get_node_knobs_setting(self, selected_preset=None): preset_name = [] knobs_nodes = [] settings = [ - node - for node in get_nuke_imageio_settings()["nodes"]["overrideNodes"] + node_settings + for node_settings in get_nuke_imageio_settings()["nodes"]["overrideNodes"] + if node_settings["nukeNodeClass"] == "Write" and node_settings["subsets"] ] if not settings: return for i, _ in enumerate(settings): - if value in settings[i]["subsets"]: + if selected_preset in settings[i]["subsets"]: knobs_nodes = settings[i]["knobs"] for setting in settings: - if setting["nukeNodeClass"] == "Write" and setting["subsets"]: - for knob in setting["subsets"]: - preset_name.append(knob) + for subset in setting["subsets"]: + preset_name.append(subset) return preset_name, knobs_nodes From 3da8dcc7fea93a0b844121acce82ef6803f3f275 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 19:23:32 +0800 Subject: [PATCH 917/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 007971fc27..2052a5a09e 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -126,8 +126,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): preset_name = [] knobs_nodes = [] settings = [ - node_settings - for node_settings in get_nuke_imageio_settings()["nodes"]["overrideNodes"] + node_settings for node_settings + in get_nuke_imageio_settings()["nodes"]["overrideNodes"] if node_settings["nukeNodeClass"] == "Write" and node_settings["subsets"] ] if not settings: From 7d99e8c8cf8941e6856e669dfe5134e8a71983fc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 20 Jun 2023 19:24:41 +0800 Subject: [PATCH 918/918] hound fix --- openpype/hosts/nuke/startup/custom_write_node.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/custom_write_node.py b/openpype/hosts/nuke/startup/custom_write_node.py index 2052a5a09e..ea53725834 100644 --- a/openpype/hosts/nuke/startup/custom_write_node.py +++ b/openpype/hosts/nuke/startup/custom_write_node.py @@ -128,7 +128,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): settings = [ node_settings for node_settings in get_nuke_imageio_settings()["nodes"]["overrideNodes"] - if node_settings["nukeNodeClass"] == "Write" and node_settings["subsets"] + if node_settings["nukeNodeClass"] == "Write" + and node_settings["subsets"] ] if not settings: return