Skip to content

Instantly share code, notes, and snippets.

@Eterea
Last active November 10, 2022 08:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Eterea/d19df3f046bc9833d7ff195a03fe5a97 to your computer and use it in GitHub Desktop.
Save Eterea/d19df3f046bc9833d7ff195a03fe5a97 to your computer and use it in GitHub Desktop.
WIP script to get modified values in selected nodes, in Substance Designer, and then create a Comment node with all those labels / values
# python
#
# print_modified_values - v.0.2 - THIS IS A WIP
# Created by Cristobal Vila - etereaestudios.com - 2022
# To automatically add child Comment with non default values in Adobe Substance 3D Designer
''' --- DIARY -----------------------------------------------------------------------------------------------------
The purpose of this plugin is to add a child Comment for the selected node containing all NON-DEFAULT values,
this is: the one that have been modified by the user.
My initial goal was to select a node, query for both the DEFAULT and MODIFIED values and then compare both
dictionaries, creating a new one containing only the modified/new values to pass that info to a Content node.
First problem arised, stated by @est (from Adobe) in Discord:
https://discord.com/channels/179919948569640960/769180873223307265/1037657970001068043
"Looking at our code, it seems default values for properties are not correctly implemented for some types of graphs.
I created a ticket in our issue tracker to fix this in the future.
A not very nice alternative for now could be to create a new node, get the property values and then delete the node.
Conclussion: for the momment is seems not possible directly query the DEFAULT values through SDK :-(
Then lets try creating automatically a new node, same as our modified one, to compare. Here arises my second problem:
adding graph nodes is not exactly simple, when these are not Atomic Nodes. Adding an Atomic node is easy:
sdSBSCompNode_Uniform = sdSBSCompGraph.newNode('sbs::compositing::uniform')
sdSBSCompNode_Blend = sdSBSCompGraph.newNode('sbs::compositing::blend')
sdSBSCompNode_Levels = sdSBSCompGraph.newNode('sbs::compositing::levels')
But if you try with:
sdSBSCompNodeShape = sdSBSCompGraph.newNode('sbs::compositing::shape')
sdSBSCompNodeSplatter = sdSBSCompGraph.newNode('sbs::compositing::splatter')
it does NOTHING (?!)
Again, the good @est came to the rescue:
https://discord.com/channels/179919948569640960/769180873223307265/1039173421159940136
"Content from the library or graphs in the explorer that you want to instanciate into a graph are not nodes,
they are node instances (of a graph). The process to instance a graph into another graph is a bit convoluted today.
Basically you need to open the package that contains the graph you want to instance, get an SDPackage,
find the SDGraph that you want to instance and call SDGraph.newInstanceNode(sdGraphToInstance)."
Until I learn those advanced things from the SDK, lets do some things manually, then. In this first stage the user needs
to add that new node to compare MANUALLY, by hand. Yes, a PITA, but much better that write all that stuff by hand...
With our new 'reference' node manually created we select both (modified and 'reference') and apply this script.
It works pretty well but soon arises a third problem (damm!):
No matter your order of selection, Designer doesn't know it (the order). This is: at comparing both nodes, there is no
way to be sure which is the 'reference' and which is the modified. Confirmed, again, by @est at Discord:
https://discord.com/channels/179919948569640960/769180873223307265/1039485816717721622
"Selections in Designer are not ordered. There are no guarantees about the order of selected items."
Once I learn how to add non-atomic nodes (that 'convoluted' procedure) I think that this limitation will dissapear...
Then, for the moment, and doing my quick tests with Python Editor inside Designer, I just introduced that 'selOrder_hack'
to change order to compare. You try a first attempt, and if Comments appear in the wrong node, then change 0 by 1,
repeat, and this time all goes fine.
Now I'm in the process to convert this in a real plugin to call it using a shortcut or maybe with an icon in the bar...
'''
# Import the required classes, tools and other sd stuff.
from functools import partial
import os
import weakref
import sd
from sd.tools import io
from sd.tools import graphlayout
from sd.ui.graphgrid import *
from sd.api.sbs.sdsbscompgraph import *
from sd.api.sdgraphobjectpin import *
from sd.api.sdgraphobjectframe import *
from sd.api.sdgraphobjectcomment import *
from sd.api.sdproperty import SDPropertyCategory
from sd.api.sdvalueserializer import SDValueSerializer
from PySide2 import QtCore, QtGui, QtWidgets, QtSvg
# Import OrderedDict because order in our dictionaries are important
from collections import OrderedDict
DEFAULT_ICON_SIZE = 24
def loadSvgIcon(iconName, size): # Literally copied from factory plugin 'node_align_tools'
currentDir = os.path.dirname(__file__)
iconFile = os.path.abspath(os.path.join(currentDir, iconName + '.svg'))
svgRenderer = QtSvg.QSvgRenderer(iconFile)
if svgRenderer.isValid():
pixmap = QtGui.QPixmap(QtCore.QSize(size, size))
if not pixmap.isNull():
pixmap.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(pixmap)
svgRenderer.render(painter)
painter.end()
return QtGui.QIcon(pixmap)
return None
class PrintModValuesToolBar(QtWidgets.QToolBar): # Adapted from factory plugin 'node_align_tools'
__toolbarList = {}
def __init__(self, graphViewID, uiMgr):
super(PrintModValuesToolBar, self).__init__(parent=uiMgr.getMainWindow())
self.setObjectName("etereaestudios.com.print_modvalues_toolbar")
self.__graphViewID = graphViewID
self.__uiMgr = uiMgr
act = self.addAction(loadSvgIcon("print_modified_values_a", DEFAULT_ICON_SIZE), "PMVa")
act.setShortcut(QtGui.QKeySequence('Q'))
act.setToolTip(self.tr("Print Modified Values from A to B"))
act.triggered.connect(self.__onPrintModValuesA)
act = self.addAction(loadSvgIcon("print_modified_values_b", DEFAULT_ICON_SIZE), "PMVb")
act.setShortcut(QtGui.QKeySequence('W'))
act.setToolTip(self.tr("Print Modified Values from B to A"))
act.triggered.connect(self.__onPrintModValuesB)
self.__toolbarList[graphViewID] = weakref.ref(self)
self.destroyed.connect(partial(PrintModValuesToolBar.__onToolbarDeleted, graphViewID=graphViewID))
def tooltip(self):
return self.tr("Print Modified Values")
# Here comes the main function A
# TO DO: This needs to be cleaned to create only 1 funtion, with arguments, and not repeat the same funtion twice
# ---------------------------------------------------------------------------------------------------------------
def __onPrintModValuesA(self):
# Get the application and UI manager object.
ctx = sd.getContext()
app = ctx.getSDApplication()
uiMgr = app.getQtForPythonUIMgr()
# Get the current graph and grid size
sdSBSCompGraph = uiMgr.getCurrentGraph()
cGridSize = GraphGrid.sGetFirstLevelSize()
# Get the currently selected nodes.
selection = uiMgr.getCurrentGraphSelectedNodes()
size = selection.getSize()
# I define these nodes, but order of selection does NOT exist in Designer
nodeA = selection.getItem(0)
nodeB = selection.getItem(1)
roundN = 2 # Overall round value for floats. Change this to 3 or 4 for extra accuracy
# Crete both Ordered Dictionaries for REFERENCE/MODIFIED nodes, and for DIFFERENCES
refmod_dict = OrderedDict()
differ_dict = OrderedDict()
for index, node in enumerate(selection):
definition = node.getDefinition()
nodeId = node.getIdentifier()
refmod_dict[index] = OrderedDict()
# Create a list of each property category enumeration item.
categories = [
SDPropertyCategory.Annotation,
SDPropertyCategory.Input,
SDPropertyCategory.Output
]
# Get node properties for each property category.
for category in categories:
props = definition.getProperties(category)
# Get the label and identifier of each property.
for prop in props:
label = prop.getLabel()
# Special cases for Rotation/Angle, to note that is Turns (not degrees)
if 'Rotation' in label:
label = 'Rot-Turns'
elif 'Angle' in label:
label = 'Angle-Turns'
# Get the value for the currently accessed property.
value = node.getPropertyValue(prop)
if value:
value = SDValueSerializer.sToString(value) # This gives a convoluted result, poor readability
# -----------------------------------------------------------------------------
# Dirty cleaner for convoluted value strings. And also for rounding floats.
# Example, to convert from:
# ('Position Random', 'SDValueFloat2(float2(0.17365,0.3249))')
# to a more simple and readable:
# ('Position Random', ('0.17', '0.32'))
if 'SDValueEnum' in value:
value = value[-2]
elif 'SDValueInt(int(' in value:
value = value.replace('SDValueInt(int(','').replace('))','')
elif 'SDValueInt2(int2(' in value:
value = value.replace('SDValueInt2(int2(','').replace('))','')
elif 'SDValueFloat(float(' in value:
value = value.replace('SDValueFloat(float(','').replace('))','')
value = str(round(float(value), roundN))
elif 'SDValueFloat2(float2(' in value:
value = value.replace('SDValueFloat2(float2(','').replace('))','')
value0 = value.split(',')[0]
value1 = value.split(',')[1]
value = str(round(float(value0), roundN)), str(round(float(value1), roundN))
elif 'SDValueFloat3(float3(' in value:
value = value.replace('SDValueFloat3(float3(','').replace('))','')
value0 = value.split(',')[0]
value1 = value.split(',')[1]
value2 = value.split(',')[2]
value = str(round(float(value0), roundN)), str(round(float(value1), roundN)), str(round(float(value2), roundN))
elif 'SDValueFloat4(float4(' in value:
value = value.replace('SDValueFloat4(float4(','').replace('))','')
value0 = value.split(',')[0]
value1 = value.split(',')[1]
value2 = value.split(',')[2]
value3 = value.split(',')[3]
value = str(round(float(value0), roundN)), str(round(float(value1), roundN)), str(round(float(value2), roundN)), str(round(float(value3), roundN))
elif 'SDValueBool(bool(' in value:
value = value.replace('SDValueBool(bool(','').replace('))','')
elif 'SDValueString(string(' in value:
value = value.replace('SDValueString(string(','').replace('))','')
elif 'SDValueTexture(SDTexture(' in value:
value = value.replace('SDValueTexture(SDTexture(','').replace('))','')
elif 'SDValueColorRGBA(ColorRGBA(' in value:
value = value.replace('SDValueColorRGBA(ColorRGBA(','').replace('))','')
else:
value = 'UNKNOW'
# -----------------------------------------------------------------------------
refmod_dict[index].update({label: value}) # Add our label/value combos to dictionaries
print('Len Dict 0 = %s' % len(refmod_dict[0]))
print('Len Dict 1 = %s' % len(refmod_dict[1]))
# Hack to solve order of selection problem (it does NOT exist)
for key, value in refmod_dict[0].items():
if key not in refmod_dict[1]:
differ_dict.update({key: value})
else:
if value != refmod_dict[1][key]:
differ_dict.update({key: value})
differ_list = list(differ_dict.items()) # Convert Ordered Dictionary to list
# Clean the resulting list for simpler and better readability, breaking lines and removing some characters
differ_str = '\n'.join(map(str, differ_list)) # To convert to various lines
differ_str = differ_str.replace("(","").replace(")","").replace("'","") # Extra step to remove (') characters
print(differ_str)
# Create New Comment attached to Node, using the order hack
sdGraphObjectComment = SDGraphObjectComment.sNewAsChild(nodeA)
sdGraphObjectComment.setPosition(float2(-cGridSize*0.5, cGridSize*0.5))
sdGraphObjectComment.setDescription('%s' % differ_str)
# Here comes the main function B
# TO DO: This needs to be cleaned to create only 1 funtion, with arguments, and not repeat the same funtion twice
# ---------------------------------------------------------------------------------------------------------------
def __onPrintModValuesB(self):
# Get the application and UI manager object.
ctx = sd.getContext()
app = ctx.getSDApplication()
uiMgr = app.getQtForPythonUIMgr()
# Get the current graph and grid size
sdSBSCompGraph = uiMgr.getCurrentGraph()
cGridSize = GraphGrid.sGetFirstLevelSize()
# Get the currently selected nodes.
selection = uiMgr.getCurrentGraphSelectedNodes()
size = selection.getSize()
# I define these nodes, but order of selection does NOT exist in Designer
nodeA = selection.getItem(0)
nodeB = selection.getItem(1)
roundN = 2 # Overall round value for floats. Change this to 3 or 4 for extra accuracy
# Crete both Ordered Dictionaries for REFERENCE/MODIFIED nodes, and for DIFFERENCES
refmod_dict = OrderedDict()
differ_dict = OrderedDict()
for index, node in enumerate(selection):
definition = node.getDefinition()
nodeId = node.getIdentifier()
refmod_dict[index] = OrderedDict()
# Create a list of each property category enumeration item.
categories = [
SDPropertyCategory.Annotation,
SDPropertyCategory.Input,
SDPropertyCategory.Output
]
# Get node properties for each property category.
for category in categories:
props = definition.getProperties(category)
# Get the label and identifier of each property.
for prop in props:
label = prop.getLabel()
# Special cases for Rotation/Angle, to note that is Turns (not degrees)
if 'Rotation' in label:
label = 'Rot-Turns'
elif 'Angle' in label:
label = 'Angle-Turns'
# Get the value for the currently accessed property.
value = node.getPropertyValue(prop)
if value:
value = SDValueSerializer.sToString(value) # This gives a convoluted result, poor readability
# -----------------------------------------------------------------------------
# Dirty cleaner for convoluted value strings. And also for rounding floats.
# Example, to convert from:
# ('Position Random', 'SDValueFloat2(float2(0.17365,0.3249))')
# to a more simple and readable:
# ('Position Random', ('0.17', '0.32'))
if 'SDValueEnum' in value:
value = value[-2]
elif 'SDValueInt(int(' in value:
value = value.replace('SDValueInt(int(','').replace('))','')
elif 'SDValueInt2(int2(' in value:
value = value.replace('SDValueInt2(int2(','').replace('))','')
elif 'SDValueFloat(float(' in value:
value = value.replace('SDValueFloat(float(','').replace('))','')
value = str(round(float(value), roundN))
elif 'SDValueFloat2(float2(' in value:
value = value.replace('SDValueFloat2(float2(','').replace('))','')
value0 = value.split(',')[0]
value1 = value.split(',')[1]
value = str(round(float(value0), roundN)), str(round(float(value1), roundN))
elif 'SDValueFloat3(float3(' in value:
value = value.replace('SDValueFloat3(float3(','').replace('))','')
value0 = value.split(',')[0]
value1 = value.split(',')[1]
value2 = value.split(',')[2]
value = str(round(float(value0), roundN)), str(round(float(value1), roundN)), str(round(float(value2), roundN))
elif 'SDValueFloat4(float4(' in value:
value = value.replace('SDValueFloat4(float4(','').replace('))','')
value0 = value.split(',')[0]
value1 = value.split(',')[1]
value2 = value.split(',')[2]
value3 = value.split(',')[3]
value = str(round(float(value0), roundN)), str(round(float(value1), roundN)), str(round(float(value2), roundN)), str(round(float(value3), roundN))
elif 'SDValueBool(bool(' in value:
value = value.replace('SDValueBool(bool(','').replace('))','')
elif 'SDValueString(string(' in value:
value = value.replace('SDValueString(string(','').replace('))','')
elif 'SDValueTexture(SDTexture(' in value:
value = value.replace('SDValueTexture(SDTexture(','').replace('))','')
elif 'SDValueColorRGBA(ColorRGBA(' in value:
value = value.replace('SDValueColorRGBA(ColorRGBA(','').replace('))','')
else:
value = 'UNKNOW'
# -----------------------------------------------------------------------------
refmod_dict[index].update({label: value}) # Add our label/value combos to dictionaries
print('Len Dict 0 = %s' % len(refmod_dict[0]))
print('Len Dict 1 = %s' % len(refmod_dict[1]))
# Hack to solve order of selection problem (it does NOT exist)
for key, value in refmod_dict[1].items():
if key not in refmod_dict[0]:
differ_dict.update({key: value})
else:
if value != refmod_dict[0][key]:
differ_dict.update({key: value})
differ_list = list(differ_dict.items()) # Convert Ordered Dictionary to list
# Clean the resulting list for simpler and better readability, breaking lines and removing some characters
differ_str = '\n'.join(map(str, differ_list)) # To convert to various lines
differ_str = differ_str.replace("(","").replace(")","").replace("'","") # Extra step to remove (') characters
print(differ_str)
# Create New Comment attached to Node, using the order hack
sdGraphObjectComment = SDGraphObjectComment.sNewAsChild(nodeB)
sdGraphObjectComment.setPosition(float2(-cGridSize*0.5, cGridSize*0.5))
sdGraphObjectComment.setDescription('%s' % differ_str)
@classmethod # Literally copied from factory plugin 'node_align_tools'
def __onToolbarDeleted(cls, graphViewID):
del cls.__toolbarList[graphViewID]
@classmethod # Literally copied from factory plugin 'node_align_tools'
def removeAllToolbars(cls):
for toolbar in cls.__toolbarList.values():
if toolbar():
toolbar().deleteLater()
def onNewGraphViewCreated(graphViewID, uiMgr): # Adapted from factory plugin 'node_align_tools'
# Ignore graph types not supported by the Python API.
if not uiMgr.getCurrentGraph():
return
toolbar = PrintModValuesToolBar(graphViewID, uiMgr)
uiMgr.addToolbarToGraphView(
graphViewID,
toolbar,
icon = loadSvgIcon("print_modified_values", DEFAULT_ICON_SIZE),
tooltip = toolbar.tooltip())
graphViewCreatedCallbackID = 0
def initializeSDPlugin(): # Literally copied from factory plugin 'node_align_tools'
# Get the application and UI manager object.
ctx = sd.getContext()
app = ctx.getSDApplication()
uiMgr = app.getQtForPythonUIMgr()
if uiMgr:
global graphViewCreatedCallbackID
graphViewCreatedCallbackID = uiMgr.registerGraphViewCreatedCallback(
partial(onNewGraphViewCreated, uiMgr=uiMgr))
def uninitializeSDPlugin(): # Adapted from factory plugin 'node_align_tools'
ctx = sd.getContext()
app = ctx.getSDApplication()
uiMgr = app.getQtForPythonUIMgr()
if uiMgr:
global graphViewCreatedCallbackID
uiMgr.unregisterCallback(graphViewCreatedCallbackID)
PrintModValuesToolBar.removeAllToolbars()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment