Merge pull request #514 from kalisp/bugfix/237-Updating_a_look_where_the_shader_name_changed_leaves_the_geo_without_a_shader

Maya: Updating a look where the shader name changed, leaves the geo without a shader
This commit is contained in:
Milan Kolar 2020-10-02 14:46:13 +02:00 committed by GitHub
commit 3e265c5ba0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 186 additions and 50 deletions

View file

@ -3,6 +3,8 @@ from avalon import api, io
import json
import pype.hosts.maya.lib
from collections import defaultdict
from pype.widgets.message_window import ScrollMessageBox
from Qt import QtWidgets
class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
@ -44,18 +46,33 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
self.update(container, representation)
def update(self, container, representation):
"""
Called by Scene Inventory when look should be updated to current
version.
If any reference edits cannot be applied, eg. shader renamed and
material not present, reference is unloaded and cleaned.
All failed edits are highlighted to the user via message box.
Args:
container: object that has look to be updated
representation: (dict): relationship data to get proper
representation from DB and persisted
data in .json
Returns:
None
"""
import os
from maya import cmds
node = container["objectName"]
path = api.get_representation_path(representation)
# Get reference node from container members
members = cmds.sets(node, query=True, nodesOnly=True)
reference_node = self._get_reference_node(members)
shader_nodes = cmds.ls(members, type='shadingEngine')
orig_nodes = set(self._get_nodes_with_shader(shader_nodes))
file_type = {
"ma": "mayaAscii",
"mb": "mayaBinary",
@ -66,6 +83,104 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
assert os.path.exists(path), "%s does not exist." % path
self._load_reference(file_type, node, path, reference_node)
# Remove any placeHolderList attribute entries from the set that
# are remaining from nodes being removed from the referenced file.
members = cmds.sets(node, query=True)
invalid = [x for x in members if ".placeHolderList" in x]
if invalid:
cmds.sets(invalid, remove=node)
# get new applied shaders and nodes from new version
shader_nodes = cmds.ls(members, type='shadingEngine')
nodes = set(self._get_nodes_with_shader(shader_nodes))
json_representation = io.find_one({
"type": "representation",
"parent": representation['parent'],
"name": "json"
})
# Load relationships
shader_relation = api.get_representation_path(json_representation)
with open(shader_relation, "r") as f:
relationships = json.load(f)
# update of reference could result in failed edits - material is not
# present because of renaming etc.
failed_edits = cmds.referenceQuery(reference_node,
editStrings=True,
failedEdits=True,
successfulEdits=False)
# highlight failed edits to user
if failed_edits:
# clean references - removes failed reference edits
cmds.file(cr=reference_node) # cleanReference
# reapply shading groups from json representation on orig nodes
pype.hosts.maya.lib.apply_shaders(relationships,
shader_nodes,
orig_nodes)
msg = ["During reference update some edits failed.",
"All successful edits were kept intact.\n",
"Failed and removed edits:"]
msg.extend(failed_edits)
msg = ScrollMessageBox(QtWidgets.QMessageBox.Warning,
"Some reference edit failed",
msg)
msg.exec_()
attributes = relationships.get("attributes", [])
# region compute lookup
nodes_by_id = defaultdict(list)
for n in nodes:
nodes_by_id[pype.hosts.maya.lib.get_id(n)].append(n)
pype.hosts.maya.lib.apply_attributes(attributes, nodes_by_id)
# Update metadata
cmds.setAttr("{}.representation".format(node),
str(representation["_id"]),
type="string")
def _get_nodes_with_shader(self, shader_nodes):
"""
Returns list of nodes belonging to specific shaders
Args:
shader_nodes: <list> of Shader groups
Returns
<list> node names
"""
import maya.cmds as cmds
# Get container members
nodes_list = []
for shader in shader_nodes:
connections = cmds.listConnections(cmds.listHistory(shader, f=1),
type='mesh')
if connections:
for connection in connections:
nodes_list.extend(cmds.listRelatives(connection,
shapes=True))
return nodes_list
def _load_reference(self, file_type, node, path, reference_node):
"""
Load reference from 'path' on 'reference_node'. Used when change
of look (version/update) is triggered.
Args:
file_type: extension of referenced file
node:
path: (string) location of referenced file
reference_node: (string) - name of node that should be applied
on
Returns:
None
"""
import maya.cmds as cmds
try:
content = cmds.file(path,
loadReference=reference_node,
@ -86,57 +201,10 @@ class LookLoader(pype.hosts.maya.plugin.ReferenceLoader):
raise
self.log.warning("Ignoring file read error:\n%s", exc)
# Fix PLN-40 for older containers created with Avalon that had the
# `.verticesOnlySet` set to True.
if cmds.getAttr("{}.verticesOnlySet".format(node)):
self.log.info("Setting %s.verticesOnlySet to False", node)
cmds.setAttr("{}.verticesOnlySet".format(node), False)
# Add new nodes of the reference to the container
cmds.sets(content, forceElement=node)
# Remove any placeHolderList attribute entries from the set that
# are remaining from nodes being removed from the referenced file.
members = cmds.sets(node, query=True)
invalid = [x for x in members if ".placeHolderList" in x]
if invalid:
cmds.sets(invalid, remove=node)
# Get container members
shader_nodes = cmds.ls(members, type='shadingEngine')
nodes_list = []
for shader in shader_nodes:
connections = cmds.listConnections(cmds.listHistory(shader, f=1),
type='mesh')
if connections:
for connection in connections:
nodes_list.extend(cmds.listRelatives(connection,
shapes=True))
nodes = set(nodes_list)
json_representation = io.find_one({
"type": "representation",
"parent": representation['parent'],
"name": "json"
})
# Load relationships
shader_relation = api.get_representation_path(json_representation)
with open(shader_relation, "r") as f:
relationships = json.load(f)
attributes = relationships.get("attributes", [])
# region compute lookup
nodes_by_id = defaultdict(list)
for n in nodes:
nodes_by_id[pype.hosts.maya.lib.get_id(n)].append(n)
pype.hosts.maya.lib.apply_attributes(attributes, nodes_by_id)
# Update metadata
cmds.setAttr("{}.representation".format(node),
str(representation["_id"]),
type="string")

View file

@ -1,4 +1,4 @@
from Qt import QtWidgets
from Qt import QtWidgets, QtCore
import sys
import logging
@ -49,6 +49,17 @@ class Window(QtWidgets.QWidget):
def message(title=None, message=None, level="info", parent=None):
"""
Produces centered dialog with specific level denoting severity
Args:
title: (string) dialog title
message: (string) message
level: (string) info|warning|critical
parent: (QtWidgets.QApplication)
Returns:
None
"""
app = parent
if not app:
app = QtWidgets.QApplication(sys.argv)
@ -68,3 +79,60 @@ def message(title=None, message=None, level="info", parent=None):
# skip all possible issues that may happen feature is not crutial
log.warning("Couldn't center message.", exc_info=True)
# sys.exit(app.exec_())
class ScrollMessageBox(QtWidgets.QDialog):
"""
Basic version of scrollable QMessageBox. No other existing dialog
implementation is scrollable.
Args:
icon: <QtWidgets.QMessageBox.Icon>
title: <string>
messages: <list> of messages
cancelable: <boolean> - True if Cancel button should be added
"""
def __init__(self, icon, title, messages, cancelable=False):
super(ScrollMessageBox, self).__init__()
self.setWindowTitle(title)
self.icon = icon
self.setWindowFlags(QtCore.Qt.WindowTitleHint)
layout = QtWidgets.QVBoxLayout(self)
scroll_widget = QtWidgets.QScrollArea(self)
scroll_widget.setWidgetResizable(True)
content_widget = QtWidgets.QWidget(self)
scroll_widget.setWidget(content_widget)
max_len = 0
content_layout = QtWidgets.QVBoxLayout(content_widget)
for message in messages:
label_widget = QtWidgets.QLabel(message, content_widget)
content_layout.addWidget(label_widget)
max_len = max(max_len, len(message))
# guess size of scrollable area
max_width = QtWidgets.QApplication.desktop().availableGeometry().width
scroll_widget.setMinimumWidth(min(max_width, max_len * 6))
layout.addWidget(scroll_widget)
if not cancelable: # if no specific buttons OK only
buttons = QtWidgets.QDialogButtonBox.Ok
else:
buttons = QtWidgets.QDialogButtonBox.Ok | \
QtWidgets.QDialogButtonBox.Cancel
btn_box = QtWidgets.QDialogButtonBox(buttons)
btn_box.accepted.connect(self.accept)
if cancelable:
btn_box.reject.connect(self.reject)
btn = QtWidgets.QPushButton('Copy to clipboard')
btn.clicked.connect(lambda: QtWidgets.QApplication.
clipboard().setText("\n".join(messages)))
btn_box.addButton(btn, QtWidgets.QDialogButtonBox.NoRole)
layout.addWidget(btn_box)
self.show()