Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
install-arguments: ".[create] PySide2 usd-core==21.11 PyOpenGL" # 21.11 enables AR-2.0 by default
- python-version: "3.10"
install-arguments: ". pygraphviz PySide2 usd-core==23.2 PyOpenGL"
- python-version: "3.13"
- python-version: "3.14"
install-arguments: ".[full,create]"
steps:
- uses: actions/checkout@v5
Expand All @@ -31,7 +31,7 @@ jobs:
uses: ts-graphviz/setup-graphviz@v2
- name: Install Required Libraries
# Needed for PySide6 CI only: https://stackoverflow.com/questions/75907862/pyside6-wsl2-importerror-libegl-so-1
if: "matrix.python-version == '3.13'"
if: "matrix.python-version == '3.14'"
run: |
sudo apt-get install -y libegl1
- name: Install
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2 # required for PY39+
build:
os: ubuntu-20.04
tools:
python: "3.10"
python: "3.13"
apt_packages:
- "graphviz"

Expand Down
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
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ classifiers =
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Programming Language :: Python :: 3.14

[options]
install_requires =
Expand All @@ -37,7 +38,7 @@ include = grill.*
# python "A:\write\code\git\OpenUSD\build_scripts\build_usd.py" -v "A:\write\builds\py314usdbuild"
#
# --- dev env ---:
# conda create -n py314usd python=3.13
# conda create -n py314usd python=3.14
# conda activate py314usd
# runtime dependencies:
# conda install conda-forge::graphviz=11 # 13.1.2 brings up libffi which messes up _ctypes from python-3.13, and 12 fails to load gvplugin_pango.dll
Expand Down
Loading
Loading