Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Maya USD Export Chaser to Filter Properties #193

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions client/ayon_maya/api/chasers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# AYON Maya USD Chasers

This folder contains AYON Maya USD python import and export chasers to be
registered on Maya startup. These chasers have the ability to influence how
USD data is imported and exported in Maya.

For example, the Filter Properties export chaser allows to filter properties
in the exported USD file to only those that match by the specified pattern
using a SideFX Houdini style pattern matching.

The chasers are registered in the `MayaHost.install` method on Maya launch.

See also the [Maya USD Import Chaser documentation](https://github.com/Autodesk/maya-usd/blob/dev/lib/mayaUsd/commands/Readme.md#import-chasers)
and [Maya USD Export Chaser documentation](https://github.com/Autodesk/maya-usd/blob/dev/lib/mayaUsd/commands/Readme.md#export-chasers-advanced).
138 changes: 138 additions & 0 deletions client/ayon_maya/api/chasers/export_filter_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import re
import fnmatch
import logging
from typing import List

import mayaUsd.lib as mayaUsdLib
from pxr import Sdf


def log_errors(fn):
"""Decorator to log errors on error"""

def wrap(*args, **kwargs):

try:
return fn(*args, **kwargs)
except Exception as exc:
logging.error(exc, exc_info=True)
raise

return wrap


def remove_spec(spec: Sdf.Spec):
"""Remove Sdf.Spec authored opinion."""
if spec.expired:
return

if isinstance(spec, Sdf.PrimSpec):
# PrimSpec
parent = spec.nameParent
if parent:
view = parent.nameChildren
else:
# Assume PrimSpec is root prim
view = spec.layer.rootPrims
del view[spec.name]

elif isinstance(spec, Sdf.PropertySpec):
# Relationship and Attribute specs
del spec.owner.properties[spec.name]

elif isinstance(spec, Sdf.VariantSetSpec):
# Owner is Sdf.PrimSpec (or can also be Sdf.VariantSpec)
del spec.owner.variantSets[spec.name]

elif isinstance(spec, Sdf.VariantSpec):
# Owner is Sdf.VariantSetSpec
spec.owner.RemoveVariant(spec)

else:
raise TypeError(f"Unsupported spec type: {spec}")


def remove_layer_specs(layer: Sdf.Layer, spec_paths: List[Sdf.Path]):
# Iterate in reverse so we iterate the highest paths
# first, so when removing a spec the children specs
# are already removed
for spec_path in reversed(spec_paths):
spec = layer.GetObjectAtPath(spec_path)
if not spec or spec.expired:
continue
remove_spec(spec)


def match_pattern(name: str, text_pattern: str) -> bool:
"""SideFX Houdini like pattern matching"""
patterns = text_pattern.split(" ")
is_match = False
for pattern in patterns:
# * means any character
# ? means any single character
# [abc] means a, b, or c
pattern = pattern.strip(" ")
if not pattern:
continue

excludes = pattern[0] == "^"

# If name is already matched against earlier pattern in the text
# pattern, then we can skip the pattern if it is not an exclude pattern
if is_match and not excludes:
continue

if excludes:
pattern = pattern[1:]

regex = fnmatch.translate(pattern)
match = re.match(regex, name)
if match:
is_match = not excludes
return is_match



class FilterPropertiesExportChaser(mayaUsdLib.ExportChaser):
"""Remove property specs based on pattern"""

name = "AYON_filterProperties"

def __init__(self, factoryContext, *args, **kwargs):
super().__init__(factoryContext, *args, **kwargs)
self.log = logging.getLogger(self.__class__.__name__)
self.stage = factoryContext.GetStage()
self.job_args = factoryContext.GetJobArgs()

@log_errors
def PostExport(self):

chaser_args = self.job_args.allChaserArgs[self.name]
# strip all or use user-specified pattern
pattern = chaser_args.get("pattern", "*")
for layer in self.stage.GetLayerStack():

specs_to_remove = []

def find_attribute_specs_to_remove(path: Sdf.Path):
if not path.IsPropertyPath():
return

spec = layer.GetObjectAtPath(path)
if not spec:
return

if not isinstance(spec, Sdf.PropertySpec):
return

if not match_pattern(spec.name, pattern):
self.log.debug("Removing spec: %s", path)
specs_to_remove.append(path)
else:
self.log.debug("Keeping spec: %s", path)

layer.Traverse("/", find_attribute_specs_to_remove)

remove_layer_specs(layer, specs_to_remove)

return True
21 changes: 21 additions & 0 deletions client/ayon_maya/api/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def install(self):
)
register_event_callback("workfile.save.after", after_workfile_save)

self._register_maya_usd_chasers()

def open_workfile(self, filepath):
return open_file(filepath)

Expand Down Expand Up @@ -239,6 +241,25 @@ def _register_callbacks(self):
self.log.info("Installed event handler _check_lock_file..")
self.log.info("Installed event handler _before_close_maya..")

def _register_maya_usd_chasers(self):
"""Register Maya USD chasers if Maya USD libraries are available."""

try:
import mayaUsd.lib # noqa
except ImportError:
# Do not register if Maya USD is not available
return

self.log.info("Installing AYON Maya USD chasers..")

from .chasers import export_filter_properties # noqa

for export_chaser in [
export_filter_properties.FilterPropertiesExportChaser
]:
mayaUsd.lib.ExportChaser.Register(export_chaser,
export_chaser.name)


def _set_project():
"""Sets the maya project to the current Session's work directory.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pyblish.api

from ayon_core.lib import TextDef
from ayon_core.pipeline.publish import AYONPyblishPluginMixin
from ayon_maya.api import plugin


class CollectMayaUsdFilterProperties(plugin.InstancePlugin,
AYONPyblishPluginMixin):

order = pyblish.api.CollectorOrder
label = "Maya USD Export Chaser: Filter Properties"
families = ["mayaUsd"]

@classmethod
def get_attribute_defs(cls):
return [
TextDef(
"filter_properties",
label="USD Filter Properties",
tooltip=(
"Filter USD properties to export."
),
placeholder="* ^xformOp* ^points"
)
]

def process(self, instance):
attr_values = self.get_attr_values_from_data(instance.data)
filter_pattern = attr_values.get("filter_properties")
if not filter_pattern:
return

self.log.debug(
"Enabling USD filter properties chaser "
f"with pattern {filter_pattern}"
)
instance.data.setdefault("chaser", []).append("AYON_filterProperties")
instance.data.setdefault("chaserArgs", []).append(
("AYON_filterProperties", "pattern", filter_pattern)
)
4 changes: 4 additions & 0 deletions client/ayon_maya/plugins/publish/extract_maya_usd.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ def options(self):

# TODO: Support more `mayaUSDExport` parameters
return {
"chaser": (list, None), # optional list
"chaserArgs": (list, None), # optional list
"defaultUSDFormat": str,
"stripNamespaces": bool,
"mergeTransformAndShape": bool,
Expand All @@ -191,6 +193,8 @@ def default_options(self):

# TODO: Support more `mayaUSDExport` parameters
return {
"chaser": None,
"chaserArgs": None,
"defaultUSDFormat": "usdc",
"stripNamespaces": False,
"mergeTransformAndShape": True,
Expand Down
Loading