Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions docs/source/connections_viewer.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
Connections Viewer
------------------

From a given :ref:`glossary:prim`, recursively traverse its :ref:`Connections <glossary:connection>` through :usdcpp:`UsdShadeConnectableAPI`.
From a given :ref:`glossary:prim`, recursively traverse its upstream :ref:`Connections <glossary:connection>`.

Every node represents a :ref:`glossary:prim` with its name at the top, and each of its :usdcpp:`Inputs <UsdShadeInput>` (dark blue if :usdcpp:`connected <UsdShadeInput::GetConnectedSources>`, otherwise light blue) and :usdcpp:`Outputs <UsdShadeOutput>` (red) below.
When the prim has :usdcpp:`UsdShadeConnectableAPI` inputs or outputs, connections are followed through the shading API. Otherwise, connections are followed through :usdcpp:`UsdAttribute` connection paths. This includes **OpenExec invertible rigs** (``IrJointScope``, ``IrFkController``, ``IrSwitchController``, and related prims) where animation and controller wiring is authored via attribute ``.connect`` relationships.

Every node represents a :ref:`glossary:prim` with its name at the top (and schema type when available). Ports below represent connected attributes or shade inputs/outputs.

Port colors
~~~~~~~~~~~

**UsdShade prims**

- :usdcpp:`Inputs <UsdShadeInput>` — dark blue if :usdcpp:`connected <UsdShadeInput::GetConnectedSources>`, otherwise light blue
- :usdcpp:`Outputs <UsdShadeOutput>` — red (light coral)

**OpenExec / attribute-connected prims**

When schema metadata is available, port colors reflect ``irRole``:

- **InputAttribute** — dark blue if connected, otherwise light blue (controller inputs, wired avars)
- **OutputAttribute** — red (light coral); computed outputs such as ``out:space`` and ``posed:space``
- **SwitchAttribute** — gold (switch tokens, rotation order)
- **PassthroughAttribute** — light gray (passthrough spaces such as ``in:defaultSpace``)

The viewer shows the **authored connection graph** on the USD stage. It does not display the compiled OpenExec evaluation network (that requires C++ ``ExecUsdSystem`` diagnostics today).

.. tab:: USDView

Expand Down
2 changes: 1 addition & 1 deletion docs/source/end_to_end.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Creating Assets
Repository Path
~~~~~~~~~~~~~~~

Creating assets requires a repository path to be set. This change lasts during the duration of the current application (or python process), so it is needed only once.
Creating assets requires a repository path to be set. Setting it lasts for the duration of the current application (or python process), so it is needed only once.

**API:** :py:data:`grill.cook.Repository`

