This notebook will look at how to extract out connectivity information from a MaterialX document.
a text representation suitable for storage rather than a run-time representation. Note that both compound
and functional
graphs (used by definitions are handled).
The logic shown here has been encapsulated as two Python classes in the mtlxutils
library insdie traversal.py
:
MtlxGraphBuilder
: Class which builds the connectivity information and allows for serialization to JSON format.MxMermaidGraphExporter
: Class which can read and parse the connectivity information to produce Mermaid graphs. For this site:
mtlxutils
file: mxtraversal.py
.mxgraphio.py
found in the pymaterialx
folder wraps up these utilities.mxgraphio.py
command line utility or the mtlxutils
library.JsMaterialGraph
is used for interactive graph generation on the Graph Editing page.The basic setup includes loading MaterialX as well as support libraries. The assumption is that at least version 1.38.7 of MaterialX has been installed.
# Helpers
import os
from IPython.display import display_markdown # For markdown display in Jupyter
# MaterialX imports
import MaterialX as mx
from mtlxutils.mxbase import *
# Do a version check
haveVersion1387 = haveVersion(1, 38, 7)
if not haveVersion1387:
print("** Warning: Recommended minimum version is 1.38.7 for tutorials. Have version: ", mx.__version__)
else:
print("Using MaterialX version:", mx.__version__)
Using MaterialX version: 1.39.0
To be able to handle any non-explicitly defined inputs and outputs and node information, the standard MaterialX node library is required. As a first step we load in libraries and create a working document.
def createWorkingDocument():
stdlib = mx.createDocument()
searchPath = mx.getDefaultDataSearchPath()
libraryFolders = mx.getDefaultDataLibraryFolders()
try:
libFiles = mx.loadLibraries(libraryFolders, searchPath, stdlib)
print('Create working document and loaded in: %s standard library definitions' % len(stdlib.getNodeDefs()))
except mx.Exception as err:
print('Failed to load standard library definitions: "', err, '"')
doc = mx.createDocument()
doc.importLibrary(stdlib)
return doc
doc = createWorkingDocument()
Create working document and loaded in: 750 standard library definitions
As a first step a "dictionary" containing all the nodes in a document, grouped by graph is generated. Each dictionary is of the form:
<graph path string> [ <node path string >... ]
such that for each graph (keyed by path), a list of node paths is kept.
As the root Document
has no path name an emptry string indicates the root graph.
Two functions are shown below to for graph dictionary building:
updateGraphDictionaryPath()
: Add a child node path to the list of node paths for a given graph path.updateGraphDictionaryItem()
: Add a new graph / node pair.def updateGraphDictionaryPath(key, item, nodetype, type, value, graphDictionary):
'''
Add a parent / child to the GraphElement dictionary
Arguments:
key: The parent graph path
value: The graph node path
nodetype: The type of the node
graphDictionary: The dictionary to add the Element to.
'''
if key in graphDictionary:
#print('add:', key, value, nodetype)
graphDictionary[key].append([item, nodetype, type, value])
else:
#print('add:', key, value, nodetype)
graphDictionary[key] = [[item, nodetype, type, value]]
def updateGraphDictionaryItem(item, graphDictionary):
"""
Add a Element to the GraphElement dictionary, where the keys are the GraphElement's path, and the value
is a list of child Element paths
"""
if not item:
return
parentElem = item.getParent()
if not parentElem or not parentElem.isA(mx.GraphElement):
return
key = parentElem.getNamePath()
value = item.getNamePath()
itemType = item.getType()
itemCategory = item.getCategory()
itemValue = ''
if item.isA(mx.Node):
inputs = item.getInputs()
if len(inputs) == 1:
itemValue = inputs[0].getValueString()
elif item.isA(mx.Input):
itemValue = item.getValueString()
updateGraphDictionaryPath(key, value, itemCategory, itemType, itemValue, graphDictionary)
To examine the contents of the dictionay a printGraphDictionary()
function is added.
def printGraphDictionary(graphDictionary: dict):
"""
Print out the graph dictionary
"""
for graphPath in graphDictionary:
if graphPath == '':
print('Root Document:')
else:
print(graphPath + ':')
filter = 'input'
# Top level document has not path, so just output some identifier string
for item in graphDictionary[graphPath]:
if item[1] != filter:
continue
print('- ', item)
filter = 'output'
# Top level document has not path, so just output some identifier string
for item in graphDictionary[graphPath]:
if item[1] != filter:
continue
print('- ', item)
filter = ['output', 'input']
# Top level document has not path, so just output some identifier string
for item in graphDictionary[graphPath]:
if item[1] not in filter:
print('- ', item)
def getParentGraph(elem):
'''
Find the parent graph of the given element
'''
while (elem and not elem.isA(mx.GraphElement)):
elem = elem.getParent()
return elem
To aid with building connection information a few additional functions are added below.
To handle when an output is not explicitly specified for a graph or node, a utility function called getDefaultOutput()
is added.
It will simply return the first output found.
This is useful when trying to find the upstream output port connecged to downstream output, but the name of the output is not explicitly specified. This is an inconsistency which must be handled where only for upstream nodes or graphs which have more than one output allows for a output to be specified -- otherwise validation fails.
def getDefaultOutput(node: mx.Element) -> str:
'''
Get the default output of a node or nodegraph. Returns the first output found.
'''
if not node:
return ''
defaultOutput = None
if node.isA(mx.Node):
nodedef = node.getNodeDef()
if nodedef:
defaultOutput = nodedef.getActiveOutputs()[0]
else:
print('Cannot find nodedef for node:', node.getNamePath())
elif node.isA(mx.NodeGraph):
defaultOutput = node.getOutputs()[0]
if defaultOutput:
return defaultOutput.getName()
return ''
A utility called appendPath()
is added as a simple helper as there is no formal API for manipulating graph paths. It is assumed that /
is always the path seperator.
def appendPath(p1: str, p2: str) -> str:
'''
Append two paths together, with a '/' separator.
Arguments:
p1: The first path
p2: The second path
Returns:
The appended path
'''
PATH_SEPARATOR = '/'
if p2:
return p1 + PATH_SEPARATOR + p2
return p1
The function buildPortConnection()
contains the core logic to determine what output node / graph and port is connected to an downstream node / nodegraph port.
Each connection is of the form:
[ <upstream element>, [<upstream output>], <downstream element>, <downstream input>, <type of connection>]
where the <upstream element>
may be path to input
or output
or other node type, <upstream output
is any output port on the upstream element (if it's not an input
or output
node), <downstream element>
a path to an input
, output
or other node type, and <downdsteam input
is the input port on the downstream element (if it's not an input
or output
node). The <type of connection
is additional meta-data to reflect the original connection syntax encountered.
Additional "undue" complexity is added as:
This differs from say OpenUSD
where a full path to a specific port is specified making it simple to just find the correct descendent from the root.
def buildPortConnection(doc: mx.GraphElement, graphDictionary: dict, portPath: str, connections: list, portIsNode: bool):
'''
Build a list of connections for the given graphElement.
Arguments:
- doc: The document to search for the portPath
- portPath: The path to the port to search for connections
- connections: The list of connections to append to. Returned.
- portIsNode: If True, the portPath is a node, otherwise it is a port
'''
root = doc.getDocument()
port = root.getDescendant(portPath)
if not port:
print('Element not found:', portPath)
return
if not (port.isA(mx.Input) or port.isA(mx.Output)):
print('Element is not an input or output')
return
parent = port.getParent()
parentPath = parent.getNamePath()
parentGraph = getParentGraph(port)
# Need to "jump out" of current graph if considering an input interfae
# on a graph
if port.isA(mx.Input) and parent.isA(mx.NodeGraph):
parentGraph = parentGraph.getParent()
if not parentGraph:
print('Cannot find parent graph of port', port)
parentGraphPath = parentGraph.getNamePath()
outputName = port.getOutputString()
destNode = portPath if portIsNode else parentPath
destPort = '' if portIsNode else port.getName()
nodename = port.getAttribute('nodename')
if nodename:
if len(parentGraphPath) == 0:
result = [appendPath(nodename, ''), outputName, destNode, destPort, 'nodename']
else:
result = [appendPath(parentGraphPath, nodename), outputName, destNode, destPort, 'nodename']
connections.append(result)
return
nodegraph = port.getNodeGraphString()
if nodegraph:
if not outputName:
outputName = getDefaultOutput(parentGraph.getChild(nodegraph))
if len(parentGraphPath) == 0:
result = [appendPath(nodegraph, outputName), '', destNode, destPort, 'nodename']
else:
result = [appendPath(parentGraphPath, nodegraph), outputName, destNode, destPort, 'nodegraph']
connections.append(result)
return
interfaceName = port.getInterfaceName()
if interfaceName:
if len(parentGraphPath) == 0:
if not outputName:
outputName = getDefaultOutput(parentGraph.getChild(interfaceName))
result = [appendPath(interfaceName, outputName), '', destNode, destPort, 'nodename']
else:
outputName = ''
# This should be invalid but you can have an input name on a nodedef be the
# same a node in the functional braph. Emit a warning and rename it.
itemValue = ''
if destNode == (parentGraphPath + '/' + interfaceName):
dictItem = graphDictionary.get(parentGraphPath)
if dictItem:
found = False
for item in dictItem:
if item[0] == parentGraphPath + '/' + interfaceName:
found = True
break
if found:
print('Warning: Rename duplicate interface:', parentGraphPath + '/' + interfaceName + ':in')
interfaceName = interfaceName + ':in'
found = False
dictItem = graphDictionary.get(parentGraphPath)
if dictItem:
for item in dictItem:
if item[0] == parentGraphPath + '/' + interfaceName:
found = True
break
if not found:
# TODO: Grab the input value from the nodedef.
#print('- Dyanmically add in interfaceName:', interfaceName, 'to graph:', parentGraphPath, '.Value: ', itemValue)
updateGraphDictionaryPath(parentGraphPath, parentGraphPath + '/' + interfaceName, 'input', port.getType(), itemValue, graphDictionary)
result = [appendPath(parentGraphPath, interfaceName), outputName, destNode, destPort, 'interfacename']
#if portIsNode:
#print('append interface connection:', result)
connections.append(result)
return
if outputName:
if len(parentGraphPath) == 0:
result = [appendPath(outputName, ''), '', parentPath, port.getName(), 'nodename']
else:
result = [appendPath(parentGraphPath, outputName), '', parentPath, port.getName(), 'output']
#if portIsNode:
#print('append connection:', result)
connections.append(result)
return
#if port.isA(mx.Input):
# portValue = port.getValueString()
# if portValue:
# result = [portValue, '', destNode, destPort, 'value']
# connections.append(result)
The buildConnections()
utility will find all connections for a graph by scanning all children elements as necessary:
Input
or Output
nodes we directly check for connectionsNode
types we scan all child Inputs
NodeGraphs
we recursively call buildConnectons()
. This will handle nested GraphElements
such as Document / NodeGraph relationships as well as NodeGraph / NodeGraph relationships. Note that any GraphElement
can be passed in -- not just the top level Document to allow connection introspection of arbitrary graphs.def buildConnections(doc, graphDictionary, graphElement, connections):
#print('get children for graph: "%s"' % graphElement.getNamePath())
root = doc.getDocument()
for elem in graphElement.getChildren():
if not elem.hasSourceUri():
if elem.isA(mx.Input):
buildPortConnection(root, graphDictionary, elem.getNamePath(), connections, True)
elif elem.isA(mx.Output):
buildPortConnection(root, graphDictionary, elem.getNamePath(), connections, True)
elif elem.isA(mx.Node):
nodeInputs = elem.getInputs()
for nodeInput in nodeInputs:
buildPortConnection(root, graphDictionary, nodeInput.getNamePath(), connections, False)
elif elem.isA(mx.NodeGraph):
nodedef = elem.getNodeDef()
if nodedef:
connections.append([elem.getNamePath(), '', nodedef.getName(), '', 'nodedef'])
visited = set()
path = elem.getNamePath()
if path not in visited:
visited.add(path)
buildConnections(root, graphDictionary, elem, connections)
In the example below load in an example which can be found in the unit test suite for MaterialX. It contains a few nodegraphs that are connected in a cascading manner and is used for testing graph traversal for shader code generation.
filename = './data/cascade_nodegraphs.mtlx'
if os.path.exists(filename):
mx.readFromXmlFile(doc, filename)
print("Read file: ", filename)
else:
print("File not found: ", filename)
Read file: ./data/cascade_nodegraphs.mtlx
The utility function will build the graph dictionary by scan through all the children of a GraphElement and building dictionary entries.
We group the entries by scanning by child type. e.g. grouping all input connections together. After building the graph we print outs it's contents.
def buildGraphDictionary(doc):
'''
Build a dictionary of the graph elements in the document. The dictionary
has the graph path as the key, and a list of child elements as the value.
Arguments:
- doc: The document to build the graph dictionary from
Returnes:
- The graph dictionary
'''
graphDictionary = {}
# Traverse all edges and add up and downstream nodes to
# the graph dictionary
root = doc.getDocument()
skipped = []
for elem in doc.getChildren():
if elem.hasSourceUri():
skipped.append(elem.getNamePath())
else:
if elem.isA(mx.Input) or elem.isA(mx.Output) or elem.isA(mx.Node):
updateGraphDictionaryItem(elem, graphDictionary)
elif (elem.isA(mx.NodeGraph)):
# Temporarily copy over inputs and from nodedef this is a
# functional graph
if elem.getAttribute('nodedef'):
nodeDef = elem.getAttribute('nodedef')
nodeDef = root.getDescendant(nodeDef)
if nodeDef:
nodeDefName = nodeDef.getName()
for nodeDefInput in nodeDef.getInputs():
newInput = elem.addInput(nodeDefInput.getName(), nodeDefInput.getType())
newInput.copyContentFrom(nodeDefInput)
for node in elem.getInputs():
updateGraphDictionaryItem(node, graphDictionary)
for node in elem.getOutputs():
updateGraphDictionaryItem(node, graphDictionary)
for node in elem.getNodes():
updateGraphDictionaryItem(node, graphDictionary)
for node in elem.getTokens():
updateGraphDictionaryItem(node, graphDictionary)
elif elem.isA(mx.NodeDef):
updateGraphDictionaryItem(elem, graphDictionary)
elif elem.isA(mx.Token):
updateGraphDictionaryItem(elem, graphDictionary)
return graphDictionary
# Build and print out dictionary
graphDictionary = buildGraphDictionary(doc)
printGraphDictionary(graphDictionary)
upstream3: - ['upstream3/file', 'input', 'filename', 'resources/Images/cloth.png'] - ['upstream3/file1', 'input', 'filename', 'resources/Images/grid.png'] - ['upstream3/out', 'output', 'color3', ''] - ['upstream3/out1', 'output', 'color3', ''] - ['upstream3/upstream_image', 'image', 'color3', ''] - ['upstream3/upstream_image1', 'image', 'color3', ''] upstream2: - ['upstream2/upstream2_in1', 'input', 'color3', ''] - ['upstream2/upstream2_in2', 'input', 'color3', ''] - ['upstream2/upstream2_out1', 'output', 'color3', ''] - ['upstream2/upstream2_out2', 'output', 'color3', ''] - ['upstream2/multiply_by_image', 'multiply', 'color3', ''] - ['upstream2/make_red', 'multiply', 'color3', ''] - ['upstream2/image', 'image', 'color3', 'resources/Images/grid.png'] upstream1: - ['upstream1/upstream1_in1', 'input', 'color3', ''] - ['upstream1/upstream1_in2', 'input', 'color3', ''] - ['upstream1/upstream1_out1', 'output', 'color3', ''] - ['upstream1/upstream1_out2', 'output', 'color3', ''] - ['upstream1/make_yellow', 'multiply', 'color3', ''] - ['upstream1/remove_red', 'multiply', 'color3', ''] Root Document: - ['top_upstream1_out1', 'output', 'color3', ''] - ['top_upstream1_out2', 'output', 'color3', ''] - ['standard_surface', 'standard_surface', 'surfaceshader', ''] - ['standard_surface1', 'standard_surface', 'surfaceshader', ''] - ['surfacematerial', 'surfacematerial', 'material', ''] - ['surfacematerial1', 'surfacematerial', 'material', '']
Next we build the connection information and again print out each connection.
connections = []
buildConnections(doc, graphDictionary, doc, connections)
for connection in connections:
print(connection)
['upstream3/file', '', 'upstream3/upstream_image', 'file', 'interfacename'] ['upstream3/file1', '', 'upstream3/upstream_image1', 'file', 'interfacename'] ['upstream3/upstream_image', '', 'upstream3/out', '', 'nodename'] ['upstream3/upstream_image1', '', 'upstream3/out1', '', 'nodename'] ['upstream3/out', '', 'upstream2/upstream2_in1', '', 'nodename'] ['upstream3/out1', '', 'upstream2/upstream2_in2', '', 'nodename'] ['upstream2/upstream2_in1', '', 'upstream2/multiply_by_image', 'in1', 'interfacename'] ['upstream2/image', '', 'upstream2/multiply_by_image', 'in2', 'nodename'] ['upstream2/upstream2_in2', '', 'upstream2/make_red', 'in1', 'interfacename'] ['upstream2/multiply_by_image', '', 'upstream2/upstream2_out1', '', 'nodename'] ['upstream2/make_red', '', 'upstream2/upstream2_out2', '', 'nodename'] ['upstream2/upstream2_out1', '', 'upstream1/upstream1_in1', '', 'nodename'] ['upstream2/upstream2_out2', '', 'upstream1/upstream1_in2', '', 'nodename'] ['upstream1/upstream1_in1', '', 'upstream1/make_yellow', 'in1', 'interfacename'] ['upstream1/upstream1_in2', '', 'upstream1/remove_red', 'in1', 'interfacename'] ['upstream1/make_yellow', '', 'upstream1/upstream1_out1', '', 'nodename'] ['upstream1/remove_red', '', 'upstream1/upstream1_out2', '', 'nodename'] ['upstream1/upstream1_out1', '', 'top_upstream1_out1', '', 'nodename'] ['upstream1/upstream1_out2', '', 'top_upstream1_out2', '', 'nodename'] ['upstream1/upstream1_out1', '', 'standard_surface', 'base_color', 'nodename'] ['upstream1/upstream1_out2', '', 'standard_surface1', 'base_color', 'nodename'] ['standard_surface', '', 'surfacematerial', 'surfaceshader', 'nodename'] ['standard_surface1', '', 'surfacematerial1', 'surfaceshader', 'nodename']
To allow for this information to be stored out the utility function exporGraphAsJSON()
is shown below.
Content in this form can be used with or without MaterialX runtime as desired.
# Export as JSON
import json
def exportGraphAsJSON(graphDictionary, connections, filename):
data = {}
data['graph'] = graphDictionary
data['connections'] = connections
with open(filename, 'w') as outfile:
# Write json with indentation
json.dump(data, outfile, indent=2)
filename = './data/sample_graph_connections.json'
print('Write graph in JSON format:', filename)
exportGraphAsJSON(graphDictionary, connections, filename)
Write graph in JSON format: ./data/sample_graph_connections.json
Below is the graph dictionary contents written to file:
Note that it is possible to view this output with better formatting by installing an appropriate plug-in
when viewed from a browser.
To demonstrate how to parse the dictionary and connections a sample Mermaid
diagram generator is provided below.
The general logic:
Of note is that the original path information is used as element identifiers with "nice" names being generated as necessary.
Note that there is no dependence on MaterialX for any of the parsing or display logic.
class MxMermaidGraphExporter:
def __init__(self, graphDictionary, connections):
self.graphDictionary = graphDictionary
self.connections = connections
self.mermaid = []
self.orientation = 'LR'
self.emitCategory = False
self.emitType = False
def setOrientation(self, orientation):
self.orientation = orientation
def setEmitCategory(self, emitCategory):
self.emitCategory = emitCategory
def setEmitType(self, emitType):
self.emitType = emitType
def sanitizeString(self, path):
#return path
path = path.replace('/default', '/default1')
path = path.replace('/', '_')
path = path.replace(' ', '_')
return path
def execute(self):
mermaid = []
mermaid.append('graph %s' % self.orientation)
for graphPath in self.graphDictionary:
isSubgraph = graphPath != ''
if isSubgraph:
mermaid.append(' subgraph %s' % graphPath)
for item in self.graphDictionary[graphPath]:
path = item[0]
# Get "base name" of the path
label = path.split('/')[-1]
# Sanitize the path name
path = self.sanitizeString(path)
if self.emitCategory:
label = item[1]
if self.emitType:
label += ":" + item[2]
if item[3]:
label += ":" + item[3]
# Color nodes
if item[1] == 'input' or item[1] == 'output':
if item[1] == 'input':
mermaid.append(' %s([%s])' % (path, label))
mermaid.append(' style %s fill:#09D, color:#111' % path)
else:
mermaid.append(' %s([%s])' % (path, label))
mermaid.append(' style %s fill:#0C0, color:#111' % path)
elif item[1] == 'surfacematerial':
mermaid.append(' %s([%s])' % (path, label))
mermaid.append(' style %s fill:#090, color:#111' % path)
elif item[1] == 'nodedef':
mermaid.append(' %s[[%s]]' % (path, label))
mermaid.append(' style %s fill:#00C, color:#111' % path)
elif item[1] in ['ifequal', 'ifgreatereq', 'switch']:
mermaid.append(' %s{%s}' % (path, label))
mermaid.append(' style %s fill:#C72, color:#111' % path)
elif item[1] == 'token':
mermaid.append(' %s{{%s}}' % (path, label))
mermaid.append(' style %s fill:#222, color:#111' % path)
elif item[1] == 'constant':
mermaid.append(' %s([%s])' % (path, label))
mermaid.append(' style %s fill:#500, color:#111' % path)
else:
mermaid.append(' %s[%s]' % (path, label))
if isSubgraph:
mermaid.append(' end')
self.mermaid = mermaid
for connection in self.connections:
source = ''
# Sanitize path names
connection[0] = self.sanitizeString(connection[0])
connection[2] = self.sanitizeString(connection[2])
# Set source node. If nodes is in a graph then we use <graph>/<node> as source
source = connection[0]
# Set destination node
dest = connection[2]
# Edge can be combo of source output port + destination input port
if len(connection[1]) > 0:
if len(connection[3]) > 0:
edge = connection[1] + '-->' + connection[3]
else:
edge = connection[1]
else:
edge = connection[3]
if connection[4] == 'value':
sourceNode = mx.createValidName(source)
if len(edge) == 0:
connectString = ' %s["%s"] --> %s' % (sourceNode, source, dest)
else:
connectString = ' %s["%s"] --%s--> %s' % (sourceNode, source, edge, dest)
else:
if len(edge) > 0:
connectString = ' %s --"%s"--> %s' % (source, edge, dest)
else:
connectString = ' %s --> %s' % (source, dest)
mermaid.append(connectString)
return mermaid
def write(self, filename):
with open(filename, 'w') as f:
for line in self.export():
f.write('%s\n' % line)
def getGraph(self, wrap=True):
result = ''
if wrap:
result = '```mermaid\n' + '\n'.join(self.mermaid) + '\n```'
else:
result = '\n'.join(self.mermaid)
# Sanitize
result = result.replace('/default', '/default1')
return result
def display(self):
display_markdown(self.getGraph(), raw=True)
# Export mermaid
def export(self, filename):
mermaidGraph = self.getGraph()
with open(filename, 'w') as outFile:
outFile.write(mermaidGraph)
To visualize the Mermaid graph we export the graph to Markdown within a HTML document.
exporter = MxMermaidGraphExporter(graphDictionary, connections)
exporter.setOrientation('TB')
exporter.execute()
exporter.display()
# In order to get the proper mermaid rendering, we need to add the mermaid script, and write to another file.
result = exporter.getGraph()
result = result.replace('```mermaid', '<div class="mermaid">')
result = result.replace('```', '</div>')
result = "<script src='https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.min.js'></script>\n" + result
with open('./data/graphtest_output.html', 'w') as f:
f.write(result)
print('Write graph to HTML file: ./data/graphtest_output.html')
mermaid
graph TB
subgraph upstream3
upstream3_file([file:resources/Images/cloth.png])
style upstream3_file fill:#09D, color:#111
upstream3_file1([file1:resources/Images/grid.png])
style upstream3_file1 fill:#09D, color:#111
upstream3_out([out])
style upstream3_out fill:#0C0, color:#111
upstream3_out1([out1])
style upstream3_out1 fill:#0C0, color:#111
upstream3_upstream_image[upstream_image]
upstream3_upstream_image1[upstream_image1]
end
subgraph upstream2
upstream2_upstream2_in1([upstream2_in1])
style upstream2_upstream2_in1 fill:#09D, color:#111
upstream2_upstream2_in2([upstream2_in2])
style upstream2_upstream2_in2 fill:#09D, color:#111
upstream2_upstream2_out1([upstream2_out1])
style upstream2_upstream2_out1 fill:#0C0, color:#111
upstream2_upstream2_out2([upstream2_out2])
style upstream2_upstream2_out2 fill:#0C0, color:#111
upstream2_multiply_by_image[multiply_by_image]
upstream2_make_red[make_red]
upstream2_image[image:resources/Images/grid.png]
end
subgraph upstream1
upstream1_upstream1_in1([upstream1_in1])
style upstream1_upstream1_in1 fill:#09D, color:#111
upstream1_upstream1_in2([upstream1_in2])
style upstream1_upstream1_in2 fill:#09D, color:#111
upstream1_upstream1_out1([upstream1_out1])
style upstream1_upstream1_out1 fill:#0C0, color:#111
upstream1_upstream1_out2([upstream1_out2])
style upstream1_upstream1_out2 fill:#0C0, color:#111
upstream1_make_yellow[make_yellow]
upstream1_remove_red[remove_red]
end
top_upstream1_out1([top_upstream1_out1])
style top_upstream1_out1 fill:#0C0, color:#111
top_upstream1_out2([top_upstream1_out2])
style top_upstream1_out2 fill:#0C0, color:#111
standard_surface[standard_surface]
standard_surface1[standard_surface1]
surfacematerial([surfacematerial])
style surfacematerial fill:#090, color:#111
surfacematerial1([surfacematerial1])
style surfacematerial1 fill:#090, color:#111
upstream3_file --"file"--> upstream3_upstream_image
upstream3_file1 --"file"--> upstream3_upstream_image1
upstream3_upstream_image --> upstream3_out
upstream3_upstream_image1 --> upstream3_out1
upstream3_out --> upstream2_upstream2_in1
upstream3_out1 --> upstream2_upstream2_in2
upstream2_upstream2_in1 --"in1"--> upstream2_multiply_by_image
upstream2_image --"in2"--> upstream2_multiply_by_image
upstream2_upstream2_in2 --"in1"--> upstream2_make_red
upstream2_multiply_by_image --> upstream2_upstream2_out1
upstream2_make_red --> upstream2_upstream2_out2
upstream2_upstream2_out1 --> upstream1_upstream1_in1
upstream2_upstream2_out2 --> upstream1_upstream1_in2
upstream1_upstream1_in1 --"in1"--> upstream1_make_yellow
upstream1_upstream1_in2 --"in1"--> upstream1_remove_red
upstream1_make_yellow --> upstream1_upstream1_out1
upstream1_remove_red --> upstream1_upstream1_out2
upstream1_upstream1_out1 --> top_upstream1_out1
upstream1_upstream1_out2 --> top_upstream1_out2
upstream1_upstream1_out1 --"base_color"--> standard_surface
upstream1_upstream1_out2 --"base_color"--> standard_surface1
standard_surface --"surfaceshader"--> surfacematerial
standard_surface1 --"surfaceshader"--> surfacematerial1
Write graph to HTML file: ./data/graphtest_output.html
As part of the utility some display options have been included. These include:
exporter = MxMermaidGraphExporter(graphDictionary, connections)
exporter.setOrientation('BT')
exporter.setEmitCategory(True)
exporter.setEmitType(True)
exporter.execute()
exporter.display()
result = exporter.getGraph()
result = result.replace('```mermaid', '<div class="mermaid">')
result = result.replace('```', '</div>')
result = "<script src='https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.min.js'></script>\n" + result
with open('./data/graphtest_output2.html', 'w') as f:
f.write(result)
print('Write graph to HTML file: ./data/graphtest_output2.html')
mermaid
graph BT
subgraph upstream3
upstream3_file([input:filename:resources/Images/cloth.png])
style upstream3_file fill:#09D, color:#111
upstream3_file1([input:filename:resources/Images/grid.png])
style upstream3_file1 fill:#09D, color:#111
upstream3_out([output:color3])
style upstream3_out fill:#0C0, color:#111
upstream3_out1([output:color3])
style upstream3_out1 fill:#0C0, color:#111
upstream3_upstream_image[image:color3]
upstream3_upstream_image1[image:color3]
end
subgraph upstream2
upstream2_upstream2_in1([input:color3])
style upstream2_upstream2_in1 fill:#09D, color:#111
upstream2_upstream2_in2([input:color3])
style upstream2_upstream2_in2 fill:#09D, color:#111
upstream2_upstream2_out1([output:color3])
style upstream2_upstream2_out1 fill:#0C0, color:#111
upstream2_upstream2_out2([output:color3])
style upstream2_upstream2_out2 fill:#0C0, color:#111
upstream2_multiply_by_image[multiply:color3]
upstream2_make_red[multiply:color3]
upstream2_image[image:color3:resources/Images/grid.png]
end
subgraph upstream1
upstream1_upstream1_in1([input:color3])
style upstream1_upstream1_in1 fill:#09D, color:#111
upstream1_upstream1_in2([input:color3])
style upstream1_upstream1_in2 fill:#09D, color:#111
upstream1_upstream1_out1([output:color3])
style upstream1_upstream1_out1 fill:#0C0, color:#111
upstream1_upstream1_out2([output:color3])
style upstream1_upstream1_out2 fill:#0C0, color:#111
upstream1_make_yellow[multiply:color3]
upstream1_remove_red[multiply:color3]
end
top_upstream1_out1([output:color3])
style top_upstream1_out1 fill:#0C0, color:#111
top_upstream1_out2([output:color3])
style top_upstream1_out2 fill:#0C0, color:#111
standard_surface[standard_surface:surfaceshader]
standard_surface1[standard_surface:surfaceshader]
surfacematerial([surfacematerial:material])
style surfacematerial fill:#090, color:#111
surfacematerial1([surfacematerial:material])
style surfacematerial1 fill:#090, color:#111
upstream3_file --"file"--> upstream3_upstream_image
upstream3_file1 --"file"--> upstream3_upstream_image1
upstream3_upstream_image --> upstream3_out
upstream3_upstream_image1 --> upstream3_out1
upstream3_out --> upstream2_upstream2_in1
upstream3_out1 --> upstream2_upstream2_in2
upstream2_upstream2_in1 --"in1"--> upstream2_multiply_by_image
upstream2_image --"in2"--> upstream2_multiply_by_image
upstream2_upstream2_in2 --"in1"--> upstream2_make_red
upstream2_multiply_by_image --> upstream2_upstream2_out1
upstream2_make_red --> upstream2_upstream2_out2
upstream2_upstream2_out1 --> upstream1_upstream1_in1
upstream2_upstream2_out2 --> upstream1_upstream1_in2
upstream1_upstream1_in1 --"in1"--> upstream1_make_yellow
upstream1_upstream1_in2 --"in1"--> upstream1_remove_red
upstream1_make_yellow --> upstream1_upstream1_out1
upstream1_remove_red --> upstream1_upstream1_out2
upstream1_upstream1_out1 --> top_upstream1_out1
upstream1_upstream1_out2 --> top_upstream1_out2
upstream1_upstream1_out1 --"base_color"--> standard_surface
upstream1_upstream1_out2 --"base_color"--> standard_surface1
standard_surface --"surfaceshader"--> surfacematerial
standard_surface1 --"surfaceshader"--> surfacematerial1
Write graph to HTML file: ./data/graphtest_output2.html