diff --git a/client/ayon_core/hosts/maya/api/alembic.py b/client/ayon_core/hosts/maya/api/alembic.py
index bf887df4c7..6bd00e1cb1 100644
--- a/client/ayon_core/hosts/maya/api/alembic.py
+++ b/client/ayon_core/hosts/maya/api/alembic.py
@@ -22,7 +22,6 @@ ALEMBIC_ARGS = {
"melPostJobCallback": str,
"noNormals": bool,
"preRoll": bool,
- "preRollStartFrame": int,
"pythonPerFrameCallback": str,
"pythonPostJobCallback": str,
"renderableOnly": bool,
@@ -54,15 +53,22 @@ def extract_alembic(
endFrame=None,
eulerFilter=True,
frameRange="",
+ melPerFrameCallback=None,
+ melPostJobCallback=None,
noNormals=False,
preRoll=False,
preRollStartFrame=0,
+ pythonPerFrameCallback=None,
+ pythonPostJobCallback=None,
renderableOnly=False,
root=None,
selection=True,
startFrame=None,
step=1.0,
stripNamespaces=True,
+ userAttr=None,
+ userAttrPrefix=None,
+ uvsOnly=False,
uvWrite=True,
verbose=False,
wholeFrameGeo=False,
@@ -102,6 +108,11 @@ def extract_alembic(
string formatted as: "startFrame endFrame". This argument
overrides `startFrame` and `endFrame` arguments.
+ melPerFrameCallback (Optional[str]): MEL callback run per frame.
+
+ melPostJobCallback (Optional[str]): MEL callback after last frame is
+ written.
+
noNormals (bool): When on, normal data from the original polygon
objects is not included in the exported Alembic cache file.
@@ -113,6 +124,11 @@ def extract_alembic(
dependent translations and can be used to evaluate run-up that
isn't actually translated. Defaults to 0.
+ pythonPerFrameCallback (Optional[str]): Python callback run per frame.
+
+ pythonPostJobCallback (Optional[str]): Python callback after last frame
+ is written.
+
renderableOnly (bool): When on, any non-renderable nodes or hierarchy,
such as hidden objects, are not included in the Alembic file.
Defaults to False.
@@ -137,6 +153,15 @@ def extract_alembic(
object with the namespace taco:foo:bar appears as bar in the
Alembic file.
+ userAttr (list of str, optional): A specific user defined attribute to
+ write out. Defaults to [].
+
+ userAttrPrefix (list of str, optional): Prefix filter for determining
+ which user defined attributes to write out. Defaults to [].
+
+ uvsOnly (bool): When on, only uv data for PolyMesh and SubD shapes
+ will be written to the Alembic file.
+
uvWrite (bool): When on, UV data from polygon meshes and subdivision
objects are written to the Alembic file. Only the current UV map is
included.
@@ -183,6 +208,8 @@ def extract_alembic(
# Ensure list arguments are valid.
attr = attr or []
attrPrefix = attrPrefix or []
+ userAttr = userAttr or []
+ userAttrPrefix = userAttrPrefix or []
root = root or []
# Pass the start and end frame on as `frameRange` so that it
@@ -213,8 +240,10 @@ def extract_alembic(
"eulerFilter": eulerFilter,
"noNormals": noNormals,
"preRoll": preRoll,
+ "root": root,
"renderableOnly": renderableOnly,
"uvWrite": uvWrite,
+ "uvsOnly": uvsOnly,
"writeColorSets": writeColorSets,
"writeFaceSets": writeFaceSets,
"wholeFrameGeo": wholeFrameGeo,
@@ -226,9 +255,10 @@ def extract_alembic(
"step": step,
"attr": attr,
"attrPrefix": attrPrefix,
+ "userAttr": userAttr,
+ "userAttrPrefix": userAttrPrefix,
"stripNamespaces": stripNamespaces,
- "verbose": verbose,
- "preRollStartFrame": preRollStartFrame
+ "verbose": verbose
}
# Validate options
@@ -264,6 +294,17 @@ def extract_alembic(
if maya_version >= 2018:
options['autoSubd'] = options.pop('writeCreases', False)
+ # Only add callbacks if they are set so that we're not passing `None`
+ callbacks = {
+ "melPerFrameCallback": melPerFrameCallback,
+ "melPostJobCallback": melPostJobCallback,
+ "pythonPerFrameCallback": pythonPerFrameCallback,
+ "pythonPostJobCallback": pythonPostJobCallback,
+ }
+ for key, callback in callbacks.items():
+ if callback:
+ options[key] = str(callback)
+
# Format the job string from options
job_args = list()
for key, value in options.items():
@@ -297,7 +338,11 @@ def extract_alembic(
# exports are made. (PLN-31)
# TODO: Make sure this actually fixes the issues
with evaluation("off"):
- cmds.AbcExport(j=job_str, verbose=verbose)
+ cmds.AbcExport(
+ j=job_str,
+ verbose=verbose,
+ preRollStartFrame=preRollStartFrame
+ )
if verbose:
log.debug("Extracted Alembic to: %s", file)
diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py
index d7f9594374..cc930e49cc 100644
--- a/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py
+++ b/client/ayon_core/hosts/maya/plugins/publish/extract_pointcache.py
@@ -6,6 +6,7 @@ from maya import cmds
from ayon_core.pipeline import publish
from ayon_core.hosts.maya.api.alembic import extract_alembic
from ayon_core.hosts.maya.api.lib import (
+ get_all_children,
suspended_refresh,
maintained_selection,
iter_visible_nodes_in_range
@@ -40,7 +41,6 @@ class ExtractAlembic(publish.Extractor, AYONPyblishPluginMixin):
# From settings
attr = []
attrPrefix = []
- autoSubd = False
bake_attributes = []
bake_attribute_prefixes = []
dataFormat = "ogawa"
@@ -63,6 +63,7 @@ class ExtractAlembic(publish.Extractor, AYONPyblishPluginMixin):
wholeFrameGeo = False
worldSpace = True
writeColorSets = False
+ writeCreases = False
writeFaceSets = False
writeNormals = True
writeUVSets = False
@@ -173,15 +174,9 @@ class ExtractAlembic(publish.Extractor, AYONPyblishPluginMixin):
"writeVisibility": attribute_values.get(
"writeVisibility", self.writeVisibility
),
- "autoSubd": attribute_values.get(
- "autoSubd", self.autoSubd
- ),
"uvsOnly": attribute_values.get(
"uvsOnly", self.uvsOnly
),
- "writeNormals": attribute_values.get(
- "writeNormals", self.writeNormals
- ),
"melPerFrameCallback": attribute_values.get(
"melPerFrameCallback", self.melPerFrameCallback
),
@@ -193,7 +188,12 @@ class ExtractAlembic(publish.Extractor, AYONPyblishPluginMixin):
),
"pythonPostJobCallback": attribute_values.get(
"pythonPostJobCallback", self.pythonPostJobCallback
- )
+ ),
+ # Note that this converts `writeNormals` to `noNormals` for the
+ # `AbcExport` equivalent in `extract_alembic`
+ "noNormals": not attribute_values.get(
+ "writeNormals", self.writeNormals
+ ),
}
if instance.data.get("visibleOnly", False):
@@ -249,7 +249,6 @@ class ExtractAlembic(publish.Extractor, AYONPyblishPluginMixin):
with maintained_selection():
cmds.select(instance.data["proxy"])
extract_alembic(**kwargs)
-
representation = {
"name": "proxy",
"ext": "abc",
@@ -268,20 +267,6 @@ class ExtractAlembic(publish.Extractor, AYONPyblishPluginMixin):
return []
override_defs = OrderedDict({
- "autoSubd": BoolDef(
- "autoSubd",
- label="Auto Subd",
- default=cls.autoSubd,
- tooltip=(
- "If this flag is present and the mesh has crease edges, "
- "crease vertices or holes, the mesh (OPolyMesh) would now "
- "be written out as an OSubD and crease info will be stored"
- " in the Alembic file. Otherwise, creases info won't be "
- "preserved in Alembic file unless a custom Boolean "
- "attribute SubDivisionMesh has been added to mesh node and"
- " its value is true."
- )
- ),
"eulerFilter": BoolDef(
"eulerFilter",
label="Euler Filter",
@@ -354,6 +339,13 @@ class ExtractAlembic(publish.Extractor, AYONPyblishPluginMixin):
default=cls.writeColorSets,
tooltip="Write vertex colors with the geometry."
),
+ "writeCreases": BoolDef(
+ "writeCreases",
+ label="Write Creases",
+ default=cls.writeCreases,
+ tooltip="Write the geometry's edge and vertex crease "
+ "information."
+ ),
"writeFaceSets": BoolDef(
"writeFaceSets",
label="Write Face Sets",
@@ -527,9 +519,7 @@ class ExtractAnimation(ExtractAlembic):
roots = cmds.sets(out_set, query=True) or []
# Include all descendants
- nodes = roots
- nodes += cmds.listRelatives(
- roots, allDescendents=True, fullPath=True
- ) or []
+ nodes = roots.copy()
+ nodes.extend(get_all_children(roots, ignore_intermediate_objects=True))
return nodes, roots
diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_alembic_options_defaults.py b/client/ayon_core/hosts/maya/plugins/publish/validate_alembic_options_defaults.py
index 5197100406..11f4c313fa 100644
--- a/client/ayon_core/hosts/maya/plugins/publish/validate_alembic_options_defaults.py
+++ b/client/ayon_core/hosts/maya/plugins/publish/validate_alembic_options_defaults.py
@@ -1,3 +1,4 @@
+import inspect
import pyblish.api
from ayon_core.pipeline import OptionalPyblishPluginMixin
@@ -29,29 +30,28 @@ class ValidateAlembicDefaultsPointcache(
@classmethod
def _get_publish_attributes(cls, instance):
- attributes = instance.data["publish_attributes"][
- cls.plugin_name(
- instance.data["publish_attributes"]
- )
- ]
-
- return attributes
+ return instance.data["publish_attributes"][cls.plugin_name]
def process(self, instance):
if not self.is_active(instance.data):
return
settings = self._get_settings(instance.context)
-
attributes = self._get_publish_attributes(instance)
- msg = (
- "Alembic Extract setting \"{}\" is not the default value:"
- "\nCurrent: {}"
- "\nDefault Value: {}\n"
- )
- errors = []
+ invalid = {}
for key, value in attributes.items():
+ if key not in settings:
+ # This may occur if attributes have changed over time and an
+ # existing instance has older legacy attributes that do not
+ # match the current settings definition.
+ self.log.warning(
+ "Publish attribute %s not found in Alembic Export "
+ "default settings. Ignoring validation for attribute.",
+ key
+ )
+ continue
+
default_value = settings[key]
# Lists are best to compared sorted since we cant rely on the order
@@ -61,10 +61,35 @@ class ValidateAlembicDefaultsPointcache(
default_value = sorted(default_value)
if value != default_value:
- errors.append(msg.format(key, value, default_value))
+ invalid[key] = value, default_value
- if errors:
- raise PublishValidationError("\n".join(errors))
+ if invalid:
+ non_defaults = "\n".join(
+ f"- {key}: {value} \t(default: {default_value})"
+ for key, (value, default_value) in invalid.items()
+ )
+
+ raise PublishValidationError(
+ "Alembic extract options differ from default values:\n"
+ f"{non_defaults}",
+ description=self.get_description()
+ )
+
+ @staticmethod
+ def get_description():
+ return inspect.cleandoc(
+ """### Alembic Extract settings differ from defaults
+
+ The alembic export options differ from the project default values.
+
+ If this is intentional you can disable this validation by
+ disabling **Validate Alembic Options Default**.
+
+ If not you may use the "Repair" action to revert all the options to
+ their default values.
+
+ """
+ )
@classmethod
def repair(cls, instance):
@@ -75,13 +100,20 @@ class ValidateAlembicDefaultsPointcache(
)
# Set the settings values on the create context then save to workfile.
- publish_attributes = instance.data["publish_attributes"]
- plugin_name = cls.plugin_name(publish_attributes)
- attributes = cls._get_publish_attributes(instance)
settings = cls._get_settings(instance.context)
- create_publish_attributes = create_instance.data["publish_attributes"]
+ attributes = cls._get_publish_attributes(create_instance)
for key in attributes:
- create_publish_attributes[plugin_name][key] = settings[key]
+ if key not in settings:
+ # This may occur if attributes have changed over time and an
+ # existing instance has older legacy attributes that do not
+ # match the current settings definition.
+ cls.log.warning(
+ "Publish attribute %s not found in Alembic Export "
+ "default settings. Ignoring repair for attribute.",
+ key
+ )
+ continue
+ attributes[key] = settings[key]
create_context.save_changes()
@@ -93,6 +125,6 @@ class ValidateAlembicDefaultsAnimation(
The defaults are defined in the project settings.
"""
- label = "Validate Alembic Options Defaults"
+ label = "Validate Alembic Options Defaults"
families = ["animation"]
plugin_name = "ExtractAnimation"
diff --git a/client/ayon_core/hosts/maya/plugins/workfile_build/script_placeholder.py b/client/ayon_core/hosts/maya/plugins/workfile_build/script_placeholder.py
new file mode 100644
index 0000000000..62e10ba023
--- /dev/null
+++ b/client/ayon_core/hosts/maya/plugins/workfile_build/script_placeholder.py
@@ -0,0 +1,201 @@
+from maya import cmds
+
+from ayon_core.hosts.maya.api.workfile_template_builder import (
+ MayaPlaceholderPlugin
+)
+from ayon_core.lib import NumberDef, TextDef, EnumDef
+from ayon_core.lib.events import weakref_partial
+
+
+EXAMPLE_SCRIPT = """
+# Access maya commands
+from maya import cmds
+
+# Access the placeholder node
+placeholder_node = placeholder.scene_identifier
+
+# Access the event callback
+if event is None:
+ print(f"Populating {placeholder}")
+else:
+ if event.topic == "template.depth_processed":
+ print(f"Processed depth: {event.get('depth')}")
+ elif event.topic == "template.finished":
+ print("Build finished.")
+""".strip()
+
+
+class MayaPlaceholderScriptPlugin(MayaPlaceholderPlugin):
+ """Execute a script at the given `order` during workfile build.
+
+ This is a very low-level placeholder to run Python scripts at a given
+ point in time during the workfile template build.
+
+ It can create either a locator or an objectSet as placeholder node.
+ It defaults to an objectSet, since allowing to run on e.g. other
+ placeholder node members can be useful, e.g. using:
+
+ >>> members = cmds.sets(placeholder.scene_identifier, query=True)
+
+ """
+
+ identifier = "maya.runscript"
+ label = "Run Python Script"
+
+ use_selection_as_parent = False
+
+ def get_placeholder_options(self, options=None):
+ options = options or {}
+ return [
+ NumberDef(
+ "order",
+ label="Order",
+ default=options.get("order") or 0,
+ decimals=0,
+ minimum=0,
+ maximum=999,
+ tooltip=(
+ "Order"
+ "\nOrder defines asset loading priority (0 to 999)"
+ "\nPriority rule is : \"lowest is first to load\"."
+ )
+ ),
+ TextDef(
+ "prepare_script",
+ label="Run at\nprepare",
+ tooltip="Run before populate at prepare order",
+ multiline=True,
+ default=options.get("prepare_script", "")
+ ),
+ TextDef(
+ "populate_script",
+ label="Run at\npopulate",
+ tooltip="Run script at populate node order
"
+ "This is the default behavior",
+ multiline=True,
+ default=options.get("populate_script", EXAMPLE_SCRIPT)
+ ),
+ TextDef(
+ "depth_processed_script",
+ label="Run after\ndepth\niteration",
+ tooltip="Run script after every build depth iteration",
+ multiline=True,
+ default=options.get("depth_processed_script", "")
+ ),
+ TextDef(
+ "finished_script",
+ label="Run after\nbuild",
+ tooltip=(
+ "Run script at build finished.
"
+ "Note: this even runs if other placeholders had "
+ "errors during the build"
+ ),
+ multiline=True,
+ default=options.get("finished_script", "")
+ ),
+ EnumDef(
+ "create_nodetype",
+ label="Nodetype",
+ items={
+ "spaceLocator": "Locator",
+ "objectSet": "ObjectSet"
+ },
+ tooltip=(
+ "The placeholder's node type to be created.
"
+ "Note this only works on create, not on update"
+ ),
+ default=options.get("create_nodetype", "objectSet")
+ ),
+ ]
+
+ def create_placeholder(self, placeholder_data):
+ nodetype = placeholder_data.get("create_nodetype", "objectSet")
+
+ if nodetype == "spaceLocator":
+ super(MayaPlaceholderScriptPlugin, self).create_placeholder(
+ placeholder_data
+ )
+ elif nodetype == "objectSet":
+ placeholder_data["plugin_identifier"] = self.identifier
+
+ # Create maya objectSet on selection
+ selection = cmds.ls(selection=True, long=True)
+ name = self._create_placeholder_name(placeholder_data)
+ node = cmds.sets(selection, name=name)
+
+ self.imprint(node, placeholder_data)
+
+ def prepare_placeholders(self, placeholders):
+ super(MayaPlaceholderScriptPlugin, self).prepare_placeholders(
+ placeholders
+ )
+ for placeholder in placeholders:
+ prepare_script = placeholder.data.get("prepare_script")
+ if not prepare_script:
+ continue
+
+ self.run_script(placeholder, prepare_script)
+
+ def populate_placeholder(self, placeholder):
+
+ populate_script = placeholder.data.get("populate_script")
+ depth_script = placeholder.data.get("depth_processed_script")
+ finished_script = placeholder.data.get("finished_script")
+
+ # Run now
+ if populate_script:
+ self.run_script(placeholder, populate_script)
+
+ if not any([depth_script, finished_script]):
+ # No callback scripts to run
+ if not placeholder.data.get("keep_placeholder", True):
+ self.delete_placeholder(placeholder)
+ return
+
+ # Run at each depth processed
+ if depth_script:
+ callback = weakref_partial(
+ self.run_script, placeholder, depth_script)
+ self.builder.add_on_depth_processed_callback(
+ callback, order=placeholder.order)
+
+ # Run at build finish
+ if finished_script:
+ callback = weakref_partial(
+ self.run_script, placeholder, finished_script)
+ self.builder.add_on_finished_callback(
+ callback, order=placeholder.order)
+
+ # If placeholder should be deleted, delete it after finish so
+ # the scripts have access to it up to the last run
+ if not placeholder.data.get("keep_placeholder", True):
+ delete_callback = weakref_partial(
+ self.delete_placeholder, placeholder)
+ self.builder.add_on_finished_callback(
+ delete_callback, order=placeholder.order + 1)
+
+ def run_script(self, placeholder, script, event=None):
+ """Run script
+
+ Even though `placeholder` is an unused arguments by exposing it as
+ an input argument it means it makes it available through
+ globals()/locals() in the `exec` call, giving the script access
+ to the placeholder.
+
+ For example:
+ >>> node = placeholder.scene_identifier
+
+ In the case the script is running at a callback level (not during
+ populate) then it has access to the `event` as well, otherwise the
+ value is None if it runs during `populate_placeholder` directly.
+
+ For example adding this as the callback script:
+ >>> if event is not None:
+ >>> if event.topic == "on_depth_processed":
+ >>> print(f"Processed depth: {event.get('depth')}")
+ >>> elif event.topic == "on_finished":
+ >>> print("Build finished.")
+
+ """
+ self.log.debug(f"Running script at event: {event}")
+ exec(script, locals())
diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py
index 47c5399cf7..4e34f9b58c 100644
--- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py
+++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py
@@ -52,6 +52,7 @@ class SelectionTypes:
class BaseGroupWidget(QtWidgets.QWidget):
selected = QtCore.Signal(str, str, str)
removed_selected = QtCore.Signal()
+ double_clicked = QtCore.Signal()
def __init__(self, group_name, parent):
super(BaseGroupWidget, self).__init__(parent)
@@ -192,6 +193,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget):
else:
widget = ConvertorItemCardWidget(item, self)
widget.selected.connect(self._on_widget_selection)
+ widget.double_clicked(self.double_clicked)
self._widgets_by_id[item.id] = widget
self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1
@@ -254,6 +256,7 @@ class InstanceGroupWidget(BaseGroupWidget):
)
widget.selected.connect(self._on_widget_selection)
widget.active_changed.connect(self._on_active_changed)
+ widget.double_clicked.connect(self.double_clicked)
self._widgets_by_id[instance.id] = widget
self._content_layout.insertWidget(widget_idx, widget)
widget_idx += 1
@@ -271,6 +274,7 @@ class CardWidget(BaseClickableFrame):
# Group identifier of card
# - this must be set because if send when mouse is released with card id
_group_identifier = None
+ double_clicked = QtCore.Signal()
def __init__(self, parent):
super(CardWidget, self).__init__(parent)
@@ -279,6 +283,11 @@ class CardWidget(BaseClickableFrame):
self._selected = False
self._id = None
+ def mouseDoubleClickEvent(self, event):
+ super(CardWidget, self).mouseDoubleClickEvent(event)
+ if self._is_valid_double_click(event):
+ self.double_clicked.emit()
+
@property
def id(self):
"""Id of card."""
@@ -312,6 +321,9 @@ class CardWidget(BaseClickableFrame):
self.selected.emit(self._id, self._group_identifier, selection_type)
+ def _is_valid_double_click(self, event):
+ return True
+
class ContextCardWidget(CardWidget):
"""Card for global context.
@@ -527,6 +539,15 @@ class InstanceCardWidget(CardWidget):
def _on_expend_clicked(self):
self._set_expanded()
+ def _is_valid_double_click(self, event):
+ widget = self.childAt(event.pos())
+ if (
+ widget is self._active_checkbox
+ or widget is self._expand_btn
+ ):
+ return False
+ return True
+
class InstanceCardView(AbstractInstanceView):
"""Publish access to card view.
@@ -534,6 +555,8 @@ class InstanceCardView(AbstractInstanceView):
Wrapper of all widgets in card view.
"""
+ double_clicked = QtCore.Signal()
+
def __init__(self, controller, parent):
super(InstanceCardView, self).__init__(parent)
@@ -715,6 +738,7 @@ class InstanceCardView(AbstractInstanceView):
)
group_widget.active_changed.connect(self._on_active_changed)
group_widget.selected.connect(self._on_widget_selection)
+ group_widget.double_clicked.connect(self.double_clicked)
self._content_layout.insertWidget(widget_idx, group_widget)
self._widgets_by_group[group_name] = group_widget
@@ -755,6 +779,7 @@ class InstanceCardView(AbstractInstanceView):
widget = ContextCardWidget(self._content_widget)
widget.selected.connect(self._on_widget_selection)
+ widget.double_clicked.connect(self.double_clicked)
self._context_widget = widget
@@ -778,6 +803,7 @@ class InstanceCardView(AbstractInstanceView):
CONVERTOR_ITEM_GROUP, self._content_widget
)
group_widget.selected.connect(self._on_widget_selection)
+ group_widget.double_clicked.connect(self.double_clicked)
self._content_layout.insertWidget(1, group_widget)
self._convertor_items_group = group_widget
diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py
index 3322a73be6..71be0ab1a4 100644
--- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py
+++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py
@@ -110,6 +110,7 @@ class InstanceListItemWidget(QtWidgets.QWidget):
This is required to be able use custom checkbox on custom place.
"""
active_changed = QtCore.Signal(str, bool)
+ double_clicked = QtCore.Signal()
def __init__(self, instance, parent):
super(InstanceListItemWidget, self).__init__(parent)
@@ -149,6 +150,12 @@ class InstanceListItemWidget(QtWidgets.QWidget):
self._set_valid_property(instance.has_valid_context)
+ def mouseDoubleClickEvent(self, event):
+ widget = self.childAt(event.pos())
+ super(InstanceListItemWidget, self).mouseDoubleClickEvent(event)
+ if widget is not self._active_checkbox:
+ self.double_clicked.emit()
+
def _set_valid_property(self, valid):
if self._has_valid_context == valid:
return
@@ -209,6 +216,8 @@ class InstanceListItemWidget(QtWidgets.QWidget):
class ListContextWidget(QtWidgets.QFrame):
"""Context (or global attributes) widget."""
+ double_clicked = QtCore.Signal()
+
def __init__(self, parent):
super(ListContextWidget, self).__init__(parent)
@@ -225,6 +234,10 @@ class ListContextWidget(QtWidgets.QFrame):
self.label_widget = label_widget
+ def mouseDoubleClickEvent(self, event):
+ super(ListContextWidget, self).mouseDoubleClickEvent(event)
+ self.double_clicked.emit()
+
class InstanceListGroupWidget(QtWidgets.QFrame):
"""Widget representing group of instances.
@@ -317,6 +330,7 @@ class InstanceListGroupWidget(QtWidgets.QFrame):
class InstanceTreeView(QtWidgets.QTreeView):
"""View showing instances and their groups."""
toggle_requested = QtCore.Signal(int)
+ double_clicked = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(InstanceTreeView, self).__init__(*args, **kwargs)
@@ -425,6 +439,9 @@ class InstanceListView(AbstractInstanceView):
This is public access to and from list view.
"""
+
+ double_clicked = QtCore.Signal()
+
def __init__(self, controller, parent):
super(InstanceListView, self).__init__(parent)
@@ -454,6 +471,7 @@ class InstanceListView(AbstractInstanceView):
instance_view.collapsed.connect(self._on_collapse)
instance_view.expanded.connect(self._on_expand)
instance_view.toggle_requested.connect(self._on_toggle_request)
+ instance_view.double_clicked.connect(self.double_clicked)
self._group_items = {}
self._group_widgets = {}
@@ -687,6 +705,7 @@ class InstanceListView(AbstractInstanceView):
self._active_toggle_enabled
)
widget.active_changed.connect(self._on_active_changed)
+ widget.double_clicked.connect(self.double_clicked)
self._instance_view.setIndexWidget(proxy_index, widget)
self._widgets_by_id[instance.id] = widget
@@ -717,6 +736,7 @@ class InstanceListView(AbstractInstanceView):
)
proxy_index = self._proxy_model.mapFromSource(index)
widget = ListContextWidget(self._instance_view)
+ widget.double_clicked.connect(self.double_clicked)
self._instance_view.setIndexWidget(proxy_index, widget)
self._context_widget = widget
diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py
index dd82185830..cedf52ae01 100644
--- a/client/ayon_core/tools/publisher/widgets/overview_widget.py
+++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py
@@ -18,6 +18,7 @@ class OverviewWidget(QtWidgets.QFrame):
instance_context_changed = QtCore.Signal()
create_requested = QtCore.Signal()
convert_requested = QtCore.Signal()
+ publish_tab_requested = QtCore.Signal()
anim_end_value = 200
anim_duration = 200
@@ -113,9 +114,15 @@ class OverviewWidget(QtWidgets.QFrame):
product_list_view.selection_changed.connect(
self._on_product_change
)
+ product_list_view.double_clicked.connect(
+ self.publish_tab_requested
+ )
product_view_cards.selection_changed.connect(
self._on_product_change
)
+ product_view_cards.double_clicked.connect(
+ self.publish_tab_requested
+ )
# Active instances changed
product_list_view.active_changed.connect(
self._on_active_changed
diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py
index 123864ff6c..1b13ced317 100644
--- a/client/ayon_core/tools/publisher/window.py
+++ b/client/ayon_core/tools/publisher/window.py
@@ -258,6 +258,9 @@ class PublisherWindow(QtWidgets.QDialog):
overview_widget.convert_requested.connect(
self._on_convert_requested
)
+ overview_widget.publish_tab_requested.connect(
+ self._go_to_publish_tab
+ )
save_btn.clicked.connect(self._on_save_clicked)
reset_btn.clicked.connect(self._on_reset_clicked)
diff --git a/server_addon/maya/package.py b/server_addon/maya/package.py
index 5c6ce923aa..fe3e3039f5 100644
--- a/server_addon/maya/package.py
+++ b/server_addon/maya/package.py
@@ -1,3 +1,3 @@
name = "maya"
title = "Maya"
-version = "0.1.17"
+version = "0.1.18"
diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py
index bc38d5f746..20523b2ca9 100644
--- a/server_addon/maya/server/settings/publishers.py
+++ b/server_addon/maya/server/settings/publishers.py
@@ -46,7 +46,6 @@ def extract_alembic_overrides_enum():
return [
{"label": "Custom Attributes", "value": "attr"},
{"label": "Custom Attributes Prefix", "value": "attrPrefix"},
- {"label": "Auto Subd", "value": "autoSubd"},
{"label": "Data Format", "value": "dataFormat"},
{"label": "Euler Filter", "value": "eulerFilter"},
{"label": "Mel Per Frame Callback", "value": "melPerFrameCallback"},
@@ -347,17 +346,6 @@ class ExtractAlembicModel(BaseSettingsModel):
families: list[str] = SettingsField(
default_factory=list,
title="Families")
- autoSubd: bool = SettingsField(
- title="Auto Subd",
- description=(
- "If this flag is present and the mesh has crease edges, crease "
- "vertices or holes, the mesh (OPolyMesh) would now be written out "
- "as an OSubD and crease info will be stored in the Alembic file. "
- "Otherwise, creases info won't be preserved in Alembic file unless"
- " a custom Boolean attribute SubDivisionMesh has been added to "
- "mesh node and its value is true."
- )
- )
eulerFilter: bool = SettingsField(
title="Euler Filter",
description="Apply Euler filter while sampling rotations."
@@ -409,6 +397,10 @@ class ExtractAlembicModel(BaseSettingsModel):
title="Write Color Sets",
description="Write vertex colors with the geometry."
)
+ writeCreases: bool = SettingsField(
+ title="Write Creases",
+ description="Write the geometry's edge and vertex crease information."
+ )
writeFaceSets: bool = SettingsField(
title="Write Face Sets",
description="Write face sets with the geometry."
@@ -1617,7 +1609,6 @@ DEFAULT_PUBLISH_SETTINGS = {
],
"attr": "",
"attrPrefix": "",
- "autoSubd": False,
"bake_attributes": [],
"bake_attribute_prefixes": [],
"dataFormat": "ogawa",
@@ -1641,7 +1632,7 @@ DEFAULT_PUBLISH_SETTINGS = {
"renderableOnly": False,
"stripNamespaces": True,
"uvsOnly": False,
- "uvWrite": False,
+ "uvWrite": True,
"userAttr": "",
"userAttrPrefix": "",
"verbose": False,
@@ -1649,6 +1640,7 @@ DEFAULT_PUBLISH_SETTINGS = {
"wholeFrameGeo": False,
"worldSpace": True,
"writeColorSets": False,
+ "writeCreases": False,
"writeFaceSets": False,
"writeNormals": True,
"writeUVSets": False,