Expand Down
10 changes: 5 additions & 5 deletions grill/cook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ def spawn_unit(parent: Usd.Prim, child: Usd.Prim, path: Sdf.Path = Sdf.Path.empt

1. Turning parent into an :ref:`glossary:assembly`.
2. Ensuring intermediate prims between parent and child are also :ref:`glossary:model`.
3. Setting explicit :ref:`glossary:instanceable`. on spawned children that are components.
3. Setting explicit :ref:`glossary:instanceable` metadata on spawned children that are components.

.. seealso:: :func:`spawn_many` and :func:`create_unit`
"""
Expand All @@ -375,11 +375,11 @@ def spawn_many(parent: Usd.Prim, child: Usd.Prim, paths: list[Sdf.Path], labels:

* Both parent and child must be existing units in the catalogue.
* ``paths`` can be relative or absolute. If absolute, they must include ``parent``'s `path <https://graphics.pixar.com/usd/docs/USD-Glossary.html#USDGlossary-Path>`_ as a prefix.
* A valid `Model Hierarchy <https://graphics.pixar.com/usd/docs/USD-Glossary.html#USDGlossary-ModelHierarchy>`_ is preserved by:
* A valid :ref:`glossary:model hierarchy` is preserved by:

1. Turning parent into an `assembly <https://graphics.pixar.com/usd/docs/USD-Glossary.html#USDGlossary-Assembly>`_ if ``child`` is a Model.
2. Ensuring intermediate prims between ``parent`` and spawned children are also `models <https://graphics.pixar.com/usd/docs/USD-Glossary.html#USDGlossary-Model>`_.
3. Setting explicit `instanceable <https://graphics.pixar.com/usd/docs/USD-Glossary.html#USDGlossary-Instanceable>`_. on spawned children that are components.
1. Turning parent into an :ref:`glossary:assembly` if ``child`` is a :ref:`glossary:model`.
2. Ensuring intermediate prims between ``parent`` and spawned children are also :ref:`glossary:model`.
3. Setting explicit :ref:`glossary:instanceable` metadata on spawned children that are components.

Spawned prims and ancestors are `defined <https://openusd.org/release/glossary.html#def>`_.

Expand Down
206 changes: 187 additions & 19 deletions grill/views/description.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,18 +222,102 @@ def _compute_composition(_prim):
)


def _graph_from_connections(prim: Usd.Prim) -> nx.MultiDiGraph:
connections_api = UsdShade.ConnectableAPI(prim)
_CONNECTION_GRAPH_OUTLINE = "#4682B4" # steelblue
_CONNECTION_GRAPH_BACKGROUND = "#F0FFFF" # azure
_CONNECTION_GRAPH_OUTPUT = "#F08080" # lightcoral
_CONNECTION_GRAPH_SWITCH = "#DAA520" # goldenrod
_CONNECTION_GRAPH_PASSTHROUGH = "#D3D3D3" # lightgray
_CONNECTION_TABLE_ROW = (
'<tr><td port="{port}" border="0" bgcolor="{color}" style="ROUNDED">{text}</td></tr>'
)


def _graphviz_port_id(port_name: str) -> str:
"""Graphviz port identifiers cannot contain ':' (e.g. posed:space)."""
return port_name.replace(":", "__")


def _init_connection_graph() -> nx.MultiDiGraph:
graph = nx.MultiDiGraph()
outline_color = "#4682B4" # 'steelblue'
background_color = "#F0FFFF" # 'azure'
graph.graph['graph'] = {'rankdir': 'LR'}

graph.graph['node'] = {'shape': 'none', 'color': outline_color, 'fillcolor': background_color} # color and fillcolor used for HTML view
graph.graph['node'] = {
'shape': 'none',
'color': _CONNECTION_GRAPH_OUTLINE,
'fillcolor': _CONNECTION_GRAPH_BACKGROUND,
}
graph.graph['edge'] = {"color": 'crimson'}
return graph

all_nodes = dict() # {node_id: {graphviz_attr: value}}
edges = list() # [(source_node_id, target_node_id, {source_port_name, target_port_name, graphviz_attrs})]

def _make_port_table_node(
prim_name: str,
port_entries: list[tuple[str, str]],
*,
prim_type: str = "",
) -> tuple[str, list]:
"""Build a Graphviz HTML table label and ports list for connection graph nodes."""
outline_color = _CONNECTION_GRAPH_OUTLINE
background_color = _CONNECTION_GRAPH_BACKGROUND
header = prim_name if not prim_type else f"{prim_name} ({prim_type})"
label = (
f'<<table border="1" cellspacing="2" style="ROUNDED" '
f'bgcolor="{background_color}" color="{outline_color}">'
)
label += _CONNECTION_TABLE_ROW.format(
port="",
color="white",
text=f'<font color="{outline_color}"><b>{header}</b></font>',
)
ports = [""]
for port_name, color in port_entries:
port_id = _graphviz_port_id(port_name)
label += _CONNECTION_TABLE_ROW.format(
port=port_id,
color=color,
text=f'<font color="#242828">{port_name}</font>',
)
ports.append(port_id)
label += '</table>>'
return label, ports


def _port_color_for_attribute(
prim: Usd.Prim,
attr_name: str,
*,
has_connections: bool,
is_edge_tail: bool,
is_edge_head: bool,
) -> str:
prop = prim.GetPrimDefinition().GetPropertyDefinition(attr_name)
role = prop.GetMetadata("irRole") if prop else None
if role == "OutputAttribute":
return _CONNECTION_GRAPH_OUTPUT
if role == "SwitchAttribute":
return _CONNECTION_GRAPH_SWITCH
if role == "PassthroughAttribute":
return _CONNECTION_GRAPH_PASSTHROUGH
if role == "InputAttribute":
return (
_CONNECTION_GRAPH_OUTLINE
if has_connections or is_edge_head
else _CONNECTION_GRAPH_BACKGROUND
)
if is_edge_tail and not is_edge_head:
return _CONNECTION_GRAPH_OUTPUT
if has_connections or is_edge_head:
return _CONNECTION_GRAPH_OUTLINE
return _CONNECTION_GRAPH_BACKGROUND


def _graph_from_shade_connections(prim: Usd.Prim) -> nx.MultiDiGraph:
connections_api = UsdShade.ConnectableAPI(prim)
graph = _init_connection_graph()
outline_color = _CONNECTION_GRAPH_OUTLINE
background_color = _CONNECTION_GRAPH_BACKGROUND

all_nodes = dict()
edges = list()

@cache
def _get_node_id(api):
Expand All @@ -245,31 +329,32 @@ def _add_edges(src_node, src_name, tgt_node, tgt_name):
edges.append((src_node, tgt_node, {"tailport": src_name, "headport": tgt_name, "tooltip": tooltip}))

port_colors = {
UsdShade.Input: outline_color, # blue
UsdShade.Output: "#F08080" # "lightcoral", # pink
UsdShade.Input: outline_color,
UsdShade.Output: _CONNECTION_GRAPH_OUTPUT,
}
table_row = '<tr><td port="{port}" border="0" bgcolor="{color}" style="ROUNDED">{text}</td></tr>'

traversed_prims = set()

def traverse(api: UsdShade.ConnectableAPI):
current_prim = api.GetPrim()
if current_prim in traversed_prims:
return
traversed_prims.add(current_prim)
node_id = _get_node_id(current_prim)
label = f'<<table border="1" cellspacing="2" style="ROUNDED" bgcolor="{background_color}" color="{outline_color}">'
label += table_row.format(port="", color="white", text=f'<font color="{outline_color}"><b>{api.GetPrim().GetName()}</b></font>')
ports = [""] # port names for this node. Empty string is used to refer to the node itself (no port).
port_entries = list()
for port in chain(api.GetInputs(), api.GetOutputs()):
port_name = port.GetBaseName()
sources, __ = port.GetConnectedSources() # (valid, invalid): we care only about valid sources (index 0)
color = port_colors[type(port)] if isinstance(port, UsdShade.Output) or sources else background_color
label += table_row.format(port=port_name, color=color, text=f'<font color="#242828">{port_name}</font>')
sources, __ = port.GetConnectedSources()
color = (
port_colors[type(port)]
if isinstance(port, UsdShade.Output) or sources
else background_color
)
port_entries.append((port_name, color))
for source in sources:
_add_edges(_get_node_id(source.source.GetPrim()), source.sourceName, node_id, port_name)
traverse(source.source)
ports.append(port_name)
label += '</table>>'
label, ports = _make_port_table_node(api.GetPrim().GetName(), port_entries)
all_nodes[node_id] = dict(label=label, ports=ports)

traverse(connections_api)
Expand All @@ -279,6 +364,89 @@ def traverse(api: UsdShade.ConnectableAPI):
return graph


def _graph_from_attribute_connections(prim: Usd.Prim) -> nx.MultiDiGraph:
graph = _init_connection_graph()
stage = prim.GetStage()
traversed_prims = set()
edges = list()
edge_tails = set()
edge_heads = set()

def traverse(current_prim: Usd.Prim):
if current_prim in traversed_prims:
return
traversed_prims.add(current_prim)
node_id = str(current_prim.GetPath())
for attr in current_prim.GetAttributes():
for src_path in attr.GetConnections():
src_attr = stage.GetAttributeAtPath(src_path)
if not src_attr:
continue
src_prim = src_attr.GetPrim()
src_node = str(src_prim.GetPath())
src_name = src_attr.GetName()
tgt_name = attr.GetName()
tooltip = f"{src_node}.{src_name} -> {node_id}.{tgt_name}"
edge_tails.add((src_node, src_name))
edge_heads.add((node_id, tgt_name))
edges.append((
src_node,
node_id,
{
"tailport": _graphviz_port_id(src_name),
"headport": _graphviz_port_id(tgt_name),
"tooltip": tooltip,
},
))
traverse(src_prim)

traverse(prim)

all_nodes = dict()
for current_prim in traversed_prims:
node_id = str(current_prim.GetPath())
port_names = set()
for attr in current_prim.GetAttributes():
name = attr.GetName()
if (
attr.GetConnections()
or (node_id, name) in edge_tails
or (node_id, name) in edge_heads
):
port_names.add(name)

port_entries = [
(
port_name,
_port_color_for_attribute(
current_prim,
port_name,
has_connections=bool(current_prim.GetAttribute(port_name).GetConnections()),
is_edge_tail=(node_id, port_name) in edge_tails,
is_edge_head=(node_id, port_name) in edge_heads,
),
)
for port_name in sorted(port_names)
]
label, ports = _make_port_table_node(
current_prim.GetName(),
port_entries,
prim_type=current_prim.GetTypeName(),
)
all_nodes[node_id] = dict(label=label, ports=ports)

graph.add_nodes_from(all_nodes.items())
graph.add_edges_from(edges)
return graph


def _graph_from_connections(prim: Usd.Prim) -> nx.MultiDiGraph:
connections_api = UsdShade.ConnectableAPI(prim)
if connections_api.GetInputs() or connections_api.GetOutputs():
return _graph_from_shade_connections(prim)
return _graph_from_attribute_connections(prim)


def _launch_content_browser(layers, parent, context, paths=tuple()):
dialog = _start_content_browser(layers, parent, context, paths)
dialog.show()
Expand Down
Loading
Loading