How to compare a port value has been changed?
-
Hi community,
I would like to know if there is a way to obtain the default value of a port(maxon.GraphNode), then catch which port in the graph is changed. I'm not sure if I missed something, but I didn't find any relevant methods in the document.
Any suggestions are welcomed!
Cheers~
DunHou -
Hi @Dunhou there is the method GraphModelInterface.GetModificationStamp which return you a TimeStamp of the last change (aka the last committed transaction).
Then with this stamp you can use GraphModelInterface.GetModificationsSince that will return you which GraphNode (so port since ports are node) are modified.
With that's said currently GetModificationsSince only accept list and not directly a callback, if you want to use a callback you need to use _GetModificationsSinceComplex.Find bellow a code example
import c4d import maxon def main(): # Retrieve the selected baseMaterial mat = c4d.BaseMaterial(c4d.Mmaterial) if mat is None: raise ValueError("Cannot create a BaseMaterial") # Retrieve the reference of the material as a node Material. nodeMaterial = mat.GetNodeMaterialReference() if nodeMaterial is None: raise ValueError("Cannot retrieve nodeMaterial reference") # Define the ID of standard material node space and print a warning when the active node space # is not the the standard material node space. nodeSpaceId = maxon.Id("net.maxon.nodespace.standard") if nodeSpaceId != c4d.GetActiveNodeSpaceId(): print (f"Warning: Active node space is not: {nodeSpaceId}") # Add a graph for the standard node space. addedGraph = nodeMaterial.CreateDefaultGraph(nodeSpaceId) if addedGraph is None: raise ValueError("Cannot add a graph node for this node space") # Retrieve the Nimbus reference for a specific node space from which we # will retrieve the graph. One could also use 'addedGraph' defined above. nimbusRef = mat.GetNimbusRef(nodeSpaceId) if nimbusRef is None: raise ValueError("Cannot retrieve the nimbus ref for that node space") # Retrieve the graph corresponding to that node space. graph = nimbusRef.GetGraph() if graph is None: raise ValueError("Cannot retrieve the graph of this nimbus ref") # Retrieve the end node of this graph endNodePath = nimbusRef.GetPath(maxon.NIMBUS_PATH.MATERIALENDNODE) endNode = graph.GetNode(endNodePath) if endNode is None: raise ValueError("Cannot retrieve the end-node of this graph") # Retrieve the predecessors. Function have been moved in R26. predecessor = list() maxon.GraphModelHelper.GetDirectPredecessors(endNode, maxon.NODE_KIND.NODE, predecessor) bsdfNode = predecessor[0] if bsdfNode is None: raise ValueError("Cannot retrieve the node connected to end-node") # Retrieve the outputs list of the BDSF node if bsdfNode is None and not bsdfNode.IsValid() : raise ValueError("Cannot retrieve the inputs list of the bsdfNode node") bsdfNodeInputs = bsdfNode.GetInputs() colordePort = bsdfNodeInputs.FindChild("color") if colordePort is None: return stamp = graph.GetModificationStamp() with graph.BeginTransaction() as transaction: # Define the value of the Color's port. colordePort.SetPortValue(maxon.ColorA(1, 0, 0, 1)) transaction.Commit() # Get the change via a list modifieds = [] graph.GetModificationsSince(stamp, modifieds, True) for node, flag in modifieds: print(flag, node) # Get the change via a callback, due to a bug that is going to be fixed, you can call GetModificationsSince with a callback def callback(node: maxon.GraphNode, flag: maxon.GraphModelInterface.MODIFIED): print(flag, node) return True graph._GetModificationsSinceComplex(stamp, callback, True) doc.InsertMaterial(mat) c4d.EventAdd() if __name__ == "__main__": main()
Sadly there is a missing bit in Python, since in C++ we have the concept of obverser, which allow for a lambda to be executed when a particular event occurs. Sadly observer are not yet implemented in Python, it's on the to-do list but for the moment it's not there. And in C++ there is the observer ObservableTransactionCommitted, which let you react to a graph transaction commit. So for the moment your best bet is to monitor for EVMSG_CHANGE or use a Timer to monitor the timestamp and check if it changed.
Cheers,
Maxime. -
Hey @m_adam ,
Thanks for that quick answer and a sweet codes for
GraphModelInterface.GetModificationStamp
I took a look at this, but these might be some details I still need.For example, I get a material from others. I want to check the material:
- oh, He/She modified the ior value to 2. That is a change applied to result.
- also He/She changed roughness 0 -> 0.2 -> 0. It has been modified but that is a default value, I didn't want to catch this.
Can we get the this stored value here, aka the default value we create the new material?
Cheers~
DunHou -
Is any luck here
-
Hi @Dunhou the default value can be retrieved via:
port.GetValue(maxon.DESCRIPTION.DATA.BASE.DEFAULTVALUE)However note that sometime the value can be None, in this case its the default value of the datatype that is used.
Cheers,
Maxime. -
Hey @m_adam ,
I think this is equal to port.GetValue('value'), and it will get the current value for the node but not the default value.
if I change the roughness to 0.5, they both return 0.5 but bot 0 for default.
Cheers~
DunHou -
Hi sorry I forget about that, so SetPortValue set the DESCRIPTION::DATA::BASE::DEFAULTVALUE at the port level. Defining a new default value for this port for the current graph. Values are coming from DataBaseDescription, then Graph. So if the graph does not define a new default value, then the value from the DataBase is used. Its a simplified explanation you can have multiple layers of "data" holder in between, but you get the idea.
If you want to retrieve the "user" default value you either have to go via PortDescription (which are not supported in Python) to retrieve the value stored in the description or use the GraphModelInterface.GetBaseValues which is also not supported in Python for the moment. I will add the later one in the next version of Cinema 4D.
With that's said you can add yourself the GetBaseValues like so, by adding that at the top of your file:
@maxon.MAXON_INTERFACE(maxon.MAXON_REFERENCE_NORMAL, "net.maxon.graph.interface.graphmodel") class GraphModelInterface(maxon.ObserverObjectInterface): @maxon.MAXON_METHOD('net.maxon.graph.interface.graphmodel.GetBaseValues') def GetBaseValues(self): pass def GetBaseValuesGN(self, attr, expectedType, receiver): return self._GetGraph().GetBaseValues(self, attr, expectedType, receiver) maxon.GraphModelInterface.GetBaseValues = GraphModelInterface.GetBaseValues maxon.GraphNode.GetBaseValues = GetBaseValuesGN
Then find bellow a function to retrieve the DefaultValue of a GraphNode.
def GetDefaultValue(node: maxon.GraphNode): defaultValue = [None] def GetDefaultValue(data, nesting): if nesting >= 0: defaultValue[0] = data return False return True node.GetBaseValues(maxon.DESCRIPTION.DATA.BASE.DEFAULTVALUE, None, GetDefaultValue) if defaultValue[0] is None: # Get the default value from the DataType default value. # We retrieve the DataType from the current value type, ideally you should go through the database description. # But database description are not available in Python for the moment. currentValue = node.GetPortValue() if currentValue is not None: dt = currentValue.GetType() defaultValue[0] = maxon.Data(dt.Create()) return defaultValue[0]
So with the code example I posted above, this give:
import c4d import maxon @maxon.MAXON_INTERFACE(maxon.MAXON_REFERENCE_NORMAL, "net.maxon.graph.interface.graphmodel") class GraphModelInterface(maxon.ObserverObjectInterface): @maxon.MAXON_METHOD('net.maxon.graph.interface.graphmodel.GetBaseValues') def GetBaseValues(self): pass def GetBaseValuesGN(self, attr, expectedType, receiver): return self._GetGraph().GetBaseValues(self, attr, expectedType, receiver) maxon.GraphModelInterface.GetBaseValues = GraphModelInterface.GetBaseValues maxon.GraphNode.GetBaseValues = GetBaseValuesGN def main(): # Retrieve the selected baseMaterial mat = c4d.BaseMaterial(c4d.Mmaterial) if mat is None: raise ValueError("Cannot create a BaseMaterial") # Retrieve the reference of the material as a node Material. nodeMaterial = mat.GetNodeMaterialReference() if nodeMaterial is None: raise ValueError("Cannot retrieve nodeMaterial reference") # Define the ID of standard material node space and print a warning when the active node space # is not the the standard material node space. nodeSpaceId = maxon.Id("net.maxon.nodespace.standard") if nodeSpaceId != c4d.GetActiveNodeSpaceId(): print (f"Warning: Active node space is not: {nodeSpaceId}") # Add a graph for the standard node space. addedGraph = nodeMaterial.CreateDefaultGraph(nodeSpaceId) if addedGraph is None: raise ValueError("Cannot add a graph node for this node space") # Retrieve the Nimbus reference for a specific node space from which we # will retrieve the graph. One could also use 'addedGraph' defined above. nimbusRef = mat.GetNimbusRef(nodeSpaceId) if nimbusRef is None: raise ValueError("Cannot retrieve the nimbus ref for that node space") # Retrieve the graph corresponding to that node space. graph = nimbusRef.GetGraph() if graph is None: raise ValueError("Cannot retrieve the graph of this nimbus ref") # Retrieve the end node of this graph endNodePath = nimbusRef.GetPath(maxon.NIMBUS_PATH.MATERIALENDNODE) endNode = graph.GetNode(endNodePath) if endNode is None: raise ValueError("Cannot retrieve the end-node of this graph") # Retrieve the predecessors. Function have been moved in R26. predecessor = list() maxon.GraphModelHelper.GetDirectPredecessors(endNode, maxon.NODE_KIND.NODE, predecessor) bsdfNode = predecessor[0] if bsdfNode is None: raise ValueError("Cannot retrieve the node connected to end-node") # Retrieve the outputs list of the BDSF node if bsdfNode is None and not bsdfNode.IsValid() : raise ValueError("Cannot retrieve the inputs list of the bsdfNode node") bsdfNodeInputs = bsdfNode.GetInputs() colordePort = bsdfNodeInputs.FindChild("color") if colordePort is None: return stamp = graph.GetModificationStamp() with graph.BeginTransaction() as transaction: # Define the value of the Color's port. colordePort.SetPortValue(maxon.ColorA(1, 1, 0, 1)) transaction.Commit() def GetDefaultValue(node: maxon.GraphNode): defaultValue = [None] def GetDefaultValue(data, nesting): if nesting >= 0: defaultValue[0] = data return False return True node.GetBaseValues(maxon.DESCRIPTION.DATA.BASE.DEFAULTVALUE, None, GetDefaultValue) if defaultValue[0] is None: # Get the default value from the DataType default value. # We retrieve the DataType from the current value type, ideally you should go through the database description. # But database description are not available in Python for the moment. currentValue = node.GetPortValue() if currentValue is not None: dt = currentValue.GetType() defaultValue[0] = maxon.Data(dt.Create()) return defaultValue[0] # Get the change via a callback, due to a bug that is going to be fixed, you can't call GetModificationsSince with a callback def callback(node: maxon.GraphNode, flag: maxon.GraphModelInterface.MODIFIED): if flag == maxon.GraphModelInterface.MODIFIED.DATA_ATTRIBUTE_MASK: print(flag, node, GetDefaultValue(node)) return True graph._GetModificationsSinceComplex(stamp, callback, True) doc.InsertMaterial(mat) c4d.EventAdd() if __name__ == "__main__": main()
Cheers,
Maxime. -
Wow @m_adam , thanks for that, I'll check this after work!