Skip to content

Commit

Permalink
Merge branch 'develop' into chore/limit_extract_active_view_for_autom…
Browse files Browse the repository at this point in the history
…atic_testing
  • Loading branch information
kalisp authored Sep 26, 2024
2 parents 2ab2ef6 + 3faf11e commit 6fdc4b7
Show file tree
Hide file tree
Showing 5 changed files with 313 additions and 12 deletions.
3 changes: 2 additions & 1 deletion client/ayon_maya/plugins/publish/extract_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def process(self, instance):
# TODO representation queries can be refactored to be faster
project_name = instance.context.data["projectName"]

for asset in cmds.sets(str(instance), query=True):
members = instance.data["setMembers"]
for asset in members:
# Find the container
project_container = self.project_container
container_list = cmds.ls(project_container)
Expand Down
6 changes: 5 additions & 1 deletion client/ayon_maya/plugins/publish/extract_pointcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from ayon_maya.api.alembic import extract_alembic
from ayon_maya.api.lib import (
get_all_children,
get_highest_in_hierarchy,
iter_visible_nodes_in_range,
maintained_selection,
suspended_refresh,
Expand Down Expand Up @@ -129,7 +130,10 @@ def process(self, instance):
# Set the root nodes if we don't want to include parents
# The roots are to be considered the ones that are the actual
# direct members of the set
root = roots
# We ignore members that are children of other members to avoid
# the parenting / ancestor relationship error on export and assume
# the user intended to export starting at the top of the two.
root = get_highest_in_hierarchy(roots)

kwargs = {
"file": path,
Expand Down
168 changes: 168 additions & 0 deletions client/ayon_maya/plugins/publish/validate_clashing_sibling_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from collections import defaultdict
import inspect

import pyblish.api
from ayon_core.pipeline.publish import (
OptionalPyblishPluginMixin,
PublishValidationError,
ValidateContentsOrder,
)

import ayon_maya.api.action
from ayon_maya.api import plugin, lib


def remove_namespace(path: str) -> str:
"""Remove namespace from full path.
Example:
>>> remove_namespace("|aa:bb:foo|aa:bb:bar|cc:hello|dd:world")
'|foo|bar|hello|world'
Arguments:
path (str): Full node path.
Returns:
str: Node path with namespaces removed.
"""
return "|".join(
name.rsplit(":", 1)[-1] for name in path.split("|")
)


class ValidateClashingSiblingNames(plugin.MayaInstancePlugin,
OptionalPyblishPluginMixin):
"""Validate siblings have unique names when namespaces are stripped."""

order = ValidateContentsOrder
families = ["pointcache", "animation", "usd"]
label = "Validate clashing sibling names"
actions = [ayon_maya.api.action.SelectInvalidAction]

@staticmethod
def get_invalid(instance):
"""Return all nodes that have non-unique names with siblings when
namespaces are stripped.
Returns:
list[str]: Non-unique siblings
"""
stripped_name_to_full_path = defaultdict(set)
for node in instance:
stripped_name = remove_namespace(node)
stripped_name_to_full_path[stripped_name].add(node)

invalid: "list[str]" = []
for _stripped_name, nodes in stripped_name_to_full_path.items():
if len(nodes) > 1:
invalid.extend(nodes)

if invalid:
# We only care about the highest conflicts since child conflicts
# only occur due to the conflicts higher up anyway
invalid = lib.get_highest_in_hierarchy(invalid)

return invalid

def process(self, instance):
"""Process all the nodes in the instance "objectSet"""
if not self.is_active(instance.data):
return

if not self.is_strip_namespaces_enabled(instance):
return

invalid = self.get_invalid(instance)
if invalid:

report_list = "\n".join(f"- {node}" for node in sorted(invalid))

raise PublishValidationError(
"With stripped namespaces there are conflicting sibling names "
"that are not unique:\n"
f"{report_list}",
description=self.get_description())

def is_strip_namespaces_enabled(self, instance) -> bool:
"""Return whether any extractor is enabled for instance that has
`stripNamespaces` enabled."""
# TODO: Preferably there would be a better way to detect whether the
# flag was enabled or not.

plugins = instance.context.data["create_context"].publish_plugins
plugins_by_name = {plugin.__name__: plugin for plugin in plugins}

def _is_plugin_active(plugin_name: str) -> bool:
"""Return whether plugin is active for instance"""
# Check if Plug-in is found
plugin = plugins_by_name.get(plugin_name)
if not plugin:
self.log.debug(f"Plugin {plugin_name} not found. "
"It may be disabled in settings")
return False

# Check if plug-in is globally enabled
if not getattr(plugin, "enabled", True):
self.log.debug(f"Plugin {plugin_name} is disabled. "
"It is disabled in settings")
return False

# Check if optional state has active state set to False
publish_attributes = instance.data["publish_attributes"]
default_active = getattr(plugin, "active", True)
active_for_instance = publish_attributes.get(
plugin_name, {}).get("active", default_active)
if not active_for_instance:
self.log.debug(
f"Plugin {plugin_name} is disabled for this instance.")
return False

# Check if the instance, according to pyblish is a match for the
# plug-in. This may e.g. be excluded due to different families
# or matching algorithm (e.g. ExtractMultiverseUsdAnim uses
# `pyblish.api.Subset`
if not pyblish.api.instances_by_plugin([instance], plugin):
self.log.debug(
f"Plugin {plugin_name} does not match for this instance.")
return False

return True

for plugin_name in [
"ExtractAlembic", # pointcache
"ExtractAnimation", # animation
"ExtractMayaUsd", # usd
"ExtractMayaUsdPointcache", # pointcache
"ExtractMayaUsdAnim", # animation
]:
if _is_plugin_active(plugin_name):
plugin = plugins_by_name[plugin_name]

# Use the value from the instance publish attributes
publish_attributes = instance.data["publish_attributes"]
strip_namespaces = publish_attributes.get(
plugin_name, {}).get("stripNamespaces")
if strip_namespaces:
return True

# Find some default on the plugin class, if any
default = getattr(plugin, "stripNamespaces", False)
if default:
self.log.debug(
f"{plugin_name} has strip namespaces enabled as "
"default value.")
return True
return False

def get_description(self):
return inspect.cleandoc("""
### Clashing sibling names with stripped namespaces
The export has **strip namespaces** enabled but a conflict on
sibling names are found where, without namespaces, they do not have
unique names and can not be exported.
To resolve this, either export with 'strip namespaces' disabled or
reorder the hierarchy so that nodes sharing the parent do not have
the same name.
""")
128 changes: 128 additions & 0 deletions client/ayon_maya/plugins/publish/validate_excluded_parents_visible.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import inspect

from ayon_core.pipeline.publish import (
OptionalPyblishPluginMixin,
PublishValidationError
)
import ayon_maya.api.action
from ayon_maya.api import plugin, lib
import pyblish.api

from maya import cmds


class ValidateExcludedParentsVisible(plugin.MayaInstancePlugin,
OptionalPyblishPluginMixin):
"""Validate whether all parents are visible in frame range when 'include
parent hierarchy' is disabled for the instance.
This validation helps to detect the issue where an animator may have hidden
or keyed visibilities on parent nodes for an export where these parents
are not included in the export. Because if so, those invisibilities would
not be included in the export either, giving a different visual result than
what the artist likely intended in their workfile
"""

order = pyblish.api.ValidatorOrder
families = ["pointcache", "animation"]
label = "Excluded parents visible"
actions = [ayon_maya.api.action.SelectInvalidAction]

@classmethod
def get_invalid(cls, instance):

# Only validate if we exclude parent hierarchy
if instance.data.get("includeParentHierarchy", True):
return []

if "out_hierarchy" in instance.data:
# Animation instances
members = instance.data["out_hierarchy"]
else:
members = instance.data["setMembers"]

members = cmds.ls(members, type="dagNode", long=True) # DAG nodes only
if not members:
cls.log.debug("No members found in instance.")
return []

roots = lib.get_highest_in_hierarchy(members)

# If there are no parents to the root we are already including the
# full hierarchy, so we can skip checking visibilities on parents
parents = cmds.listRelatives(roots, parent=True, fullPath=True)
if not parents:
return []

# Include ancestors to check for visibilities on them
ancestors = list(parents)
for parent in parents:
ancestors.extend(lib.iter_parents(parent))

# Check if the parent is hidden anywhere within the frame range
invalid = []
frame_start = int(instance.data["frameStartHandle"])
frame_end = int(instance.data["frameEndHandle"])

cls.log.debug(
"Validating invisibilities for excluded ancestors in frame "
f"range {frame_start}-{frame_end}: {ancestors}.")
for ancestor in ancestors:
attr = f"{ancestor}.visibility"

# We need to check whether the ancestor is ever invisible
# during the frame range if it has inputs
has_inputs = bool(cmds.listConnections(
attr, source=True, destination=False))
if has_inputs:
for frame in range(frame_start, frame_end+1):
if cmds.getAttr(attr, time=frame):
continue

# We found an invisible frame
cls.log.warning(
"Excluded parent is invisible on frame "
f"{frame}: {ancestor}")
invalid.append(ancestor)
break

# If no inputs, check the current visibility
elif not cmds.getAttr(attr):
cls.log.warning(f"Excluded parent is invisible: {ancestor}")
invalid.append(ancestor)

return invalid

def process(self, instance):
"""Process all the nodes in the instance 'objectSet'"""
if not self.is_active(instance.data):
return

invalid = self.get_invalid(instance)
if invalid:
invalid_list = "\n".join(f"- {node}" for node in invalid)

raise PublishValidationError(
"Invisible parents found that are excluded from the export:\n"
"{0}".format(invalid_list),
title="Excluded parents are invisible",
description=self.get_description()
)

@staticmethod
def get_description():
return inspect.cleandoc("""### Excluded parents are invisible
The instance is set to exclude the parent hierarchy, however the
excluded parents are invisible within the exported frame range.
This may be on all frames, of if animated on only certain frames.
Because the export excludes those parents the exported geometry will
**not** have these (animated) invisibilities and will appear visible
in the output regardless of how your scene looked on export.
To resolve this, either move the invisibility down into the hierarchy
that you are including in the export. Or, export with include parent
hierarchy enabled.
""")
20 changes: 10 additions & 10 deletions server/settings/publishers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@
def linear_unit_enum():
"""Get linear units enumerator."""
return [
{"label": "mm", "value": "millimeter"},
{"label": "cm", "value": "centimeter"},
{"label": "m", "value": "meter"},
{"label": "km", "value": "kilometer"},
{"label": "in", "value": "inch"},
{"label": "ft", "value": "foot"},
{"label": "yd", "value": "yard"},
{"label": "mi", "value": "mile"}
{"label": "millimeter", "value": "mm"},
{"label": "centimeter", "value": "cm"},
{"label": "meter", "value": "m"},
{"label": "kilometer", "value": "km"},
{"label": "inch", "value": "in"},
{"label": "foot", "value": "ft"},
{"label": "yard", "value": "yd"},
{"label": "mile", "value": "mi"}
]


def angular_unit_enum():
"""Get angular units enumerator."""
return [
{"label": "deg", "value": "degree"},
{"label": "rad", "value": "radian"},
{"label": "degree", "value": "deg"},
{"label": "radian", "value": "rad"},
]


Expand Down

0 comments on commit 6fdc4b7

Please sign in to comment.