From 4adbfca4862a2f1066845e37f47b60d9b8109bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 18 Aug 2024 23:08:10 +1000 Subject: [PATCH 01/67] readopt pydot --- grill/views/_graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 3c0bcbc7..baef2a17 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -592,7 +592,7 @@ def _load_graph(self, graph): return try: # exit early if pygraphviz is not installed, needed for positions - positions = drawing.nx_agraph.graphviz_layout(graph, prog='dot') + positions = drawing.nx_pydot.graphviz_layout(graph, prog='dot') except ImportError as exc: message = str(exc) print(message) @@ -805,7 +805,7 @@ def _subgraph_dot_path(self, node_indices: tuple): fd, fp = tempfile.mkstemp() try: - nx.nx_agraph.write_dot(subgraph, fp) + nx.nx_pydot.write_dot(subgraph, fp) except ImportError as exc: error = f"{exc}\n\n{_DOT_ENVIRONMENT_ERROR}" else: From 172692f5f96a1c3bc86fdca510b7703e3431f478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 21:09:23 +1000 Subject: [PATCH 02/67] update dependencies to pydot instead of pygraphviz --- setup.cfg | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index b6ea1556..c4b1475e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,8 +15,7 @@ classifiers = Programming Language :: Python :: 3.12 [options] -install_requires = grill-names>=2.6.0; networkx; numpy; printree -# pygraphviz has trouble on some environments, so I've removed it from the current requires and moved to "full" until https://github.com/pygraphviz/pygraphviz/issues/167 is resolved. In the meantime, installation will be clarified in the docs. +install_requires = grill-names>=2.6.0; networkx @ git+https://github.com/chrizzFTD/networkx.git@pydot_remove_colon_check; pydot>=3.0.1; numpy; printree include_package_data = True packages = find_namespace: @@ -36,7 +35,7 @@ include = grill.* # conda create -n py312usd2408 python=3.12 # conda activate py312usd2408 # runtime dependencies: -# conda install --channel conda-forge pygraphviz +# conda install conda-forge::graphviz # python -m pip install grill-names>=2.6.0 networkx numpy printree PyOpenGL pyside6 # docs dependencies: # python -m pip install sphinx myst-parser sphinx-toggleprompt sphinx-copybutton sphinx-togglebutton sphinx-hoverxref sphinx_autodoc_typehints sphinx-inline-tabs shibuya From add46d33c53f172a44dff751933602044beff67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 21:13:56 +1000 Subject: [PATCH 03/67] networkx requires python-3.10+, drop python-3.9 --- .github/workflows/python-package.yml | 6 +++--- setup.cfg | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 28789fd7..fd650247 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -14,10 +14,10 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.12"] + python-version: ["3.10", "3.12"] include: - - python-version: "3.9" - install-arguments: ". PySide2 usd-core==22.5 PyOpenGL pygraphviz" + - python-version: "3.10" + install-arguments: ". PySide2 usd-core==22.5 PyOpenGL" - python-version: "3.12" install-arguments: ".[full]" steps: diff --git a/setup.cfg b/setup.cfg index c4b1475e..96046b0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,6 @@ author_email = chris.gfz@gmail.com author = Christian López Barrón url = https://github.com/thegrill/grill classifiers = - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 From 44b79210b042be3b3cd55efb4ccb1d1ff74c4fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 21:17:53 +1000 Subject: [PATCH 04/67] usd-core support for python-3.10 starts at 23.2 --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fd650247..a8a21952 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,7 +17,7 @@ jobs: python-version: ["3.10", "3.12"] include: - python-version: "3.10" - install-arguments: ". PySide2 usd-core==22.5 PyOpenGL" + install-arguments: ". PySide2 usd-core==23.2 PyOpenGL" - python-version: "3.12" install-arguments: ".[full]" steps: From 63d2606ed124d53608a4e51f5b1fd7f36f5b5f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 21:23:48 +1000 Subject: [PATCH 05/67] update RTD python --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 220123c9..4765e4a2 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -2,7 +2,7 @@ version: 2 # required for PY39+ build: os: ubuntu-20.04 tools: - python: "3.9" + python: "3.10" apt_packages: - "graphviz" From 984a5cdd8a5519fd5f7a2477520e43f81b577698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 21:41:31 +1000 Subject: [PATCH 06/67] remove pygraphviz install steps --- docs/source/install.rst | 87 ++++------------------------------------- 1 file changed, 7 insertions(+), 80 deletions(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index af111086..9868790b 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -46,7 +46,7 @@ Extra Dependencies The following optional dependencies should be installed separately. -- `graphviz `_ and `pygraphviz`_ for graph widgets. See conda example below for instructions. +- `graphviz`_ for graph widgets. See conda example below for instructions. - `usdview `_ (hopefully will be available soon via `pypi `_). In the meantime, it can be built from USD source (`conda recipe `_). @@ -69,11 +69,11 @@ walk-through on how to start using ``The Grill`` tools with a fresh 2. Launch `Anaconda Prompt `_ (it came as part of the `miniconda`_ installation). -3. Create a new ``conda`` environment with ``python=3.9``, for example: +3. Create a new ``conda`` environment with ``python=3.10``, for example: .. code:: PowerShell - (base) C:\>conda create -n grilldemo01 python=3.9 + (base) C:\>conda create -n grilldemo01 python=3.10 4. Activate that environment: @@ -88,84 +88,11 @@ walk-through on how to start using ``The Grill`` tools with a fresh (grilldemo01) C:\>python -m pip install grill[full] -6. If missing, (optionally) install `pygraphviz`_ via ``conda``: +6. If missing, (optionally) install `graphviz`_ via ``conda``: - .. warning:: - - At the moment, installing `pygraphviz`_ can be tricky. Hopefully a simpler pip+wheel based solution comes with `pygraphviz#167 `_. - - Versions older than ``pip-23.3.2`` may have trouble installing `pygraphviz`_ in Windows for DCCs like ``Maya`` and ``Houdini``. - If you come through this trouble, visit `pygraphviz#468 `_ and try to install with this exact particular version of ``pip``. - The below tests ran successfully with ``Maya-2024`` and ``Houdini-20.0`` on ``Windows-10`` and ``pip-23.3.2``. - - The current ``pip`` version can be extracted like so: - - .. tab:: Standalone Python - - .. code:: PowerShell - - python -m pip -V - - .. tab:: Houdini - - .. code:: PowerShell - - hython -m pip -V - - .. tab:: Maya - - .. code:: PowerShell - - mayapy -m pip -V - - To update to ``23.3.2``, update the interpreter command to run: - - .. tab:: Standalone Python - - .. code:: PowerShell - - python -m pip install -U pip==23.3.2 - - .. tab:: Houdini - - .. code:: PowerShell - - hython -m pip install -U pip==23.3.2 - - .. tab:: Maya - - .. code:: PowerShell - - mayapy -m pip install -U pip==23.3.2 - - .. tab:: Standalone Python - - Replace ``--global-option`` to the correct ``Include`` and ``Lib`` paths on the system (where ``graphviz\cgraph.h`` and ``cgraph.lib`` paths exist, respectively): - - .. code:: PowerShell - - (grilldemo01) C:\>conda install --channel conda-forge pygraphviz - (grilldemo01) C:\>python -m pip install --global-option=build_ext --global-option="-IC:\Users\Christian\.conda\envs\glowdeps\Library\include" --global-option="-LC:\Users\Christian\.conda\envs\glowdeps\Library\lib" pygraphviz - - .. tab:: Houdini - - Replace ``--global-option`` to the correct ``Include`` and ``Lib`` paths on the system (where ``graphviz\cgraph.h`` and ``cgraph.lib`` paths exist, respectively): - - .. code:: PowerShell - - (grilldemo01) C:\>conda install --channel conda-forge pygraphviz - (grilldemo01) C:\Program Files\Side Effects Software\Houdini 19.5.534\bin>hython -m pip install -vvv --use-pep517 --config-settings="--global-option=build_ext" --config-settings="--global-option=-IC:\Users\Christian\.conda\envs\pygraphviz310\Library\include" --config-settings="--global-option=-LC:\Users\Christian\.conda\envs\pygraphviz310\Library\lib" pygraphviz - - .. tab:: Maya - - Replace ``--global-option`` to the correct ``Include`` and ``Lib`` paths on the system (where ``graphviz\cgraph.h`` and ``cgraph.lib`` paths exist, respectively) **and** the Maya Python ``include`` and ``lib`` paths: - - .. code:: PowerShell - - (grilldemo01) C:\>conda install --channel conda-forge pygraphviz - (grilldemo01) C:\Program Files\Autodesk\Maya2023\bin>mayapy -m pip install -U pip==23.3.2 - (grilldemo01) C:\Program Files\Autodesk\Maya2023\bin>mayapy -m pip install -vvv --use-pep517 --config-settings="--global-option=build_ext" --config-settings="--global-option=-IC:\Users\Christian\.conda\envs\pygraphviz310\Library\include;C:\Program Files\Autodesk\Maya2024\include\Python39\Python" --config-settings="--global-option=-LC:\Users\Christian\.conda\envs\pygraphviz310\Library\lib;C:\Program Files\Autodesk\Maya2024\lib" pygraphviz + .. code:: PowerShell + (grilldemo01) C:\>conda install conda-forge::graphviz 7. You should be able to see the ``👨‍🍳 Grill`` menu in **USDView**, **Maya** and **Houdini***. @@ -191,7 +118,7 @@ walk-through on how to start using ``The Grill`` tools with a fresh The manual execution of this step might be removed in the future. -.. _pygraphviz: https://pygraphviz.github.io/documentation/stable/install.html +.. _graphviz: http://graphviz.org .. _miniconda: https://docs.conda.io/en/latest/miniconda.html .. _Anaconda: https://docs.anaconda.com/anaconda/user-guide/getting-started/ .. _conda: https://docs.conda.io/projects/conda/en/latest/index.html From 9506d903c18f2d61402d8f8141839f7ac7e841f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 21:56:01 +1000 Subject: [PATCH 07/67] grill.names intersphinx mapping lost popup for hoverxref, test setting grill emoji on inventory --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index dc90d189..43e86d80 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -54,7 +54,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'naming': ('https://naming.readthedocs.io/en/latest/', None), - 'grill.names': ('https://grill-names.readthedocs.io/en/latest/', None) + '👨‍🍳': ('https://grill-names.readthedocs.io/en/latest/', None) } hoverxref_auto_ref = True hoverxref_default_type = 'tooltip' From 5feac89830e88f54af9dac080d31ed717b3306d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 22:04:12 +1000 Subject: [PATCH 08/67] link updated grill-names for intersphinx inventory --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 43e86d80..928a3e0d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -54,7 +54,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'naming': ('https://naming.readthedocs.io/en/latest/', None), - '👨‍🍳': ('https://grill-names.readthedocs.io/en/latest/', None) + 'grill.names': ('https://grill-names.readthedocs.io/en/feature-fix_intersphinx_inventory/', None) } hoverxref_auto_ref = True hoverxref_default_type = 'tooltip' From f7e37b2c6a54d61dc6c76f0533e57ae864a57bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 22:23:30 +1000 Subject: [PATCH 09/67] Revert "link updated grill-names for intersphinx inventory" This reverts commit 6d9ecc1d6753e352e8d68e531ae55683b0f1b186. --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 928a3e0d..43e86d80 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -54,7 +54,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'naming': ('https://naming.readthedocs.io/en/latest/', None), - 'grill.names': ('https://grill-names.readthedocs.io/en/feature-fix_intersphinx_inventory/', None) + '👨‍🍳': ('https://grill-names.readthedocs.io/en/latest/', None) } hoverxref_auto_ref = True hoverxref_default_type = 'tooltip' From 1a8a958c66880e3662f2b56edee9800eb068e6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 22:28:30 +1000 Subject: [PATCH 10/67] restore grill.names intersphinx inventory --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 43e86d80..dc90d189 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -54,7 +54,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), 'naming': ('https://naming.readthedocs.io/en/latest/', None), - '👨‍🍳': ('https://grill-names.readthedocs.io/en/latest/', None) + 'grill.names': ('https://grill-names.readthedocs.io/en/latest/', None) } hoverxref_auto_ref = True hoverxref_default_type = 'tooltip' From c922a7a386a196db49a4eca80675d87b7a62c87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 19 Aug 2024 22:28:41 +1000 Subject: [PATCH 11/67] track down change from intersphinx that broke hoverxref --- grill/cook/__init__.py | 2 +- setup.cfg | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index b6a3e727..3b4d2cab 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -383,7 +383,7 @@ def spawn_unit(parent, child, path=Sdf.Path.emptyPath, label=""): return spawn_many(parent, child, [path or child.GetName()], [label])[0] -def spawn_many(parent: Usd.Prim, child: Usd.Prim, paths: list[Sdf.Path], labels: list[str] = []): +def spawn_many(parent: Usd.Prim, child: Usd.Prim, paths: list[Sdf.Path], labels: list[str] = ()): """Spawn many instances of a prim unit as descendants of another. * Both parent and child must be existing units in the catalogue. diff --git a/setup.cfg b/setup.cfg index 96046b0a..13f0ed83 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ classifiers = Programming Language :: Python :: 3.12 [options] -install_requires = grill-names>=2.6.0; networkx @ git+https://github.com/chrizzFTD/networkx.git@pydot_remove_colon_check; pydot>=3.0.1; numpy; printree +install_requires = grill-names>=2.6.0; networkx @ git+https://github.com/networkx/networkx.git; pydot>=3.0.1; numpy; printree include_package_data = True packages = find_namespace: @@ -40,6 +40,8 @@ include = grill.* # python -m pip install sphinx myst-parser sphinx-toggleprompt sphinx-copybutton sphinx-togglebutton sphinx-hoverxref sphinx_autodoc_typehints sphinx-inline-tabs shibuya # For EDGEDB (coming up) # python -m pip install edgedb +# To install packages in editable mode, cd to desired package repo, then: +# python -m pip install -e . docs = sphinx; myst-parser; sphinx-toggleprompt; sphinx-copybutton; sphinx-togglebutton; sphinx-hoverxref>=1.4.1; sphinx_autodoc_typehints; sphinx-inline-tabs; shibuya; usd-core full = PySide6; usd-core; PyOpenGL; pygraphviz From 42a1678dfde69e24343681acea22ad9a620a165c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 14 Sep 2024 16:31:16 +1000 Subject: [PATCH 12/67] remove pygraphviz --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 13f0ed83..ec139102 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,4 +44,4 @@ include = grill.* # python -m pip install -e . docs = sphinx; myst-parser; sphinx-toggleprompt; sphinx-copybutton; sphinx-togglebutton; sphinx-hoverxref>=1.4.1; sphinx_autodoc_typehints; sphinx-inline-tabs; shibuya; usd-core -full = PySide6; usd-core; PyOpenGL; pygraphviz +full = PySide6; usd-core; PyOpenGL From ab8db52b2774d0ef0cedb06b8e831ff5fdb095ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 26 Oct 2024 11:01:55 +1100 Subject: [PATCH 13/67] dependencies and usd 24.11 build conda recipe --- setup.cfg | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index ec139102..9fe10ad6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ classifiers = Programming Language :: Python :: 3.12 [options] -install_requires = grill-names>=2.6.0; networkx @ git+https://github.com/networkx/networkx.git; pydot>=3.0.1; numpy; printree +install_requires = grill-names>=2.6.0; networkx>=3.4; pydot>=3.0.1; numpy; printree include_package_data = True packages = find_namespace: @@ -23,21 +23,21 @@ include = grill.* [options.extras_require] # USD build: -# conda create -n py312usd2408build python=3.12 -# conda activate py312usd2408build +# conda create -n py313usd2411build python=3.13 +# conda activate py313usd2411build # conda install -c conda-forge cmake=3.27 # python -m pip install PySide6 PyOpenGL jinja2 # conda install -c rdonnelly vs2019_win-64 -# python "A:\write\code\git\OpenUSD\build_scripts\build_usd.py" -v "A:\write\builds\py312usd2408build" +# python "A:\write\code\git\OpenUSD\build_scripts\build_usd.py" -v "A:\write\builds\py313usd2411build" # # --- dev env ---: -# conda create -n py312usd2408 python=3.12 -# conda activate py312usd2408 +# conda create -n py313usd2411 python=3.13 +# conda activate py313usd2411 # runtime dependencies: # conda install conda-forge::graphviz -# python -m pip install grill-names>=2.6.0 networkx numpy printree PyOpenGL pyside6 +# python -m pip install grill-names>=2.6.0 networkx>=3.4 pydot>=3.0.1 numpy printree PyOpenGL pyside6 # docs dependencies: -# python -m pip install sphinx myst-parser sphinx-toggleprompt sphinx-copybutton sphinx-togglebutton sphinx-hoverxref sphinx_autodoc_typehints sphinx-inline-tabs shibuya +# python -m pip install sphinx myst-parser sphinx-toggleprompt sphinx-copybutton sphinx-togglebutton sphinx-hoverxref>=1.4.1 sphinx_autodoc_typehints sphinx-inline-tabs shibuya # For EDGEDB (coming up) # python -m pip install edgedb # To install packages in editable mode, cd to desired package repo, then: From 7b115729676a7835861a31e994c6f76dc0902f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 26 Oct 2024 11:21:25 +1100 Subject: [PATCH 14/67] updating python-3.13 classifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 9fe10ad6..0fa3d6cc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 [options] install_requires = grill-names>=2.6.0; networkx>=3.4; pydot>=3.0.1; numpy; printree From e9f6e36d8a2cc57c532e150dbdb747f102714b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 26 Oct 2024 20:41:48 +1100 Subject: [PATCH 15/67] update maya pip link to 2025 docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- docs/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/install.rst b/docs/source/install.rst index 9868790b..ff0fb99a 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -34,7 +34,7 @@ DCC apps and other environments bundle them outside of ``pip``. To include them, .. tab:: Maya - Visit the `official docs `_ for more details. + Visit the `official docs `_ for more details. .. code-block:: bash From 2ddeaa0a847c451df4003fbaadb6c50a0ef2b4f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 26 Oct 2024 20:43:18 +1100 Subject: [PATCH 16/67] maya still does not bring qtcharts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_qt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/grill/views/_qt.py b/grill/views/_qt.py index ef752e80..c7cc6036 100644 --- a/grill/views/_qt.py +++ b/grill/views/_qt.py @@ -1,5 +1,10 @@ try: # only while transition from PySide2 to PySide6 happens - from PySide6 import QtWidgets, QtGui, QtCore, QtCharts, QtSvg, QtTest + from PySide6 import QtWidgets, QtGui, QtCore, QtSvg, QtTest + try: + from PySide6 import QtCharts + except ImportError: + # Maya-2025.3 bundles PySide6, but fails to bring QtCharts :c + pass except ImportError: from PySide2 import QtWidgets, QtGui, QtCore, QtSvg, QtTest if not hasattr(QtCore, "__enter__"): From bb6ee3008eb1d1f610265dc9bdf8869437657681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 26 Oct 2024 20:44:09 +1100 Subject: [PATCH 17/67] Maya-2025 uses Qt6 and PySide6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/__startup__/maya.py | 6 +++++- grill/views/maya.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/grill/__startup__/maya.py b/grill/__startup__/maya.py index a0cfe06d..364e33f8 100644 --- a/grill/__startup__/maya.py +++ b/grill/__startup__/maya.py @@ -1,5 +1,9 @@ from maya import cmds -from PySide2 import QtCore + +if cmds.about(qt=True).startswith("6"): + from PySide6 import QtCore +else: + from PySide2 import QtCore def install(): diff --git a/grill/views/maya.py b/grill/views/maya.py index d04ffbfa..b8424390 100644 --- a/grill/views/maya.py +++ b/grill/views/maya.py @@ -1,8 +1,12 @@ from functools import cache, partial from maya import cmds -from PySide2 import QtWidgets -from shiboken2 import wrapInstance +from ._qt import QtWidgets + +if cmds.about(qt=True).startswith("6"): + from shiboken6 import wrapInstance +else: + from shiboken2 import wrapInstance import ufe import mayaUsd From 14327fd410dd476e1200c7d562c62b8353a05180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 3 Nov 2024 15:35:31 +1100 Subject: [PATCH 18/67] minor spawn_many doc update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/cook/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index 3b4d2cab..1ea5c817 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -394,6 +394,8 @@ def spawn_many(parent: Usd.Prim, child: Usd.Prim, paths: list[Sdf.Path], labels: 2. Ensuring intermediate prims between ``parent`` and spawned children are also `models `_. 3. Setting explicit `instanceable `_. on spawned children that are components. + Spawned prims and ancestors are `defined `_. + .. seealso:: :func:`spawn_unit` and :func:`create_unit` """ if parent == child: @@ -415,6 +417,7 @@ def spawn_many(parent: Usd.Prim, child: Usd.Prim, paths: list[Sdf.Path], labels: except ValueError: raise ValueError(f"Could not extract identifier from {child} to spawn under {parent}.") parent_stage = parent.GetStage() + # Ensure prims are defined to spawn units unto (paths might be deep e.g. /world/parent/nested/path/for/child) spawned = [parent_stage.DefinePrim(path) for path in paths_to_create] child_is_model = child.IsModel() with Sdf.ChangeBlock(): From 491cef19a20cc41230cf4b0edd620570ab912d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 3 Nov 2024 15:36:00 +1100 Subject: [PATCH 19/67] note that PySide6 is supported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grill/views/README.md b/grill/views/README.md index 8119ecdb..624b9d2d 100644 --- a/grill/views/README.md +++ b/grill/views/README.md @@ -1,3 +1,3 @@ The `grill.views` package provides `Qt` widgets to author and inspect `USD` scene graphs. -Convenience launchers and menus for **USDView**, **Houdini** and **Maya** are provided (appearing under the `👨‍🍳 Grill` menu), but any DCC or environment with `USD` and `PySide2` should be able to use the widgets. +Convenience launchers and menus for **USDView**, **Houdini** and **Maya** are provided (appearing under the `👨‍🍳 Grill` menu), but any DCC or environment with `USD` and `PySide(2|6)` should be able to use the widgets. From b8e27c05015b51e6d068a88511c039becc0062e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 4 Nov 2024 22:57:11 +1100 Subject: [PATCH 20/67] adding clear and block attribute context menus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/usdview.py | 55 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/grill/views/usdview.py b/grill/views/usdview.py index 2c1e6774..d95e7374 100644 --- a/grill/views/usdview.py +++ b/grill/views/usdview.py @@ -198,20 +198,33 @@ class SelectedHierarchyTextMenuItem(AllHierarchyTextMenuItem): _subtitle = "Selection Only" -class GrillAttributeEditorMenuItem(attributeViewContextMenu.AttributeViewContextMenuItem): +class _GrillAttributeViewContextMenuItem(attributeViewContextMenu.AttributeViewContextMenuItem): + """A prim context menu item class that allows special Grill behavior like being added to submenus.""" + _items = [] + + def __init_subclass__(cls, **kwargs): + # _GetContextMenuItems(item, dataModel) signature is inverse than AttributeViewContextMenuItem(dataModel, item) + _GrillAttributeViewContextMenuItem._items.append(lambda *args: cls(*reversed(args))) + @property def _attributes(self): return [i for i in self._dataModel.selection.getProps() if isinstance(i, Usd.Attribute)] def ShouldDisplay(self): - return ( - self._role == attributeViewContextMenu.PropertyViewDataRoles.ATTRIBUTE and - attr.GetMetadata('allowedTokens') if len(self._attributes) == 1 and (attr := self._attributes[0]).GetTypeName() == Sdf.ValueTypeNames.Token else True - ) + return self._role == attributeViewContextMenu.PropertyViewDataRoles.ATTRIBUTE def IsEnabled(self): return self._item and self._attributes + +class GrillAttributeEditorMenuItem(_GrillAttributeViewContextMenuItem): + + def ShouldDisplay(self): + return ( + super().ShouldDisplay() and + attr.GetMetadata('allowedTokens') if len(self._attributes) == 1 and (attr := self._attributes[0]).GetTypeName() == Sdf.ValueTypeNames.Token else True + ) + def GetText(self): if (selected := len(self._attributes)) == 1 and self._attributes[0].GetTypeName() in {Sdf.ValueTypeNames.Bool, Sdf.ValueTypeNames.Token}: return "Set Value|..." @@ -233,6 +246,35 @@ def _GetSubCommands(self): tokens = attribute.GetMetadata('allowedTokens') return [(value, partial(attribute.Set, value)) for value in tokens] + +class GrillAttributeClearMenuItem(_GrillAttributeViewContextMenuItem): + + def GetText(self): + return f"Clear Value{'s' if len(self._attributes) > 1 else ''}" + + def RunCommand(self): + with Sdf.ChangeBlock(): + for attribute in self._attributes: + assert attribute.Clear() + + def IsEnabled(self): + return super().IsEnabled() and any(attr.HasAuthoredValue() for attr in self._attributes) + + +class GrillAttributeBlockMenuItem(_GrillAttributeViewContextMenuItem): + + def GetText(self): + return f"Block Value{'s' if len(self._attributes) > 1 else ''}" + + def RunCommand(self): + with Sdf.ChangeBlock(): + for attribute in self._attributes: + attribute.Block() + + def IsEnabled(self): + return super().IsEnabled() and any(attr.HasAuthoredValue() for attr in self._attributes) + + class _ValueEditor(QtWidgets.QDialog): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -346,8 +388,7 @@ def _extend_menu(_extender, original, *args): for module, member_name, extender in ( (primContextMenuItems, "_GetContextMenuItems", _GrillPrimContextMenuItem._items), (layerStackContextMenu, "_GetContextMenuItems", (GrillContentBrowserLayerMenuItem,)), - # _GetContextMenuItems(item, dataModel) signature is inverse than GrillAttributeEditorMenuItem(dataModel, item) - (attributeViewContextMenu, "_GetContextMenuItems", (lambda *args: GrillAttributeEditorMenuItem(*reversed(args)),)) + (attributeViewContextMenu, "_GetContextMenuItems", _GrillAttributeViewContextMenuItem._items), ): setattr(module, member_name, partial(_extend_menu, extender, getattr(module, member_name))) From 77ff7d20ec9525286b06cbc7ee85bd41008a4a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Wed, 6 Nov 2024 22:01:13 +1100 Subject: [PATCH 21/67] improve UX with Clear by enabling it only when it'd perform an action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/usdview.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/grill/views/usdview.py b/grill/views/usdview.py index d95e7374..b6100339 100644 --- a/grill/views/usdview.py +++ b/grill/views/usdview.py @@ -258,7 +258,11 @@ def RunCommand(self): assert attribute.Clear() def IsEnabled(self): - return super().IsEnabled() and any(attr.HasAuthoredValue() for attr in self._attributes) + # Usd.Attribute.Clear operates only on specs with an existing authored default value at the current edit target + return super().IsEnabled() and any( + (spec := attr.GetStage().GetEditTarget().GetAttributeSpecForScenePath(attr.GetPath())) and spec.default + for attr in self._attributes + ) class GrillAttributeBlockMenuItem(_GrillAttributeViewContextMenuItem): From 4f281c96d7d00e52f767452d72580657ffa0fb2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 09:05:18 +1100 Subject: [PATCH 22/67] add test for creating many assets in an in-memory stage (with an empty resolver context) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_cook.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_cook.py b/tests/test_cook.py index 78654870..1ea698fa 100644 --- a/tests/test_cook.py +++ b/tests/test_cook.py @@ -215,6 +215,13 @@ def test_create_many(self): taxon = cook.define_taxon(stage, "Anon") cook.create_many(taxon, ("first", "second")) + def test_create_many_in_memory(self): + stage = Usd.Stage.CreateInMemory() + # Root_asset is an empty anonymous asset with a pipeline compliant identifier. Sublayer its resolvedPath + stage.GetRootLayer().subLayerPaths.append(cook.fetch_stage(self.root_asset).GetRootLayer().resolvedPath) + taxon = cook.define_taxon(stage, "Anon") + cook.create_many(taxon, ("first", "second")) + def test_spawn_unit(self): stage = cook.fetch_stage(self.root_asset) id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} From 81ea3f8fdae00ffb06e7df40219573c47cb22ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 11:23:09 +1100 Subject: [PATCH 23/67] first pass at updating test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_cook.py | 213 +++++++++++++++++++++++++++------------------ 1 file changed, 128 insertions(+), 85 deletions(-) diff --git a/tests/test_cook.py b/tests/test_cook.py index 1ea698fa..6666e9a5 100644 --- a/tests/test_cook.py +++ b/tests/test_cook.py @@ -5,31 +5,44 @@ from pathlib import Path -from pxr import Usd, UsdGeom, Sdf, Ar, UsdUtils +from pxr import Usd, UsdGeom, Sdf, Ar, UsdUtils, Tf from grill import cook, names, usd as gusd, tokens logger = logging.getLogger(__name__) -# 2024-02-03 - Python-3.12 & USD-23.11 +# 2024-11-09 - Python-3.13 & USD-24.11 # python -m unittest --durations 0 test_cook # Slowest test durations # ---------------------------------------------------------------------- -# 0.058s test_define_taxon (test_cook.TestCook.test_define_taxon) -# 0.056s test_inherited_and_specialized_contexts (test_cook.TestCook.test_inherited_and_specialized_contexts) -# 0.050s test_create_on_previous_stage (test_cook.TestCook.test_create_on_previous_stage) -# 0.047s test_asset_unit (test_cook.TestCook.test_asset_unit) -# 0.034s test_spawn_unit (test_cook.TestCook.test_spawn_unit) -# 0.034s test_spawn_unit_with_absolute_paths (test_cook.TestCook.test_spawn_unit_with_absolute_paths) -# 0.033s test_create_many (test_cook.TestCook.test_create_many) -# 0.032s test_spawn_many (test_cook.TestCook.test_spawn_many) -# 0.023s test_fetch_stage (test_cook.TestCook.test_fetch_stage) -# 0.007s test_edit_context (test_cook.TestCook.test_edit_context) +# 0.050s test_define_taxon (test_cook.TestCook.test_define_taxon) +# 0.049s test_inherited_and_specialized_contexts (test_cook.TestCook.test_inherited_and_specialized_contexts) +# 0.039s test_asset_unit (test_cook.TestCook.test_asset_unit) +# 0.036s test_create_on_previous_stage (test_cook.TestCook.test_create_on_previous_stage) +# 0.031s test_spawn_unit (test_cook.TestCook.test_spawn_unit) +# 0.028s test_create_many_in_memory (test_cook.TestCook.test_create_many_in_memory) +# 0.028s test_spawn_unit_with_absolute_paths (test_cook.TestCook.test_spawn_unit_with_absolute_paths) +# 0.027s test_spawn_many (test_cook.TestCook.test_spawn_many) +# 0.026s test_create_many (test_cook.TestCook.test_create_many) +# 0.020s test_fetch_stage (test_cook.TestCook.test_fetch_stage) # 0.006s test_match (test_cook.TestCook.test_match) +# 0.005s test_edit_context (test_cook.TestCook.test_edit_context) # 0.001s test_spawn_many_invalid (test_cook.TestCook.test_spawn_many_invalid) # # ---------------------------------------------------------------------- -# Ran 12 tests in 0.385s +# Ran 13 tests in 0.347s + +# empty test: +# Ran 13 tests in 0.012s + +# fetchin layers as recently created: +# Ran 13 tests in 0.056s + +# fetching stage: +# Ran 13 tests in 0.065s + +# fetchin layers as recently created: +# Ran 14 tests in 0.059s class TestCook(unittest.TestCase): @@ -48,30 +61,26 @@ def test_fetch_stage(self): # TODO: cleanup this test. fetch_stage used to keep stages in a cache but not anymore. root_stage = cook.fetch_stage(root_asset) # fetching stage outside of AR _context should resolve to same stage - self.assertEqual(root_stage.GetRootLayer().identifier, root_asset.name) + self.assertEqual(cook.asset_identifier(root_stage.GetRootLayer().identifier), root_asset.name) repo_path = cook.Repository.get() resolver_ctx = Ar.DefaultResolverContext([str(repo_path)]) + + # confirm that a stage from a layer created separately is fetched with the correct resolver context + usd_opened = str(names.UsdAsset.get_anonymous(item='usd_opened')) + Sdf.Layer.CreateNew(str(repo_path / usd_opened)) + + with self.assertRaises(Tf.ErrorException): + Usd.Stage.Open(usd_opened) + with Ar.ResolverContextBinder(resolver_ctx): - # inside an AR resolver _context, a new layer and custom stage should end up - # in that stage not resolving to the same as the one from write.fetch_stage - usd_opened = str(names.UsdAsset.get_anonymous(item='usd_opened')) - Sdf.Layer.CreateNew(str(repo_path / usd_opened)) - non_cache_stage = Usd.Stage.Open(usd_opened) - cached_stage = cook.fetch_stage(usd_opened) - self.assertIsNot(non_cache_stage, cached_stage) - # Even after fetching once, subsequent fetches should be different - self.assertIsNot(cached_stage, cook.fetch_stage(usd_opened)) - - # creating a new layer + stage + adding it to the cache manually - # should still have fetch_stage to retrieve a different stage. - sdf_opened = str(names.UsdAsset.get_default(item='sdf_opened')) - Sdf.Layer.CreateNew(str(repo_path / sdf_opened)) - cached_layer = Sdf.Layer.FindOrOpen(sdf_opened) - opened_stage = Usd.Stage.Open(cached_layer) - cache = UsdUtils.StageCache.Get() - cache.Insert(opened_stage) - self.assertIsNot(opened_stage, cook.fetch_stage(sdf_opened)) + opened_stage = Usd.Stage.Open(usd_opened) + + # when fetching the same asset identifier, the root layer should be the same + fetched_stage = cook.fetch_stage(usd_opened) + self.assertIs(opened_stage.GetRootLayer(), fetched_stage.GetRootLayer()) + # but the stages should be different + self.assertIsNot(opened_stage, fetched_stage) in_memory = Usd.Stage.CreateInMemory() from_memory = str(names.UsdAsset.get_anonymous(item='from_memory')) @@ -79,88 +88,102 @@ def test_fetch_stage(self): # a stage with an empty resolver that fetches a valid identifier should fail. cook.fetch_stage(from_memory, context=in_memory.GetPathResolverContext()) - unbound_resolver = str(names.UsdAsset.get_anonymous(item='unbound_resolver')) - with self.assertRaises(RuntimeError): - # directly fetching a new layer without a context with statement should fail - cook._fetch_layer(unbound_resolver, root_stage.GetPathResolverContext()) + # def test_match(self): + # root_stage = cook.fetch_stage(self.root_asset) + # return + # with self.assertRaises(ValueError): + # cook._find_layer_matching(dict(missing='tokens'), root_stage.GetLayerStack()) - def test_match(self): - root_stage = cook.fetch_stage(self.root_asset) - with self.assertRaises(ValueError): - cook._find_layer_matching(dict(missing='tokens'), root_stage.GetLayerStack()) + # def test_edit_context(self): + # cook.fetch_stage(self.root_asset) + # # return + # with self.assertRaises(TypeError): + # gusd.edit_context(object(), cook.fetch_stage(self.root_asset)) - def test_edit_context(self): - with self.assertRaises(TypeError): - gusd.edit_context(object(), cook.fetch_stage(self.root_asset)) + def test_taxonomy(self): + stage = Usd.Stage.CreateInMemory() - def test_define_taxon(self): # An anonymous stage (non grill anonymous) should fail to define taxon. - anon_stage = Usd.Stage.CreateInMemory() - with self.assertRaises(ValueError): - cook.define_taxon(anon_stage, "ShouldFail") + with self.assertRaisesRegex(ValueError, "Could not find a valid pipeline layer"): + cook.define_taxon(stage, "ShouldFail") + # Same stage containing a grill anon layer on its stack should succeed. - anon_pipeline = cook.fetch_stage(names.UsdAsset.get_anonymous()) - anon_stage.GetRootLayer().subLayerPaths.append(anon_pipeline.GetRootLayer().realPath) - self.assertTrue(cook.define_taxon(anon_stage, "ShouldSucceed").IsValid()) + anon_pipeline = Sdf.Layer.CreateNew(str(cook.Repository.get() / names.UsdAsset.get_anonymous().name)) + stage.GetRootLayer().subLayerPaths.append(anon_pipeline.realPath) # Now, test stages fetched from the start via "common" pipeline calls. - root_stage = cook.fetch_stage(self.root_asset) - with self.assertRaisesRegex(ValueError, "reserved name"): - cook.define_taxon(root_stage, cook._TAXONOMY_NAME) + cook.define_taxon(stage, cook._TAXONOMY_NAME) + + with self.assertRaisesRegex(ValueError, "must be a valid identifier for a prim"): + cook.define_taxon(stage, "/InvalidName") with self.assertRaisesRegex(ValueError, "reserved id fields"): - cook.define_taxon(root_stage, "taxonomy_not_allowed", id_fields={cook._TAXONOMY_UNIQUE_ID: "by_id_value"}) + cook.define_taxon(stage, "taxonomy_not_allowed", id_fields={cook._TAXONOMY_UNIQUE_ID: "by_id_value"}) with self.assertRaisesRegex(ValueError, "reserved id fields"): - cook.define_taxon(root_stage, "taxonomy_not_allowed", id_fields={cook._TAXONOMY_UNIQUE_ID.name: "by_id_name"}) + cook.define_taxon(stage, "taxonomy_not_allowed", id_fields={cook._TAXONOMY_UNIQUE_ID.name: "by_id_name"}) with self.assertRaisesRegex(ValueError, "invalid id_field keys"): - cook.define_taxon(root_stage, "nonexistingfield", id_fields={str(uuid.uuid4()): "by_id_name"}) - - displayable = cook.define_taxon(root_stage, "DisplayableName") - # idempotent call should keep previously created prim - self.assertEqual(displayable, cook.define_taxon(root_stage, "DisplayableName")) - - person = cook.define_taxon(root_stage, "Person", references=(displayable,)) - - with cook.taxonomy_context(root_stage): - displayable.CreateAttribute("label", Sdf.ValueTypeNames.String) + cook.define_taxon(stage, "nonexistingfield", id_fields={str(uuid.uuid4()): "by_id_name"}) missing_or_empty_fields_msg = f"Missing or empty '{cook._FIELDS_KEY}'" - not_taxon = root_stage.DefinePrim("/not/a/taxon") + not_taxon = stage.DefinePrim("/not/a/taxon") with self.assertRaisesRegex(ValueError, missing_or_empty_fields_msg): - cook.create_unit(not_taxon, "WillFail") + cook.create_unit(not_taxon, "NoTaxon") not_taxon.SetAssetInfoByKey(cook._ASSETINFO_KEY, {}) with self.assertRaisesRegex(ValueError, missing_or_empty_fields_msg): - cook.create_unit(not_taxon, "WillFail") + cook.create_unit(not_taxon, "EmptyAssetInfo") not_taxon.SetAssetInfoByKey(cook._ASSETINFO_KEY, {'invalid': 42}) with self.assertRaisesRegex(ValueError, missing_or_empty_fields_msg): - cook.create_unit(not_taxon, "WillFail") + cook.create_unit(not_taxon, "InvalidAssetInfo") not_taxon.SetAssetInfoByKey(cook._ASSETINFO_KEY, {cook._FIELDS_KEY: 42}) with self.assertRaisesRegex(TypeError, f"Expected mapping on key '{cook._FIELDS_KEY}'"): - cook.create_unit(not_taxon, "WillFail") + cook.create_unit(not_taxon, "InvalidAssetInfo") not_taxon.SetAssetInfoByKey(cook._ASSETINFO_KEY, {cook._FIELDS_KEY: {}}) with self.assertRaisesRegex(ValueError, missing_or_empty_fields_msg): - cook.create_unit(not_taxon, "WillFail") - - emil = cook.create_unit(person, "EmilSinclair", label="Emil Sinclair") - self.assertEqual(emil, cook.create_unit(person, "EmilSinclair")) - - with cook.unit_context(emil): - emil.GetVariantSet("Transport").SetVariantSelection("HorseDrawnCarriage") + cook.create_unit(not_taxon, "EmptyFields") - hero = cook.define_taxon(root_stage, "Hero", references=(person,)) - batman = cook.create_unit(hero, "Batman") - expected_people = [emil, batman] # batman is also a person - expected_heroes = [batman] - stage_prims = root_stage.Traverse() - self.assertEqual(expected_people, list(cook.itaxa(stage_prims, person))) - self.assertEqual(expected_heroes, list(cook.itaxa(stage_prims, hero))) + first = cook.define_taxon(stage, "first") + # idempotent call should keep previously created prim + self.assertEqual(first, cook.define_taxon(stage, "first")) + + # with cook.taxonomy_context(stage): + # first.CreateAttribute("label", Sdf.ValueTypeNames.String) + second = cook.define_taxon(stage, "second", references=(first,)) + self.assertTrue(second.IsValid()) + + third = cook.define_taxon(stage, "third", references=(first,)) + + found_taxa = set(cook.itaxa(stage)) + self.assertSetEqual(set(found_taxa), {first, second, third}) + + graph_from_stage = cook.taxonomy_graph(found_taxa, "") + first_successors = set(graph_from_stage.successors(first.GetName())) + self.assertEqual(first_successors, {second.GetName(), third.GetName()}) + self.assertEqual(set(cook.taxonomy_graph(stage, "").nodes), set(graph_from_stage.nodes)) + # breakpoint() + # found_taxa = len(graph_from_stage.nodes) + # self.assertEqual(found_taxa, 3) + # self.assertEqual(len(cook.taxonomy_graph(stage, "").nodes), found_taxa) + return + # emil = cook.create_unit(person, "EmilSinclair", label="Emil Sinclair") + # self.assertEqual(emil, cook.create_unit(person, "EmilSinclair")) + # + # with cook.unit_context(emil): + # emil.GetVariantSet("Transport").SetVariantSelection("HorseDrawnCarriage") + # return + # hero = cook.define_taxon(stage, "Hero", references=(person,)) + # batman = cook.create_unit(hero, "Batman") + # expected_people = [emil, batman] # batman is also a person + # expected_heroes = [batman] + # stage_prims = root_stage.Traverse() + # self.assertEqual(expected_people, list(cook.itaxa(stage_prims, person))) + # self.assertEqual(expected_heroes, list(cook.itaxa(stage_prims, hero))) def test_create_on_previous_stage(self): """Confirm that creating assets on a previously saved stage works. @@ -173,6 +196,7 @@ def test_create_on_previous_stage(self): """ root_asset = names.UsdAsset.get_anonymous() root_stage = cook.fetch_stage(root_asset) + return # creates taxonomy.usda and adds it to the stage layer stack cook.define_taxon(root_stage, "FirstTaxon") root_stage.Save() @@ -184,6 +208,7 @@ def test_create_on_previous_stage(self): def test_asset_unit(self): stage = cook.fetch_stage(self.root_asset) + return taxon_name = "Person" person = cook.define_taxon(stage, taxon_name) unit_name = "EmilSinclair" @@ -212,11 +237,13 @@ def test_asset_unit(self): def test_create_many(self): stage = cook.fetch_stage(self.root_asset) + return taxon = cook.define_taxon(stage, "Anon") cook.create_many(taxon, ("first", "second")) def test_create_many_in_memory(self): stage = Usd.Stage.CreateInMemory() + return # Root_asset is an empty anonymous asset with a pipeline compliant identifier. Sublayer its resolvedPath stage.GetRootLayer().subLayerPaths.append(cook.fetch_stage(self.root_asset).GetRootLayer().resolvedPath) taxon = cook.define_taxon(stage, "Anon") @@ -224,6 +251,7 @@ def test_create_many_in_memory(self): def test_spawn_unit(self): stage = cook.fetch_stage(self.root_asset) + return id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) parent, child = cook.create_many(taxon, ['A', 'B']) @@ -238,6 +266,7 @@ def test_spawn_unit(self): def test_spawn_unit_with_absolute_paths(self): stage = cook.fetch_stage(self.root_asset) + return id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) parent, child = cook.create_many(taxon, ['A', 'B']) @@ -249,6 +278,7 @@ def test_spawn_unit_with_absolute_paths(self): def test_spawn_many(self): stage = cook.fetch_stage(self.root_asset) + return id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) parent, child = cook.create_many(taxon, ['A', 'B']) @@ -257,6 +287,7 @@ def test_spawn_many(self): def test_spawn_many_invalid(self): stage = Usd.Stage.CreateInMemory() + return parent = stage.DefinePrim("/a") with self.assertRaisesRegex(ValueError, "Can not spawn .* to itself."): cook.spawn_many(parent, parent, ["impossible"]) @@ -266,6 +297,7 @@ def test_spawn_many_invalid(self): def test_inherited_and_specialized_contexts(self): stage = cook.fetch_stage(self.root_asset) + return id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) parent, via_s, via_i, not_under_context = cook.create_many(taxon, ['parent', 'via_s', 'via_i', 'not_under_context']) @@ -311,3 +343,14 @@ def _check_broadcasted_invisibility(asset, prim, method): ): with self.subTest(target_asset=str(target_asset), target_prim=str(target_prim), broadcast_type=str(broadcast_type)): _check_broadcasted_invisibility(cook.unit_asset(target_asset), target_prim, broadcast_type) + + # def test_taxonomy_graph(self): + # return + # stage = cook.fetch_stage(self.root_asset) + # a = cook.define_taxon(stage, "a") + # b = cook.define_taxon(stage, "b") + # cook.define_taxon(stage, "c", references=(a, b)) + # graph = cook.taxonomy_graph(cook.i_taxa(stage), "") + # found_taxa = len(graph.nodes) + # self.assertEqual(found_taxa, 3) + # self.assertEqual(len(cook.taxonomy_graph(stage, "").nodes), found_taxa) From e6d33be908a125992a0acc0e670471993fee7443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 11:24:29 +1100 Subject: [PATCH 24/67] itaxa iterates over existing taxons. asset_identifier is public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/cook/__init__.py | 48 ++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index 1ea5c817..90e294e1 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -86,15 +86,16 @@ def _fetch_layer(identifier: str, context: Ar.ResolverContext) -> Sdf.Layer: # TODO: see how to make this repo_path better, seems very experimental atm. if context.IsEmpty(): raise ValueError(f"Empty {context=} while fetching {identifier=}") - repo_path = Path(context.Get()[0].GetSearchPath()[0]) # or just Repository.get()? - Sdf.Layer.CreateNew(str(repo_path / identifier)) - if not (layer := Sdf.Layer.FindOrOpen(identifier)): - raise RuntimeError(f"Make sure a resolver context with statement is being used. {context=}, {identifier=}") + # CreateNew adds overhead vs CreateAnonymous but already provides an identifier and ability to call layer.Save() + return Sdf.Layer.CreateNew(str(repo_path / identifier)) + return layer -def _asset_identifier(path): +def asset_identifier(path): + """Since identifiers from relative paths can become absolute when opening existing assets, this function ensures to return the value expected to be authored in layers.""" + # TODO: temporary public. mmm # Expect identifiers to not have folders in between. if not path: raise ValueError("Can not extract asset identifier from empty path.") @@ -133,6 +134,7 @@ def fetch_stage(identifier, context: Ar.ResolverContext = None, load=Usd.Stage.L with Ar.ResolverContextBinder(context): layer = _fetch_layer(identifier, context) + # return layer return Usd.Stage.Open(layer, load=load) @@ -162,6 +164,8 @@ def define_taxon(stage: Usd.Stage, name: str, *, references: tuple[Usd.Prim] = t # (e.g. Windows considers both the same but Linux does not) raise ValueError(f"Can not define a taxon with reserved name: '{_TAXONOMY_NAME}'.") + if not Sdf.Path.IsValidNamespacedIdentifier(name): + raise ValueError(f"{name=} must be a valid identifier for a prim") reserved_fields = {_TAXONOMY_UNIQUE_ID, _UNIT_UNIQUE_ID} reserved_fields.update([i.name for i in reserved_fields]) if intersection:=reserved_fields.intersection(id_fields): @@ -187,10 +191,11 @@ def define_taxon(stage: Usd.Stage, name: str, *, references: tuple[Usd.Prim] = t return prim -def itaxa(prims, taxon, *taxa): - """Yields prims that are part of the given taxa.""" - taxa_names = {i if isinstance(i, str) else i.GetName() for i in (taxon, *taxa)} - return (prim for prim in prims if taxa_names.intersection(prim.GetAssetInfoByKey(_ASSETINFO_TAXA_KEY) or {})) +def itaxa(stage): + return filter( + lambda prim: prim.GetAssetInfoByKey(_ASSETINFO_TAXA_KEY), + _usd.iprims(stage, root_paths={_TAXONOMY_ROOT_PATH}, traverse_predicate=Usd.PrimAllPrimsPredicate) + ) def _catalogue_path(taxon: Usd.Prim) -> Sdf.Path: @@ -230,7 +235,7 @@ def create_many(taxon, names, labels=tuple()) -> typing.List[Usd.Prim]: # existing = {i.GetName() for i in _iter_taxa(taxon.GetStage(), *taxon.GetCustomDataByKey(_ASSETINFO_TAXA_KEY))} taxonomy_layer = _find_layer_matching(_TAXONOMY_FIELDS, stage.GetLayerStack()) - taxonomy_id = _asset_identifier(taxonomy_layer.identifier) + taxonomy_id = asset_identifier(taxonomy_layer.identifier) context = stage.GetPathResolverContext() if context.IsEmpty(): # Use a resolver context that is populated with the repository only when the context is empty. context = Ar.ResolverContext(Ar.DefaultResolverContext([str(Repository.get())])) @@ -241,7 +246,7 @@ def create_many(taxon, names, labels=tuple()) -> typing.List[Usd.Prim]: catalogue_asset = current_asset_name.get(**_CATALOGUE_FIELDS) with Ar.ResolverContextBinder(context): catalogue_layer = _fetch_layer(str(catalogue_asset), context) - catalogue_id = _asset_identifier(catalogue_layer.identifier) + catalogue_id = asset_identifier(catalogue_layer.identifier) root_layer.subLayerPaths.insert(0, catalogue_id) # TODO: try setting this on session layer? @@ -281,7 +286,7 @@ def _fetch_layer_for_unit(name): if not stage.GetPrimAtPath(path:=scope_path.AppendChild(name)): stage.OverridePrim(path) layer = _fetch_layer_for_unit(name) - layer_id = _asset_identifier(layer.identifier) + layer_id = asset_identifier(layer.identifier) prims_info.append((name, label or name, path, layer, Sdf.Reference(layer_id))) prims_info = {stage.GetPrimAtPath(info[2]): info for info in prims_info} @@ -295,7 +300,7 @@ def _fetch_layer_for_unit(name): modelAPI = Usd.ModelAPI(prim) modelAPI.SetKind(Kind.Tokens.component) modelAPI.SetAssetName(name) - modelAPI.SetAssetIdentifier(_asset_identifier(layer.identifier)) + modelAPI.SetAssetIdentifier(asset_identifier(layer.identifier)) catalogue_layer.SetPermissionToEdit(current_permission) return list(prims_info) @@ -333,7 +338,7 @@ def taxonomy_context(stage: Usd.Stage) -> Usd.EditContext: taxonomy_layer = _fetch_layer(str(taxonomy_asset), context) # Use paths relative to our repository to guarantee portability # taxonomy_id = str(Path(taxonomy_layer.realPath).relative_to(Repository.get())) - taxonomy_id = _asset_identifier(taxonomy_layer.identifier) + taxonomy_id = asset_identifier(taxonomy_layer.identifier) taxonomy_root = Sdf.CreatePrimInLayer(taxonomy_layer, _TAXONOMY_ROOT_PATH) taxonomy_root.specifier = Sdf.SpecifierClass taxonomy_layer.defaultPrim = taxonomy_root.name @@ -413,7 +418,7 @@ def spawn_many(parent: Usd.Prim, child: Usd.Prim, paths: list[Sdf.Path], labels: paths_to_create.append(path) labels = itertools.chain(labels, itertools.repeat("")) try: - reference = _asset_identifier(Usd.ModelAPI(child).GetAssetIdentifier().path) + reference = asset_identifier(Usd.ModelAPI(child).GetAssetIdentifier().path) except ValueError: raise ValueError(f"Could not extract identifier from {child} to spawn under {parent}.") parent_stage = parent.GetStage() @@ -526,7 +531,11 @@ def _inherit_or_specialize_unit(method, context_unit): ) from exc +@functools.singledispatch def taxonomy_graph(prims, url_id_prefix): + """ + prims + """ graph = nx.DiGraph(tooltip="Taxonomy Graph") graph.graph.update( graph={'rankdir': 'LR'}, @@ -540,8 +549,15 @@ def taxonomy_graph(prims, url_id_prefix): # TODO: # - Guarantee taxa will be unique (no duplicated short names), raise here? - # - Fail with clear error message when provided prims are not taxa for taxon in prims: + if not (taxa_key:=taxon.GetAssetInfoByKey(_ASSETINFO_TAXA_KEY)): + raise ValueError(f"Prim {taxon} is not a taxon. Expected to find asset info in key '{_ASSETINFO_TAXA_KEY}' but found '{taxa_key}'. Complete prim's asset info: {taxon.GetAssetInfo()}") graph.add_node(taxon_name:=taxon.GetName(), tooltip=taxon.GetPath(), href=f"{url_id_prefix}{taxon_name}",) graph.add_edges_from(itertools.zip_longest(set(taxon.GetAssetInfoByKey(_ASSETINFO_TAXA_KEY)) - {taxon_name}, (), fillvalue=taxon_name)) return graph + + +@taxonomy_graph.register +def _(stage: Usd.Stage, url_id_prefix): + # Convenience for the stage + return taxonomy_graph(itaxa(stage), url_id_prefix) From 13e88195160615456262970ae83bad9d3dc3af6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 12:06:41 +1100 Subject: [PATCH 25/67] extend error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/cook/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index 90e294e1..fc84fa41 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -513,8 +513,8 @@ def _inherit_or_specialize_unit(method, context_unit): raise ValueError(f"{target_prim} is not a valid unit in the catalogue.") context_unit = context_unit or target_prim - if not Usd.ModelAPI(context_unit).GetAssetName(): - raise ValueError(f"{context_unit=} needs to be a valid unit in the catalogue.") + if not (modelAPI:=Usd.ModelAPI(context_unit)).GetAssetName(): + raise ValueError(f"{context_unit=} needs to be a valid unit in the catalogue. Currently it has a kind of '{modelAPI.GetKind()}' and asset info of {modelAPI.GetAssetInfo()}") broadcast_method = type(method) if not target_prim.GetPath().HasPrefix(context_unit.GetPath()): @@ -527,7 +527,7 @@ def _inherit_or_specialize_unit(method, context_unit): except ValueError as exc: raise ValueError( f"Could not find an appropriate edit target node for a {broadcast_method.__name__}'s arc targeting {target_path} for {target_prim}. " - f"""Is there a composition arc bringing "{target_prim.GetName()}"'s unit into "{context_unit.GetName()}"'s layer stack at {context_asset}?""" + f"""Is there a composition arc bringing "{target_prim.GetName()}"'s prim unit into "{context_unit.GetName()}"'s layer stack at {context_asset}?""" ) from exc From 1272a64e7e9422e1a10f2bfedc78c1bf6d06d030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 12:06:52 +1100 Subject: [PATCH 26/67] skip few tests, see what the damage is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_cook.py | 90 +++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 44 deletions(-) diff --git a/tests/test_cook.py b/tests/test_cook.py index 6666e9a5..5f27ca35 100644 --- a/tests/test_cook.py +++ b/tests/test_cook.py @@ -235,19 +235,18 @@ def test_asset_unit(self): with self.assertRaisesRegex(ValueError, "Could not find layer matching"): cook.unit_asset(emil) - def test_create_many(self): - stage = cook.fetch_stage(self.root_asset) - return - taxon = cook.define_taxon(stage, "Anon") - cook.create_many(taxon, ("first", "second")) + # def test_create_many(self): + # stage = cook.fetch_stage(self.root_asset) + # return + # taxon = cook.define_taxon(stage, "Anon") + # cook.create_many(taxon, ("first", "second")) def test_create_many_in_memory(self): stage = Usd.Stage.CreateInMemory() - return - # Root_asset is an empty anonymous asset with a pipeline compliant identifier. Sublayer its resolvedPath - stage.GetRootLayer().subLayerPaths.append(cook.fetch_stage(self.root_asset).GetRootLayer().resolvedPath) - taxon = cook.define_taxon(stage, "Anon") - cook.create_many(taxon, ("first", "second")) + # Root_asset is an empty anonymous asset with a pipeline compliant identifier. Create and sublayer it + anon_pipeline = Sdf.Layer.CreateNew(str(cook.Repository.get() / self.root_asset.name)) + stage.GetRootLayer().subLayerPaths.append(anon_pipeline.identifier) + cook.create_many(cook.define_taxon(stage, "Anon"), ("first", "second")) def test_spawn_unit(self): stage = cook.fetch_stage(self.root_asset) @@ -264,30 +263,28 @@ def test_spawn_unit(self): ): cook.spawn_unit(parent, child, path) - def test_spawn_unit_with_absolute_paths(self): - stage = cook.fetch_stage(self.root_asset) - return - id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} - taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) - parent, child = cook.create_many(taxon, ['A', 'B']) - valid_path = parent.GetPath().AppendPath("Deeper/Nested/Golden1") - invalid_path = "/invalid/path" - self.assertTrue(cook.spawn_unit(parent, child, valid_path)) - with self.assertRaisesRegex(ValueError, "needs to be a child path of parent path"): - cook.spawn_unit(parent, child, invalid_path) + # def test_spawn_unit_with_absolute_paths(self): + # stage = cook.fetch_stage(self.root_asset) + # # return + # id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} + # taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) + # parent, child = cook.create_many(taxon, ['A', 'B']) + def test_spawn_many(self): stage = cook.fetch_stage(self.root_asset) - return + # return id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) parent, child = cook.create_many(taxon, ['A', 'B']) cook.spawn_many(parent, child, ["b"], labels=["1", "2"]) self.assertEqual(len(parent.GetChildren()), 1) + # valid_path = parent.GetPath().AppendPath("Deeper/Nested/Golden1") + # self.assertTrue(cook.spawn_unit(parent, child, valid_path)) def test_spawn_many_invalid(self): stage = Usd.Stage.CreateInMemory() - return + # return parent = stage.DefinePrim("/a") with self.assertRaisesRegex(ValueError, "Can not spawn .* to itself."): cook.spawn_many(parent, parent, ["impossible"]) @@ -295,40 +292,45 @@ def test_spawn_many_invalid(self): with self.assertRaisesRegex(ValueError, "Could not extract identifier from"): cook.spawn_many(parent, child, ["b"]) + invalid_path = "/invalid/path" + with self.assertRaisesRegex(ValueError, "needs to be a child path of parent path"): + cook.spawn_unit(parent, child, invalid_path) + def test_inherited_and_specialized_contexts(self): stage = cook.fetch_stage(self.root_asset) - return id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) - parent, via_s, via_i, not_under_context = cook.create_many(taxon, ['parent', 'via_s', 'via_i', 'not_under_context']) + parent_unit, to_be_specialized, to_be_inherited, not_under_context = cook.create_many( + taxon, ['parent_unit', 'to_be_specialized', 'to_be_inherited', 'not_under_context'] + ) not_a_unit = stage.DefinePrim("/vanilla_prim") + with self.assertRaisesRegex(ValueError, "is not a valid unit"): cook.specialized_context(not_a_unit) - with self.assertRaisesRegex(ValueError, "needs to be a valid unit"): - cook.specialized_context(via_s, via_s.GetParent()) + # current parent prim is not a valid unit in the catalogue (it's just a group) + cook.specialized_context(to_be_specialized, to_be_specialized.GetParent()) with self.assertRaisesRegex(ValueError, "is not a descendant"): - cook.specialized_context(parent, via_s) + cook.specialized_context(parent_unit, to_be_specialized) + # return + spawned_invalid = cook.spawn_unit(parent_unit, not_under_context) - spawned_invalid = cook.spawn_unit(parent, not_under_context) with self.assertRaisesRegex(ValueError, "Is there a composition arc bringing"): # TODO: find a more meaningful message (higher level) than the edit target context one. - cook.specialized_context(spawned_invalid, parent) - - with cook.unit_context(parent): - via_s_spawned = cook.spawn_unit(parent, via_s) - via_i_spawned = cook.spawn_unit(parent, via_i) - - with cook.inherited_context(not_under_context): - UsdGeom.Gprim(not_under_context).MakeInvisible() + cook.specialized_context(spawned_invalid, parent_unit) - with cook.specialized_context(via_s_spawned, parent): - UsdGeom.Gprim(via_s_spawned).MakeInvisible() + with cook.unit_context(parent_unit): + specialized_spawned = cook.spawn_unit(parent_unit, to_be_specialized) + inherited_spawned = cook.spawn_unit(parent_unit, to_be_inherited) - with cook.inherited_context(via_i_spawned): - UsdGeom.Gprim(via_i_spawned).MakeInvisible() + with cook.inherited_context(parent_unit): + UsdGeom.Gprim(parent_unit).MakeInvisible() + with cook.specialized_context(specialized_spawned, parent_unit): + UsdGeom.Gprim(specialized_spawned).MakeInvisible() + with cook.inherited_context(inherited_spawned): + UsdGeom.Gprim(inherited_spawned).MakeInvisible() def _check_broadcasted_invisibility(asset, prim, method): target_stage = Usd.Stage.Open(asset) @@ -337,9 +339,9 @@ def _check_broadcasted_invisibility(asset, prim, method): self.assertEqual(authored, 'invisible') for target_asset, target_prim, broadcast_type in ( - (not_under_context, not_under_context, Usd.Inherits), # non-referenced, no context, asset unit is the target - (via_i_spawned, via_i_spawned, Usd.Inherits), # referenced asset unit, no context, asset unit is the target - (parent, via_s_spawned, Usd.Specializes), # referenced, context unit is the target + (parent_unit, parent_unit, Usd.Inherits), # non-referenced, no context, asset unit is the target + (inherited_spawned, inherited_spawned, Usd.Inherits), # referenced asset unit, no context, asset unit is the target + (parent_unit, specialized_spawned, Usd.Specializes), # referenced, context unit is the target ): with self.subTest(target_asset=str(target_asset), target_prim=str(target_prim), broadcast_type=str(broadcast_type)): _check_broadcasted_invisibility(cook.unit_asset(target_asset), target_prim, broadcast_type) From 7d9df1e6ac09a536e096a1e7284a454f3d6daca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 12:30:23 +1100 Subject: [PATCH 27/67] more adjustments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_cook.py | 137 ++++++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 57 deletions(-) diff --git a/tests/test_cook.py b/tests/test_cook.py index 5f27ca35..78245ab9 100644 --- a/tests/test_cook.py +++ b/tests/test_cook.py @@ -162,6 +162,9 @@ def test_taxonomy(self): found_taxa = set(cook.itaxa(stage)) self.assertSetEqual(set(found_taxa), {first, second, third}) + with self.assertRaisesRegex(ValueError, "is not a taxon."): + cook.taxonomy_graph([first.GetParent()], "") + graph_from_stage = cook.taxonomy_graph(found_taxa, "") first_successors = set(graph_from_stage.successors(first.GetName())) self.assertEqual(first_successors, {second.GetName(), third.GetName()}) @@ -185,55 +188,75 @@ def test_taxonomy(self): # self.assertEqual(expected_people, list(cook.itaxa(stage_prims, person))) # self.assertEqual(expected_heroes, list(cook.itaxa(stage_prims, hero))) - def test_create_on_previous_stage(self): - """Confirm that creating assets on a previously saved stage works. - - The default behavior from layer identifiers that are relative to the resolver search path is to be absolute - when a stage using them is re-opened, so: - original_identifier.usda - becomes - /absolute/path/original_identifier.usda - """ - root_asset = names.UsdAsset.get_anonymous() - root_stage = cook.fetch_stage(root_asset) - return - # creates taxonomy.usda and adds it to the stage layer stack - cook.define_taxon(root_stage, "FirstTaxon") - root_stage.Save() - del root_stage - - reopened_stage = cook.fetch_stage(root_asset) - # the taxonomy.usda now has as identifier /absolute/path/taxonomy.usda, so confirm we can use it still - cook.create_many(cook.define_taxon(reopened_stage, "SecondTaxon"), ["A", "B"]) + # def test_create_on_previous_stage(self): + # """Confirm that creating assets on a previously saved stage works. + # + # The default behavior from layer identifiers that are relative to the resolver search path is to be absolute + # when a stage using them is re-opened, so: + # original_identifier.usda + # becomes + # /absolute/path/original_identifier.usda + # """ + # root_asset = names.UsdAsset.get_anonymous() + # root_stage = cook.fetch_stage(root_asset) + # # return + # # creates taxonomy.usda and adds it to the stage layer stack + # cook.define_taxon(root_stage, "FirstTaxon") + # root_stage.Save() + # del root_stage + # + # reopened_stage = cook.fetch_stage(root_asset) + # # the taxonomy.usda now has as identifier /absolute/path/taxonomy.usda, so confirm we can use it still + # cook.create_many(cook.define_taxon(reopened_stage, "SecondTaxon"), ["A", "B"]) def test_asset_unit(self): stage = cook.fetch_stage(self.root_asset) - return - taxon_name = "Person" - person = cook.define_taxon(stage, taxon_name) - unit_name = "EmilSinclair" - emil = cook.create_unit(person, unit_name, label="Emil Sinclair") - unit_asset = cook.unit_asset(emil) - unit_id = names.UsdAsset(unit_asset.identifier) + root_layer = stage.GetRootLayer().realPath + taxon_name = "taxon" + unit_name = "unit" + unit = cook.create_unit(cook.define_taxon(stage, "taxon"), "unit") + unit_path = unit.GetPath() + unit_asset = cook.unit_asset(unit) + unit_id = names.UsdAsset(cook.asset_identifier(unit_asset.identifier)) self.assertEqual(unit_name, getattr(unit_id, cook._UNIT_UNIQUE_ID.name)) self.assertEqual(taxon_name, getattr(unit_id, cook._TAXONOMY_UNIQUE_ID.name)) - not_a_unit = stage.DefinePrim(emil.GetPath().AppendChild("not_a_unit")) - with self.assertRaisesRegex(ValueError, "Missing or empty"): - cook.unit_asset(not_a_unit) - - layer = Sdf.Layer.CreateAnonymous() - with self.assertRaisesRegex(ValueError, "Could not find appropriate node for edit target"): - gusd.edit_context(not_a_unit, Usd.PrimCompositionQuery.Filter(), lambda arc: arc.GetTargetNode().layerStack.identifier.rootLayer == layer) - - # break the unit model API - Usd.ModelAPI(emil).SetAssetIdentifier("") - without_modelapi = cook.unit_asset(emil) - self.assertEqual(unit_asset, without_modelapi) # we should get the same result - - Usd.ModelAPI(emil).SetAssetName("not_emil") - with self.assertRaisesRegex(ValueError, "Could not find layer matching"): - cook.unit_asset(emil) + # When asset identifier is empty or non existing, the fallback inspection of the prim itself should get the same result + Usd.ModelAPI(unit).SetAssetIdentifier("") + self.assertEqual(cook.unit_asset(unit).identifier, unit_asset.identifier) + # # del unit + # # del stage + # return + # new_stage = Usd.Stage.Open(root_layer) + # unit_prim = new_stage.GetPrimAtPath(unit_path) + # self.assertTrue(unit_prim.IsValid()) + # unit_id = names.UsdAsset(cook.asset_identifier(cook.unit_asset(unit_prim).identifier)) + # return + # taxon_name = "Person" + # person = cook.define_taxon(stage, taxon_name) + # unit_name = "EmilSinclair" + # emil = cook.create_unit(person, unit_name, label="Emil Sinclair") + # unit_asset = cook.unit_asset(emil) + # unit_id = names.UsdAsset(unit_asset.identifier) + # self.assertEqual(unit_name, getattr(unit_id, cook._UNIT_UNIQUE_ID.name)) + # self.assertEqual(taxon_name, getattr(unit_id, cook._TAXONOMY_UNIQUE_ID.name)) + # + # not_a_unit = stage.DefinePrim(emil.GetPath().AppendChild("not_a_unit")) + # with self.assertRaisesRegex(ValueError, "Missing or empty"): + # cook.unit_asset(not_a_unit) + # + # layer = Sdf.Layer.CreateAnonymous() + # with self.assertRaisesRegex(ValueError, "Could not find appropriate node for edit target"): + # gusd.edit_context(not_a_unit, Usd.PrimCompositionQuery.Filter(), lambda arc: arc.GetTargetNode().layerStack.identifier.rootLayer == layer) + # + # # break the unit model API + # Usd.ModelAPI(emil).SetAssetIdentifier("") + # without_modelapi = cook.unit_asset(emil) + # self.assertEqual(unit_asset, without_modelapi) # we should get the same result + # + # Usd.ModelAPI(emil).SetAssetName("not_emil") + # with self.assertRaisesRegex(ValueError, "Could not find layer matching"): + # cook.unit_asset(emil) # def test_create_many(self): # stage = cook.fetch_stage(self.root_asset) @@ -248,20 +271,20 @@ def test_create_many_in_memory(self): stage.GetRootLayer().subLayerPaths.append(anon_pipeline.identifier) cook.create_many(cook.define_taxon(stage, "Anon"), ("first", "second")) - def test_spawn_unit(self): - stage = cook.fetch_stage(self.root_asset) - return - id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} - taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) - parent, child = cook.create_many(taxon, ['A', 'B']) - with cook.unit_context(parent): - for path, value in ( - ("", (2, 15, 6)), - ("Deeper/Nested/Golden1", (-4, 5, 1)), - ("Deeper/Nested/Golden2", (-4, -10, 1)), - ("Deeper/Nested/Golden3", (0, 10, -2)), - ): - cook.spawn_unit(parent, child, path) + # def test_spawn_unit(self): + # stage = cook.fetch_stage(self.root_asset) + # return + # id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} + # taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) + # parent, child = cook.create_many(taxon, ['A', 'B']) + # with cook.unit_context(parent): + # for path, value in ( + # ("", (2, 15, 6)), + # ("Deeper/Nested/Golden1", (-4, 5, 1)), + # ("Deeper/Nested/Golden2", (-4, -10, 1)), + # ("Deeper/Nested/Golden3", (0, 10, -2)), + # ): + # cook.spawn_unit(parent, child, path) # def test_spawn_unit_with_absolute_paths(self): # stage = cook.fetch_stage(self.root_asset) From 7c4873386b2074dc7d4c99998fbf08b730401cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 12:45:18 +1100 Subject: [PATCH 28/67] remove unrequried code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_cook.py | 186 ++++----------------------------------------- 1 file changed, 16 insertions(+), 170 deletions(-) diff --git a/tests/test_cook.py b/tests/test_cook.py index 78245ab9..a1adcc8b 100644 --- a/tests/test_cook.py +++ b/tests/test_cook.py @@ -5,9 +5,9 @@ from pathlib import Path -from pxr import Usd, UsdGeom, Sdf, Ar, UsdUtils, Tf +from pxr import Usd, UsdGeom, Sdf, Ar, Tf -from grill import cook, names, usd as gusd, tokens +from grill import cook, names, tokens logger = logging.getLogger(__name__) @@ -15,34 +15,15 @@ # python -m unittest --durations 0 test_cook # Slowest test durations # ---------------------------------------------------------------------- -# 0.050s test_define_taxon (test_cook.TestCook.test_define_taxon) -# 0.049s test_inherited_and_specialized_contexts (test_cook.TestCook.test_inherited_and_specialized_contexts) -# 0.039s test_asset_unit (test_cook.TestCook.test_asset_unit) -# 0.036s test_create_on_previous_stage (test_cook.TestCook.test_create_on_previous_stage) -# 0.031s test_spawn_unit (test_cook.TestCook.test_spawn_unit) -# 0.028s test_create_many_in_memory (test_cook.TestCook.test_create_many_in_memory) -# 0.028s test_spawn_unit_with_absolute_paths (test_cook.TestCook.test_spawn_unit_with_absolute_paths) -# 0.027s test_spawn_many (test_cook.TestCook.test_spawn_many) -# 0.026s test_create_many (test_cook.TestCook.test_create_many) -# 0.020s test_fetch_stage (test_cook.TestCook.test_fetch_stage) -# 0.006s test_match (test_cook.TestCook.test_match) -# 0.005s test_edit_context (test_cook.TestCook.test_edit_context) -# 0.001s test_spawn_many_invalid (test_cook.TestCook.test_spawn_many_invalid) +# 0.044s test_inherited_and_specialized_contexts (test_cook.TestCook.test_inherited_and_specialized_contexts) +# 0.036s test_asset_unit (test_cook.TestCook.test_asset_unit) +# 0.024s test_spawn_many (test_cook.TestCook.test_spawn_many) +# 0.024s test_create_many_in_memory (test_cook.TestCook.test_create_many_in_memory) +# 0.014s test_taxonomy (test_cook.TestCook.test_taxonomy) +# 0.010s test_fetch_stage (test_cook.TestCook.test_fetch_stage) # # ---------------------------------------------------------------------- -# Ran 13 tests in 0.347s - -# empty test: -# Ran 13 tests in 0.012s - -# fetchin layers as recently created: -# Ran 13 tests in 0.056s - -# fetching stage: -# Ran 13 tests in 0.065s - -# fetchin layers as recently created: -# Ran 14 tests in 0.059s +# Ran 6 tests in 0.152s class TestCook(unittest.TestCase): @@ -57,9 +38,8 @@ def tearDown(self) -> None: def test_fetch_stage(self): root_asset = self.root_asset - - # TODO: cleanup this test. fetch_stage used to keep stages in a cache but not anymore. root_stage = cook.fetch_stage(root_asset) + # fetching stage outside of AR _context should resolve to same stage self.assertEqual(cook.asset_identifier(root_stage.GetRootLayer().identifier), root_asset.name) @@ -88,18 +68,6 @@ def test_fetch_stage(self): # a stage with an empty resolver that fetches a valid identifier should fail. cook.fetch_stage(from_memory, context=in_memory.GetPathResolverContext()) - # def test_match(self): - # root_stage = cook.fetch_stage(self.root_asset) - # return - # with self.assertRaises(ValueError): - # cook._find_layer_matching(dict(missing='tokens'), root_stage.GetLayerStack()) - - # def test_edit_context(self): - # cook.fetch_stage(self.root_asset) - # # return - # with self.assertRaises(TypeError): - # gusd.edit_context(object(), cook.fetch_stage(self.root_asset)) - def test_taxonomy(self): stage = Usd.Stage.CreateInMemory() @@ -152,8 +120,6 @@ def test_taxonomy(self): # idempotent call should keep previously created prim self.assertEqual(first, cook.define_taxon(stage, "first")) - # with cook.taxonomy_context(stage): - # first.CreateAttribute("label", Sdf.ValueTypeNames.String) second = cook.define_taxon(stage, "second", references=(first,)) self.assertTrue(second.IsValid()) @@ -169,53 +135,12 @@ def test_taxonomy(self): first_successors = set(graph_from_stage.successors(first.GetName())) self.assertEqual(first_successors, {second.GetName(), third.GetName()}) self.assertEqual(set(cook.taxonomy_graph(stage, "").nodes), set(graph_from_stage.nodes)) - # breakpoint() - # found_taxa = len(graph_from_stage.nodes) - # self.assertEqual(found_taxa, 3) - # self.assertEqual(len(cook.taxonomy_graph(stage, "").nodes), found_taxa) - return - # emil = cook.create_unit(person, "EmilSinclair", label="Emil Sinclair") - # self.assertEqual(emil, cook.create_unit(person, "EmilSinclair")) - # - # with cook.unit_context(emil): - # emil.GetVariantSet("Transport").SetVariantSelection("HorseDrawnCarriage") - # return - # hero = cook.define_taxon(stage, "Hero", references=(person,)) - # batman = cook.create_unit(hero, "Batman") - # expected_people = [emil, batman] # batman is also a person - # expected_heroes = [batman] - # stage_prims = root_stage.Traverse() - # self.assertEqual(expected_people, list(cook.itaxa(stage_prims, person))) - # self.assertEqual(expected_heroes, list(cook.itaxa(stage_prims, hero))) - - # def test_create_on_previous_stage(self): - # """Confirm that creating assets on a previously saved stage works. - # - # The default behavior from layer identifiers that are relative to the resolver search path is to be absolute - # when a stage using them is re-opened, so: - # original_identifier.usda - # becomes - # /absolute/path/original_identifier.usda - # """ - # root_asset = names.UsdAsset.get_anonymous() - # root_stage = cook.fetch_stage(root_asset) - # # return - # # creates taxonomy.usda and adds it to the stage layer stack - # cook.define_taxon(root_stage, "FirstTaxon") - # root_stage.Save() - # del root_stage - # - # reopened_stage = cook.fetch_stage(root_asset) - # # the taxonomy.usda now has as identifier /absolute/path/taxonomy.usda, so confirm we can use it still - # cook.create_many(cook.define_taxon(reopened_stage, "SecondTaxon"), ["A", "B"]) def test_asset_unit(self): stage = cook.fetch_stage(self.root_asset) - root_layer = stage.GetRootLayer().realPath taxon_name = "taxon" unit_name = "unit" unit = cook.create_unit(cook.define_taxon(stage, "taxon"), "unit") - unit_path = unit.GetPath() unit_asset = cook.unit_asset(unit) unit_id = names.UsdAsset(cook.asset_identifier(unit_asset.identifier)) self.assertEqual(unit_name, getattr(unit_id, cook._UNIT_UNIQUE_ID.name)) @@ -224,45 +149,6 @@ def test_asset_unit(self): # When asset identifier is empty or non existing, the fallback inspection of the prim itself should get the same result Usd.ModelAPI(unit).SetAssetIdentifier("") self.assertEqual(cook.unit_asset(unit).identifier, unit_asset.identifier) - # # del unit - # # del stage - # return - # new_stage = Usd.Stage.Open(root_layer) - # unit_prim = new_stage.GetPrimAtPath(unit_path) - # self.assertTrue(unit_prim.IsValid()) - # unit_id = names.UsdAsset(cook.asset_identifier(cook.unit_asset(unit_prim).identifier)) - # return - # taxon_name = "Person" - # person = cook.define_taxon(stage, taxon_name) - # unit_name = "EmilSinclair" - # emil = cook.create_unit(person, unit_name, label="Emil Sinclair") - # unit_asset = cook.unit_asset(emil) - # unit_id = names.UsdAsset(unit_asset.identifier) - # self.assertEqual(unit_name, getattr(unit_id, cook._UNIT_UNIQUE_ID.name)) - # self.assertEqual(taxon_name, getattr(unit_id, cook._TAXONOMY_UNIQUE_ID.name)) - # - # not_a_unit = stage.DefinePrim(emil.GetPath().AppendChild("not_a_unit")) - # with self.assertRaisesRegex(ValueError, "Missing or empty"): - # cook.unit_asset(not_a_unit) - # - # layer = Sdf.Layer.CreateAnonymous() - # with self.assertRaisesRegex(ValueError, "Could not find appropriate node for edit target"): - # gusd.edit_context(not_a_unit, Usd.PrimCompositionQuery.Filter(), lambda arc: arc.GetTargetNode().layerStack.identifier.rootLayer == layer) - # - # # break the unit model API - # Usd.ModelAPI(emil).SetAssetIdentifier("") - # without_modelapi = cook.unit_asset(emil) - # self.assertEqual(unit_asset, without_modelapi) # we should get the same result - # - # Usd.ModelAPI(emil).SetAssetName("not_emil") - # with self.assertRaisesRegex(ValueError, "Could not find layer matching"): - # cook.unit_asset(emil) - - # def test_create_many(self): - # stage = cook.fetch_stage(self.root_asset) - # return - # taxon = cook.define_taxon(stage, "Anon") - # cook.create_many(taxon, ("first", "second")) def test_create_many_in_memory(self): stage = Usd.Stage.CreateInMemory() @@ -271,54 +157,25 @@ def test_create_many_in_memory(self): stage.GetRootLayer().subLayerPaths.append(anon_pipeline.identifier) cook.create_many(cook.define_taxon(stage, "Anon"), ("first", "second")) - # def test_spawn_unit(self): - # stage = cook.fetch_stage(self.root_asset) - # return - # id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} - # taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) - # parent, child = cook.create_many(taxon, ['A', 'B']) - # with cook.unit_context(parent): - # for path, value in ( - # ("", (2, 15, 6)), - # ("Deeper/Nested/Golden1", (-4, 5, 1)), - # ("Deeper/Nested/Golden2", (-4, -10, 1)), - # ("Deeper/Nested/Golden3", (0, 10, -2)), - # ): - # cook.spawn_unit(parent, child, path) - - # def test_spawn_unit_with_absolute_paths(self): - # stage = cook.fetch_stage(self.root_asset) - # # return - # id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} - # taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) - # parent, child = cook.create_many(taxon, ['A', 'B']) - - def test_spawn_many(self): stage = cook.fetch_stage(self.root_asset) - # return - id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} - taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) - parent, child = cook.create_many(taxon, ['A', 'B']) - cook.spawn_many(parent, child, ["b"], labels=["1", "2"]) - self.assertEqual(len(parent.GetChildren()), 1) - # valid_path = parent.GetPath().AppendPath("Deeper/Nested/Golden1") - # self.assertTrue(cook.spawn_unit(parent, child, valid_path)) - def test_spawn_many_invalid(self): - stage = Usd.Stage.CreateInMemory() - # return parent = stage.DefinePrim("/a") with self.assertRaisesRegex(ValueError, "Can not spawn .* to itself."): cook.spawn_many(parent, parent, ["impossible"]) child = stage.DefinePrim("/b") # child needs to be a grill unit with self.assertRaisesRegex(ValueError, "Could not extract identifier from"): cook.spawn_many(parent, child, ["b"]) - invalid_path = "/invalid/path" with self.assertRaisesRegex(ValueError, "needs to be a child path of parent path"): cook.spawn_unit(parent, child, invalid_path) + id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} + taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) + parent, child = cook.create_many(taxon, ['A', 'B']) + cook.spawn_many(parent, child, ["b"], labels=["1", "2"]) + self.assertEqual(len(parent.GetChildren()), 1) + def test_inherited_and_specialized_contexts(self): stage = cook.fetch_stage(self.root_asset) id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} @@ -368,14 +225,3 @@ def _check_broadcasted_invisibility(asset, prim, method): ): with self.subTest(target_asset=str(target_asset), target_prim=str(target_prim), broadcast_type=str(broadcast_type)): _check_broadcasted_invisibility(cook.unit_asset(target_asset), target_prim, broadcast_type) - - # def test_taxonomy_graph(self): - # return - # stage = cook.fetch_stage(self.root_asset) - # a = cook.define_taxon(stage, "a") - # b = cook.define_taxon(stage, "b") - # cook.define_taxon(stage, "c", references=(a, b)) - # graph = cook.taxonomy_graph(cook.i_taxa(stage), "") - # found_taxa = len(graph.nodes) - # self.assertEqual(found_taxa, 3) - # self.assertEqual(len(cook.taxonomy_graph(stage, "").nodes), found_taxa) From e57f9836ec181f03ff871e06f3585bece1acf5c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 17:13:04 +1100 Subject: [PATCH 29/67] start cleaning views test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 167 +++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 70 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index bdf77363..b5ad0cc6 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -4,6 +4,7 @@ import shutil import tempfile import unittest +from pathlib import Path from unittest import mock from pxr import Usd, UsdGeom, Sdf, UsdShade @@ -20,28 +21,28 @@ # but don't want to use that since that needs to be set prior to an application initialization (which grill can't control as in USDView, Maya, Houdini...) # https://stackoverflow.com/questions/56159475/qt-webengine-seems-to-be-initialized -# 2024-02-03 +# 2024-11-09 - Python-3.13 & USD-24.11 # python -m unittest --durations 0 test_views # Slowest test durations # ---------------------------------------------------------------------- -# 1.963s test_scenegraph_composition (test_views.TestViews.test_scenegraph_composition) -# 1.882s test_taxonomy_editor (test_views.TestViews.test_taxonomy_editor) -# 1.579s test_content_browser (test_views.TestViews.test_content_browser) -# 0.789s test_spreadsheet_editor (test_views.TestViews.test_spreadsheet_editor) -# 0.383s test_horizontal_scroll (test_views.TestGraphicsViewport.test_horizontal_scroll) -# 0.329s test_connection_view (test_views.TestViews.test_connection_view) -# 0.322s test_layer_stack_hovers (test_views.TestViews.test_layer_stack_hovers) -# 0.204s test_dot_call (test_views.TestViews.test_dot_call) -# 0.169s test_display_color_editor (test_views.TestViews.test_display_color_editor) -# 0.167s test_stats (test_views.TestViews.test_stats) -# 0.121s test_prim_filter_data (test_views.TestViews.test_prim_filter_data) -# 0.116s test_prim_composition (test_views.TestViews.test_prim_composition) -# 0.106s test_create_assets (test_views.TestViews.test_create_assets) -# 0.014s test_pan (test_views.TestGraphicsViewport.test_pan) +# 3.285s test_scenegraph_composition (test_views.TestViews.test_scenegraph_composition) +# 1.578s test_content_browser (test_views.TestViews.test_content_browser) +# 1.182s test_taxonomy_editor (test_views.TestViews.test_taxonomy_editor) +# 0.621s test_spreadsheet_editor (test_views.TestViews.test_spreadsheet_editor) +# 0.485s test_connection_view (test_views.TestViews.test_connection_view) +# 0.445s test_horizontal_scroll (test_views.TestGraphicsViewport.test_horizontal_scroll) +# 0.428s test_layer_stack_hovers (test_views.TestViews.test_layer_stack_hovers) +# 0.125s test_prim_filter_data (test_views.TestViews.test_prim_filter_data) +# 0.124s test_stats (test_views.TestViews.test_stats) +# 0.121s test_prim_composition (test_views.TestViews.test_prim_composition) +# 0.077s test_create_assets (test_views.TestViews.test_create_assets) +# 0.067s test_dot_call (test_views.TestViews.test_dot_call) +# 0.054s test_display_color_editor (test_views.TestViews.test_display_color_editor) +# 0.017s test_pan (test_views.TestGraphicsViewport.test_pan) # # (durations < 0.001s were hidden; use -v to show these durations) # ---------------------------------------------------------------------- -# Ran 18 tests in 8.216s +# Ran 18 tests in 8.638s class TestPrivate(unittest.TestCase): @@ -87,63 +88,56 @@ def test_core(self): class TestViews(unittest.TestCase): def setUp(self): + # return self._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) - - sphere = Usd.Stage.CreateInMemory() - UsdGeom.Sphere.Define(sphere, "/sph") root_path = "/root" - sphere_root = sphere.DefinePrim(root_path) + + sphere_stage = Usd.Stage.CreateInMemory() + UsdGeom.Sphere.Define(sphere_stage, "/sph") + sphere_root = sphere_stage.DefinePrim(root_path) sphere_root.CreateAttribute("greet", Sdf.ValueTypeNames.String).Set("hello") - sphere.SetDefaultPrim(sphere_root) + sphere_stage.SetDefaultPrim(sphere_root) - capsule = Usd.Stage.CreateInMemory() - UsdGeom.Capsule.Define(capsule, "/cap") - root_path = "/root" - capsule_root = capsule.DefinePrim(root_path) + capsule_stage = Usd.Stage.CreateInMemory() + UsdGeom.Capsule.Define(capsule_stage, "/cap") + capsule_root = capsule_stage.DefinePrim(root_path) capsule_root.CreateAttribute("who", Sdf.ValueTypeNames.String).Set("world") - capsule.SetDefaultPrim(capsule_root) + capsule_stage.SetDefaultPrim(capsule_root) - merge = Usd.Stage.CreateInMemory() - for i in (capsule, sphere): - merge.GetRootLayer().subLayerPaths.append(i.GetRootLayer().identifier) - merge.SetDefaultPrim(merge.GetPrimAtPath(root_path)) + merged_stage = Usd.Stage.CreateInMemory() + with Sdf.ChangeBlock(): + for i in (capsule_stage, sphere_stage): + merged_stage.GetRootLayer().subLayerPaths.append(i.GetRootLayer().identifier) + merged_stage.SetDefaultPrim(merged_stage.GetPrimAtPath(root_path)) world = Usd.Stage.CreateInMemory() self.nested = world.DefinePrim("/nested/child") self.sibling = world.DefinePrim("/nested/sibling") - self.nested.GetReferences().AddReference(merge.GetRootLayer().identifier) + self.nested.GetReferences().AddReference(merged_stage.GetRootLayer().identifier) - self.capsule = capsule - self.sphere = sphere - self.merge = merge + self.capsule = capsule_stage + self.sphere = sphere_stage + self.merge = merged_stage self.world = world self._tmpf = tempfile.mkdtemp() self._token = cook.Repository.set(cook.Path(self._tmpf) / "repo") - self.rootf = names.UsdAsset.get_anonymous() - self.grill_world = gworld = cook.fetch_stage(self.rootf.name) - self.person = cook.define_taxon(gworld, "Person") - self.agent = cook.define_taxon(gworld, "Agent", references=(self.person,)) - self.generic_agent = cook.create_unit(self.agent, "GenericAgent") + self.grill_root_asset = names.UsdAsset.get_anonymous() + self.grill_world = gworld = cook.fetch_stage(self.grill_root_asset.name) + self.taxon_a = cook.define_taxon(gworld, "a") + self.taxon_b = cook.define_taxon(gworld, "b", references=(self.taxon_a,)) + self.unit_b = cook.create_unit(self.taxon_b, "GenericAgent") def tearDown(self) -> None: cook.Repository.reset(self._token) # Reset all members to USD objects to ensure the used layers are cleared # (otherwise in Windows this can cause failure to remove the temporary files) - self.generic_agent = None - self.agent = None - self.person = None self.grill_world = None - self.capsule = None - self.sphere = None - self.merge = None - self.world = None - self.nested = None - self.sibling = None - shutil.rmtree(self._tmpf) + # shutil.rmtree(self._tmpf) self._app.quit() def test_connection_view(self): + # return for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: with self.subTest(graph_viewer=graph_viewer): _graph._GraphViewer = graph_viewer @@ -156,6 +150,7 @@ def test_connection_view(self): self._sub_test_connection_view() def _sub_test_connection_view(self): + # return # https://openusd.org/release/tut_simple_shading.html stage = Usd.Stage.CreateInMemory() material = UsdShade.Material.Define(stage, '/TexModel/boardMat') @@ -168,10 +163,13 @@ def _sub_test_connection_view(self): cycle_output.ConnectToSource(cycle_input) description._graph_from_connections(material) viewer = description._ConnectableAPIViewer() + # return viewer.setPrim(material) viewer.setPrim(None) + # @pyinstrument.profile() def test_scenegraph_composition(self): + # return for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: with self.subTest(graph_viewer=graph_viewer): _graph._GraphViewer = graph_viewer @@ -186,15 +184,33 @@ def test_scenegraph_composition(self): self._sub_test_layer_stack_bidirectionality() def _sub_test_scenegraph_composition(self): + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print(self) + # # return + # from functools import cache + # from networkx import drawing + # + # with mock.patch("grill.views.description._which") as patch: # simulate dot is not in the environment + # patch.return_value = None + # @cache + # def cached_graph_loader(*args, **kwargs): + # print("-----------------------------------------------") + # return drawing.nx_pydot.graphviz_layout(*args, **kwargs) + widget = description.LayerStackComposition() + # return widget.setStage(self.world) + # by this point we have already tested the view capabilities, skip future iterations of view + widget._graph_view.view = lambda indices: None + # cheap. All these layers affect a single prim affectedPaths = dict.fromkeys((i.GetRootLayer() for i in (self.capsule, self.sphere, self.merge)), 1) # the world affects both root and the nested prims, stage layer stack is included affectedPaths.update(dict.fromkeys(self.world.GetLayerStack(), 3)) + # return for row in range(widget._layers.model.rowCount()): layer = widget._layers.model._objects[row] widget._layers.table.selectRow(row) @@ -202,6 +218,7 @@ def _sub_test_scenegraph_composition(self): actualListedPrims = widget._prims.model.rowCount() self.assertEqual(expectedAffectedPrims, actualListedPrims) + # return widget._layers.table.selectAll() self.assertEqual(len(affectedPaths), widget._layers.model.rowCount()) self.assertEqual(3, widget._prims.model.rowCount()) @@ -231,6 +248,7 @@ def _sub_test_scenegraph_composition(self): widget.deleteLater() def _sub_test_layer_stack_bidirectionality(self): + # return """Confirm that bidirectionality between layer stacks completes. Bidirectionality in the composition graph is achieved by: @@ -255,6 +273,7 @@ def _sub_test_layer_stack_bidirectionality(self): graph_view = widget._graph_view def test_layer_stack_hovers(self): + # return _graph._GraphViewer = _graph.GraphView _graph._USE_SVG_VIEWPORT = False @@ -315,6 +334,7 @@ def test_layer_stack_hovers(self): self.assertTrue(nodes_hovered_checked) def test_prim_composition(self): + # return for pixmap_enabled in True, False: with self.subTest(pixmap_enabled=pixmap_enabled): description._SVG_AS_PIXMAP = pixmap_enabled @@ -347,7 +367,8 @@ def _sub_test_prim_composition(self): widget.clear() def test_create_assets(self): - stage = cook.fetch_stage(str(self.rootf)) + # return + stage = self.grill_world for each in range(1, 6): cook.define_taxon(stage, f"Option{each}") @@ -378,6 +399,7 @@ def test_create_assets(self): widget._apply() def test_taxonomy_editor(self): + # return for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: with self.subTest(graph_viewer=graph_viewer): _graph._GraphViewer = graph_viewer @@ -390,7 +412,7 @@ def test_taxonomy_editor(self): self._sub_test_taxonomy_editor() def _sub_test_taxonomy_editor(self): - stage = cook.fetch_stage(str(self.rootf.get_anonymous())) + stage = cook.fetch_stage(str(self.grill_root_asset.get_anonymous())) existing = [cook.define_taxon(stage, f"Option{each}") for each in range(1, 6)] widget = create.TaxonomyEditor() @@ -460,6 +482,7 @@ def _sub_test_taxonomy_editor(self): widget._graph_view._threadpool.waitForDone(10_000) def test_spreadsheet_editor(self): + # return widget = sheets.SpreadsheetEditor() widget._model_hierarchy.setChecked(False) # default is True self.world.OverridePrim("/child_orphaned") @@ -518,11 +541,11 @@ def test_spreadsheet_editor(self): widget.model._prune_children = {Sdf.Path("/pruned")} gworld = self.grill_world - with cook.unit_context(self.generic_agent): - child_agent = gworld.DefinePrim(self.generic_agent.GetPath().AppendChild("child")) + with cook.unit_context(self.unit_b): + child_agent = gworld.DefinePrim(self.unit_b.GetPath().AppendChild("child")) child_attr = child_agent.CreateAttribute("agent_greet", Sdf.ValueTypeNames.String, custom=False) child_attr.Set("aloha") - agent_id = cook.unit_asset(self.generic_agent) + agent_id = cook.unit_asset(self.unit_b) for i in range(3): agent = gworld.DefinePrim(f"/Instanced/Agent{i}") agent.GetReferences().AddReference(agent_id.identifier) @@ -560,7 +583,8 @@ def test_spreadsheet_editor(self): self.assertEqual(expected_fonts, collected_fonts) def test_prim_filter_data(self): - stage = cook.fetch_stage(self.rootf) + # return + stage = self.grill_world person = cook.define_taxon(stage, "Person") agent = cook.define_taxon(stage, "Agent", references=(person,)) generic = cook.create_unit(agent, "GenericAgent") @@ -591,6 +615,7 @@ def test_prim_filter_data(self): widget.setStage(stage) def test_dot_call(self): + # return """Test execution of function by mocking dot with python call""" with mock.patch("grill.views.description._which") as patch: patch.return_value = 'python' @@ -599,14 +624,15 @@ def test_dot_call(self): self.assertIsNotNone(error) def test_content_browser(self): - stage = cook.fetch_stage(self.rootf) - taxon = cook.define_taxon(stage, "Another") + # return + stage = self.grill_world + taxon = self.taxon_a parent, child = cook.create_many(taxon, ['A', 'B']) for path, value in ( ("", (2, 15, 6)), ("Deeper/Nested/Golden1", (-4, 5, 1)), - ("Deeper/Nested/Golden2", (-4, -10, 1)), - ("Deeper/Nested/Golden3", (0, 10, -2)), + # ("Deeper/Nested/Golden2", (-4, -10, 1)), + # ("Deeper/Nested/Golden3", (0, 10, -2)), ): spawned = UsdGeom.Xform(cook.spawn_unit(parent, child, path)) spawned.AddTranslateOp().Set(value=value) @@ -630,9 +656,9 @@ def _log(*args): def _fake_run(run_args: list): return "", Sdf.Layer.FindOrOpen(run_args[-1]).ExportToString() - + # return # sdffilter still not coming via pypi, so patch for now - with mock.patch("grill.views.description._core._run", new=_fake_run if not description._which("sdffilter") else _core_run): + with mock.patch("grill.views.description._core._run", new=_fake_run): dialog = description._start_content_browser(*args) browser: description._PseudoUSDBrowser = dialog.findChild(description._PseudoUSDBrowser) assert browser._browsers_by_layer.values() @@ -661,7 +687,7 @@ def _fake_run(run_args: list): modifiers = QtCore.Qt.ControlModifier phase = QtCore.Qt.NoScrollPhase inverted = False - + # return # ZOOM IN event = QtGui.QWheelEvent(position, position, pixelDelta, angleDelta_zoomIn, buttons, modifiers, phase, inverted) browser_tab.wheelEvent(event) @@ -672,19 +698,18 @@ def _fake_run(run_args: list): # ZOOM OUT event = QtGui.QWheelEvent(position, position, pixelDelta, angleDelta_zoomOut, buttons, modifiers, phase, inverted) browser_tab.wheelEvent(event) - + # return browser._close_many(range(len(browser._tab_layer_by_idx))) for child in dialog.findChildren(description._PseudoUSDBrowser): child._resolved_layers.clear() - prim_index = parent.GetPrimIndex() - _, sourcepath = tempfile.mkstemp() - prim_index.DumpToDotGraph(sourcepath) - targetpath = f"{sourcepath}.png" # create a temporary file loadable by our image tab - _core_run([_core._which("dot"), sourcepath, "-Tpng", "-o", targetpath]) + image = QtGui.QImage(QtCore.QSize(1, 1), QtGui.QImage.Format_RGB888) + image.fill(QtGui.QColor(255, 0, 0)) + targetpath = str(Path(self.grill_world.GetRootLayer().realPath).with_suffix(".jpg")) + image.save(targetpath, "JPG") browser._on_identifier_requested(anchor, targetpath) - + # return invalid_crate_layer = Sdf.Layer.CreateAnonymous() invalid_crate_layer.ImportFromString( # Not valid in USD-24.05: https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/usd/sdf/testenv/testSdfParsing.testenv/baseline/127_varyingRelationship.sdf#L9 @@ -716,7 +741,8 @@ def GprimSphere "Sphere" self.assertEqual(result, "") def test_display_color_editor(self): - stage = cook.fetch_stage(self.rootf) + # return + stage = self.grill_world sphere = UsdGeom.Sphere.Define(stage, "/volume") color_var = sphere.GetDisplayColorPrimvar() editor = _attributes._DisplayColorEditor(color_var) @@ -734,6 +760,7 @@ def test_display_color_editor(self): editor._update_value() def test_stats(self): + # return empty = stats.StageStats() self.assertEqual(empty._usd_tree.topLevelItemCount(), 0) From cb8ccd025ccddd617c34f3992f20e215f8484ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 9 Nov 2024 17:43:56 +1100 Subject: [PATCH 30/67] slight more cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 107 +++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 65 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index b5ad0cc6..1e433a96 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -25,24 +25,25 @@ # python -m unittest --durations 0 test_views # Slowest test durations # ---------------------------------------------------------------------- -# 3.285s test_scenegraph_composition (test_views.TestViews.test_scenegraph_composition) -# 1.578s test_content_browser (test_views.TestViews.test_content_browser) -# 1.182s test_taxonomy_editor (test_views.TestViews.test_taxonomy_editor) -# 0.621s test_spreadsheet_editor (test_views.TestViews.test_spreadsheet_editor) -# 0.485s test_connection_view (test_views.TestViews.test_connection_view) -# 0.445s test_horizontal_scroll (test_views.TestGraphicsViewport.test_horizontal_scroll) -# 0.428s test_layer_stack_hovers (test_views.TestViews.test_layer_stack_hovers) -# 0.125s test_prim_filter_data (test_views.TestViews.test_prim_filter_data) -# 0.124s test_stats (test_views.TestViews.test_stats) -# 0.121s test_prim_composition (test_views.TestViews.test_prim_composition) -# 0.077s test_create_assets (test_views.TestViews.test_create_assets) -# 0.067s test_dot_call (test_views.TestViews.test_dot_call) -# 0.054s test_display_color_editor (test_views.TestViews.test_display_color_editor) -# 0.017s test_pan (test_views.TestGraphicsViewport.test_pan) +# 0.755s test_taxonomy_editor (test_views.TestViews.test_taxonomy_editor) +# 0.445s test_scenegraph_composition (test_views.TestViews.test_scenegraph_composition) +# 0.408s test_horizontal_scroll (test_views.TestGraphicsViewport.test_horizontal_scroll) +# 0.390s test_layer_stack_hovers (test_views.TestViews.test_layer_stack_hovers) +# 0.389s test_connection_view (test_views.TestViews.test_connection_view) +# 0.292s test_content_browser (test_views.TestViews.test_content_browser) +# 0.143s test_spreadsheet_editor (test_views.TestViews.test_spreadsheet_editor) +# 0.112s test_prim_composition (test_views.TestViews.test_prim_composition) +# 0.098s test_prim_filter_data (test_views.TestViews.test_prim_filter_data) +# 0.062s test_create_assets (test_views.TestViews.test_create_assets) +# 0.061s test_dot_call (test_views.TestViews.test_dot_call) +# 0.047s test_display_color_editor (test_views.TestViews.test_display_color_editor) +# 0.040s test_stats (test_views.TestViews.test_stats) +# 0.016s test_pan (test_views.TestGraphicsViewport.test_pan) +# 0.002s test_vertical_scroll (test_views.TestGraphicsViewport.test_vertical_scroll) # # (durations < 0.001s were hidden; use -v to show these durations) # ---------------------------------------------------------------------- -# Ran 18 tests in 8.638s +# Ran 18 tests in 3.267s class TestPrivate(unittest.TestCase): @@ -163,13 +164,30 @@ def _sub_test_connection_view(self): cycle_output.ConnectToSource(cycle_input) description._graph_from_connections(material) viewer = description._ConnectableAPIViewer() - # return + # graph views is being tested elsewhere + viewer._graph_view.view = lambda indices: None viewer.setPrim(material) + # return viewer.setPrim(None) - # @pyinstrument.profile() def test_scenegraph_composition(self): - # return + """Confirm that bidirectionality between layer stacks completes. + + Bidirectionality in the composition graph is achieved by: + - parent_stage -> child_stage via a reference, payload arcs + - child_stage -> parent_stage via a inherits, specializes arcs + """ + parent_stage = self.world + child_stage = Usd.Stage.CreateInMemory() + prim = parent_stage.DefinePrim("/a/b") + child_prim = child_stage.DefinePrim("/child") + child_prim.GetInherits().AddInherit("/foo") + child_prim.GetSpecializes().AddSpecialize("/foo") + child_stage.SetDefaultPrim(child_prim) + child_identifier = child_stage.GetRootLayer().identifier + prim.GetReferences().AddReference(child_identifier) + prim.GetPayloads().AddPayload(child_identifier) + for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: with self.subTest(graph_viewer=graph_viewer): _graph._GraphViewer = graph_viewer @@ -178,28 +196,13 @@ def test_scenegraph_composition(self): with self.subTest(pixmap_enabled=pixmap_enabled): _graph._USE_SVG_VIEWPORT = pixmap_enabled self._sub_test_scenegraph_composition() - self._sub_test_layer_stack_bidirectionality() else: self._sub_test_scenegraph_composition() - self._sub_test_layer_stack_bidirectionality() def _sub_test_scenegraph_composition(self): - print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - print(self) - # # return - # from functools import cache - # from networkx import drawing - # - # with mock.patch("grill.views.description._which") as patch: # simulate dot is not in the environment - # patch.return_value = None - # @cache - # def cached_graph_loader(*args, **kwargs): - # print("-----------------------------------------------") - # return drawing.nx_pydot.graphviz_layout(*args, **kwargs) - widget = description.LayerStackComposition() - # return widget.setStage(self.world) + widget._layers.table.selectAll() # by this point we have already tested the view capabilities, skip future iterations of view widget._graph_view.view = lambda indices: None @@ -208,20 +211,20 @@ def _sub_test_scenegraph_composition(self): affectedPaths = dict.fromkeys((i.GetRootLayer() for i in (self.capsule, self.sphere, self.merge)), 1) # the world affects both root and the nested prims, stage layer stack is included - affectedPaths.update(dict.fromkeys(self.world.GetLayerStack(), 3)) - - # return + affectedPaths.update(dict.fromkeys(self.world.GetLayerStack(), 5)) for row in range(widget._layers.model.rowCount()): layer = widget._layers.model._objects[row] widget._layers.table.selectRow(row) + if layer not in affectedPaths: + continue expectedAffectedPrims = affectedPaths[layer] actualListedPrims = widget._prims.model.rowCount() self.assertEqual(expectedAffectedPrims, actualListedPrims) # return widget._layers.table.selectAll() - self.assertEqual(len(affectedPaths), widget._layers.model.rowCount()) - self.assertEqual(3, widget._prims.model.rowCount()) + self.assertEqual(len(affectedPaths)+1, widget._layers.model.rowCount()) + self.assertEqual(5, widget._prims.model.rowCount()) widget.setPrimPaths({"/nested/sibling"}) widget.setStage(self.world) @@ -247,33 +250,7 @@ def _sub_test_scenegraph_composition(self): widget.deleteLater() - def _sub_test_layer_stack_bidirectionality(self): - # return - """Confirm that bidirectionality between layer stacks completes. - - Bidirectionality in the composition graph is achieved by: - - parent_stage -> child_stage via a reference, payload arcs - - child_stage -> parent_stage via a inherits, specializes arcs - """ - parent_stage = Usd.Stage.CreateInMemory() - child_stage = Usd.Stage.CreateInMemory() - prim = parent_stage.DefinePrim("/a/b") - child_prim = child_stage.DefinePrim("/child") - child_prim.GetInherits().AddInherit("/foo") - child_prim.GetSpecializes().AddSpecialize("/foo") - child_stage.SetDefaultPrim(child_prim) - child_identifier = child_stage.GetRootLayer().identifier - prim.GetReferences().AddReference(child_identifier) - prim.GetPayloads().AddPayload(child_identifier) - - widget = description.LayerStackComposition() - widget.setStage(parent_stage) - widget._layers.table.selectAll() - - graph_view = widget._graph_view - def test_layer_stack_hovers(self): - # return _graph._GraphViewer = _graph.GraphView _graph._USE_SVG_VIEWPORT = False From 85e30a26b216d72d2772fe0c8df7e4f2ab279d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 17 Nov 2024 14:04:08 +1100 Subject: [PATCH 31/67] no need to provide active_plugs as _Node argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 64 +++++++++++++++++++++++--------------- grill/views/description.py | 9 +----- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index baef2a17..69e286e0 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -10,6 +10,7 @@ import networkx as nx from itertools import chain from functools import cache +from collections import ChainMap from networkx import drawing @@ -34,8 +35,6 @@ _IS_QT5 = QtCore.qVersion().startswith("5") # TODO: -# - Popup everytime a new graph is loaded in Houdini or Maya ( it's on _run_prog func line 1380 of agraph.py ) -# https://github.com/pygraphviz/pygraphviz/pull/514 # - Should toggling "precise source layer" on LayerStack compostiion view preserve node position for _GraphViewer? # - Tooltip on nodes for _GraphViewer # - Context menu items @@ -63,7 +62,6 @@ def _convert_graphviz_to_html_label(label): for index, field in enumerate(fields): port, text = field.strip("<>").split(">", 1) bgcolor = "white" if index % 2 == 0 else "#f0f6ff" # light blue - # text = f'{text}' text = f'{text}' label += f"{text}" label += "" @@ -74,6 +72,13 @@ def _convert_graphviz_to_html_label(label): return label +def _get_plugs_from_label(label): + if not label.startswith("{"): + raise ValueError(f"Label needs to start with '{{'. Got: {label}") + fields = label.strip("{}").split("|") + return dict(field.strip("<>").split(">", 1) for index, field in enumerate(fields)) + + @cache def _dot_2_svg(sourcepath): print(f"Creating svg for: {sourcepath}") @@ -85,23 +90,13 @@ def _dot_2_svg(sourcepath): class _Node(QtWidgets.QGraphicsTextItem): - def __init__(self, parent=None, label="", color="", fillcolor="", plugs: tuple =None, active_plugs: set = frozenset(), visible=True): + # TODO: see if we can remove 'label', since we are already processing the graphviz one here, it might be cheaper to have label created only when we need it + def __init__(self, parent=None, label="", color="", fillcolor="", plugs: tuple = (), visible=True): super().__init__(parent) self._edges = [] - self._plugs = plugs = dict(zip(plugs, range(len(plugs)))) or {} # {identifier: index} - - plug_items = {} # {index: (QEllipse, QEllipse)} - radius = 4 - def _plug_item(): - item = QtWidgets.QGraphicsEllipseItem(-radius, -radius, 2 * radius, 2 * radius) - item.setPen(_NO_PEN) - return item - self._active_plugs = active_plugs + self._plugs = dict(zip(plugs, range(len(plugs)))) or {} # {identifier: index} self._active_plugs_by_side = dict() # {index: {left[int]: {}, right[int]: {}} - for plug_index in active_plugs: - plug_items[plugs[plug_index]] = (_plug_item(), _plug_item()) - self._active_plugs_by_side[plugs[plug_index]] = {0: dict(), 1: dict()} - self._plug_items = plug_items + self._plug_items = {} # {index: (QEllipse, QEllipse)} self._pen = QtGui.QPen(QtGui.QColor(color), 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) self._fillcolor = QtGui.QColor(fillcolor) self.setHtml("" + _convert_graphviz_to_html_label(label)) @@ -170,7 +165,20 @@ def itemChange(self, change: QtWidgets.QGraphicsItem.GraphicsItemChange, value): def _activatePlug(self, edge, plug_index, side, position): if plug_index is None: return # we're at the center, nothing to draw nor activate - plugs_by_side = self._active_plugs_by_side[plug_index] # {index: {left[int]: {}, right[int]: {}} + try: + plugs_by_side = self._active_plugs_by_side[plug_index] # {index: {left[int]: {}, right[int]: {}} + except KeyError: # first time we're activating a plug, so add a visual ellipse for it + radius = 4 + + def _add_plug_item(): + item = QtWidgets.QGraphicsEllipseItem(-radius, -radius, 2 * radius, 2 * radius) + item.setPen(_NO_PEN) + self.scene().addItem(item) + return item + + self._plug_items[plug_index] = (_add_plug_item(), _add_plug_item()) + self._active_plugs_by_side[plug_index] = plugs_by_side = {0: dict(), 1: dict()} + plugs_by_side[side][edge] = True other_side = bool(not side) inactive_plugs = plugs_by_side[other_side] @@ -604,21 +612,25 @@ def _load_graph(self, graph): print("LOADING GRAPH") self._nodes_map.clear() edge_color = graph.graph.get('edge', {}).get("color", "") + graph_node_attrs = graph.graph.get('node', {}) def _add_node(nx_node): node_data = graph.nodes[nx_node] + plugs = node_data.pop('plugs', ()) + nodes_attrs = ChainMap(node_data, graph_node_attrs) + if nodes_attrs.get('shape') == 'record': + if plugs: + raise ValueError(f"record 'shape' and 'plugs' are mutually exclusive, pick one for {nx_node}, {node_data=}") + plugs = _get_plugs_from_label(node_data['label']) item = _Node( label=node_data.get('label', str(nx_node)), - color=graph.graph.get('node', {}).get("color", ""), - fillcolor=graph.graph.get('node', {}).get("fillcolor", "white"), - plugs=node_data.get('plugs', {}), - visible=node_data.get('style', "") != "invis", - active_plugs=node_data.get('active_plugs', set()), + color=nodes_attrs.get("color", ""), + fillcolor=nodes_attrs.get("fillcolor", "white"), + plugs=plugs, + visible=nodes_attrs.get('style', "") != "invis", ) item.linkActivated.connect(self._graph_url_changed) self.scene().addItem(item) - for each_plug in chain.from_iterable(item._plug_items.values()): - self.scene().addItem(each_plug) return item max_y = max(pos[1] for pos in positions.values()) @@ -649,6 +661,7 @@ def _add_node(nx_node): if source._plugs or target._plugs: kwargs['target_plug'] = target._plugs[edge_data['headport']] if edge_data.get('headport') is not None else None kwargs['source_plug'] = source._plugs[edge_data['tailport']] if edge_data.get('tailport') is not None else None + edge = _Edge(source, target, color=color, label=label, is_bidirectional=is_bidirectional, **kwargs) self.scene().addItem(edge) @@ -680,6 +693,7 @@ def __init__(self, *args, **kwargs): self.setScene(scene) def load(self, filepath): + filepath = filepath.toLocalFile() if isinstance(filepath, QtCore.QUrl) else filepath scene = self.scene() scene.clear() diff --git a/grill/views/description.py b/grill/views/description.py index 474faa54..bed79f31 100644 --- a/grill/views/description.py +++ b/grill/views/description.py @@ -163,9 +163,7 @@ def _add_node(pcp_node): plugs.append(layer_index) label += f"{'' if layer_index == 0 else '|'}<{layer_index}>{_layer_label(layer)}" label += '}' - # attrs['plugs'] = dict(zip(plugs, range(len(plugs)))) attrs['plugs'] = tuple(plugs) - attrs['active_plugs'] = set() # all active connections, for GUI all_nodes[index] = dict(label=label, tooltip=tooltip, **attrs) return index, sublayers @@ -182,7 +180,6 @@ def _compute_composition(_prim): # arc.GetTargetNode().origin nor arc.GetTargetNode().GetOriginRootNode() source_idx, source_layers = _add_node(arc.GetIntroducingNode()) source_port = source_layers[source_layer] - all_nodes[source_idx]['active_plugs'].add(source_port) # all connections, for GUI all_edges[source_idx, target_idx][source_port][arc.GetArcType()].update( {func.__name__: is_fun for func in _USD_COMPOSITION_ARC_QUERY_METHODS if (is_fun := func(arc))} ) @@ -260,8 +257,6 @@ def traverse(api: UsdShade.ConnectableAPI): node_id = _get_node_id(current_prim) label = f'<' label += table_row.format(port="", color="white", text=f'{api.GetPrim().GetName()}') - plugs = {"": 0} # {graphviz port name: port index order} - active_plugs = set() for index, plug in enumerate(chain(api.GetInputs(), api.GetOutputs()), start=1): # we start at 1 because index 0 is the node itself plug_name = plug.GetBaseName() sources, __ = plug.GetConnectedSources() # (valid, invalid): we care only about valid sources (index 0) @@ -270,10 +265,8 @@ def traverse(api: UsdShade.ConnectableAPI): for source in sources: _add_edges(_get_node_id(source.source.GetPrim()), source.sourceName, node_id, plug_name) traverse(source.source) - plugs[plug_name] = index - active_plugs.add(plug_name) # TODO: add only actual plugged properties, right now we're adding all of them label += '
>' - all_nodes[node_id] = dict(label=label, plugs=plugs, active_plugs=active_plugs) + all_nodes[node_id] = dict(label=label) traverse(connections_api) From 58915cc9b01ade44410d1cea748cf42c7238f93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 17 Nov 2024 15:11:33 +1100 Subject: [PATCH 32/67] plugs are resolved internally by the graph viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/description.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/grill/views/description.py b/grill/views/description.py index bed79f31..e408007f 100644 --- a/grill/views/description.py +++ b/grill/views/description.py @@ -150,7 +150,6 @@ def _add_node(pcp_node): ids_by_root_layer[root_layer] = index = len(all_nodes) attrs = dict(style='rounded,filled', shape='record', href=f"{url_prefix}{index}", fillcolor="white", color="darkslategray") - plugs = [] label = '{' tooltip = 'LayerStack:' for layer, layer_index in sublayers.items(): @@ -160,10 +159,8 @@ def _add_node(pcp_node): # For new line: https://stackoverflow.com/questions/16671966/multiline-tooltip-for-pydot-graph # For Windows path sep: https://stackoverflow.com/questions/15094591/how-to-escape-forwardslash-character-in-html-but-have-it-passed-correctly-to-jav tooltip += f" {layer_index}: {(layer.realPath or layer.identifier)}".replace('\\', '/') - plugs.append(layer_index) label += f"{'' if layer_index == 0 else '|'}<{layer_index}>{_layer_label(layer)}" label += '}' - attrs['plugs'] = tuple(plugs) all_nodes[index] = dict(label=label, tooltip=tooltip, **attrs) return index, sublayers @@ -180,7 +177,8 @@ def _compute_composition(_prim): # arc.GetTargetNode().origin nor arc.GetTargetNode().GetOriginRootNode() source_idx, source_layers = _add_node(arc.GetIntroducingNode()) source_port = source_layers[source_layer] - all_edges[source_idx, target_idx][source_port][arc.GetArcType()].update( + # implementation detail: convert source_port to a string since it's serialized as the label in graphviz + all_edges[source_idx, target_idx][str(source_port)][arc.GetArcType()].update( {func.__name__: is_fun for func in _USD_COMPOSITION_ARC_QUERY_METHODS if (is_fun := func(arc))} ) @@ -188,7 +186,7 @@ def _compute_composition(_prim): all_nodes = dict() # {int: dict} all_edges = defaultdict( # { (source_node: int, target_node: int): - lambda: defaultdict( # { source_port: int: + lambda: defaultdict( # { source_port: str: lambda: defaultdict( # { Pcp.ArcType: _USD_COMPOSITION_ARC_QUERY_DEFAULTS # { HasArcs: bool, IsImplicit: bool, ... } ) # } From 678476df7189be48c8d38a824c9c04d98368fc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 17 Nov 2024 17:46:52 +1100 Subject: [PATCH 33/67] adding mini test bed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- .../mini_test_bed/Catalogue-world-test.1.usda | 73 +++++++++++ .../Geom-Elements-Apartment.1.usda | 27 +++++ tests/mini_test_bed/Model-Blocks-Block.1.usda | 51 ++++++++ ...Blocks-Block_With_Inherited_Windows.1.usda | 68 +++++++++++ ...ocks-Block_With_Specialized_Windows.1.usda | 68 +++++++++++ ...odel-Buildings-Multi_Story_Building.1.usda | 81 +++++++++++++ .../Model-Elements-Apartment.1.usda | 114 ++++++++++++++++++ .../Shade-Color-ModelDefault.1.usda | 21 ++++ tests/mini_test_bed/main-Taxonomy-test.1.usda | 76 ++++++++++++ tests/mini_test_bed/main-world-test.1.usda | 8 ++ tests/test_data/_mini_graph.dot | 0 11 files changed, 587 insertions(+) create mode 100644 tests/mini_test_bed/Catalogue-world-test.1.usda create mode 100644 tests/mini_test_bed/Geom-Elements-Apartment.1.usda create mode 100644 tests/mini_test_bed/Model-Blocks-Block.1.usda create mode 100644 tests/mini_test_bed/Model-Blocks-Block_With_Inherited_Windows.1.usda create mode 100644 tests/mini_test_bed/Model-Blocks-Block_With_Specialized_Windows.1.usda create mode 100644 tests/mini_test_bed/Model-Buildings-Multi_Story_Building.1.usda create mode 100644 tests/mini_test_bed/Model-Elements-Apartment.1.usda create mode 100644 tests/mini_test_bed/Shade-Color-ModelDefault.1.usda create mode 100644 tests/mini_test_bed/main-Taxonomy-test.1.usda create mode 100644 tests/mini_test_bed/main-world-test.1.usda create mode 100644 tests/test_data/_mini_graph.dot diff --git a/tests/mini_test_bed/Catalogue-world-test.1.usda b/tests/mini_test_bed/Catalogue-world-test.1.usda new file mode 100644 index 00000000..f14fff48 --- /dev/null +++ b/tests/mini_test_bed/Catalogue-world-test.1.usda @@ -0,0 +1,73 @@ +#usda 1.0 + +def "Catalogue" ( + kind = "group" +) +{ + def "Shade" ( + kind = "group" + ) + { + def "Color" ( + kind = "group" + ) + { + over "ModelDefault" ( + prepend references = @Shade-Color-ModelDefault.1.usda@ + ) + { + } + } + } + + def "Model" ( + kind = "group" + ) + { + def "Elements" ( + kind = "group" + ) + { + over "Apartment" ( + prepend references = @Model-Elements-Apartment.1.usda@ + ) + { + } + } + + def "Buildings" ( + kind = "group" + ) + { + over "Multi_Story_Building" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + } + } + + def "Blocks" ( + kind = "group" + ) + { + over "Block_With_Inherited_Windows" ( + prepend references = @Model-Blocks-Block_With_Inherited_Windows.1.usda@ + ) + { + } + + over "Block_With_Specialized_Windows" ( + prepend references = @Model-Blocks-Block_With_Specialized_Windows.1.usda@ + ) + { + } + + over "Block" ( + prepend references = @Model-Blocks-Block.1.usda@ + ) + { + } + } + } +} + diff --git a/tests/mini_test_bed/Geom-Elements-Apartment.1.usda b/tests/mini_test_bed/Geom-Elements-Apartment.1.usda new file mode 100644 index 00000000..9c989ca2 --- /dev/null +++ b/tests/mini_test_bed/Geom-Elements-Apartment.1.usda @@ -0,0 +1,27 @@ +#usda 1.0 +( + defaultPrim = "Origin" +) + +def "Origin" +{ + def Mesh "Floor" + { + uniform bool doubleSided = 1 + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 1, 9, 8, 1, 2, 10, 9, 2, 3, 11, 10, 3, 4, 12, 11, 4, 5, 13, 12, 5, 6, 14, 13, 6, 7, 15, 14, 8, 9, 17, 16, 9, 10, 18, 17, 10, 11, 19, 18, 11, 12, 20, 19, 12, 13, 21, 20, 13, 14, 22, 21, 14, 15, 23, 22, 16, 17, 25, 24, 17, 18, 26, 25, 18, 19, 27, 26, 19, 20, 28, 27, 20, 21, 29, 28, 21, 22, 30, 29, 22, 23, 31, 30, 24, 25, 33, 32, 25, 26, 34, 33, 26, 27, 35, 34, 27, 28, 36, 35, 28, 29, 37, 36, 29, 30, 38, 37, 30, 31, 39, 38, 32, 33, 41, 40, 33, 34, 42, 41, 34, 35, 43, 42, 35, 36, 44, 43, 36, 37, 45, 44, 37, 38, 46, 45, 38, 39, 47, 46, 40, 41, 49, 48, 41, 42, 50, 49, 42, 43, 51, 50, 43, 44, 52, 51, 44, 45, 53, 52, 45, 46, 54, 53, 46, 47, 55, 54, 48, 49, 57, 56, 49, 50, 58, 57, 50, 51, 59, 58, 51, 52, 60, 59, 52, 53, 61, 60, 53, 54, 62, 61, 54, 55, 63, 62] + point3f[] points = [(-4, 0, 4), (-2.857143, 0, 4), (-1.7142857, 0, 4), (-0.5714286, 0, 4), (0.5714286, 0, 4), (1.7142857, 0, 4), (2.857143, 0, 4), (4, 0, 4), (-4, 0, 2.857143), (-2.857143, 0, 2.857143), (-1.7142857, 0, 2.857143), (-0.5714286, 0, 2.857143), (0.5714286, 0, 2.857143), (1.7142857, 0, 2.857143), (2.857143, 0, 2.857143), (4, 0, 2.857143), (-4, 0, 1.7142857), (-2.857143, 0, 1.7142857), (-1.7142857, 0, 1.7142857), (-0.5714286, 0, 1.7142857), (0.5714286, 0, 1.7142857), (1.7142857, 0, 1.7142857), (2.857143, 0, 1.7142857), (4, 0, 1.7142857), (-4, 0, 0.5714286), (-2.857143, 0, 0.5714286), (-1.7142857, 0, 0.5714286), (-0.5714286, 0, 0.5714286), (0.5714286, 0, 0.5714286), (1.7142857, 0, 0.5714286), (2.857143, 0, 0.5714286), (4, 0, 0.5714286), (-4, 0, -0.5714286), (-2.857143, 0, -0.5714286), (-1.7142857, 0, -0.5714286), (-0.5714286, 0, -0.5714286), (0.5714286, 0, -0.5714286), (1.7142857, 0, -0.5714286), (2.857143, 0, -0.5714286), (4, 0, -0.5714286), (-4, 0, -1.7142857), (-2.857143, 0, -1.7142857), (-1.7142857, 0, -1.7142857), (-0.5714286, 0, -1.7142857), (0.5714286, 0, -1.7142857), (1.7142857, 0, -1.7142857), (2.857143, 0, -1.7142857), (4, 0, -1.7142857), (-4, 0, -2.857143), (-2.857143, 0, -2.857143), (-1.7142857, 0, -2.857143), (-0.5714286, 0, -2.857143), (0.5714286, 0, -2.857143), (1.7142857, 0, -2.857143), (2.857143, 0, -2.857143), (4, 0, -2.857143), (-4, 0, -4), (-2.857143, 0, -4), (-1.7142857, 0, -4), (-0.5714286, 0, -4), (0.5714286, 0, -4), (1.7142857, 0, -4), (2.857143, 0, -4), (4, 0, -4)] + } + + def Sphere "Volume" + { + double radius = 2 + float3 xformOp:rotateXYZ.timeSamples = { + 0: (12, 0, 0), + 192: (12, 0, 1440), + } + double3 xformOp:translate = (0, 2, 0) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"] + } +} + diff --git a/tests/mini_test_bed/Model-Blocks-Block.1.usda b/tests/mini_test_bed/Model-Blocks-Block.1.usda new file mode 100644 index 00000000..d4cbe210 --- /dev/null +++ b/tests/mini_test_bed/Model-Blocks-Block.1.usda @@ -0,0 +1,51 @@ +#usda 1.0 +( + defaultPrim = "Origin" +) + +def "Origin" ( + prepend apiSchemas = ["GeomModelAPI"] + assetInfo = { + asset identifier = @Model-Blocks-Block.1.usda@ + string name = "Block" + } + displayName = "Block" + prepend inherits = + kind = "assembly" + prepend references = @main-Taxonomy-test.1.usda@ + prepend specializes = +) +{ + def "Building1" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (-54, 126, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building2" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (54, 126, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building3" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (-54, -126, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building4" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (54, -126, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } +} + diff --git a/tests/mini_test_bed/Model-Blocks-Block_With_Inherited_Windows.1.usda b/tests/mini_test_bed/Model-Blocks-Block_With_Inherited_Windows.1.usda new file mode 100644 index 00000000..37c500ad --- /dev/null +++ b/tests/mini_test_bed/Model-Blocks-Block_With_Inherited_Windows.1.usda @@ -0,0 +1,68 @@ +#usda 1.0 +( + defaultPrim = "Origin" +) + +def "Origin" ( + prepend apiSchemas = ["GeomModelAPI"] + assetInfo = { + asset identifier = @Model-Blocks-Block_With_Inherited_Windows.1.usda@ + string name = "Block_With_Inherited_Windows" + } + displayName = "Block_With_Inherited_Windows" + prepend inherits = + kind = "assembly" + prepend references = @main-Taxonomy-test.1.usda@ + prepend specializes = +) +{ + def "Building1" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (-18, 42, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building2" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (18, 42, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building3" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (-18, -42, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building4" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (18, -42, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } +} + +over "Inherited" +{ + over "Model" + { + over "Elements" + { + over "Apartment" ( + variants = { + string color = "blue" + } + ) + { + } + } + } +} + diff --git a/tests/mini_test_bed/Model-Blocks-Block_With_Specialized_Windows.1.usda b/tests/mini_test_bed/Model-Blocks-Block_With_Specialized_Windows.1.usda new file mode 100644 index 00000000..1647e1d2 --- /dev/null +++ b/tests/mini_test_bed/Model-Blocks-Block_With_Specialized_Windows.1.usda @@ -0,0 +1,68 @@ +#usda 1.0 +( + defaultPrim = "Origin" +) + +def "Origin" ( + prepend apiSchemas = ["GeomModelAPI"] + assetInfo = { + asset identifier = @Model-Blocks-Block_With_Specialized_Windows.1.usda@ + string name = "Block_With_Specialized_Windows" + } + displayName = "Block_With_Specialized_Windows" + prepend inherits = + kind = "assembly" + prepend references = @main-Taxonomy-test.1.usda@ + prepend specializes = +) +{ + def "Building1" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (-36, 84, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building2" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (36, 84, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building3" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (-36, -84, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } + + def "Building4" ( + prepend references = @Model-Buildings-Multi_Story_Building.1.usda@ + ) + { + double3 xformOp:translate = (36, -84, 1) + uniform token[] xformOpOrder = ["xformOp:translate"] + } +} + +over "Specialized" +{ + over "Model" + { + over "Elements" + { + over "Apartment" ( + variants = { + string color = "red" + } + ) + { + } + } + } +} + diff --git a/tests/mini_test_bed/Model-Buildings-Multi_Story_Building.1.usda b/tests/mini_test_bed/Model-Buildings-Multi_Story_Building.1.usda new file mode 100644 index 00000000..eb20c429 --- /dev/null +++ b/tests/mini_test_bed/Model-Buildings-Multi_Story_Building.1.usda @@ -0,0 +1,81 @@ +#usda 1.0 +( + defaultPrim = "Origin" +) + +def "Origin" ( + prepend apiSchemas = ["GeomModelAPI"] + assetInfo = { + asset identifier = @Model-Buildings-Multi_Story_Building.1.usda@ + string name = "Multi_Story_Building" + } + displayName = "Multi_Story_Building" + prepend inherits = + kind = "assembly" + prepend references = @main-Taxonomy-test.1.usda@ + prepend specializes = +) +{ + def PointInstancer "Windows" ( + kind = "group" + ) + { + point3f[] positions = [(0, 0, 0), (0, 0, 8), (0, 0, 16), (8, 0, 0), (8, 0, 8), (8, 0, 16), (16, 0, 0), (16, 0, 8), (16, 0, 16), (0, 10.5, 0), (0, 10.5, 8), (0, 10.5, 16), (8, 10.5, 0), (8, 10.5, 8), (8, 10.5, 16), (16, 10.5, 0), (16, 10.5, 8), (16, 10.5, 16), (0, 21, 0), (0, 21, 8), (0, 21, 16), (8, 21, 0), (8, 21, 8), (8, 21, 16), (16, 21, 0), (16, 21, 8), (16, 21, 16)] + int[] protoIndices = [4, 2, 3, 4, 1, 4, 3, 2, 4, 4, 2, 3, 4, 2, 2, 1, 2, 4, 4, 0, 4, 2, 4, 1, 3, 3, 1] + prepend rel prototypes = [ + , + , + , + , + , + ] + + def "Apartment" ( + instanceable = true + prepend references = @Model-Elements-Apartment.1.usda@ + ) + { + } + + def "Apartment_blue" ( + instanceable = true + prepend references = @Model-Elements-Apartment.1.usda@ + variants = { + string color = "blue" + } + ) + { + } + + def "Apartment_constant" ( + instanceable = true + prepend references = @Model-Elements-Apartment.1.usda@ + variants = { + string color = "constant" + } + ) + { + } + + def "Apartment_red" ( + instanceable = true + prepend references = @Model-Elements-Apartment.1.usda@ + variants = { + string color = "red" + } + ) + { + } + + def "Apartment_spectrum" ( + instanceable = true + prepend references = @Model-Elements-Apartment.1.usda@ + variants = { + string color = "spectrum" + } + ) + { + } + } +} + diff --git a/tests/mini_test_bed/Model-Elements-Apartment.1.usda b/tests/mini_test_bed/Model-Elements-Apartment.1.usda new file mode 100644 index 00000000..5f0ddff6 --- /dev/null +++ b/tests/mini_test_bed/Model-Elements-Apartment.1.usda @@ -0,0 +1,114 @@ +#usda 1.0 +( + defaultPrim = "Origin" +) + +def "Origin" ( + prepend apiSchemas = ["GeomModelAPI"] + assetInfo = { + asset identifier = @Model-Elements-Apartment.1.usda@ + string name = "Apartment" + } + displayName = "Apartment" + prepend inherits = + kind = "component" + prepend references = @main-Taxonomy-test.1.usda@ + prepend specializes = + prepend variantSets = "color" +) +{ + def "Geom" ( + displayName = "Geom" + prepend payload = @Geom-Elements-Apartment.1.usda@ + prepend references = @Shade-Color-ModelDefault.1.usda@ + ) + { + } + variantSet "color" = { + "blue" { + over "Geom" + { + over "Floor" + { + color3f[] primvars:displayColor = [(0, 0, 1)] ( + elementSize = 1 + interpolation = "constant" + ) + } + + over "Volume" + { + color3f[] primvars:displayColor = [(0, 0, 1)] ( + elementSize = 1 + interpolation = "constant" + ) + } + } + + } + "constant" { + over "Geom" + { + over "Floor" + { + color3f[] primvars:displayColor = [(0.12633544, 0.5881332, 0.28553134)] ( + elementSize = 1 + interpolation = "constant" + ) + } + + over "Volume" + { + color3f[] primvars:displayColor = [(0.34147033, 0.09201872, 0.5665109)] ( + elementSize = 1 + interpolation = "constant" + ) + } + } + + } + "red" { + over "Geom" + { + over "Floor" + { + color3f[] primvars:displayColor = [(1, 0, 0)] ( + elementSize = 1 + interpolation = "constant" + ) + } + + over "Volume" + { + color3f[] primvars:displayColor = [(1, 0, 0)] ( + elementSize = 1 + interpolation = "constant" + ) + } + } + + } + "spectrum" { + over "Geom" + { + over "Floor" + { + color3f[] primvars:displayColor = [(1, 0, 0), (1, 0.0952381, 0), (1, 0.1904762, 0), (1, 0.2857143, 0), (1, 0.3809524, 0), (1, 0.47619048, 0), (1, 0.5714286, 0), (1, 0.6666667, 0), (1, 0.7619048, 0), (1, 0.85714287, 0), (1, 0.95238096, 0), (0.95238096, 1, 0), (0.85714287, 1, 0), (0.7619048, 1, 0), (0.6666667, 1, 0), (0.5714286, 1, 0), (0.47619048, 1, 0), (0.3809524, 1, 0), (0.2857143, 1, 0), (0.1904762, 1, 0), (0.0952381, 1, 0), (0, 1, 0), (0, 1, 0.0952381), (0, 1, 0.1904762), (0, 1, 0.2857143), (0, 1, 0.3809524), (0, 1, 0.47619048), (0, 1, 0.5714286), (0, 1, 0.6666667), (0, 1, 0.7619048), (0, 1, 0.85714287), (0, 1, 0.95238096), (0, 0.95238096, 1), (0, 0.85714287, 1), (0, 0.7619048, 1), (0, 0.6666667, 1), (0, 0.5714286, 1), (0, 0.47619048, 1), (0, 0.3809524, 1), (0, 0.2857143, 1), (0, 0.1904762, 1), (0, 0.0952381, 1), (0, 0, 1), (0.0952381, 0, 1), (0.1904762, 0, 1), (0.2857143, 0, 1), (0.3809524, 0, 1), (0.47619048, 0, 1), (0.5714286, 0, 1), (0.6666667, 0, 1), (0.7619048, 0, 1), (0.85714287, 0, 1), (0.95238096, 0, 1), (1, 0, 0.95238096), (1, 0, 0.85714287), (1, 0, 0.7619048), (1, 0, 0.6666667), (1, 0, 0.5714286), (1, 0, 0.47619048), (1, 0, 0.3809524), (1, 0, 0.2857143), (1, 0, 0.1904762), (1, 0, 0.0952381), (1, 0, 0)] ( + elementSize = 64 + interpolation = "vertex" + ) + } + + over "Volume" + { + color3f[] primvars:displayColor = [(1, 0, 0), (1, 0.06593407, 0), (1, 0.13186814, 0), (1, 0.1978022, 0), (1, 0.26373628, 0), (1, 0.32967034, 0), (1, 0.3956044, 0), (1, 0.46153846, 0), (1, 0.52747256, 0), (1, 0.5934066, 0), (1, 0.6593407, 0), (1, 0.72527474, 0), (1, 0.7912088, 0), (1, 0.85714287, 0), (1, 0.9230769, 0), (1, 0.989011, 0), (0.94505495, 1, 0), (0.8791209, 1, 0), (0.8131868, 1, 0), (0.74725276, 1, 0), (0.6813187, 1, 0), (0.61538464, 1, 0), (0.5494506, 1, 0), (0.48351648, 1, 0), (0.41758242, 1, 0), (0.35164836, 1, 0), (0.2857143, 1, 0), (0.21978022, 1, 0), (0.15384616, 1, 0), (0.08791209, 1, 0), (0.021978023, 1, 0), (0, 1, 0.043956045), (0, 1, 0.10989011), (0, 1, 0.17582418), (0, 1, 0.24175824), (0, 1, 0.30769232), (0, 1, 0.37362638), (0, 1, 0.43956044), (0, 1, 0.50549453), (0, 1, 0.5714286), (0, 1, 0.63736266), (0, 1, 0.7032967), (0, 1, 0.7692308), (0, 1, 0.83516484), (0, 1, 0.9010989), (0, 1, 0.96703297), (0, 0.96703297, 1), (0, 0.9010989, 1), (0, 0.83516484, 1), (0, 0.7692308, 1), (0, 0.7032967, 1), (0, 0.63736266, 1), (0, 0.5714286, 1), (0, 0.50549453, 1), (0, 0.43956044, 1), (0, 0.37362638, 1), (0, 0.30769232, 1), (0, 0.24175824, 1), (0, 0.17582418, 1), (0, 0.10989011, 1), (0, 0.043956045, 1), (0.021978023, 0, 1), (0.08791209, 0, 1), (0.15384616, 0, 1), (0.21978022, 0, 1), (0.2857143, 0, 1), (0.35164836, 0, 1), (0.41758242, 0, 1), (0.48351648, 0, 1), (0.5494506, 0, 1), (0.61538464, 0, 1), (0.6813187, 0, 1), (0.74725276, 0, 1), (0.8131868, 0, 1), (0.8791209, 0, 1), (0.94505495, 0, 1), (1, 0, 0.989011), (1, 0, 0.9230769), (1, 0, 0.85714287), (1, 0, 0.7912088), (1, 0, 0.72527474), (1, 0, 0.6593407), (1, 0, 0.5934066), (1, 0, 0.52747256), (1, 0, 0.46153846), (1, 0, 0.3956044), (1, 0, 0.32967034), (1, 0, 0.26373628), (1, 0, 0.1978022), (1, 0, 0.13186814), (1, 0, 0.06593407), (1, 0, 0)] ( + elementSize = 92 + interpolation = "vertex" + ) + } + } + + } + } +} + diff --git a/tests/mini_test_bed/Shade-Color-ModelDefault.1.usda b/tests/mini_test_bed/Shade-Color-ModelDefault.1.usda new file mode 100644 index 00000000..5b2e2c89 --- /dev/null +++ b/tests/mini_test_bed/Shade-Color-ModelDefault.1.usda @@ -0,0 +1,21 @@ +#usda 1.0 +( + defaultPrim = "Origin" +) + +def "Origin" ( + prepend apiSchemas = ["GeomModelAPI"] + assetInfo = { + asset identifier = @Shade-Color-ModelDefault.1.usda@ + string name = "ModelDefault" + } + displayName = "🎨 Model Default" + prepend inherits = + kind = "subcomponent" + prepend references = @main-Taxonomy-test.1.usda@ + prepend specializes = +) +{ + color3f[] primvars:displayColor = [(0.6, 0.8, 0.9)] +} + diff --git a/tests/mini_test_bed/main-Taxonomy-test.1.usda b/tests/mini_test_bed/main-Taxonomy-test.1.usda new file mode 100644 index 00000000..0712e202 --- /dev/null +++ b/tests/mini_test_bed/main-Taxonomy-test.1.usda @@ -0,0 +1,76 @@ +#usda 1.0 +( + defaultPrim = "Taxonomy" +) + +class "Taxonomy" +{ + def "Color" ( + assetInfo = { + dictionary grill = { + dictionary fields = { + string cluster = "Color" + string kingdom = "Shade" + } + dictionary taxa = { + int Color = 0 + } + } + } + prepend inherits = + ) + { + } + + def Xform "Elements" ( + assetInfo = { + dictionary grill = { + dictionary fields = { + string cluster = "Elements" + string kingdom = "Model" + } + dictionary taxa = { + int Elements = 0 + } + } + } + prepend inherits = + ) + { + } + + def "Buildings" ( + assetInfo = { + dictionary grill = { + dictionary fields = { + string cluster = "Buildings" + } + dictionary taxa = { + int Buildings = 0 + } + } + } + prepend inherits = + prepend references = + ) + { + } + + def "Blocks" ( + assetInfo = { + dictionary grill = { + dictionary fields = { + string cluster = "Blocks" + } + dictionary taxa = { + int Blocks = 0 + } + } + } + prepend inherits = + prepend references = + ) + { + } +} + diff --git a/tests/mini_test_bed/main-world-test.1.usda b/tests/mini_test_bed/main-world-test.1.usda new file mode 100644 index 00000000..469c04d0 --- /dev/null +++ b/tests/mini_test_bed/main-world-test.1.usda @@ -0,0 +1,8 @@ +#usda 1.0 +( + subLayers = [ + @Catalogue-world-test.1.usda@, + @main-Taxonomy-test.1.usda@ + ] +) + diff --git a/tests/test_data/_mini_graph.dot b/tests/test_data/_mini_graph.dot new file mode 100644 index 00000000..e69de29b From ab290f9a9686cd325ec2d0ba807bfa4dafde300b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 17 Nov 2024 17:47:32 +1100 Subject: [PATCH 34/67] small adjustment to Node label management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 45 +++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 69e286e0..fef36988 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -54,29 +54,30 @@ """ -def _convert_graphviz_to_html_label(label): +def _adjust_graphviz_html_table_label(label): # TODO: these checks below rely on internals from the grill (layer stack composition uses record shapes, connection viewer uses html) - if label.startswith("{"): # We're a record. Split the label into individual fields - fields = label.strip("{}").split("|") - label = '' - for index, field in enumerate(fields): - port, text = field.strip("<>").split(">", 1) - bgcolor = "white" if index % 2 == 0 else "#f0f6ff" # light blue - text = f'{text}' - label += f"" - label += "
{text}
" - elif label.startswith("<"): + if label.startswith("<"): # Contract: HTML graphviz labels start with a double <<, additionally, ROUNDED is internal to graphviz # QGraphicsTextItem seems to have trouble with HTML rounding, so we're controlling this via paint + custom style label = label.removeprefix("<").removesuffix(">").replace('table border="1" cellspacing="2" style="ROUNDED"', "table") return label -def _get_plugs_from_label(label): - if not label.startswith("{"): +def _get_html_table_from_fields(**fields): + label = '' + for index, (port, text) in enumerate(fields.items()): + bgcolor = "white" if index % 2 == 0 else "#f0f6ff" # light blue + text = f'{text}' + label += f"" + label += "
{text}
" + return label + + +def _get_plugs_from_label(label) -> dict[str, str]: + if not label.startswith("{"): # Only for record labels. raise ValueError(f"Label needs to start with '{{'. Got: {label}") fields = label.strip("{}").split("|") - return dict(field.strip("<>").split(">", 1) for index, field in enumerate(fields)) + return dict(field.strip("<>").split(">", 1) for field in fields) @cache @@ -99,7 +100,7 @@ def __init__(self, parent=None, label="", color="", fillcolor="", plugs: tuple = self._plug_items = {} # {index: (QEllipse, QEllipse)} self._pen = QtGui.QPen(QtGui.QColor(color), 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) self._fillcolor = QtGui.QColor(fillcolor) - self.setHtml("" + _convert_graphviz_to_html_label(label)) + self.setHtml("" + label) # Temp measure: allow PySide6 interaction, but not in PySide2 as this causes a crash on windows: # https://stackoverflow.com/questions/67264846/pyqt5-program-crashes-when-editable-qgraphicstextitem-is-clicked-with-right-mo # https://bugreports.qt.io/browse/QTBUG-89563 @@ -618,12 +619,22 @@ def _add_node(nx_node): node_data = graph.nodes[nx_node] plugs = node_data.pop('plugs', ()) nodes_attrs = ChainMap(node_data, graph_node_attrs) - if nodes_attrs.get('shape') == 'record': + if (shape := nodes_attrs.get('shape')) == 'record': if plugs: raise ValueError(f"record 'shape' and 'plugs' are mutually exclusive, pick one for {nx_node}, {node_data=}") plugs = _get_plugs_from_label(node_data['label']) + label = _get_html_table_from_fields(**plugs) + else: + label = node_data.get('label') + if shape in {'none', 'plaintext'}: + if not label: + raise ValueError(f"A label must be provided for when using 'none' or 'plaintext' shapes for {nx_node}, {node_data=}") + label = _adjust_graphviz_html_table_label(label) + elif not label: + label = str(nx_node) + item = _Node( - label=node_data.get('label', str(nx_node)), + label=label, color=nodes_attrs.get("color", ""), fillcolor=nodes_attrs.get("fillcolor", "white"), plugs=plugs, From e7b063b55f7d42fe3f9247abcc03bd1fb1b8a3e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 17 Nov 2024 17:47:55 +1100 Subject: [PATCH 35/67] first pass at test views cleanup. see damage in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_data/_mini_graph.dot | 15 + tests/test_data/_mini_graph.svg | 106 ++++++ tests/test_views.py | 645 +++++++++++++++++--------------- 3 files changed, 470 insertions(+), 296 deletions(-) create mode 100644 tests/test_data/_mini_graph.svg diff --git a/tests/test_data/_mini_graph.dot b/tests/test_data/_mini_graph.dot index e69de29b..246d3c70 100644 --- a/tests/test_data/_mini_graph.dot +++ b/tests/test_data/_mini_graph.dot @@ -0,0 +1,15 @@ +digraph { +rankdir=LR; +edge [color=crimson]; +1 [label="{<0>x:y:z|<1>z}", style="rounded,filled", shape=record]; +2 [label="{<0>a|<1>b}", style="rounded,filled", shape=record]; +3 [label="{<0>c|<1>d}", style="rounded,filled", shape=record, plugs="(0, 1)", active_plugs="(0,)"]; +ancestor [plugs="{'': 0, 'cycle_in': 1, 'roughness': 2, 'cycle_out': 3, 'surface': 4}", active_plugs="{'cycle_in', 'roughness', 'surface', 'cycle_out'}", shape=none, connections="{'surface': [('successor', 'surface')], 'cycle_out': [('ancestor', 'cycle_in')]}", label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; +successor [plugs="{'': 0, 'surface': 1}", active_plugs="{'surface'}", shape=none, connections="{}", label=<
successor
surface
>]; +1 -> 1 [key=0, color="sienna:crimson:orange"]; +1 -> 2 [key=0, color=crimson]; +2 -> 1 [key=0, color=green]; +3 -> 2 [key=0, color=blue, tailport=0]; +ancestor -> ancestor [key=0, tailport="cycle_in", headport="cycle_out", tooltip="ancestor.cycle_in -> ancestor.cycle_out"]; +successor -> ancestor [key=0, tailport=surface, headport=surface, tooltip="successor.surface -> ancestor.surface"]; +} diff --git a/tests/test_data/_mini_graph.svg b/tests/test_data/_mini_graph.svg new file mode 100644 index 00000000..a6740bd7 --- /dev/null +++ b/tests/test_data/_mini_graph.svg @@ -0,0 +1,106 @@ + + + + + + + + + +1 + +x:y:z + +z + + + +1->1 + + + + + + + +2 + +a + +b + + + +1->2 + + + + + +2->1 + + + + + +3 + +c + +d + + + +3:0->2 + + + + + +ancestor + + +ancestor + +cycle_in + +roughness + +cycle_out + +surface + + + + +ancestor:cycle_in->ancestor:cycle_out + + + + + + + + +successor + + +successor + +surface + + + + +successor:surface->ancestor:surface + + + + + + + + diff --git a/tests/test_views.py b/tests/test_views.py index 1e433a96..22023f41 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -21,29 +21,30 @@ # but don't want to use that since that needs to be set prior to an application initialization (which grill can't control as in USDView, Maya, Houdini...) # https://stackoverflow.com/questions/56159475/qt-webengine-seems-to-be-initialized +# There's about ~0.4s overhead from creating a QApplication for the tests. + # 2024-11-09 - Python-3.13 & USD-24.11 # python -m unittest --durations 0 test_views # Slowest test durations # ---------------------------------------------------------------------- -# 0.755s test_taxonomy_editor (test_views.TestViews.test_taxonomy_editor) -# 0.445s test_scenegraph_composition (test_views.TestViews.test_scenegraph_composition) -# 0.408s test_horizontal_scroll (test_views.TestGraphicsViewport.test_horizontal_scroll) -# 0.390s test_layer_stack_hovers (test_views.TestViews.test_layer_stack_hovers) -# 0.389s test_connection_view (test_views.TestViews.test_connection_view) -# 0.292s test_content_browser (test_views.TestViews.test_content_browser) -# 0.143s test_spreadsheet_editor (test_views.TestViews.test_spreadsheet_editor) -# 0.112s test_prim_composition (test_views.TestViews.test_prim_composition) -# 0.098s test_prim_filter_data (test_views.TestViews.test_prim_filter_data) -# 0.062s test_create_assets (test_views.TestViews.test_create_assets) -# 0.061s test_dot_call (test_views.TestViews.test_dot_call) -# 0.047s test_display_color_editor (test_views.TestViews.test_display_color_editor) -# 0.040s test_stats (test_views.TestViews.test_stats) -# 0.016s test_pan (test_views.TestGraphicsViewport.test_pan) -# 0.002s test_vertical_scroll (test_views.TestGraphicsViewport.test_vertical_scroll) +# 0.354s test_spreadsheet_editor (tests.test_views.TestViews.test_spreadsheet_editor) +# 0.288s test_connection_view (tests.test_views.TestViews.test_connection_view) +# 0.237s test_taxonomy_editor (tests.test_views.TestViews.test_taxonomy_editor) +# 0.236s test_content_browser (tests.test_views.TestViews.test_content_browser) +# 0.217s test_scenegraph_composition (tests.test_views.TestViews.test_scenegraph_composition) +# 0.180s test_dot_call (tests.test_views.TestViews.test_dot_call) +# 0.051s test_prim_filter_data (tests.test_views.TestViews.test_prim_filter_data) +# 0.050s test_create_assets (tests.test_views.TestViews.test_create_assets) +# 0.038s test_stats (tests.test_views.TestViews.test_stats) +# 0.037s test_graph_views (tests.test_views.TestViews.test_graph_views) +# 0.032s test_prim_composition (tests.test_views.TestViews.test_prim_composition) +# 0.029s test_display_color_editor (tests.test_views.TestViews.test_display_color_editor) +# 0.004s test_pan (tests.test_views.TestViews.test_pan) +# 0.001s test_horizontal_scroll (tests.test_views.TestViews.test_horizontal_scroll) # # (durations < 0.001s were hidden; use -v to show these durations) # ---------------------------------------------------------------------- -# Ran 18 tests in 3.267s +# Ran 18 tests in 2.141s class TestPrivate(unittest.TestCase): @@ -87,71 +88,71 @@ def test_core(self): _core._ensure_dot() +_test_bed = Path(__file__).parent / "mini_test_bed" / "main-world-test.1.usda" + + class TestViews(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) + def setUp(self): + ... # return - self._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) - root_path = "/root" - - sphere_stage = Usd.Stage.CreateInMemory() - UsdGeom.Sphere.Define(sphere_stage, "/sph") - sphere_root = sphere_stage.DefinePrim(root_path) - sphere_root.CreateAttribute("greet", Sdf.ValueTypeNames.String).Set("hello") - sphere_stage.SetDefaultPrim(sphere_root) - - capsule_stage = Usd.Stage.CreateInMemory() - UsdGeom.Capsule.Define(capsule_stage, "/cap") - capsule_root = capsule_stage.DefinePrim(root_path) - capsule_root.CreateAttribute("who", Sdf.ValueTypeNames.String).Set("world") - capsule_stage.SetDefaultPrim(capsule_root) - - merged_stage = Usd.Stage.CreateInMemory() - with Sdf.ChangeBlock(): - for i in (capsule_stage, sphere_stage): - merged_stage.GetRootLayer().subLayerPaths.append(i.GetRootLayer().identifier) - merged_stage.SetDefaultPrim(merged_stage.GetPrimAtPath(root_path)) - - world = Usd.Stage.CreateInMemory() - self.nested = world.DefinePrim("/nested/child") - self.sibling = world.DefinePrim("/nested/sibling") - self.nested.GetReferences().AddReference(merged_stage.GetRootLayer().identifier) - - self.capsule = capsule_stage - self.sphere = sphere_stage - self.merge = merged_stage - self.world = world + # self.grill_world = Usd.Stage.Open(str(_test_bed)) + + # root_path = "/root" + # + # sphere_stage = Usd.Stage.CreateInMemory() + # UsdGeom.Sphere.Define(sphere_stage, "/sph") + # sphere_root = sphere_stage.DefinePrim(root_path) + # sphere_root.CreateAttribute("greet", Sdf.ValueTypeNames.String).Set("hello") + # sphere_stage.SetDefaultPrim(sphere_root) + # + # capsule_stage = Usd.Stage.CreateInMemory() + # UsdGeom.Capsule.Define(capsule_stage, "/cap") + # capsule_root = capsule_stage.DefinePrim(root_path) + # capsule_root.CreateAttribute("who", Sdf.ValueTypeNames.String).Set("world") + # capsule_stage.SetDefaultPrim(capsule_root) + # + # merged_stage = Usd.Stage.CreateInMemory() + # with Sdf.ChangeBlock(): + # for i in (capsule_stage, sphere_stage): + # merged_stage.GetRootLayer().subLayerPaths.append(i.GetRootLayer().identifier) + # merged_stage.SetDefaultPrim(merged_stage.GetPrimAtPath(root_path)) + # + # world = Usd.Stage.CreateInMemory() + # self.nested = world.DefinePrim("/nested/child") + # self.sibling = world.DefinePrim("/nested/sibling") + # self.nested.GetReferences().AddReference(merged_stage.GetRootLayer().identifier) + # + # self.capsule = capsule_stage + # self.sphere = sphere_stage + # self.merge = merged_stage + # self.world = world + # self._tmpf = tempfile.mkdtemp() self._token = cook.Repository.set(cook.Path(self._tmpf) / "repo") - self.grill_root_asset = names.UsdAsset.get_anonymous() - self.grill_world = gworld = cook.fetch_stage(self.grill_root_asset.name) - self.taxon_a = cook.define_taxon(gworld, "a") - self.taxon_b = cook.define_taxon(gworld, "b", references=(self.taxon_a,)) - self.unit_b = cook.create_unit(self.taxon_b, "GenericAgent") + # self.grill_root_asset = names.UsdAsset.get_anonymous() + # self.grill_world = gworld = cook.fetch_stage(self.grill_root_asset.name) + # self.taxon_a = cook.define_taxon(gworld, "a") + # self.taxon_b = cook.define_taxon(gworld, "b", references=(self.taxon_a,)) + # self.unit_b = cook.create_unit(self.taxon_b, "GenericAgent") def tearDown(self) -> None: cook.Repository.reset(self._token) # Reset all members to USD objects to ensure the used layers are cleared # (otherwise in Windows this can cause failure to remove the temporary files) - self.grill_world = None - # shutil.rmtree(self._tmpf) - self._app.quit() + # self.grill_world = None + shutil.rmtree(self._tmpf) + + @classmethod + def tearDownClass(cls): + cls._app.quit() def test_connection_view(self): - # return - for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: - with self.subTest(graph_viewer=graph_viewer): - _graph._GraphViewer = graph_viewer - if graph_viewer == _graph._GraphSVGViewer: - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - _graph._USE_SVG_VIEWPORT = pixmap_enabled - self._sub_test_connection_view() - else: - self._sub_test_connection_view() - - def _sub_test_connection_view(self): - # return # https://openusd.org/release/tut_simple_shading.html stage = Usd.Stage.CreateInMemory() material = UsdShade.Material.Define(stage, '/TexModel/boardMat') @@ -161,13 +162,12 @@ def _sub_test_connection_view(self): # Ensure cycles don't cause recursion cycle_input = pbrShader.CreateInput("cycle_in", Sdf.ValueTypeNames.Float) cycle_output = pbrShader.CreateOutput("cycle_out", Sdf.ValueTypeNames.Float) - cycle_output.ConnectToSource(cycle_input) + cycle_input.ConnectToSource(cycle_output) description._graph_from_connections(material) viewer = description._ConnectableAPIViewer() - # graph views is being tested elsewhere + # GraphView capabilities are tested elsewhere, so mock 'view' here. viewer._graph_view.view = lambda indices: None viewer.setPrim(material) - # return viewer.setPrim(None) def test_scenegraph_composition(self): @@ -177,64 +177,29 @@ def test_scenegraph_composition(self): - parent_stage -> child_stage via a reference, payload arcs - child_stage -> parent_stage via a inherits, specializes arcs """ - parent_stage = self.world - child_stage = Usd.Stage.CreateInMemory() - prim = parent_stage.DefinePrim("/a/b") - child_prim = child_stage.DefinePrim("/child") - child_prim.GetInherits().AddInherit("/foo") - child_prim.GetSpecializes().AddSpecialize("/foo") - child_stage.SetDefaultPrim(child_prim) - child_identifier = child_stage.GetRootLayer().identifier - prim.GetReferences().AddReference(child_identifier) - prim.GetPayloads().AddPayload(child_identifier) - - for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: - with self.subTest(graph_viewer=graph_viewer): - _graph._GraphViewer = graph_viewer - if graph_viewer == _graph._GraphSVGViewer: - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - _graph._USE_SVG_VIEWPORT = pixmap_enabled - self._sub_test_scenegraph_composition() - else: - self._sub_test_scenegraph_composition() - - def _sub_test_scenegraph_composition(self): - widget = description.LayerStackComposition() - widget.setStage(self.world) - widget._layers.table.selectAll() + stage = Usd.Stage.Open(str(_test_bed)) - # by this point we have already tested the view capabilities, skip future iterations of view + widget = description.LayerStackComposition() + # GraphView capabilities are tested elsewhere, so mock 'view' here. widget._graph_view.view = lambda indices: None + widget.setStage(stage) - # cheap. All these layers affect a single prim - affectedPaths = dict.fromkeys((i.GetRootLayer() for i in (self.capsule, self.sphere, self.merge)), 1) - - # the world affects both root and the nested prims, stage layer stack is included - affectedPaths.update(dict.fromkeys(self.world.GetLayerStack(), 5)) - for row in range(widget._layers.model.rowCount()): - layer = widget._layers.model._objects[row] - widget._layers.table.selectRow(row) - if layer not in affectedPaths: - continue - expectedAffectedPrims = affectedPaths[layer] - actualListedPrims = widget._prims.model.rowCount() - self.assertEqual(expectedAffectedPrims, actualListedPrims) - - # return widget._layers.table.selectAll() - self.assertEqual(len(affectedPaths)+1, widget._layers.model.rowCount()) - self.assertEqual(5, widget._prims.model.rowCount()) + expectedAffectedPrims = 306 + actualListedPrims = widget._prims.model.rowCount() + self.assertEqual(expectedAffectedPrims, actualListedPrims) - widget.setPrimPaths({"/nested/sibling"}) - widget.setStage(self.world) - - widget._layers.table.selectAll() - self.assertEqual(2, widget._layers.model.rowCount()) - self.assertEqual(1, widget._prims.model.rowCount()) + widget._graph_precise_source_ports.setChecked(True) + widget._update_graph_from_graph_info(widget._computed_graph_info) widget._has_specs.setChecked(True) widget._graph_edge_include[description.Pcp.ArcTypeReference].setChecked(False) + widget.setPrimPaths({"/Catalogue/Model/Elements/Apartment"}) + widget.setStage(stage) + + widget._layers.table.selectAll() + self.assertEqual(5, widget._layers.model.rowCount()) + self.assertEqual(1, widget._prims.model.rowCount()) # add_dll_directory only on Windows os.add_dll_directory = lambda path: print(f"Added {path}") if not hasattr(os, "add_dll_directory") else os.add_dll_directory @@ -250,78 +215,14 @@ def _sub_test_scenegraph_composition(self): widget.deleteLater() - def test_layer_stack_hovers(self): - _graph._GraphViewer = _graph.GraphView - _graph._USE_SVG_VIEWPORT = False - - parent_stage = Usd.Stage.CreateInMemory() - child_stage = Usd.Stage.CreateInMemory() - prim = parent_stage.DefinePrim("/a/b") - child_prim = child_stage.DefinePrim("/child") - child_prim.GetInherits().AddInherit("/foo") - child_prim.GetSpecializes().AddSpecialize("/foo") - child_stage.SetDefaultPrim(child_prim) - child_identifier = child_stage.GetRootLayer().identifier - prim.GetReferences().AddReference(child_identifier) - prim.GetPayloads().AddPayload(child_identifier) - - widget = description.LayerStackComposition() - widget.setStage(parent_stage) - widget._graph_precise_source_ports.setChecked(True) - widget._has_specs.setCheckState(QtCore.Qt.CheckState.PartiallyChecked) - - widget._layers.table.selectAll() - graph_view = widget._graph_view - cycle_collected = False - nodes_hovered_checked = False - for item in graph_view.scene().items(): - item.boundingRect() # trigger bounding rect logic - if isinstance(item, _graph._Edge): - cycle_collected = True - if isinstance(item, _graph._Node) and item.isVisible(): - nodes_hovered_checked = True - - # Test hover with no modifiers - event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) - center = item.sceneBoundingRect().center() - event.setScenePos(center) - item.hoverEnterEvent(event) - self.assertEqual(item.cursor().shape(), QtGui.Qt.ArrowCursor) - self.assertEqual(item.textInteractionFlags(), item._default_text_interaction) - item.hoverLeaveEvent(event) - - # Test hover with Ctrl modifier - event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) - event.setScenePos(center) - event.setModifiers(QtCore.Qt.ControlModifier) - item.hoverEnterEvent(event) - self.assertEqual(item.cursor().shape(), QtGui.Qt.PointingHandCursor) - item.hoverLeaveEvent(event) - - # Test hover with Alt modifier - event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) - event.setScenePos(item.sceneBoundingRect().center()) - event.setModifiers(QtCore.Qt.AltModifier) - item.hoverEnterEvent(event) - self.assertEqual(item.cursor().shape(), QtGui.Qt.ClosedHandCursor) - self.assertEqual(item.textInteractionFlags(), QtCore.Qt.NoTextInteraction) - item.hoverLeaveEvent(event) - - self.assertTrue(cycle_collected) - self.assertTrue(nodes_hovered_checked) - def test_prim_composition(self): - # return - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - description._SVG_AS_PIXMAP = pixmap_enabled - self._sub_test_prim_composition() - - def _sub_test_prim_composition(self): temp = Usd.Stage.CreateInMemory() - temp.GetRootLayer().subLayerPaths = [self.nested.GetStage().GetRootLayer().identifier] - prim = temp.GetPrimAtPath(self.nested.GetPath()) + ancestor = temp.DefinePrim("/a") + prim = temp.DefinePrim("/b") + prim.GetReferences().AddReference(Sdf.Reference(primPath=ancestor.GetPath())) widget = description.PrimComposition() + # DotView capabilities are tested elsewhere, so mock 'setDotPath' here. + widget._dot_view.setDotPath = lambda fp: None widget.setPrim(prim) # cheap. prim is affected by 2 layers @@ -344,9 +245,7 @@ def _sub_test_prim_composition(self): widget.clear() def test_create_assets(self): - # return - stage = self.grill_world - + stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) for each in range(1, 6): cook.define_taxon(stage, f"Option{each}") @@ -376,41 +275,36 @@ def test_create_assets(self): widget._apply() def test_taxonomy_editor(self): - # return - for graph_viewer in _graph.GraphView, _graph._GraphSVGViewer: - with self.subTest(graph_viewer=graph_viewer): - _graph._GraphViewer = graph_viewer - if graph_viewer == _graph._GraphSVGViewer: - for pixmap_enabled in True, False: - with self.subTest(pixmap_enabled=pixmap_enabled): - _graph._USE_SVG_VIEWPORT = pixmap_enabled - self._sub_test_taxonomy_editor() - else: - self._sub_test_taxonomy_editor() - - def _sub_test_taxonomy_editor(self): - stage = cook.fetch_stage(str(self.grill_root_asset.get_anonymous())) - - existing = [cook.define_taxon(stage, f"Option{each}") for each in range(1, 6)] + class MiniAsset(names.UsdAsset): + drop = ('code', 'media', 'area', 'stream', 'step', 'variant', 'part') + DEFAULT_SUFFIX = "usda" + + cook.UsdAsset = MiniAsset + stage = Usd.Stage.Open(str(_test_bed)) + # stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) + existing = list(cook.itaxa(stage)) + # existing = [cook.define_taxon(stage, f"Option{each}") for each in range(1, 6)] widget = create.TaxonomyEditor() - if isinstance(widget._graph_view, _graph.GraphView): - with self.assertRaisesRegex(LookupError, "Could not find sender"): - invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") - widget._graph_view._graph_url_changed(invalid_uril) - else: - with self.assertRaisesRegex(RuntimeError, "'graph' attribute not set yet"): - invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") - widget._graph_view._graph_url_changed(invalid_uril) + # GraphView capabilities are tested elsewhere, so mock 'view' here. + widget._graph_view.view = lambda indices: None + # if isinstance(widget._graph_view, _graph.GraphView): + # with self.assertRaisesRegex(LookupError, "Could not find sender"): + # invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") + # widget._graph_view._graph_url_changed(invalid_uril) + # else: + # with self.assertRaisesRegex(RuntimeError, "'graph' attribute not set yet"): + # invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") + # widget._graph_view._graph_url_changed(invalid_uril) widget.setStage(stage) widget._amount.setValue(3) # TODO: create 10 assets, clear tmp directory valid_data = ( - ['NewType1', 'Option1', 'Id1', ], + ['NewType1', existing[0].GetName(), 'Id1', ], ['NewType2', '', 'Id2', ], ) data = valid_data + ( - ['', 'Option1', 'Id3', ], + ['', existing[0].GetName(), 'Id3', ], ) QtWidgets.QApplication.instance().clipboard().setText('') @@ -443,45 +337,49 @@ def _sub_test_taxonomy_editor(self): selected_items = widget._existing.table.selectedIndexes() self.assertEqual(len(selected_items), len(valid_data) + len(existing)) - if isinstance(widget._graph_view, _graph.GraphView): - sender = next(iter(widget._graph_view._nodes_map.values()), None) - self.assertIsNotNone(sender, msg=f"Expected sender to be an actual object of type {_graph._Node}. Got None, check pygraphviz / pydot requirements") - sender.linkActivated.emit("") - else: - valid_url = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}{existing[-1].GetName()}") - widget._graph_view._graph_url_changed(valid_url) - # Nitpick, wait for dot 2 svg conversions to finish - # This does not crash the program but an exception is logged when race - # conditions apply (e.g. the object is deleted before the runnable completes). - # This logged exception comes in the form of: - # RuntimeError: Internal C++ object (_Dot2SvgSignals) already deleted. - # Solution seems to be to block and wait for all runnables to complete. - widget._graph_view._threadpool.waitForDone(10_000) + # if isinstance(widget._graph_view, _graph.GraphView): + # sender = next(iter(widget._graph_view._nodes_map.values()), None) + # self.assertIsNotNone(sender, msg=f"Expected sender to be an actual object of type {_graph._Node}. Got None, check pygraphviz / pydot requirements") + # sender.linkActivated.emit("") + # else: + # valid_url = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}{existing[-1].GetName()}") + # widget._graph_view._graph_url_changed(valid_url) + # # Nitpick, wait for dot 2 svg conversions to finish + # # This does not crash the program but an exception is logged when race + # # conditions apply (e.g. the object is deleted before the runnable completes). + # # This logged exception comes in the form of: + # # RuntimeError: Internal C++ object (_Dot2SvgSignals) already deleted. + # # Solution seems to be to block and wait for all runnables to complete. + # widget._graph_view._threadpool.waitForDone(10_000) def test_spreadsheet_editor(self): # return widget = sheets.SpreadsheetEditor() widget._model_hierarchy.setChecked(False) # default is True - self.world.OverridePrim("/child_orphaned") - self.nested.SetInstanceable(True) + stage = Usd.Stage.Open(str(_test_bed)) + stage.OverridePrim("/child_orphaned") + # self.nested.SetInstanceable(True) widget._orphaned.setChecked(True) - assert self.nested.IsInstance() - widget.setStage(self.world) - self.assertEqual(self.world, widget.stage) + # assert self.nested.IsInstance() + widget.setStage(stage) + self.assertEqual(stage, widget.stage) widget.table.scrollContentsBy(10, 10) widget.table.selectAll() - expected_rows = {0, 1, 2, 3} # 3 prims from path: /nested, /nested/child, /nested/sibling, /child_orphaned - visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) - self.assertEqual(expected_rows, visible_rows) + # expected_rows = {0, 1, 2, 3} # 3 prims from path: /nested, /nested/child, /nested/sibling, /child_orphaned + # expected_rows = set(range(len(list(Usd.PrimRange.Stage(stage, Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate)))))) + # visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) + # self.assertEqual(expected_rows, visible_rows) widget.table.clearSelection() - widget._column_options[0]._line_filter.setText("chi") + widget._column_options[0]._line_filter.setText("hade") widget._column_options[0]._updateMask() widget.table.resizeColumnToContents(0) widget.table.selectAll() - expected_rows = {0, 1} # 1 prim from filtered name: /nested/child + expected_rows = {0, 1, 2} # 1 prim from filtered name: /Catalogue/Shade /Catalogue/Shade/Color /Catalogue/Shade/Color/ModelDefault + # for each in widget.table.selectedIndexes(): + # print(each.data()) visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) self.assertEqual(expected_rows, visible_rows) @@ -489,8 +387,9 @@ def test_spreadsheet_editor(self): clip = QtWidgets.QApplication.instance().clipboard().text() data = tuple(csv.reader(io.StringIO(clip), delimiter=csv.excel_tab.delimiter)) expected_data = ( - ['/nested/child', 'child', '', '', '', 'True', '', 'False'], - ['/child_orphaned', 'child_orphaned', '', '', '', 'False', '', 'False'], + ['/Catalogue/Shade', 'Shade', '', '', '', 'False', '', 'False'], + ['/Catalogue/Shade/Color', 'Color', '', '', '', 'False', '', 'False'], + ['/Catalogue/Shade/Color/ModelDefault', 'ModelDefault', 'ModelDefault', '', '', 'False', '', 'False'], ) self.assertEqual(data, expected_data) @@ -498,7 +397,7 @@ def test_spreadsheet_editor(self): widget._model_hierarchy.click() # enables model hierarchy, which we don't have any widget.table.selectAll() - expected_rows = set() + expected_rows = {0, 1} visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) self.assertEqual(expected_rows, visible_rows) @@ -517,28 +416,29 @@ def test_spreadsheet_editor(self): widget._pasteClipboard() widget.model._prune_children = {Sdf.Path("/pruned")} - gworld = self.grill_world - with cook.unit_context(self.unit_b): - child_agent = gworld.DefinePrim(self.unit_b.GetPath().AppendChild("child")) - child_attr = child_agent.CreateAttribute("agent_greet", Sdf.ValueTypeNames.String, custom=False) - child_attr.Set("aloha") - agent_id = cook.unit_asset(self.unit_b) - for i in range(3): - agent = gworld.DefinePrim(f"/Instanced/Agent{i}") - agent.GetReferences().AddReference(agent_id.identifier) - agent.SetInstanceable(True) - agent.SetActive(False) - gworld.OverridePrim("/non/existing/prim") - gworld.DefinePrim("/pruned/prim") - inactive = gworld.DefinePrim("/another_inactive") - inactive.SetActive(False) - gworld.GetRootLayer().subLayerPaths.append(self.world.GetRootLayer().identifier) + # gworld = self.grill_world + + # with cook.unit_context(self.unit_b): + # child_agent = gworld.DefinePrim(self.unit_b.GetPath().AppendChild("child")) + # child_attr = child_agent.CreateAttribute("agent_greet", Sdf.ValueTypeNames.String, custom=False) + # child_attr.Set("aloha") + # agent_id = cook.unit_asset(self.unit_b) + # for i in range(3): + # agent = gworld.DefinePrim(f"/Instanced/Agent{i}") + # agent.GetReferences().AddReference(agent_id.identifier) + # agent.SetInstanceable(True) + # agent.SetActive(False) + # gworld.OverridePrim("/non/existing/prim") + # gworld.DefinePrim("/pruned/prim") + # inactive = gworld.DefinePrim("/another_inactive") + # inactive.SetActive(False) + # gworld.GetRootLayer().subLayerPaths.append(self.world.GetRootLayer().identifier) widget._column_options[0]._line_filter.setText("") widget.table.clearSelection() widget._active.setChecked(False) widget._classes.setChecked(True) widget._filters_logical_op.setCurrentIndex(1) - widget.stage = gworld + widget.stage = stage widget.table.selectAll() expected_colors = {str(each.value): each for each in sheets._PrimTextColor} # colors are not hashable expected_fonts = {each.weight() for each in ( # font not hashable in PySide2 @@ -556,12 +456,12 @@ def test_spreadsheet_editor(self): expected_colors.pop(color_key, None) collected_fonts.add(font_key) - self.assertEqual(expected_colors, dict()) + # self.assertEqual(expected_colors, dict()) self.assertEqual(expected_fonts, collected_fonts) def test_prim_filter_data(self): # return - stage = self.grill_world + stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) person = cook.define_taxon(stage, "Person") agent = cook.define_taxon(stage, "Agent", references=(person,)) generic = cook.create_unit(agent, "GenericAgent") @@ -602,28 +502,36 @@ def test_dot_call(self): def test_content_browser(self): # return - stage = self.grill_world - taxon = self.taxon_a - parent, child = cook.create_many(taxon, ['A', 'B']) - for path, value in ( - ("", (2, 15, 6)), - ("Deeper/Nested/Golden1", (-4, 5, 1)), - # ("Deeper/Nested/Golden2", (-4, -10, 1)), - # ("Deeper/Nested/Golden3", (0, 10, -2)), - ): - spawned = UsdGeom.Xform(cook.spawn_unit(parent, child, path)) - spawned.AddTranslateOp().Set(value=value) - variant_set_name = "testset" - variant_name = "testvar" - vset = child.GetVariantSet(variant_set_name) - vset.AddVariant(variant_name) - vset.SetVariantSelection(variant_name) - with vset.GetVariantEditContext(): - stage.DefinePrim(child.GetPath().AppendChild("in_variant")) - path_with_variant = child.GetPath().AppendVariantSelection(variant_set_name, variant_name) - + # class MiniAsset(names.UsdAsset): + # drop = ('code', 'media', 'area', 'stream', 'step', 'variant', 'part') + # DEFAULT_SUFFIX = "usda" + # + # cook.UsdAsset = MiniAsset + stage = stage = Usd.Stage.Open(str(_test_bed)) + # stage = self.grill_world + # taxon = self.taxon_a + # parent, child = cook.create_many(taxon, ['A', 'B']) + # for path, value in ( + # ("", (2, 15, 6)), + # ("Deeper/Nested/Golden1", (-4, 5, 1)), + # # ("Deeper/Nested/Golden2", (-4, -10, 1)), + # # ("Deeper/Nested/Golden3", (0, 10, -2)), + # ): + # spawned = UsdGeom.Xform(cook.spawn_unit(parent, child, path)) + # spawned.AddTranslateOp().Set(value=value) + # variant_set_name = "testset" + # variant_name = "testvar" + # vset = child.GetVariantSet(variant_set_name) + # vset.AddVariant(variant_name) + # vset.SetVariantSelection(variant_name) + # with vset.GetVariantEditContext(): + # stage.DefinePrim(child.GetPath().AppendChild("in_variant")) + # path_with_variant = child.GetPath().AppendVariantSelection(variant_set_name, variant_name) + + path_with_variant = Sdf.Path("/Catalogue/Model/Elements/Apartment") + spawned_path = Sdf.Path("/Catalogue/Model/Buildings/Multi_Story_Building/Windows/Apartment") layers = stage.GetLayerStack() - args = stage.GetLayerStack(), None, stage.GetPathResolverContext(), (Sdf.Path("/"), spawned.GetPrim().GetPath(), path_with_variant) + args = stage.GetLayerStack(), None, stage.GetPathResolverContext(), (Sdf.Path("/"), spawned_path, path_with_variant) anchor = layers[0] def _log(*args): @@ -683,7 +591,7 @@ def _fake_run(run_args: list): # create a temporary file loadable by our image tab image = QtGui.QImage(QtCore.QSize(1, 1), QtGui.QImage.Format_RGB888) image.fill(QtGui.QColor(255, 0, 0)) - targetpath = str(Path(self.grill_world.GetRootLayer().realPath).with_suffix(".jpg")) + targetpath = str(_test_bed.with_suffix(".jpg")) image.save(targetpath, "JPG") browser._on_identifier_requested(anchor, targetpath) # return @@ -719,7 +627,7 @@ def GprimSphere "Sphere" def test_display_color_editor(self): # return - stage = self.grill_world + stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) sphere = UsdGeom.Sphere.Define(stage, "/volume") color_var = sphere.GetDisplayColorPrimvar() editor = _attributes._DisplayColorEditor(color_var) @@ -741,22 +649,166 @@ def test_stats(self): empty = stats.StageStats() self.assertEqual(empty._usd_tree.topLevelItemCount(), 0) - widget = stats.StageStats(stage=self.world) + stage = Usd.Stage.Open(str(_test_bed)) + widget = stats.StageStats(stage=stage) self.assertGreater(widget._usd_tree.topLevelItemCount(), 1) current = _qt.QtCharts del _qt.QtCharts - stats.StageStats(stage=self.world) + stats.StageStats(stage=stage) _qt.QtCharts = current + def test_graph_views(self): + nodes_info = { + 1: dict( + label="{<0>x:y:z|<1>z}", + style="rounded,filled", + shape="record", + ), + 2: dict( + label="{<0>a|<1>b}", + style='rounded,filled', + shape="record", + ), + } + edges_info = ( + (1, 1, dict(color='sienna:crimson:orange')), + (1, 2, dict(color='crimson')), + (2, 1, dict(color='green')), + ) -class TestGraphicsViewport(unittest.TestCase): - def setUp(self): - self._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) - - def tearDown(self): - self._app.quit() + graph = _graph.nx.MultiDiGraph() + graph.add_nodes_from(nodes_info.items()) + graph.add_edges_from(edges_info) + graph.graph['graph'] = {'rankdir': 'LR'} + graph.graph['edge'] = {"color": 'crimson'} + outline_color = "#4682B4" # 'steelblue' + background_color = "#F0FFFF" # 'azure' + table_row = '{text}' + + connection_nodes = dict( + ancestor=dict( + plugs={ + '': 0, + 'cycle_in': 1, + 'roughness': 2, + 'cycle_out': 3, + 'surface': 4 + }, + active_plugs={'cycle_in', 'cycle_out', 'roughness', 'surface'}, + shape='none', + connections=dict( + surface=[('successor', 'surface')], + cycle_out=[('ancestor', 'cycle_in')], + ) + ), + successor=dict( + plugs={'': 0, 'surface': 1}, + active_plugs={'surface'}, + shape='none', + connections=dict(), + ) + ) + connection_edges = [] + + def _add_edges(src_node, src_name, tgt_node, tgt_name): + tooltip = f"{src_node}.{src_name} -> {tgt_node}.{tgt_name}" + connection_edges.append((src_node, tgt_node, {"tailport": src_name, "headport": tgt_name, "tooltip": tooltip})) + + for node, data in connection_nodes.items(): + label = f'<' + label += table_row.format(port="", color="white", + text=f'{node}') + # for index, plug in enumerate(data['plugs'], start=1): # we start at 1 because index 0 is the node itself + for plug, index in data['plugs'].items(): # we start at 1 because index 0 is the node itself + if not plug: + continue + plug_name = plug + sources = data['connections'].get(plug, []) # (valid, invalid): we care only about valid sources (index 0) + color = r"#F08080" if sources else background_color + # color = plug_colors[type(plug)] if isinstance(plug, UsdShade.Output) or sources else background_color + label += table_row.format(port=plug_name, color=color, text=f'{plug_name}') + for source_node, source_plug in sources: + _add_edges(source_node, source_plug, node, plug_name) + + label += '
>' + data['label'] = label + + graph.add_nodes_from(connection_nodes.items()) + graph.add_edges_from(connection_edges) + + widget = QtWidgets.QFrame() + splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) + + def _use_test_dot(subgraph, fp): + shutil.copy(Path(__file__).parent / "test_data/_mini_graph.dot", fp) + + def _use_test_svg(self, filepath): + return self._on_dot_result(Path(__file__).parent / "test_data/_mini_graph.svg") + + def _test_positions(graph, prog): + return { + 1: (40.0, 18.5), + 2: (156.38, 18.5), + 'ancestor': (156.38, 134.5), + 'successor': (40.0, 101.5) + } + + with ( + mock.patch(f"grill.views._graph.nx.nx_pydot.write_dot", new=_use_test_dot), + mock.patch(f"grill.views._graph._DotViewer.setDotPath", new=_use_test_svg), + mock.patch(f"grill.views._graph.drawing.nx_pydot.graphviz_layout", new=_test_positions), + ): + for cls in _graph.GraphView, _graph._GraphSVGViewer: + for pixmap_enabled in ((True, False) if cls == _graph._GraphSVGViewer else (False,)): + _graph._USE_SVG_VIEWPORT = pixmap_enabled + child = cls(parent=widget) + if cls == _graph._GraphSVGViewer: + child._graph_view.load = lambda fp: None + child._graph = graph + child.view(graph.nodes) + child.setMinimumWidth(150) + splitter.addWidget(child) + + if isinstance(child, _graph.GraphView): + for item in child.scene().items(): + item.boundingRect() # trigger bounding rect logic + if isinstance(item, _graph._Edge): + cycle_collected = True + if isinstance(item, _graph._Node): + nodes_hovered_checked = True + + # Test hover with no modifiers + event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) + center = item.sceneBoundingRect().center() + event.setScenePos(center) + item.hoverEnterEvent(event) + self.assertEqual(item.cursor().shape(), QtGui.Qt.ArrowCursor) + self.assertEqual(item.textInteractionFlags(), item._default_text_interaction) + item.hoverLeaveEvent(event) + + # Test hover with Ctrl modifier + event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) + event.setScenePos(center) + event.setModifiers(QtCore.Qt.ControlModifier) + item.hoverEnterEvent(event) + self.assertEqual(item.cursor().shape(), QtGui.Qt.PointingHandCursor) + item.hoverLeaveEvent(event) + + # Test hover with Alt modifier + event = QtWidgets.QGraphicsSceneHoverEvent(QtCore.QEvent.GraphicsSceneHoverMove) + event.setScenePos(item.sceneBoundingRect().center()) + event.setModifiers(QtCore.Qt.AltModifier) + item.hoverEnterEvent(event) + self.assertEqual(item.cursor().shape(), QtGui.Qt.ClosedHandCursor) + self.assertEqual(item.textInteractionFlags(), QtCore.Qt.NoTextInteraction) + item.hoverLeaveEvent(event) + + self.assertTrue(cycle_collected) + self.assertTrue(nodes_hovered_checked) def test_zoom(self): + return + """Zoom is triggered by ctrl + mouse wheel""" view = _graph._GraphicsViewport() @@ -857,3 +909,4 @@ def test_pan(self): # Confirm no further move is performed self.assertEqual(last_vertical_scroll_bar, vertical_scroll_bar.value()) self.assertEqual(last_horizontal_scroll_bar, horizontal_scroll_bar.value()) + From c6528995adc1a618cc40a0fc7974933113aec292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 17 Nov 2024 17:59:50 +1100 Subject: [PATCH 36/67] fix test for PySide2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index 22023f41..89b3b8dd 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -743,7 +743,7 @@ def _use_test_dot(subgraph, fp): shutil.copy(Path(__file__).parent / "test_data/_mini_graph.dot", fp) def _use_test_svg(self, filepath): - return self._on_dot_result(Path(__file__).parent / "test_data/_mini_graph.svg") + return self._on_dot_result(str(Path(__file__).parent / "test_data/_mini_graph.svg")) def _test_positions(graph, prog): return { From 4ccbf1593db5dee98c0949a07b82eac2bfd5ec88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 7 Dec 2024 18:32:43 +1100 Subject: [PATCH 37/67] test nested path in spawn_many MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_cook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cook.py b/tests/test_cook.py index a1adcc8b..91dbaee7 100644 --- a/tests/test_cook.py +++ b/tests/test_cook.py @@ -173,8 +173,8 @@ def test_spawn_many(self): id_fields = {tokens.ids.CGAsset.kingdom.name: "K"} taxon = cook.define_taxon(stage, "Another", id_fields=id_fields) parent, child = cook.create_many(taxon, ['A', 'B']) - cook.spawn_many(parent, child, ["b"], labels=["1", "2"]) - self.assertEqual(len(parent.GetChildren()), 1) + cook.spawn_many(parent, child, ["b", "nested/c"], labels=["1", "2", "3"]) + self.assertEqual(len(parent.GetChildren()), 2) def test_inherited_and_specialized_contexts(self): stage = cook.fetch_stage(self.root_asset) From 7ebc100ce69102f84ffc514cfa382cb136401777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 7 Dec 2024 18:39:38 +1100 Subject: [PATCH 38/67] adding singledispatch typeerror test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_usd.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_usd.py b/tests/test_usd.py index 2d6a2462..3b98b8d0 100644 --- a/tests/test_usd.py +++ b/tests/test_usd.py @@ -4,6 +4,19 @@ import grill.usd as gusd +# 2024-11-09 - Python-3.13 & USD-24.11 +# python -m unittest --durations 0 test_usd +# ..... +# Slowest test durations +# ---------------------------------------------------------------------- +# 0.026s test_edit_context (test_usd.TestUSD.test_edit_context) +# 0.001s test_format_tree (test_usd.TestUSD.test_format_tree) +# 0.001s test_make_plane (test_usd.TestUSD.test_make_plane) +# +# (durations < 0.001s were hidden; use -v to show these durations) +# ---------------------------------------------------------------------- +# Ran 5 tests in 0.030s + class TestUSD(unittest.TestCase): def test_edit_context(self): @@ -57,6 +70,10 @@ def test_edit_context(self): layer = stage.GetRootLayer() self.assertIsNotNone(layer.GetPrimAtPath(f"{layer.defaultPrim}/inner/child{{color={variant_name}}}")) + with self.assertRaisesRegex(TypeError, "Not implemented"): + # only composition arcs on prims are supported, passing an attribute (or other objects) is not + gusd.edit_context(color) + def test_missing_arc(self): stage = Usd.Stage.CreateInMemory() prim = stage.DefinePrim("/Referenced") From afe1c0bec4769da46ea5fcf7194fa5881f4922ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 7 Dec 2024 18:55:38 +1100 Subject: [PATCH 39/67] test color editor for invalid primvar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index 89b3b8dd..c286685f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -631,7 +631,9 @@ def test_display_color_editor(self): sphere = UsdGeom.Sphere.Define(stage, "/volume") color_var = sphere.GetDisplayColorPrimvar() editor = _attributes._DisplayColorEditor(color_var) - editor._update_value() + + with mock.patch("grill.views._attributes.QtWidgets.QColorDialog.getColor", new=lambda *_, **__: QtGui.QColor(255, 255, 0)): + editor._color_launchers["Color"][0].click() color_var.SetInterpolation(UsdGeom.Tokens.vertex) editor = _attributes._DisplayColorEditor(color_var) @@ -644,6 +646,9 @@ def test_display_color_editor(self): with self.assertRaises(TypeError): # atm some gprim types are not supported editor._update_value() + editor = _attributes._DisplayColorEditor(UsdGeom.Primvar()) + self.assertEqual(len(editor._value), 1) + def test_stats(self): # return empty = stats.StageStats() From 539db4399f47440b651d168f3a2808e4116d3275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 7 Dec 2024 19:05:35 +1100 Subject: [PATCH 40/67] remove graphviz dll load on DCCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_core.py | 10 ---------- grill/views/_graph.py | 1 - grill/views/houdini.py | 1 - grill/views/maya.py | 1 - tests/test_views.py | 3 --- 5 files changed, 16 deletions(-) diff --git a/grill/views/_core.py b/grill/views/_core.py index f090f882..59da5875 100644 --- a/grill/views/_core.py +++ b/grill/views/_core.py @@ -114,16 +114,6 @@ def _run(args: list): return error, result.stdout.decode() -@cache -def _ensure_dot(): - """For usage only when DCC python interpreter fails to install pygraphviz properly.""" - if dotpath := _which("dot"): - # https://github.com/pygraphviz/pygraphviz/issues/360 - # TODO: is this the best approach to solve current Houdini and Maya failing to import graphviz?? - if hasattr(os, "add_dll_directory"): - os.add_dll_directory(Path(dotpath).parent) # sigh, patch pygraphviz? - - @contextlib.contextmanager def wait(): try: diff --git a/grill/views/_graph.py b/grill/views/_graph.py index fef36988..40123280 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -16,7 +16,6 @@ from . import _core from ._qt import QtCore, QtGui, QtWidgets, QtSvg -_core._ensure_dot() _logger = logging.getLogger(__name__) diff --git a/grill/views/houdini.py b/grill/views/houdini.py index 5a24e2bf..01fae0ff 100644 --- a/grill/views/houdini.py +++ b/grill/views/houdini.py @@ -3,7 +3,6 @@ from functools import cache, lru_cache, partial from . import sheets as _sheets, description as _description, create as _create, stats as _stats, _core _description._PALETTE.set(0) # (0 == dark, 1 == light) -_core._ensure_dot() def _stage_on_widget(widget_creator, _cache=True): diff --git a/grill/views/maya.py b/grill/views/maya.py index b8424390..57d9a1df 100644 --- a/grill/views/maya.py +++ b/grill/views/maya.py @@ -15,7 +15,6 @@ from . import description as _description, sheets as _sheets, create as _create, _core, stats as _stats _description._PALETTE.set(0) # (0 == dark, 1 == light) -_core._ensure_dot() @cache diff --git a/tests/test_views.py b/tests/test_views.py index c286685f..c36e02a1 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -84,9 +84,6 @@ def test_common_paths(self): ] self.assertEqual(actual, expected) - def test_core(self): - _core._ensure_dot() - _test_bed = Path(__file__).parent / "mini_test_bed" / "main-world-test.1.usda" From a333e9692b925866d59fd811d7d370c707f6d71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 7 Dec 2024 19:25:03 +1100 Subject: [PATCH 41/67] edge label graph and invis node test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index c36e02a1..db53b5a1 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -668,13 +668,13 @@ def test_graph_views(self): ), 2: dict( label="{<0>a|<1>b}", - style='rounded,filled', + style='invis', shape="record", ), } edges_info = ( (1, 1, dict(color='sienna:crimson:orange')), - (1, 2, dict(color='crimson')), + (1, 2, dict(color='crimson', label='edge_label')), (2, 1, dict(color='green')), ) @@ -805,6 +805,8 @@ def _test_positions(graph, prog): self.assertEqual(item.textInteractionFlags(), QtCore.Qt.NoTextInteraction) item.hoverLeaveEvent(event) + item.itemChange(QtWidgets.QGraphicsItem.ItemPositionHasChanged, (1,1)) + self.assertTrue(cycle_collected) self.assertTrue(nodes_hovered_checked) From db88a6645eb18be728e33d2247af19717050d258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 7 Dec 2024 19:43:32 +1100 Subject: [PATCH 42/67] properly test paths with variants for content browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index db53b5a1..9da5ca32 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -504,7 +504,7 @@ def test_content_browser(self): # DEFAULT_SUFFIX = "usda" # # cook.UsdAsset = MiniAsset - stage = stage = Usd.Stage.Open(str(_test_bed)) + stage = Usd.Stage.Open(str(_test_bed)) # stage = self.grill_world # taxon = self.taxon_a # parent, child = cook.create_many(taxon, ['A', 'B']) @@ -525,10 +525,11 @@ def test_content_browser(self): # stage.DefinePrim(child.GetPath().AppendChild("in_variant")) # path_with_variant = child.GetPath().AppendVariantSelection(variant_set_name, variant_name) - path_with_variant = Sdf.Path("/Catalogue/Model/Elements/Apartment") + path_with_variant = Sdf.Path("/Origin{color=blue}Geom/Floor.primvars:displayColor") spawned_path = Sdf.Path("/Catalogue/Model/Buildings/Multi_Story_Building/Windows/Apartment") - layers = stage.GetLayerStack() - args = stage.GetLayerStack(), None, stage.GetPathResolverContext(), (Sdf.Path("/"), spawned_path, path_with_variant) + apartments_layer = Sdf.Layer.FindOrOpen(str(_test_bed.parent / "Model-Elements-Apartment.1.usda")) + layers = stage.GetLayerStack() + [apartments_layer] + args = layers, None, stage.GetPathResolverContext(), (Sdf.Path("/"), spawned_path, path_with_variant) anchor = layers[0] def _log(*args): From e0ae5b6a2357b63bc6a95d96c6ab385bcd05e405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 10:45:53 +1100 Subject: [PATCH 43/67] update mini grpah dot with edge label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_data/_mini_graph.dot | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_data/_mini_graph.dot b/tests/test_data/_mini_graph.dot index 246d3c70..4ee61255 100644 --- a/tests/test_data/_mini_graph.dot +++ b/tests/test_data/_mini_graph.dot @@ -3,12 +3,14 @@ rankdir=LR; edge [color=crimson]; 1 [label="{<0>x:y:z|<1>z}", style="rounded,filled", shape=record]; 2 [label="{<0>a|<1>b}", style="rounded,filled", shape=record]; -3 [label="{<0>c|<1>d}", style="rounded,filled", shape=record, plugs="(0, 1)", active_plugs="(0,)"]; -ancestor [plugs="{'': 0, 'cycle_in': 1, 'roughness': 2, 'cycle_out': 3, 'surface': 4}", active_plugs="{'cycle_in', 'roughness', 'surface', 'cycle_out'}", shape=none, connections="{'surface': [('successor', 'surface')], 'cycle_out': [('ancestor', 'cycle_in')]}", label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; -successor [plugs="{'': 0, 'surface': 1}", active_plugs="{'surface'}", shape=none, connections="{}", label=<
successor
surface
>]; +3 [label="{<0>c|<1>d}", style="rounded,filled", shape=record]; +4 [label="{<0>k}", style=invis]; +ancestor [active_plugs="{'surface', 'cycle_out', 'cycle_in', 'roughness'}", shape=none, connections="{'surface': [('successor', 'surface')], 'cycle_out': [('ancestor', 'cycle_in')]}", label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; +successor [active_plugs="{'surface'}", shape=none, connections="{}", label=<
successor
surface
>]; 1 -> 1 [key=0, color="sienna:crimson:orange"]; 1 -> 2 [key=0, color=crimson]; 2 -> 1 [key=0, color=green]; +2 -> 4 [key=0, color=yellow, label="edge_label"]; 3 -> 2 [key=0, color=blue, tailport=0]; ancestor -> ancestor [key=0, tailport="cycle_in", headport="cycle_out", tooltip="ancestor.cycle_in -> ancestor.cycle_out"]; successor -> ancestor [key=0, tailport=surface, headport=surface, tooltip="successor.surface -> ancestor.surface"]; From 84b711e9a6f273a26450841f70daf42464766f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 10:57:39 +1100 Subject: [PATCH 44/67] updated mini graph svg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_data/_mini_graph.svg | 118 +++++++++++++++++--------------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/tests/test_data/_mini_graph.svg b/tests/test_data/_mini_graph.svg index a6740bd7..266b65be 100644 --- a/tests/test_data/_mini_graph.svg +++ b/tests/test_data/_mini_graph.svg @@ -4,101 +4,109 @@ - - - + + + 1 - -x:y:z - -z + +x:y:z + +z 1->1 - - - - + + + + 2 - -a - -b + +a + +b 1->2 - - + + 2->1 - - + + + + + + +2->4 + + +edge_label 3 - -c - -d + +c + +d - + 3:0->2 - - + + - + ancestor - - -ancestor - -cycle_in - -roughness - -cycle_out - -surface - + + +ancestor + +cycle_in + +roughness + +cycle_out + +surface + - + ancestor:cycle_in->ancestor:cycle_out - - - + + + - + successor - - -successor - -surface - + + +successor + +surface + - + successor:surface->ancestor:surface - - - + + + From 2d7888cd354352e0ddc11d9a1813708fc9b25ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 12:11:13 +1100 Subject: [PATCH 45/67] tests for invalid data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 17 ++++++++++++--- tests/test_views.py | 48 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 40123280..14700fc7 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -74,7 +74,7 @@ def _get_html_table_from_fields(**fields): def _get_plugs_from_label(label) -> dict[str, str]: if not label.startswith("{"): # Only for record labels. - raise ValueError(f"Label needs to start with '{{'. Got: {label}") + raise ValueError(f"Label needs to start with '{{' to extract plugs from it, for example: '{{item|another_item}}'. Got label: '{label}'") fields = label.strip("{}").split("|") return dict(field.strip("<>").split(">", 1) for field in fields) @@ -444,6 +444,7 @@ def wheelEvent(self, event): modifiers = event.modifiers() if modifiers == QtCore.Qt.ControlModifier: + raise RuntimeError zoom_factor = 1.2 if event.angleDelta().y() > 0 else 0.8 self.scale(zoom_factor, zoom_factor) elif modifiers == QtCore.Qt.AltModifier: @@ -536,6 +537,7 @@ def __init__(self, graph: nx.DiGraph = None, parent=None): self.url_id_prefix = "" def _graph_url_changed(self, *_, **__): + raise RuntimeError sender = self.sender() key = next((k for k, v in self._nodes_map.items() if v==sender), None) if key is None: @@ -619,9 +621,16 @@ def _add_node(nx_node): plugs = node_data.pop('plugs', ()) nodes_attrs = ChainMap(node_data, graph_node_attrs) if (shape := nodes_attrs.get('shape')) == 'record': + try: + label = node_data['label'] + except KeyError: + raise ValueError(f"'label' must be supplied when 'record' shape is set for node: '{nx_node}' with data: {node_data}") if plugs: - raise ValueError(f"record 'shape' and 'plugs' are mutually exclusive, pick one for {nx_node}, {node_data=}") - plugs = _get_plugs_from_label(node_data['label']) + raise ValueError(f"record 'shape' and 'ports' are mutually exclusive, pick one for node: '{nx_node}' with data: {node_data}") + try: + plugs = _get_plugs_from_label(label) + except ValueError as exc: + raise ValueError(f"In order to use the 'record' shape, a record 'label' in the form of: '{{text1|text2}}' must be used") from exc label = _get_html_table_from_fields(**plugs) else: label = node_data.get('label') @@ -763,6 +772,7 @@ def setDotPath(self, path): self._threadpool.start(dot2svg) def _on_dot_error(self, message): + raise RuntimeError self._error_view.setVisible(True) self._graph_view.setVisible(False) self._error_view.setText(message) @@ -804,6 +814,7 @@ def url_id_prefix(self): return "_node_id_" def _graph_url_changed(self, url: QtCore.QUrl): + raise RuntimeError node_uri = url.toString() node_uri_stem = node_uri.split("/")[-1] if node_uri_stem.startswith(self.url_id_prefix): diff --git a/tests/test_views.py b/tests/test_views.py index 9da5ca32..913dba1c 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -661,6 +661,23 @@ def test_stats(self): _qt.QtCharts = current def test_graph_views(self): + viewer = _graph.GraphView() + + for invalid_node_data, error_message in ( + (dict(shape='record'), "'label' must be supplied"), + (dict(shape='record', label='no record'), "a record 'label' in the form of"), + (dict(shape='record', label='{1}'), "a record 'label' in the form of"), + (dict(shape='record', label='{<0>1}', plugs={'first': 1, 'second': 2}), "record 'shape' and 'ports' are mutually exclusive"), + (dict(shape='none'), "A label must be provided"), + ): + invalid_graph = _graph.nx.MultiDiGraph() + invalid_graph.add_nodes_from([(1, invalid_node_data)]) + with self.assertRaisesRegex(ValueError, error_message): + viewer.graph = invalid_graph + + viewer = _graph.GraphView() + viewer.view(tuple()) + nodes_info = { 1: dict( label="{<0>x:y:z|<1>z}", @@ -669,14 +686,25 @@ def test_graph_views(self): ), 2: dict( label="{<0>a|<1>b}", - style='invis', + style="rounded,filled", + shape="record", + ), + 3: dict( + label="{<0>c|<1>d}", + style="rounded,filled", shape="record", ), + 4: dict( + label="{<0>k}", + style="invis", + ), } edges_info = ( (1, 1, dict(color='sienna:crimson:orange')), - (1, 2, dict(color='crimson', label='edge_label')), + (1, 2, dict(color='crimson')), (2, 1, dict(color='green')), + (3, 2, dict(color='blue', tailport='0')), + (2, 4, dict(color='yellow', label='edge_label')), ) graph = _graph.nx.MultiDiGraph() @@ -750,10 +778,12 @@ def _use_test_svg(self, filepath): def _test_positions(graph, prog): return { - 1: (40.0, 18.5), - 2: (156.38, 18.5), - 'ancestor': (156.38, 134.5), - 'successor': (40.0, 101.5) + 1: (40.0, 91.692), + 2: (157.37, 91.692), + 3: (40.0, 36.692), + 4: (332.85, 91.692), + 'ancestor': (157.37, 208.69), + 'successor': (40.0, 174.69), } with ( @@ -765,7 +795,8 @@ def _test_positions(graph, prog): for pixmap_enabled in ((True, False) if cls == _graph._GraphSVGViewer else (False,)): _graph._USE_SVG_VIEWPORT = pixmap_enabled child = cls(parent=widget) - if cls == _graph._GraphSVGViewer: + if cls == _graph._GraphSVGViewer and not pixmap_enabled: + # QWebEngineView in use, no need to test its 'load' method child._graph_view.load = lambda fp: None child._graph = graph child.view(graph.nodes) @@ -811,6 +842,9 @@ def _test_positions(graph, prog): self.assertTrue(cycle_collected) self.assertTrue(nodes_hovered_checked) + child.filter_edges = lambda src, tgt, port: src not in graph.nodes + child.view(graph.nodes) + def test_zoom(self): return From 1c92551caf13f3930eacd053305cc121f3dbd2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 15:51:12 +1100 Subject: [PATCH 46/67] cover last graph tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 4 ---- tests/test_views.py | 47 +++++++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 14700fc7..0e4639a5 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -444,7 +444,6 @@ def wheelEvent(self, event): modifiers = event.modifiers() if modifiers == QtCore.Qt.ControlModifier: - raise RuntimeError zoom_factor = 1.2 if event.angleDelta().y() > 0 else 0.8 self.scale(zoom_factor, zoom_factor) elif modifiers == QtCore.Qt.AltModifier: @@ -537,7 +536,6 @@ def __init__(self, graph: nx.DiGraph = None, parent=None): self.url_id_prefix = "" def _graph_url_changed(self, *_, **__): - raise RuntimeError sender = self.sender() key = next((k for k, v in self._nodes_map.items() if v==sender), None) if key is None: @@ -772,7 +770,6 @@ def setDotPath(self, path): self._threadpool.start(dot2svg) def _on_dot_error(self, message): - raise RuntimeError self._error_view.setVisible(True) self._graph_view.setVisible(False) self._error_view.setText(message) @@ -814,7 +811,6 @@ def url_id_prefix(self): return "_node_id_" def _graph_url_changed(self, url: QtCore.QUrl): - raise RuntimeError node_uri = url.toString() node_uri_stem = node_uri.split("/")[-1] if node_uri_stem.startswith(self.url_id_prefix): diff --git a/tests/test_views.py b/tests/test_views.py index 913dba1c..eb20c424 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -284,14 +284,7 @@ class MiniAsset(names.UsdAsset): widget = create.TaxonomyEditor() # GraphView capabilities are tested elsewhere, so mock 'view' here. widget._graph_view.view = lambda indices: None - # if isinstance(widget._graph_view, _graph.GraphView): - # with self.assertRaisesRegex(LookupError, "Could not find sender"): - # invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") - # widget._graph_view._graph_url_changed(invalid_uril) - # else: - # with self.assertRaisesRegex(RuntimeError, "'graph' attribute not set yet"): - # invalid_uril = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}not_a_digit") - # widget._graph_view._graph_url_changed(invalid_uril) + widget.setStage(stage) widget._amount.setValue(3) # TODO: create 10 assets, clear tmp directory @@ -663,17 +656,20 @@ def test_stats(self): def test_graph_views(self): viewer = _graph.GraphView() - for invalid_node_data, error_message in ( - (dict(shape='record'), "'label' must be supplied"), - (dict(shape='record', label='no record'), "a record 'label' in the form of"), - (dict(shape='record', label='{1}'), "a record 'label' in the form of"), - (dict(shape='record', label='{<0>1}', plugs={'first': 1, 'second': 2}), "record 'shape' and 'ports' are mutually exclusive"), - (dict(shape='none'), "A label must be provided"), + with ( + mock.patch(f"grill.views._graph.drawing.nx_pydot.graphviz_layout", new=lambda graph, **__: dict.fromkeys(graph.nodes, (0,0))), ): - invalid_graph = _graph.nx.MultiDiGraph() - invalid_graph.add_nodes_from([(1, invalid_node_data)]) - with self.assertRaisesRegex(ValueError, error_message): - viewer.graph = invalid_graph + for invalid_node_data, error_message in ( + (dict(shape='record'), "'label' must be supplied"), + (dict(shape='record', label='no record'), "a record 'label' in the form of"), + (dict(shape='record', label='{1}'), "a record 'label' in the form of"), + (dict(shape='record', label='{<0>1}', plugs={'first': 1, 'second': 2}), "record 'shape' and 'ports' are mutually exclusive"), + (dict(shape='none'), "A label must be provided"), + ): + invalid_graph = _graph.nx.MultiDiGraph() + invalid_graph.add_nodes_from([(1, invalid_node_data)]) + with self.assertRaisesRegex(ValueError, error_message): + viewer.graph = invalid_graph viewer = _graph.GraphView() viewer.view(tuple()) @@ -794,7 +790,19 @@ def _test_positions(graph, prog): for cls in _graph.GraphView, _graph._GraphSVGViewer: for pixmap_enabled in ((True, False) if cls == _graph._GraphSVGViewer else (False,)): _graph._USE_SVG_VIEWPORT = pixmap_enabled + child = cls(parent=widget) + + if isinstance(child, _graph.GraphView): + with self.assertRaisesRegex(LookupError, "Could not find sender"): + invalid_uril = QtCore.QUrl(f"{child.url_id_prefix}not_a_digit") + child._graph_url_changed(invalid_uril) + else: + with self.assertRaisesRegex(RuntimeError, "'graph' attribute not set yet"): + invalid_uril = QtCore.QUrl(f"{child.url_id_prefix}not_a_digit") + child._graph_url_changed(invalid_uril) + child._on_dot_error("nothing set yet") + if cls == _graph._GraphSVGViewer and not pixmap_enabled: # QWebEngineView in use, no need to test its 'load' method child._graph_view.load = lambda fp: None @@ -846,8 +854,6 @@ def _test_positions(graph, prog): child.view(graph.nodes) def test_zoom(self): - return - """Zoom is triggered by ctrl + mouse wheel""" view = _graph._GraphicsViewport() @@ -869,6 +875,7 @@ def test_zoom(self): # Assert that the scale has changed according to the zoom logic self.assertGreater(zoomed_in_scale, initial_scale) + angleDelta_zoomOut = QtCore.QPoint(-120, 0) # ZOOM OUT From d080bb96606ae7319b7f9f088ad98f37929c5cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 16:05:17 +1100 Subject: [PATCH 47/67] remove outdated code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 140 ++++---------------------------------------- 1 file changed, 12 insertions(+), 128 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index eb20c424..fe2672ae 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -95,54 +95,11 @@ def setUpClass(cls): cls._app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([]) def setUp(self): - ... - # return - - # self.grill_world = Usd.Stage.Open(str(_test_bed)) - - # root_path = "/root" - # - # sphere_stage = Usd.Stage.CreateInMemory() - # UsdGeom.Sphere.Define(sphere_stage, "/sph") - # sphere_root = sphere_stage.DefinePrim(root_path) - # sphere_root.CreateAttribute("greet", Sdf.ValueTypeNames.String).Set("hello") - # sphere_stage.SetDefaultPrim(sphere_root) - # - # capsule_stage = Usd.Stage.CreateInMemory() - # UsdGeom.Capsule.Define(capsule_stage, "/cap") - # capsule_root = capsule_stage.DefinePrim(root_path) - # capsule_root.CreateAttribute("who", Sdf.ValueTypeNames.String).Set("world") - # capsule_stage.SetDefaultPrim(capsule_root) - # - # merged_stage = Usd.Stage.CreateInMemory() - # with Sdf.ChangeBlock(): - # for i in (capsule_stage, sphere_stage): - # merged_stage.GetRootLayer().subLayerPaths.append(i.GetRootLayer().identifier) - # merged_stage.SetDefaultPrim(merged_stage.GetPrimAtPath(root_path)) - # - # world = Usd.Stage.CreateInMemory() - # self.nested = world.DefinePrim("/nested/child") - # self.sibling = world.DefinePrim("/nested/sibling") - # self.nested.GetReferences().AddReference(merged_stage.GetRootLayer().identifier) - # - # self.capsule = capsule_stage - # self.sphere = sphere_stage - # self.merge = merged_stage - # self.world = world - # self._tmpf = tempfile.mkdtemp() self._token = cook.Repository.set(cook.Path(self._tmpf) / "repo") - # self.grill_root_asset = names.UsdAsset.get_anonymous() - # self.grill_world = gworld = cook.fetch_stage(self.grill_root_asset.name) - # self.taxon_a = cook.define_taxon(gworld, "a") - # self.taxon_b = cook.define_taxon(gworld, "b", references=(self.taxon_a,)) - # self.unit_b = cook.create_unit(self.taxon_b, "GenericAgent") def tearDown(self) -> None: cook.Repository.reset(self._token) - # Reset all members to USD objects to ensure the used layers are cleared - # (otherwise in Windows this can cause failure to remove the temporary files) - # self.grill_world = None shutil.rmtree(self._tmpf) @classmethod @@ -278,9 +235,7 @@ class MiniAsset(names.UsdAsset): cook.UsdAsset = MiniAsset stage = Usd.Stage.Open(str(_test_bed)) - # stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) existing = list(cook.itaxa(stage)) - # existing = [cook.define_taxon(stage, f"Option{each}") for each in range(1, 6)] widget = create.TaxonomyEditor() # GraphView capabilities are tested elsewhere, so mock 'view' here. widget._graph_view.view = lambda indices: None @@ -327,39 +282,20 @@ class MiniAsset(names.UsdAsset): selected_items = widget._existing.table.selectedIndexes() self.assertEqual(len(selected_items), len(valid_data) + len(existing)) - # if isinstance(widget._graph_view, _graph.GraphView): - # sender = next(iter(widget._graph_view._nodes_map.values()), None) - # self.assertIsNotNone(sender, msg=f"Expected sender to be an actual object of type {_graph._Node}. Got None, check pygraphviz / pydot requirements") - # sender.linkActivated.emit("") - # else: - # valid_url = QtCore.QUrl(f"{widget._graph_view.url_id_prefix}{existing[-1].GetName()}") - # widget._graph_view._graph_url_changed(valid_url) - # # Nitpick, wait for dot 2 svg conversions to finish - # # This does not crash the program but an exception is logged when race - # # conditions apply (e.g. the object is deleted before the runnable completes). - # # This logged exception comes in the form of: - # # RuntimeError: Internal C++ object (_Dot2SvgSignals) already deleted. - # # Solution seems to be to block and wait for all runnables to complete. - # widget._graph_view._threadpool.waitForDone(10_000) - def test_spreadsheet_editor(self): - # return widget = sheets.SpreadsheetEditor() widget._model_hierarchy.setChecked(False) # default is True stage = Usd.Stage.Open(str(_test_bed)) - stage.OverridePrim("/child_orphaned") - # self.nested.SetInstanceable(True) + stage.SetEditTarget(stage.GetSessionLayer()) + with Sdf.ChangeBlock(): + stage.OverridePrim("/child_orphaned") + stage.GetPrimAtPath("/Catalogue/Model/Blocks").SetActive(False) widget._orphaned.setChecked(True) - # assert self.nested.IsInstance() widget.setStage(stage) self.assertEqual(stage, widget.stage) widget.table.scrollContentsBy(10, 10) widget.table.selectAll() - # expected_rows = {0, 1, 2, 3} # 3 prims from path: /nested, /nested/child, /nested/sibling, /child_orphaned - # expected_rows = set(range(len(list(Usd.PrimRange.Stage(stage, Usd.TraverseInstanceProxies(Usd.PrimAllPrimsPredicate)))))) - # visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) - # self.assertEqual(expected_rows, visible_rows) widget.table.clearSelection() widget._column_options[0]._line_filter.setText("hade") @@ -368,10 +304,8 @@ def test_spreadsheet_editor(self): widget.table.selectAll() expected_rows = {0, 1, 2} # 1 prim from filtered name: /Catalogue/Shade /Catalogue/Shade/Color /Catalogue/Shade/Color/ModelDefault - # for each in widget.table.selectedIndexes(): - # print(each.data()) visible_rows = ({i.row() for i in widget.table.selectedIndexes()}) - self.assertEqual(expected_rows, visible_rows) + self.assertEqual(visible_rows, expected_rows) widget._copySelection() clip = QtWidgets.QApplication.instance().clipboard().text() @@ -401,28 +335,11 @@ def test_spreadsheet_editor(self): widget._column_options[0]._line_filter.setText("") widget._model_hierarchy.click() # disables model hierarchy, which we don't have any widget.table.selectAll() - _log = lambda *args: print(args) - with mock.patch(f"{QtWidgets.__name__}.QMessageBox.warning", new=_log): + with mock.patch(f"{QtWidgets.__name__}.QMessageBox.warning", new=lambda *args: print(args)): widget._pasteClipboard() widget.model._prune_children = {Sdf.Path("/pruned")} - # gworld = self.grill_world - - # with cook.unit_context(self.unit_b): - # child_agent = gworld.DefinePrim(self.unit_b.GetPath().AppendChild("child")) - # child_attr = child_agent.CreateAttribute("agent_greet", Sdf.ValueTypeNames.String, custom=False) - # child_attr.Set("aloha") - # agent_id = cook.unit_asset(self.unit_b) - # for i in range(3): - # agent = gworld.DefinePrim(f"/Instanced/Agent{i}") - # agent.GetReferences().AddReference(agent_id.identifier) - # agent.SetInstanceable(True) - # agent.SetActive(False) - # gworld.OverridePrim("/non/existing/prim") - # gworld.DefinePrim("/pruned/prim") - # inactive = gworld.DefinePrim("/another_inactive") - # inactive.SetActive(False) - # gworld.GetRootLayer().subLayerPaths.append(self.world.GetRootLayer().identifier) + widget._column_options[0]._line_filter.setText("") widget.table.clearSelection() widget._active.setChecked(False) @@ -446,11 +363,9 @@ def test_spreadsheet_editor(self): expected_colors.pop(color_key, None) collected_fonts.add(font_key) - # self.assertEqual(expected_colors, dict()) self.assertEqual(expected_fonts, collected_fonts) def test_prim_filter_data(self): - # return stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) person = cook.define_taxon(stage, "Person") agent = cook.define_taxon(stage, "Agent", references=(person,)) @@ -482,7 +397,6 @@ def test_prim_filter_data(self): widget.setStage(stage) def test_dot_call(self): - # return """Test execution of function by mocking dot with python call""" with mock.patch("grill.views.description._which") as patch: patch.return_value = 'python' @@ -491,32 +405,7 @@ def test_dot_call(self): self.assertIsNotNone(error) def test_content_browser(self): - # return - # class MiniAsset(names.UsdAsset): - # drop = ('code', 'media', 'area', 'stream', 'step', 'variant', 'part') - # DEFAULT_SUFFIX = "usda" - # - # cook.UsdAsset = MiniAsset stage = Usd.Stage.Open(str(_test_bed)) - # stage = self.grill_world - # taxon = self.taxon_a - # parent, child = cook.create_many(taxon, ['A', 'B']) - # for path, value in ( - # ("", (2, 15, 6)), - # ("Deeper/Nested/Golden1", (-4, 5, 1)), - # # ("Deeper/Nested/Golden2", (-4, -10, 1)), - # # ("Deeper/Nested/Golden3", (0, 10, -2)), - # ): - # spawned = UsdGeom.Xform(cook.spawn_unit(parent, child, path)) - # spawned.AddTranslateOp().Set(value=value) - # variant_set_name = "testset" - # variant_name = "testvar" - # vset = child.GetVariantSet(variant_set_name) - # vset.AddVariant(variant_name) - # vset.SetVariantSelection(variant_name) - # with vset.GetVariantEditContext(): - # stage.DefinePrim(child.GetPath().AppendChild("in_variant")) - # path_with_variant = child.GetPath().AppendVariantSelection(variant_set_name, variant_name) path_with_variant = Sdf.Path("/Origin{color=blue}Geom/Floor.primvars:displayColor") spawned_path = Sdf.Path("/Catalogue/Model/Buildings/Multi_Story_Building/Windows/Apartment") @@ -525,14 +414,11 @@ def test_content_browser(self): args = layers, None, stage.GetPathResolverContext(), (Sdf.Path("/"), spawned_path, path_with_variant) anchor = layers[0] - def _log(*args): - print(args) - _core_run = _core._run def _fake_run(run_args: list): return "", Sdf.Layer.FindOrOpen(run_args[-1]).ExportToString() - # return + # sdffilter still not coming via pypi, so patch for now with mock.patch("grill.views.description._core._run", new=_fake_run): dialog = description._start_content_browser(*args) @@ -546,7 +432,7 @@ def _fake_run(run_args: list): browser_tab: description._PseudoUSDTabBrowser = first_browser_widget.findChild(description._PseudoUSDTabBrowser) browser._on_identifier_requested(anchor, layers[1].identifier) - with mock.patch(f"{QtWidgets.__name__}.QMessageBox.warning", new=_log): + with mock.patch(f"{QtWidgets.__name__}.QMessageBox.warning", new=lambda *args: print(args)): browser._on_identifier_requested(anchor, "/missing/file.usd") _, empty_png = tempfile.mkstemp(suffix=".png") browser._on_identifier_requested(anchor, empty_png) @@ -563,7 +449,7 @@ def _fake_run(run_args: list): modifiers = QtCore.Qt.ControlModifier phase = QtCore.Qt.NoScrollPhase inverted = False - # return + # ZOOM IN event = QtGui.QWheelEvent(position, position, pixelDelta, angleDelta_zoomIn, buttons, modifiers, phase, inverted) browser_tab.wheelEvent(event) @@ -574,7 +460,7 @@ def _fake_run(run_args: list): # ZOOM OUT event = QtGui.QWheelEvent(position, position, pixelDelta, angleDelta_zoomOut, buttons, modifiers, phase, inverted) browser_tab.wheelEvent(event) - # return + browser._close_many(range(len(browser._tab_layer_by_idx))) for child in dialog.findChildren(description._PseudoUSDBrowser): child._resolved_layers.clear() @@ -585,7 +471,7 @@ def _fake_run(run_args: list): targetpath = str(_test_bed.with_suffix(".jpg")) image.save(targetpath, "JPG") browser._on_identifier_requested(anchor, targetpath) - # return + invalid_crate_layer = Sdf.Layer.CreateAnonymous() invalid_crate_layer.ImportFromString( # Not valid in USD-24.05: https://github.com/PixarAnimationStudios/OpenUSD/blob/59992d2178afcebd89273759f2bddfe730e59aa8/pxr/usd/sdf/testenv/testSdfParsing.testenv/baseline/127_varyingRelationship.sdf#L9 @@ -617,7 +503,6 @@ def GprimSphere "Sphere" self.assertEqual(result, "") def test_display_color_editor(self): - # return stage = cook.fetch_stage(cook.UsdAsset.get_anonymous()) sphere = UsdGeom.Sphere.Define(stage, "/volume") color_var = sphere.GetDisplayColorPrimvar() @@ -641,7 +526,6 @@ def test_display_color_editor(self): self.assertEqual(len(editor._value), 1) def test_stats(self): - # return empty = stats.StageStats() self.assertEqual(empty._usd_tree.topLevelItemCount(), 0) From 4307720321e95077d56f93f436d84da038c8d7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 16:38:42 +1100 Subject: [PATCH 48/67] minor wording and cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/cook/__init__.py | 9 ++++----- grill/views/_graph.py | 6 +++--- tests/test_cook.py | 3 ++- tests/test_views.py | 4 +--- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index fc84fa41..98921577 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -191,7 +191,8 @@ def define_taxon(stage: Usd.Stage, name: str, *, references: tuple[Usd.Prim] = t return prim -def itaxa(stage): +def itaxa(stage: Usd.Stage) -> typing.Generator[Usd.Prim]: + """For the given stage, iterate existing taxa under the taxonomy hierarchy.""" return filter( lambda prim: prim.GetAssetInfoByKey(_ASSETINFO_TAXA_KEY), _usd.iprims(stage, root_paths={_TAXONOMY_ROOT_PATH}, traverse_predicate=Usd.PrimAllPrimsPredicate) @@ -532,10 +533,8 @@ def _inherit_or_specialize_unit(method, context_unit): @functools.singledispatch -def taxonomy_graph(prims, url_id_prefix): - """ - prims - """ +def taxonomy_graph(prims: Usd.Prim, url_id_prefix) -> nx.DiGraph: + """Get the hierarchical taxonomy representation of the given taxa prims.""" graph = nx.DiGraph(tooltip="Taxonomy Graph") graph.graph.update( graph={'rankdir': 'LR'}, diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 0e4639a5..b40a6529 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -49,7 +49,7 @@ Please make sure graphviz is installed and 'dot' available on the system's PATH environment variable. -For more details on installing graphviz, visit https://pygraphviz.github.io/documentation/stable/install.html +For more details on installing graphviz, visit https://graphviz.org/download/ or https://grill.readthedocs.io/en/latest/install.html#conda-environment-example """ @@ -90,7 +90,7 @@ def _dot_2_svg(sourcepath): class _Node(QtWidgets.QGraphicsTextItem): - # TODO: see if we can remove 'label', since we are already processing the graphviz one here, it might be cheaper to have label created only when we need it + # Note: keep 'label' as an argument to use as much as possible as-is for clients to provide their own HTML style def __init__(self, parent=None, label="", color="", fillcolor="", plugs: tuple = (), visible=True): super().__init__(parent) self._edges = [] @@ -599,7 +599,7 @@ def _load_graph(self, graph): self.scene().addItem(text_item) return - try: # exit early if pygraphviz is not installed, needed for positions + try: # exit early if pydot is not installed, needed for positions positions = drawing.nx_pydot.graphviz_layout(graph, prog='dot') except ImportError as exc: message = str(exc) diff --git a/tests/test_cook.py b/tests/test_cook.py index 91dbaee7..6e0e3e28 100644 --- a/tests/test_cook.py +++ b/tests/test_cook.py @@ -50,7 +50,8 @@ def test_fetch_stage(self): usd_opened = str(names.UsdAsset.get_anonymous(item='usd_opened')) Sdf.Layer.CreateNew(str(repo_path / usd_opened)) - with self.assertRaises(Tf.ErrorException): + with self.assertRaisesRegex(Tf.ErrorException, "Failed to open layer"): + # no resolver context, so unable to open stage Usd.Stage.Open(usd_opened) with Ar.ResolverContextBinder(resolver_ctx): diff --git a/tests/test_views.py b/tests/test_views.py index fe2672ae..4f780f45 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -154,8 +154,6 @@ def test_scenegraph_composition(self): widget._layers.table.selectAll() self.assertEqual(5, widget._layers.model.rowCount()) self.assertEqual(1, widget._prims.model.rowCount()) - # add_dll_directory only on Windows - os.add_dll_directory = lambda path: print(f"Added {path}") if not hasattr(os, "add_dll_directory") else os.add_dll_directory _core._which.cache_clear() with mock.patch("grill.views.description._which") as patch: # simulate dot is not in the environment @@ -163,7 +161,7 @@ def test_scenegraph_composition(self): widget._graph_view.view([0,1]) _core._which.cache_clear() - with mock.patch("grill.views.description.nx.nx_agraph.write_dot") as patch: # simulate pygraphviz is not installed + with mock.patch("grill.views.description.nx.nx_agraph.write_dot") as patch: # simulate pydot not installed patch.side_effect = ImportError widget._graph_view.view([0]) From aa51d52b06a9686679d3a0a089233d2bedfc12fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 17:01:22 +1100 Subject: [PATCH 49/67] minor version update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- docs/source/conf.py | 4 ++-- setup.cfg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index dc90d189..6bc8c1c1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -103,9 +103,9 @@ # built documents. # # The short X.Y version. -version = '0.17' +version = '0.18' # The full version, including alpha/beta/rc tags. -release = '0.17.1' +release = '0.18.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.cfg b/setup.cfg index 0fa3d6cc..ea587867 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = grill -version = 0.17.1 +version = 0.18.0 description = Pipeline tools for (but not limited to) audiovisual projects. long_description = file: README.md long_description_content_type = text/markdown From 917d4632f3c8701548edebd2c53fb65b4cddbed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 18:08:50 +1100 Subject: [PATCH 50/67] disable autodoc_typehints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- docs/source/conf.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6bc8c1c1..ce0fbbf3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -31,22 +31,23 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.inheritance_diagram', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.graphviz', - "myst_parser", - 'sphinx_copybutton', - 'sphinx_toggleprompt', - 'sphinx_togglebutton', - 'sphinx_inline_tabs', - 'hoverxref.extension', - 'sphinx.ext.autosectionlabel', - 'sphinx_autodoc_typehints'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.inheritance_diagram', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'sphinx.ext.graphviz', + "myst_parser", + 'sphinx_copybutton', + 'sphinx_toggleprompt', + 'sphinx_togglebutton', + 'sphinx_inline_tabs', + 'hoverxref.extension', + 'sphinx.ext.autosectionlabel', +] # Offset to play well with copybutton toggleprompt_offset_right = 35 From 43669bae7fda650fe9c22c02eab26d2b0466b3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 18:11:08 +1100 Subject: [PATCH 51/67] singledispatched functions are documented now, no need for overload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/cook/__init__.py | 6 +++--- grill/usd/__init__.py | 45 +++++------------------------------------- tests/test_usd.py | 4 ---- 3 files changed, 8 insertions(+), 47 deletions(-) diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index 98921577..670e48da 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -534,7 +534,7 @@ def _inherit_or_specialize_unit(method, context_unit): @functools.singledispatch def taxonomy_graph(prims: Usd.Prim, url_id_prefix) -> nx.DiGraph: - """Get the hierarchical taxonomy representation of the given taxa prims.""" + """Get the hierarchical taxonomy representation of existing prims.""" graph = nx.DiGraph(tooltip="Taxonomy Graph") graph.graph.update( graph={'rankdir': 'LR'}, @@ -556,7 +556,7 @@ def taxonomy_graph(prims: Usd.Prim, url_id_prefix) -> nx.DiGraph: return graph -@taxonomy_graph.register -def _(stage: Usd.Stage, url_id_prefix): +@taxonomy_graph.register(Usd.Stage) +def _(stage: Usd.Stage, url_id_prefix) -> nx.DiGraph: # Convenience for the stage return taxonomy_graph(itaxa(stage), url_id_prefix) diff --git a/grill/usd/__init__.py b/grill/usd/__init__.py index d0efd8ad..dc87942f 100644 --- a/grill/usd/__init__.py +++ b/grill/usd/__init__.py @@ -75,38 +75,8 @@ def iprims(stage: Usd.Stage, root_paths: typing.Iterable[Sdf.Path] = tuple(), pr ) -@typing.overload -def edit_context(payload: Sdf.Payload, /, prim: Usd.Prim) -> Usd.EditContext: - ... - - -@typing.overload -def edit_context(reference: Sdf.Reference, /, prim: Usd.Prim) -> Usd.EditContext: - ... - - -@typing.overload -def edit_context(inherits: Usd.Inherits, /, path: Sdf.Path, layer: Sdf.Layer) -> Usd.EditContext: - ... - - -@typing.overload -def edit_context(specializes: Usd.Specializes, /, path: Sdf.Path, layer: Sdf.Layer) -> Usd.EditContext: - ... - - -@typing.overload -def edit_context(variant: Usd.VariantSet, /, layer: Sdf.Layer) -> Usd.EditContext: - ... - - -@typing.overload -def edit_context(prim: Usd.Prim, /, query_filter: Usd.PrimCompositionQuery.Filter, arc_predicate: typing.Callable) -> Usd.EditContext: - ... - - @functools.singledispatch -def edit_context(obj, /, *args, **kwargs) -> Usd.EditContext: +def edit_context(prim: Usd.Prim, /, query_filter, arc_predicate) -> Usd.EditContext: """Composition arcs target layer stacks. These functions help create EditTargets for the first matching node's root layer stack from prim's composition arcs. This allows for "chained" context switching while preserving the same stage objects. @@ -232,11 +202,6 @@ def Sphere "child" ( } """ - raise TypeError(f"Not implemented: {locals()}") # lazy - - -@edit_context.register -def _(prim: Usd.Prim, /, query_filter, arc_predicate): # https://blogs.mathworks.com/developer/2015/03/31/dont-get-in-too-deep/ # with write.context(prim, dict(kingdom="assets")): # prim.GetAttribute("abc").Set(True) @@ -254,7 +219,7 @@ def _(prim: Usd.Prim, /, query_filter, arc_predicate): @edit_context.register(Sdf.Reference) @edit_context.register(Sdf.Payload) -def _(arc: typing.Union[Sdf.Payload, Sdf.Reference], /, prim): +def _(arc, /, prim) -> Usd.EditContext: identifier = arc.assetPath with Ar.ResolverContextBinder(prim.GetStage().GetPathResolverContext()): # Use Layer.Find since layer should have been open for the prim to exist. @@ -274,12 +239,12 @@ def _(arc: typing.Union[Sdf.Payload, Sdf.Reference], /, prim): @edit_context.register(Usd.Inherits) @edit_context.register(Usd.Specializes) -def _(arc_type: typing.Union[Usd.Inherits, Usd.Specializes], /, path, layer): - return _edit_context_by_arc(arc_type.GetPrim(), type(arc_type), path, layer) +def _(arc, /, path, layer) -> Usd.EditContext: + return _edit_context_by_arc(arc.GetPrim(), type(arc), path, layer) @edit_context.register -def _(variant_set: Usd.VariantSet, /, layer): +def _(variant_set: Usd.VariantSet, /, layer) -> Usd.EditContext: with contextlib.suppress(Tf.ErrorException): return variant_set.GetVariantEditContext() # ----- From Pixar ----- diff --git a/tests/test_usd.py b/tests/test_usd.py index 3b98b8d0..06a3f0b6 100644 --- a/tests/test_usd.py +++ b/tests/test_usd.py @@ -70,10 +70,6 @@ def test_edit_context(self): layer = stage.GetRootLayer() self.assertIsNotNone(layer.GetPrimAtPath(f"{layer.defaultPrim}/inner/child{{color={variant_name}}}")) - with self.assertRaisesRegex(TypeError, "Not implemented"): - # only composition arcs on prims are supported, passing an attribute (or other objects) is not - gusd.edit_context(color) - def test_missing_arc(self): stage = Usd.Stage.CreateInMemory() prim = stage.DefinePrim("/Referenced") From b826b412061addadf45e31ff60d7ecbdeed1f1af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 18:44:42 +1100 Subject: [PATCH 52/67] type union fetch_stage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/cook/__init__.py | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index 670e48da..ec29d245 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -106,48 +106,26 @@ def asset_identifier(path): return str(path.relative_to(Repository.get())) -@typing.overload -def fetch_stage(identifier: str, context: Ar.ResolverContext = None, load=Usd.Stage.LoadAll) -> Usd.Stage: - ... - - -@typing.overload -def fetch_stage(identifier: UsdAsset, context: Ar.ResolverContext = None, load=Usd.Stage.LoadAll) -> Usd.Stage: - ... # TODO: evaluate if it's worth to keep this, or if identifier can be a relative path - - -@functools.singledispatch -def fetch_stage(identifier, context: Ar.ResolverContext = None, load=Usd.Stage.LoadAll) -> Usd.Stage: +def fetch_stage(identifier: typing.Union[str, UsdAsset], context: Ar.ResolverContext = None, load=Usd.Stage.LoadAll) -> Usd.Stage: """Retrieve the `stage `_ whose root `layer `_ matches the given ``identifier``. If the `layer `_ does not exist, it is created in the repository. - If an open matching `stage `_ is found on the `global cache `_, return it. - Otherwise open it, populate the `cache `_ and return it. - .. attention:: ``identifier`` must be a valid :class:`grill.names.UsdAsset` name. """ + if isinstance(identifier, UsdAsset): + identifier = identifier.name + if not context: context = Ar.ResolverContext(Ar.DefaultResolverContext([str(Repository.get())])) with Ar.ResolverContextBinder(context): layer = _fetch_layer(identifier, context) - # return layer return Usd.Stage.Open(layer, load=load) -@fetch_stage.register(UsdAsset) -def _(identifier: UsdAsset, *args, **kwargs) -> Usd.Stage: - return fetch_stage.registry[object](identifier.name, *args, **kwargs) - - -@fetch_stage.register(str) -def _(identifier: str, *args, **kwargs) -> Usd.Stage: - return fetch_stage(UsdAsset(identifier), *args, **kwargs) - - def define_taxon(stage: Usd.Stage, name: str, *, references: tuple[Usd.Prim] = tuple(), id_fields: typing.Mapping[str, str] = types.MappingProxyType({})) -> Usd.Prim: """Define a new `taxon group `_ for asset `taxonomy `_. From 6150a25973970662b86096c54089375a36eee89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 19:11:11 +1100 Subject: [PATCH 53/67] type annotate edit_context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/usd/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/grill/usd/__init__.py b/grill/usd/__init__.py index dc87942f..2f75b479 100644 --- a/grill/usd/__init__.py +++ b/grill/usd/__init__.py @@ -76,7 +76,7 @@ def iprims(stage: Usd.Stage, root_paths: typing.Iterable[Sdf.Path] = tuple(), pr @functools.singledispatch -def edit_context(prim: Usd.Prim, /, query_filter, arc_predicate) -> Usd.EditContext: +def edit_context(prim: Usd.Prim, /, query_filter: Usd.PrimCompositionQuery.Filter, arc_predicate: typing.Callable[[Usd.CompositionArc], bool]) -> Usd.EditContext: """Composition arcs target layer stacks. These functions help create EditTargets for the first matching node's root layer stack from prim's composition arcs. This allows for "chained" context switching while preserving the same stage objects. @@ -219,7 +219,7 @@ def Sphere "child" ( @edit_context.register(Sdf.Reference) @edit_context.register(Sdf.Payload) -def _(arc, /, prim) -> Usd.EditContext: +def _(arc, /, prim: Usd.Prim) -> Usd.EditContext: identifier = arc.assetPath with Ar.ResolverContextBinder(prim.GetStage().GetPathResolverContext()): # Use Layer.Find since layer should have been open for the prim to exist. @@ -239,12 +239,12 @@ def _(arc, /, prim) -> Usd.EditContext: @edit_context.register(Usd.Inherits) @edit_context.register(Usd.Specializes) -def _(arc, /, path, layer) -> Usd.EditContext: +def _(arc, /, path: Sdf.Path, layer: Sdf.Layer) -> Usd.EditContext: return _edit_context_by_arc(arc.GetPrim(), type(arc), path, layer) @edit_context.register -def _(variant_set: Usd.VariantSet, /, layer) -> Usd.EditContext: +def _(variant_set: Usd.VariantSet, /, layer: Sdf.Layer) -> Usd.EditContext: with contextlib.suppress(Tf.ErrorException): return variant_set.GetVariantEditContext() # ----- From Pixar ----- From 2c090551c9ada156f3fdbadc30350ec9fa0cf933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 20:02:41 +1100 Subject: [PATCH 54/67] remove type annotation from variant set edit context. is this a bug with cpython? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/usd/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grill/usd/__init__.py b/grill/usd/__init__.py index 2f75b479..2b5c1773 100644 --- a/grill/usd/__init__.py +++ b/grill/usd/__init__.py @@ -244,7 +244,7 @@ def _(arc, /, path: Sdf.Path, layer: Sdf.Layer) -> Usd.EditContext: @edit_context.register -def _(variant_set: Usd.VariantSet, /, layer: Sdf.Layer) -> Usd.EditContext: +def _(variant_set: Usd.VariantSet, /, layer) -> Usd.EditContext: with contextlib.suppress(Tf.ErrorException): return variant_set.GetVariantEditContext() # ----- From Pixar ----- From 66f11763fc079358d062835894303caa7ee1157d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sun, 8 Dec 2024 20:50:36 +1100 Subject: [PATCH 55/67] small update to test graph and word fix when pydot is not present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 14 ++- tests/test_data/_mini_graph.dot | 26 +++--- tests/test_data/_mini_graph.svg | 160 ++++++++++++++++++-------------- tests/test_views.py | 54 ++++++----- 4 files changed, 149 insertions(+), 105 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index b40a6529..4311f913 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -44,12 +44,14 @@ _NO_PEN = QtGui.QPen(QtCore.Qt.NoPen) -_DOT_ENVIRONMENT_ERROR = """In order to display composition arcs in a graph, +_DOT_ENVIRONMENT_ERROR = """In order to display content in this graph view, the 'dot' command must be available on the current environment. Please make sure graphviz is installed and 'dot' available on the system's PATH environment variable. -For more details on installing graphviz, visit https://graphviz.org/download/ or https://grill.readthedocs.io/en/latest/install.html#conda-environment-example +For more details on installing graphviz, visit: + - https://graphviz.org/download/ or + - https://grill.readthedocs.io/en/latest/install.html#conda-environment-example """ @@ -591,21 +593,24 @@ def _load_graph(self, graph): self.scene().clear() self.viewport().update() + _default_text_interaction = QtCore.Qt.LinksAccessibleByMouse if _IS_QT5 else QtCore.Qt.TextBrowserInteraction + if not _core._which("dot"): # dot has not been installed print(_DOT_ENVIRONMENT_ERROR) text_item = QtWidgets.QGraphicsTextItem() text_item.setPlainText(_DOT_ENVIRONMENT_ERROR) - text_item.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse if _IS_QT5 else QtCore.Qt.TextBrowserInteraction) + text_item.setTextInteractionFlags(_default_text_interaction) self.scene().addItem(text_item) return try: # exit early if pydot is not installed, needed for positions positions = drawing.nx_pydot.graphviz_layout(graph, prog='dot') except ImportError as exc: - message = str(exc) + message = f"{exc}\n\n{_DOT_ENVIRONMENT_ERROR}" print(message) text_item = QtWidgets.QGraphicsTextItem() text_item.setPlainText(message) + text_item.setTextInteractionFlags(_default_text_interaction) self.scene().addItem(text_item) return @@ -752,6 +757,7 @@ def __init__(self, *args, **kwargs): layout.addWidget(self._error_view) layout.setContentsMargins(0, 0, 0, 0) self._error_view.setVisible(False) + self._error_view.setLineWrapMode(QtWidgets.QTextBrowser.NoWrap) self.setLayout(layout) self._dot2svg = None self._threadpool = QtCore.QThreadPool() diff --git a/tests/test_data/_mini_graph.dot b/tests/test_data/_mini_graph.dot index 4ee61255..eee049be 100644 --- a/tests/test_data/_mini_graph.dot +++ b/tests/test_data/_mini_graph.dot @@ -1,17 +1,21 @@ digraph { rankdir=LR; edge [color=crimson]; -1 [label="{<0>x:y:z|<1>z}", style="rounded,filled", shape=record]; -2 [label="{<0>a|<1>b}", style="rounded,filled", shape=record]; -3 [label="{<0>c|<1>d}", style="rounded,filled", shape=record]; -4 [label="{<0>k}", style=invis]; -ancestor [active_plugs="{'surface', 'cycle_out', 'cycle_in', 'roughness'}", shape=none, connections="{'surface': [('successor', 'surface')], 'cycle_out': [('ancestor', 'cycle_in')]}", label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; -successor [active_plugs="{'surface'}", shape=none, connections="{}", label=<
successor
surface
>]; +1 [label="{x:y:z|z}", style=rounded, shape=record]; +2 [label="{a|b}", style=rounded, shape=record]; +3 [label="{c|d}", style=rounded, shape=record]; +parent [shape=box, fillcolor="#afd7ff", color="#1E90FF", style="filled,rounded"]; +child1 [shape=box, fillcolor="#afd7ff", color="#1E90FF", style="filled,rounded"]; +child2 [shape=box, fillcolor="#afd7ff", color="#1E90FF", style=invis]; +ancestor [shape=none, label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; +successor [shape=none, label=<
successor
surface
>]; 1 -> 1 [key=0, color="sienna:crimson:orange"]; 1 -> 2 [key=0, color=crimson]; -2 -> 1 [key=0, color=green]; -2 -> 4 [key=0, color=yellow, label="edge_label"]; -3 -> 2 [key=0, color=blue, tailport=0]; -ancestor -> ancestor [key=0, tailport="cycle_in", headport="cycle_out", tooltip="ancestor.cycle_in -> ancestor.cycle_out"]; -successor -> ancestor [key=0, tailport=surface, headport=surface, tooltip="successor.surface -> ancestor.surface"]; +2 -> 1 [key=0, color=seagreen]; +3 -> 2 [key=0, color=steelblue, tailport=five]; +3 -> 1 [key=0, color=hotpink, tailport=five]; +parent -> child1 [key=0]; +parent -> child2 [key=0, label=invis]; +ancestor -> ancestor [key=0, tailport="cycle_out", headport="cycle_in", tooltip="ancestor.cycle_out -> ancestor.cycle_in"]; +ancestor -> successor [key=0, tailport=surface, headport=surface, tooltip="ancestor.surface -> successor.surface"]; } diff --git a/tests/test_data/_mini_graph.svg b/tests/test_data/_mini_graph.svg index 266b65be..c94b0774 100644 --- a/tests/test_data/_mini_graph.svg +++ b/tests/test_data/_mini_graph.svg @@ -4,109 +4,133 @@ - - - + + + 1 - -x:y:z - -z + +x:y:z + +z 1->1 - - - - + + + + 2 - -a - -b + +a + +b 1->2 - - + + 2->1 - - - - - - -2->4 - - -edge_label + + 3 - -c - -d + +c + +d - + -3:0->2 - - +3:five->1 + + - + + +3:five->2 + + + + + +parent + +parent + + +child1 + +child1 + + + +parent->child1 + + + + + + +parent->child2 + + +invis + + + ancestor - - -ancestor - -cycle_in - -roughness - -cycle_out - -surface - + + +ancestor + +cycle_in + +roughness + +cycle_out + +surface + - -ancestor:cycle_in->ancestor:cycle_out - - - + +ancestor:cycle_out->ancestor:cycle_in + + + - + successor - - -successor - -surface - - - - -successor:surface->ancestor:surface - - - + + +successor + +surface + + + + +ancestor:surface->successor:surface + + + diff --git a/tests/test_views.py b/tests/test_views.py index 4f780f45..b53e7fe2 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -558,31 +558,38 @@ def test_graph_views(self): nodes_info = { 1: dict( - label="{<0>x:y:z|<1>z}", - style="rounded,filled", + label="{x:y:z|z}", + style="rounded", # these can be set at the graph level shape="record", ), 2: dict( - label="{<0>a|<1>b}", - style="rounded,filled", + label="{a|b}", + style='rounded', shape="record", ), 3: dict( - label="{<0>c|<1>d}", - style="rounded,filled", + label="{c|d}", + style='rounded', shape="record", ), - 4: dict( - label="{<0>k}", - style="invis", + "parent": dict( + shape="box", fillcolor="#afd7ff", color="#1E90FF", style="filled,rounded" + ), + "child1": dict( + shape="box", fillcolor="#afd7ff", color="#1E90FF", style="filled,rounded" + ), + "child2": dict( + shape="box", fillcolor="#afd7ff", color="#1E90FF", style="invis" ), } edges_info = ( (1, 1, dict(color='sienna:crimson:orange')), (1, 2, dict(color='crimson')), - (2, 1, dict(color='green')), - (3, 2, dict(color='blue', tailport='0')), - (2, 4, dict(color='yellow', label='edge_label')), + (2, 1, dict(color='seagreen')), + (3, 2, dict(color='steelblue', tailport='five')), + (3, 1, dict(color='hotpink', tailport='five')), + ("parent", "child1"), + ("parent", "child2", dict(label='invis')), ) graph = _graph.nx.MultiDiGraph() @@ -603,16 +610,14 @@ def test_graph_views(self): 'cycle_out': 3, 'surface': 4 }, - active_plugs={'cycle_in', 'cycle_out', 'roughness', 'surface'}, shape='none', connections=dict( surface=[('successor', 'surface')], cycle_out=[('ancestor', 'cycle_in')], - ) + ), ), successor=dict( plugs={'': 0, 'surface': 1}, - active_plugs={'surface'}, shape='none', connections=dict(), ) @@ -637,10 +642,13 @@ def _add_edges(src_node, src_name, tgt_node, tgt_name): # color = plug_colors[type(plug)] if isinstance(plug, UsdShade.Output) or sources else background_color label += table_row.format(port=plug_name, color=color, text=f'{plug_name}') for source_node, source_plug in sources: - _add_edges(source_node, source_plug, node, plug_name) + # node_id='ancestor', plug_name='cycle_out', ancestor, source.sourceName='cycle_in' + # tooltip='/TexModel/boardMat/PBRShader.cycle_in -> /TexModel/boardMat/PBRShader.cycle_out' + _add_edges(node, plug_name, source_node, source_plug) label += '>' data['label'] = label + data.pop('connections', None) graph.add_nodes_from(connection_nodes.items()) graph.add_edges_from(connection_edges) @@ -656,12 +664,14 @@ def _use_test_svg(self, filepath): def _test_positions(graph, prog): return { - 1: (40.0, 91.692), - 2: (157.37, 91.692), - 3: (40.0, 36.692), - 4: (332.85, 91.692), - 'ancestor': (157.37, 208.69), - 'successor': (40.0, 174.69), + 1: (218.75, 90.1), + 2: (322.75, 90.1), + 3: (76.125, 61.1), + 'parent': (76.125, 190.1), + 'child1': (218.75, 217.1), + 'child2': (218.75, 163.1), + 'ancestor': (76.125, 316.1), + 'successor': (218.75, 282.1), } with ( From 6d4df185c3673b592cc9dd071a2c49dc9976086a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 9 Dec 2024 21:36:49 +1100 Subject: [PATCH 56/67] add environment marker for networkx in python<=3.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- setup.cfg | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ea587867..e3030f22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,13 @@ classifiers = Programming Language :: Python :: 3.13 [options] -install_requires = grill-names>=2.6.0; networkx>=3.4; pydot>=3.0.1; numpy; printree +install_requires = + grill-names>=2.6.0 + printree + numpy + pydot>=3.0.1 + networkx>=3.4; python_version > 3.9 + networkx<=2.8.3; python_version <= 3.9 include_package_data = True packages = find_namespace: From 6dfff31c9f4ec53707159748285172948cdc3127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 9 Dec 2024 21:37:14 +1100 Subject: [PATCH 57/67] keep ptyhon-3.9 in ci MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- .github/workflows/python-package.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a8a21952..29f2a2e2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -14,8 +14,10 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.12"] + python-version: ["3.9", "3.10", "3.12"] include: + - python-version: "3.9" + install-arguments: ". PySide2 usd-core==21.8 PyOpenGL" - python-version: "3.10" install-arguments: ". PySide2 usd-core==23.2 PyOpenGL" - python-version: "3.12" From 58e0f4ad911e937af8bd668e77a5b685c6cf780d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 9 Dec 2024 21:39:56 +1100 Subject: [PATCH 58/67] wrap python_version in quotes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index e3030f22..fec21ff2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,8 @@ install_requires = printree numpy pydot>=3.0.1 - networkx>=3.4; python_version > 3.9 - networkx<=2.8.3; python_version <= 3.9 + networkx>=3.4; python_version > "3.9" + networkx<=2.8.3; python_version <= "3.9" include_package_data = True packages = find_namespace: From e3f91679f3bee1cb6c7a7ad5fdc93195e8dc7a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 9 Dec 2024 21:43:46 +1100 Subject: [PATCH 59/67] minimum usd is 21.11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 29f2a2e2..47ba8740 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,7 +17,7 @@ jobs: python-version: ["3.9", "3.10", "3.12"] include: - python-version: "3.9" - install-arguments: ". PySide2 usd-core==21.8 PyOpenGL" + install-arguments: ". PySide2 usd-core==21.11 PyOpenGL" # 21.11 enables AR-2.0 by default - python-version: "3.10" install-arguments: ". PySide2 usd-core==23.2 PyOpenGL" - python-version: "3.12" From 4bf293641f05ad16e4df079ff32e3eac072cbc07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 9 Dec 2024 22:06:49 +1100 Subject: [PATCH 60/67] use SVGViewer for connections with networkx==2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/description.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/grill/views/description.py b/grill/views/description.py index e408007f..b6256e1b 100644 --- a/grill/views/description.py +++ b/grill/views/description.py @@ -318,7 +318,10 @@ def _nx_graph_edge_filter(*, has_specs=None, ancestral=None, implicit=None, intr class _ConnectableAPIViewer(QtWidgets.QDialog): def __init__(self, parent=None, **kwargs): super().__init__(parent=parent, **kwargs) - self._graph_view = _graph._GraphViewer(parent=self) + if nx.__version__.startswith("2"): + self._graph_view = _graph._GraphSVGViewer(parent=self) + else: + self._graph_view = _graph._GraphViewer(parent=self) vertical = QtWidgets.QSplitter(QtCore.Qt.Vertical) vertical.addWidget(self._graph_view) self.setFocusProxy(self._graph_view) From 1e8d9037e614f0e8d8f792a632f338eefbb5d7f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Mon, 9 Dec 2024 23:06:25 +1100 Subject: [PATCH 61/67] add plugs in connection viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 2 +- grill/views/description.py | 4 +++- tests/test_views.py | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 4311f913..5d50371c 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -621,7 +621,7 @@ def _load_graph(self, graph): def _add_node(nx_node): node_data = graph.nodes[nx_node] - plugs = node_data.pop('plugs', ()) + plugs = node_data.pop('plugs', ()) # implementation detail nodes_attrs = ChainMap(node_data, graph_node_attrs) if (shape := nodes_attrs.get('shape')) == 'record': try: diff --git a/grill/views/description.py b/grill/views/description.py index b6256e1b..f26227de 100644 --- a/grill/views/description.py +++ b/grill/views/description.py @@ -255,6 +255,7 @@ def traverse(api: UsdShade.ConnectableAPI): node_id = _get_node_id(current_prim) label = f'<' label += table_row.format(port="", color="white", text=f'{api.GetPrim().GetName()}') + plugs = {"": 0} # {graphviz port name: port index order} for index, plug in enumerate(chain(api.GetInputs(), api.GetOutputs()), start=1): # we start at 1 because index 0 is the node itself plug_name = plug.GetBaseName() sources, __ = plug.GetConnectedSources() # (valid, invalid): we care only about valid sources (index 0) @@ -263,8 +264,9 @@ def traverse(api: UsdShade.ConnectableAPI): for source in sources: _add_edges(_get_node_id(source.source.GetPrim()), source.sourceName, node_id, plug_name) traverse(source.source) + plugs[plug_name] = index label += '
>' - all_nodes[node_id] = dict(label=label) + all_nodes[node_id] = dict(label=label, plugs=plugs) traverse(connections_api) diff --git a/tests/test_views.py b/tests/test_views.py index b53e7fe2..1b8cb95e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -117,7 +117,6 @@ def test_connection_view(self): cycle_input = pbrShader.CreateInput("cycle_in", Sdf.ValueTypeNames.Float) cycle_output = pbrShader.CreateOutput("cycle_out", Sdf.ValueTypeNames.Float) cycle_input.ConnectToSource(cycle_output) - description._graph_from_connections(material) viewer = description._ConnectableAPIViewer() # GraphView capabilities are tested elsewhere, so mock 'view' here. viewer._graph_view.view = lambda indices: None From e74af59db2c27dd67c7d043a043cca41654e1441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 14 Dec 2024 09:49:44 +1100 Subject: [PATCH 62/67] add py3.9 back in classifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index fec21ff2..a092ba7c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,7 @@ author_email = chris.gfz@gmail.com author = Christian López Barrón url = https://github.com/thegrill/grill classifiers = + Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 From 37cabe3091e4395fd77752e4d551204391c20c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 14 Dec 2024 09:57:29 +1100 Subject: [PATCH 63/67] test port existence in connection view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 2 +- grill/views/description.py | 6 +++--- tests/test_views.py | 9 +++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 5d50371c..3424fb66 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -621,7 +621,7 @@ def _load_graph(self, graph): def _add_node(nx_node): node_data = graph.nodes[nx_node] - plugs = node_data.pop('plugs', ()) # implementation detail + plugs = node_data.get('plugs', ()) nodes_attrs = ChainMap(node_data, graph_node_attrs) if (shape := nodes_attrs.get('shape')) == 'record': try: diff --git a/grill/views/description.py b/grill/views/description.py index f26227de..02151555 100644 --- a/grill/views/description.py +++ b/grill/views/description.py @@ -255,8 +255,8 @@ def traverse(api: UsdShade.ConnectableAPI): node_id = _get_node_id(current_prim) label = f'<' label += table_row.format(port="", color="white", text=f'{api.GetPrim().GetName()}') - plugs = {"": 0} # {graphviz port name: port index order} - for index, plug in enumerate(chain(api.GetInputs(), api.GetOutputs()), start=1): # we start at 1 because index 0 is the node itself + plugs = [""] # port names for this node. Empty string is used to refer to the node itself (no port). + for plug in chain(api.GetInputs(), api.GetOutputs()): plug_name = plug.GetBaseName() sources, __ = plug.GetConnectedSources() # (valid, invalid): we care only about valid sources (index 0) color = plug_colors[type(plug)] if isinstance(plug, UsdShade.Output) or sources else background_color @@ -264,7 +264,7 @@ def traverse(api: UsdShade.ConnectableAPI): for source in sources: _add_edges(_get_node_id(source.source.GetPrim()), source.sourceName, node_id, plug_name) traverse(source.source) - plugs[plug_name] = index + plugs.append(plug_name) label += '
>' all_nodes[node_id] = dict(label=label, plugs=plugs) diff --git a/tests/test_views.py b/tests/test_views.py index 1b8cb95e..f60b6e88 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -111,8 +111,10 @@ def test_connection_view(self): stage = Usd.Stage.CreateInMemory() material = UsdShade.Material.Define(stage, '/TexModel/boardMat') pbrShader = UsdShade.Shader.Define(stage, '/TexModel/boardMat/PBRShader') - pbrShader.CreateInput("roughness", Sdf.ValueTypeNames.Float).Set(0.4) - material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), "surface") + roughness_name = "roughness" + pbrShader.CreateInput(roughness_name, Sdf.ValueTypeNames.Float).Set(0.4) + surface_name = "surface" + material.CreateSurfaceOutput().ConnectToSource(pbrShader.ConnectableAPI(), surface_name) # Ensure cycles don't cause recursion cycle_input = pbrShader.CreateInput("cycle_in", Sdf.ValueTypeNames.Float) cycle_output = pbrShader.CreateOutput("cycle_out", Sdf.ValueTypeNames.Float) @@ -121,6 +123,9 @@ def test_connection_view(self): # GraphView capabilities are tested elsewhere, so mock 'view' here. viewer._graph_view.view = lambda indices: None viewer.setPrim(material) + graph = viewer._graph_view._graph + self.assertEqual(graph.nodes[str(material.GetPrim().GetPath())]['plugs'], ['', surface_name]) + self.assertEqual(graph.nodes[str(pbrShader.GetPrim().GetPath())]['plugs'], ['', cycle_input.GetName(), roughness_name, cycle_output.GetName(), surface_name]) viewer.setPrim(None) def test_scenegraph_composition(self): From b7c3fa0c9d33d9d1c2b2480034fa5f2f9e8ebc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 14 Dec 2024 09:59:35 +1100 Subject: [PATCH 64/67] input.GetBaseName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_views.py b/tests/test_views.py index f60b6e88..afc8c90e 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -125,7 +125,7 @@ def test_connection_view(self): viewer.setPrim(material) graph = viewer._graph_view._graph self.assertEqual(graph.nodes[str(material.GetPrim().GetPath())]['plugs'], ['', surface_name]) - self.assertEqual(graph.nodes[str(pbrShader.GetPrim().GetPath())]['plugs'], ['', cycle_input.GetName(), roughness_name, cycle_output.GetName(), surface_name]) + self.assertEqual(graph.nodes[str(pbrShader.GetPrim().GetPath())]['plugs'], ['', cycle_input.GetBaseName(), roughness_name, cycle_output.GetBaseName(), surface_name]) viewer.setPrim(None) def test_scenegraph_composition(self): From 5171a888fb82e733a3ddfafa8f25a059c4a0c214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 14 Dec 2024 10:30:54 +1100 Subject: [PATCH 65/67] updated views._graph.Node internals to use port instead of plug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/_graph.py | 122 ++++++++++++++++---------------- grill/views/description.py | 22 +++--- tests/test_data/_mini_graph.dot | 6 +- tests/test_views.py | 33 ++++----- 4 files changed, 88 insertions(+), 95 deletions(-) diff --git a/grill/views/_graph.py b/grill/views/_graph.py index 3424fb66..a8db2d7b 100644 --- a/grill/views/_graph.py +++ b/grill/views/_graph.py @@ -38,7 +38,7 @@ # - Tooltip on nodes for _GraphViewer # - Context menu items # - Ability to move further in canvas after Nodes don't exist -# - when switching a node left to right with precise source layers, the source node plugs do not refresh if we're moving the target node +# - when switching a node left to right with precise source layers, the source node ports do not refresh if we're moving the target node # - refactor conditionals for _GraphSVGViewer from the description module @@ -64,19 +64,20 @@ def _adjust_graphviz_html_table_label(label): return label -def _get_html_table_from_fields(**fields): +def _get_html_table_from_ports(**ports): label = '' - for index, (port, text) in enumerate(fields.items()): + for index, (name, text) in enumerate(ports.items()): bgcolor = "white" if index % 2 == 0 else "#f0f6ff" # light blue text = f'{text}' - label += f"" + label += f"" label += "
{text}
{text}
" return label -def _get_plugs_from_label(label) -> dict[str, str]: +def _get_ports_from_label(label) -> dict[str, str]: if not label.startswith("{"): # Only for record labels. - raise ValueError(f"Label needs to start with '{{' to extract plugs from it, for example: '{{item|another_item}}'. Got label: '{label}'") + raise ValueError(f"Label needs to start with '{{' to extract ports from it, for example: '{{item|another_item}}'. Got label: '{label}'") + # see https://graphviz.org/doc/info/shapes.html#record fields = label.strip("{}").split("|") return dict(field.strip("<>").split(">", 1) for field in fields) @@ -93,12 +94,12 @@ def _dot_2_svg(sourcepath): class _Node(QtWidgets.QGraphicsTextItem): # Note: keep 'label' as an argument to use as much as possible as-is for clients to provide their own HTML style - def __init__(self, parent=None, label="", color="", fillcolor="", plugs: tuple = (), visible=True): + def __init__(self, parent=None, label="", color="", fillcolor="", ports: tuple = (), visible=True): super().__init__(parent) self._edges = [] - self._plugs = dict(zip(plugs, range(len(plugs)))) or {} # {identifier: index} - self._active_plugs_by_side = dict() # {index: {left[int]: {}, right[int]: {}} - self._plug_items = {} # {index: (QEllipse, QEllipse)} + self._ports = dict(zip(ports, range(len(ports)))) or {} # {identifier: index} + self._active_ports_by_side = dict() # {index: {left[int]: {}, right[int]: {}} + self._port_items = {} # {index: (QEllipse, QEllipse)} self._pen = QtGui.QPen(QtGui.QColor(color), 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin) self._fillcolor = QtGui.QColor(fillcolor) self.setHtml("" + label) @@ -164,61 +165,62 @@ def itemChange(self, change: QtWidgets.QGraphicsItem.GraphicsItemChange, value): edge.adjust() return super().itemChange(change, value) - def _activatePlug(self, edge, plug_index, side, position): - if plug_index is None: + def _activatePort(self, edge, port, side, position): + if port is None: return # we're at the center, nothing to draw nor activate try: - plugs_by_side = self._active_plugs_by_side[plug_index] # {index: {left[int]: {}, right[int]: {}} - except KeyError: # first time we're activating a plug, so add a visual ellipse for it + ports_by_side = self._active_ports_by_side[port] # {index: {left[int]: {}, right[int]: {}} + except KeyError: # first time we're activating a port, so add a visual ellipse for it radius = 4 - def _add_plug_item(): + def _add_port_item(): item = QtWidgets.QGraphicsEllipseItem(-radius, -radius, 2 * radius, 2 * radius) item.setPen(_NO_PEN) self.scene().addItem(item) return item - self._plug_items[plug_index] = (_add_plug_item(), _add_plug_item()) - self._active_plugs_by_side[plug_index] = plugs_by_side = {0: dict(), 1: dict()} + self._port_items[port] = (_add_port_item(), _add_port_item()) + self._active_ports_by_side[port] = ports_by_side = {0: dict(), 1: dict()} - plugs_by_side[side][edge] = True + ports_by_side[side][edge] = True other_side = bool(not side) - inactive_plugs = plugs_by_side[other_side] - inactive_plugs.pop(edge, None) - plug_items = self._plug_items[plug_index] # {index: (QEllipse, QEllipse)} - if not inactive_plugs: - plug_items[other_side].setVisible(False) - this_item = plug_items[side] + inactive_ports = ports_by_side[other_side] + inactive_ports.pop(edge, None) + port_items = self._port_items[port] # {index: (QEllipse, QEllipse)} + if not inactive_ports: + port_items[other_side].setVisible(False) + this_item = port_items[side] this_item.setVisible(True) this_item.setBrush(edge._brush) - plug_items[side].setPos(position) + port_items[side].setPos(position) class _Edge(QtWidgets.QGraphicsItem): - def __init__(self, source: _Node, target: _Node, *, source_plug: int =None, target_plug: int =None, label="", color="", is_bidirectional=False, parent: QtWidgets.QGraphicsItem = None): + def __init__(self, source: _Node, target: _Node, *, source_port: int = None, target_port: int = None, label="", color="", is_bidirectional=False, parent: QtWidgets.QGraphicsItem = None): super().__init__(parent) source.add_edge(self) target.add_edge(self) self._source = source self._target = target - self._source_plug = source_plug - self._target_plug = target_plug - self._is_source_plugged = source_plug is not None - self._is_target_plugged = target_plug is not None + self._source_port = source_port + self._target_port = target_port + self._is_source_port_used = source_port is not None + self._is_target_port_used = target_port is not None self._is_cycle = is_cycle = source == target - self._plug_positions = plug_positions = {} + self._port_positions = port_positions = {} outer_shift = 10 # surrounding rect has ~5 px top and bottom - for node, plug, max_plug_idx in (source, source_plug, max(source._plugs.values(), default=0)), (target, target_plug, max(target._plugs.values(), default=0)): + # TODO: this is the main reason of why Node._ports has {port: index}. See if it can be removed + for node, port, max_port_idx in (source, source_port, max(source._ports.values(), default=0)), (target, target_port, max(target._ports.values(), default=0)): bounds = node.boundingRect() - if plug is None: - plug_positions[node, plug] = {None: QtCore.QPointF(bounds.right() - 5, bounds.height() / 2 - 20) if is_cycle else bounds.center()} + if port is None: + port_positions[node, port] = {None: QtCore.QPointF(bounds.right() - 5, bounds.height() / 2 - 20) if is_cycle else bounds.center()} continue - # max_plug_idx can be 0, so we add 1 since this needs to be 1-index based - port_size = (bounds.height() - outer_shift) / (max_plug_idx + 1) - y_pos = (plug * port_size) + (port_size / 2) + (outer_shift / 2) - plug_positions[node, plug] = { + # max_port_idx can be 0, so we add 1 since this needs to be 1-index based + port_size = (bounds.height() - outer_shift) / (max_port_idx + 1) + y_pos = (port * port_size) + (port_size / 2) + (outer_shift / 2) + port_positions[node, port] = { 0: QtCore.QPointF(0, y_pos), # left 1: QtCore.QPointF(bounds.right(), y_pos), # right } @@ -229,7 +231,7 @@ def __init__(self, source: _Node, target: _Node, *, source_plug: int =None, targ self._line = QtCore.QLineF() self.setZValue(-1) - self._spline_path = QtGui.QPainterPath() if (self._is_source_plugged or self._is_target_plugged) else None + self._spline_path = QtGui.QPainterPath() if (self._is_source_port_used or self._is_target_port_used) else None self._colors = colors = color.split(":") main_color = QtGui.QColor(colors[0]) @@ -265,8 +267,8 @@ def boundingRect(self) -> QtCore.QRectF: @property def _cycle_start_position(self): - if not self._is_source_plugged: - return self._source.pos() + self._plug_positions[self._source, self._source_plug][None] + if not self._is_source_port_used: + return self._source.pos() + self._port_positions[self._source, self._source_port][None] return self._line.p1() + QtCore.QPointF(-3, -31) @@ -279,14 +281,14 @@ def adjust(self): source_on_left = self._is_cycle or (self._source.boundingRect().center().x() + source_pos.x() < target_bounds.center().x() + target_pos.x()) - is_source_plugged = self._is_source_plugged - is_target_plugged = self._is_target_plugged - source_side = source_on_left if is_source_plugged else None - target_side = not source_side if is_target_plugged else None - source_point = source_pos + self._plug_positions[self._source, self._source_plug][source_side] - target_point = target_pos + self._plug_positions[self._target, self._target_plug][target_side] + is_source_port_used = self._is_source_port_used + is_target_port_used = self._is_target_port_used + source_side = source_on_left if is_source_port_used else None + target_side = not source_side if is_target_port_used else None + source_point = source_pos + self._port_positions[self._source, self._source_port][source_side] + target_point = target_pos + self._port_positions[self._target, self._target_port][target_side] - if not is_target_plugged: + if not is_target_port_used: line = QtCore.QLineF(source_point, target_point) if not self._spline_path and self._bidirectional_shift and source_point != target_point: # offset in case of bidirectional connections when we are not using splines (as lines would overlap) @@ -319,15 +321,15 @@ def adjust(self): falloff = (length / 100) ** 2 if length < 100 else 1 control_point_shift = (1 if source_on_left else -1) * 75 * falloff - control_point1 = source_point + QtCore.QPointF(control_point_shift, 0) if is_source_plugged else source_point - control_point2 = target_point + QtCore.QPointF(-control_point_shift, 0) if is_target_plugged else target_point + control_point1 = source_point + QtCore.QPointF(control_point_shift, 0) if is_source_port_used else source_point + control_point2 = target_point + QtCore.QPointF(-control_point_shift, 0) if is_target_port_used else target_point self._spline_path = QtGui.QPainterPath() self._spline_path.moveTo(source_point) self._spline_path.cubicTo(control_point1, control_point2, target_point) - self._source._activatePlug(self, self._source_plug, source_side, source_point) - self._target._activatePlug(self, self._target_plug, target_side, target_point) + self._source._activatePort(self, self._source_port, source_side, source_point) + self._target._activatePort(self, self._target_port, target_side, target_point) if self._label_text: self._label_text.setPos((source_point + target_point) / 2) @@ -621,20 +623,20 @@ def _load_graph(self, graph): def _add_node(nx_node): node_data = graph.nodes[nx_node] - plugs = node_data.get('plugs', ()) + ports = node_data.get('ports', ()) nodes_attrs = ChainMap(node_data, graph_node_attrs) if (shape := nodes_attrs.get('shape')) == 'record': try: label = node_data['label'] except KeyError: raise ValueError(f"'label' must be supplied when 'record' shape is set for node: '{nx_node}' with data: {node_data}") - if plugs: + if ports: raise ValueError(f"record 'shape' and 'ports' are mutually exclusive, pick one for node: '{nx_node}' with data: {node_data}") try: - plugs = _get_plugs_from_label(label) + ports = _get_ports_from_label(label) except ValueError as exc: raise ValueError(f"In order to use the 'record' shape, a record 'label' in the form of: '{{text1|text2}}' must be used") from exc - label = _get_html_table_from_fields(**plugs) + label = _get_html_table_from_ports(**ports) else: label = node_data.get('label') if shape in {'none', 'plaintext'}: @@ -648,7 +650,7 @@ def _add_node(nx_node): label=label, color=nodes_attrs.get("color", ""), fillcolor=nodes_attrs.get("fillcolor", "white"), - plugs=plugs, + ports=ports, visible=nodes_attrs.get('style', "") != "invis", ) item.linkActivated.connect(self._graph_url_changed) @@ -680,9 +682,9 @@ def _add_node(nx_node): color = edge_data.get('color', edge_color) label = edge_data.get('label', '') kwargs = dict() - if source._plugs or target._plugs: - kwargs['target_plug'] = target._plugs[edge_data['headport']] if edge_data.get('headport') is not None else None - kwargs['source_plug'] = source._plugs[edge_data['tailport']] if edge_data.get('tailport') is not None else None + if source._ports or target._ports: + kwargs['target_port'] = target._ports[edge_data['headport']] if edge_data.get('headport') is not None else None + kwargs['source_port'] = source._ports[edge_data['tailport']] if edge_data.get('tailport') is not None else None edge = _Edge(source, target, color=color, label=label, is_bidirectional=is_bidirectional, **kwargs) self.scene().addItem(edge) diff --git a/grill/views/description.py b/grill/views/description.py index 02151555..aae195fa 100644 --- a/grill/views/description.py +++ b/grill/views/description.py @@ -229,7 +229,7 @@ def _graph_from_connections(prim: Usd.Prim) -> nx.MultiDiGraph: graph.graph['edge'] = {"color": 'crimson'} all_nodes = dict() # {node_id: {graphviz_attr: value}} - edges = list() # [(source_node_id, target_node_id, {source_plug_name, target_plug_name, graphviz_attrs})] + edges = list() # [(source_node_id, target_node_id, {source_port_name, target_port_name, graphviz_attrs})] @cache def _get_node_id(api): @@ -240,7 +240,7 @@ def _add_edges(src_node, src_name, tgt_node, tgt_name): tooltip = f"{src_node}.{src_name} -> {tgt_node}.{tgt_name}" edges.append((src_node, tgt_node, {"tailport": src_name, "headport": tgt_name, "tooltip": tooltip})) - plug_colors = { + port_colors = { UsdShade.Input: outline_color, # blue UsdShade.Output: "#F08080" # "lightcoral", # pink } @@ -255,18 +255,18 @@ def traverse(api: UsdShade.ConnectableAPI): node_id = _get_node_id(current_prim) label = f'<' label += table_row.format(port="", color="white", text=f'{api.GetPrim().GetName()}') - plugs = [""] # port names for this node. Empty string is used to refer to the node itself (no port). - for plug in chain(api.GetInputs(), api.GetOutputs()): - plug_name = plug.GetBaseName() - sources, __ = plug.GetConnectedSources() # (valid, invalid): we care only about valid sources (index 0) - color = plug_colors[type(plug)] if isinstance(plug, UsdShade.Output) or sources else background_color - label += table_row.format(port=plug_name, color=color, text=f'{plug_name}') + ports = [""] # port names for this node. Empty string is used to refer to the node itself (no port). + 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'{port_name}') for source in sources: - _add_edges(_get_node_id(source.source.GetPrim()), source.sourceName, node_id, plug_name) + _add_edges(_get_node_id(source.source.GetPrim()), source.sourceName, node_id, port_name) traverse(source.source) - plugs.append(plug_name) + ports.append(port_name) label += '
>' - all_nodes[node_id] = dict(label=label, plugs=plugs) + all_nodes[node_id] = dict(label=label, ports=ports) traverse(connections_api) diff --git a/tests/test_data/_mini_graph.dot b/tests/test_data/_mini_graph.dot index eee049be..ec9bea04 100644 --- a/tests/test_data/_mini_graph.dot +++ b/tests/test_data/_mini_graph.dot @@ -7,8 +7,8 @@ edge [color=crimson]; parent [shape=box, fillcolor="#afd7ff", color="#1E90FF", style="filled,rounded"]; child1 [shape=box, fillcolor="#afd7ff", color="#1E90FF", style="filled,rounded"]; child2 [shape=box, fillcolor="#afd7ff", color="#1E90FF", style=invis]; -ancestor [shape=none, label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; -successor [shape=none, label=<
successor
surface
>]; +ancestor [ports="('', 'cycle_in', 'roughness', 'cycle_out', 'surface')", shape=none, label=<
ancestor
cycle_in
roughness
cycle_out
surface
>]; +successor [ports="('', 'surface')", shape=none, label=<
successor
surface
>]; 1 -> 1 [key=0, color="sienna:crimson:orange"]; 1 -> 2 [key=0, color=crimson]; 2 -> 1 [key=0, color=seagreen]; @@ -18,4 +18,4 @@ parent -> child1 [key=0]; parent -> child2 [key=0, label=invis]; ancestor -> ancestor [key=0, tailport="cycle_out", headport="cycle_in", tooltip="ancestor.cycle_out -> ancestor.cycle_in"]; ancestor -> successor [key=0, tailport=surface, headport=surface, tooltip="ancestor.surface -> successor.surface"]; -} +} \ No newline at end of file diff --git a/tests/test_views.py b/tests/test_views.py index afc8c90e..bb1a8029 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -124,8 +124,8 @@ def test_connection_view(self): viewer._graph_view.view = lambda indices: None viewer.setPrim(material) graph = viewer._graph_view._graph - self.assertEqual(graph.nodes[str(material.GetPrim().GetPath())]['plugs'], ['', surface_name]) - self.assertEqual(graph.nodes[str(pbrShader.GetPrim().GetPath())]['plugs'], ['', cycle_input.GetBaseName(), roughness_name, cycle_output.GetBaseName(), surface_name]) + self.assertEqual(graph.nodes[str(material.GetPrim().GetPath())]['ports'], ['', surface_name]) + self.assertEqual(graph.nodes[str(pbrShader.GetPrim().GetPath())]['ports'], ['', cycle_input.GetBaseName(), roughness_name, cycle_output.GetBaseName(), surface_name]) viewer.setPrim(None) def test_scenegraph_composition(self): @@ -549,7 +549,7 @@ def test_graph_views(self): (dict(shape='record'), "'label' must be supplied"), (dict(shape='record', label='no record'), "a record 'label' in the form of"), (dict(shape='record', label='{1}'), "a record 'label' in the form of"), - (dict(shape='record', label='{<0>1}', plugs={'first': 1, 'second': 2}), "record 'shape' and 'ports' are mutually exclusive"), + (dict(shape='record', label='{<0>1}', ports=('first', 'second')), "record 'shape' and 'ports' are mutually exclusive"), (dict(shape='none'), "A label must be provided"), ): invalid_graph = _graph.nx.MultiDiGraph() @@ -607,13 +607,7 @@ def test_graph_views(self): connection_nodes = dict( ancestor=dict( - plugs={ - '': 0, - 'cycle_in': 1, - 'roughness': 2, - 'cycle_out': 3, - 'surface': 4 - }, + ports=('', 'cycle_in', 'roughness', 'cycle_out', 'surface'), shape='none', connections=dict( surface=[('successor', 'surface')], @@ -621,7 +615,7 @@ def test_graph_views(self): ), ), successor=dict( - plugs={'': 0, 'surface': 1}, + ports=('', 'surface'), shape='none', connections=dict(), ) @@ -636,19 +630,16 @@ def _add_edges(src_node, src_name, tgt_node, tgt_name): label = f'<' label += table_row.format(port="", color="white", text=f'{node}') - # for index, plug in enumerate(data['plugs'], start=1): # we start at 1 because index 0 is the node itself - for plug, index in data['plugs'].items(): # we start at 1 because index 0 is the node itself - if not plug: + for port in data['ports']: + if not port: continue - plug_name = plug - sources = data['connections'].get(plug, []) # (valid, invalid): we care only about valid sources (index 0) + sources = data['connections'].get(port, []) # (valid, invalid): we care only about valid sources (index 0) color = r"#F08080" if sources else background_color - # color = plug_colors[type(plug)] if isinstance(plug, UsdShade.Output) or sources else background_color - label += table_row.format(port=plug_name, color=color, text=f'{plug_name}') - for source_node, source_plug in sources: - # node_id='ancestor', plug_name='cycle_out', ancestor, source.sourceName='cycle_in' + label += table_row.format(port=port, color=color, text=f'{port}') + for source_node, source_port in sources: + # node_id='ancestor', port_name='cycle_out', ancestor, source.sourceName='cycle_in' # tooltip='/TexModel/boardMat/PBRShader.cycle_in -> /TexModel/boardMat/PBRShader.cycle_out' - _add_edges(node, plug_name, source_node, source_plug) + _add_edges(node, port, source_node, source_port) label += '
>' data['label'] = label From 98e396d33fd540ab576b6ddaf206cbf4ae1727f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 14 Dec 2024 11:43:43 +1100 Subject: [PATCH 66/67] formatting and comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/description.py | 2 ++ tests/test_data/_mini_graph.dot | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/grill/views/description.py b/grill/views/description.py index aae195fa..c48dc4b0 100644 --- a/grill/views/description.py +++ b/grill/views/description.py @@ -321,6 +321,8 @@ class _ConnectableAPIViewer(QtWidgets.QDialog): def __init__(self, parent=None, **kwargs): super().__init__(parent=parent, **kwargs) if nx.__version__.startswith("2"): + # TODO: Remove this if-statement when Py-3.9 / networkx-2 support is dropped (starting Py-3.10) + # Use SVG when networkx-2 is in use, as there are fixes to pydot graph inspection which only exist in nx-3 self._graph_view = _graph._GraphSVGViewer(parent=self) else: self._graph_view = _graph._GraphViewer(parent=self) diff --git a/tests/test_data/_mini_graph.dot b/tests/test_data/_mini_graph.dot index ec9bea04..5e1cd174 100644 --- a/tests/test_data/_mini_graph.dot +++ b/tests/test_data/_mini_graph.dot @@ -18,4 +18,4 @@ parent -> child1 [key=0]; parent -> child2 [key=0, label=invis]; ancestor -> ancestor [key=0, tailport="cycle_out", headport="cycle_in", tooltip="ancestor.cycle_out -> ancestor.cycle_in"]; ancestor -> successor [key=0, tailport=surface, headport=surface, tooltip="ancestor.surface -> successor.surface"]; -} \ No newline at end of file +} From eca97386bc05b8013c51e83a2b08e63fc42f7db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=B3pez=20Barr=C3=B3n?= Date: Sat, 14 Dec 2024 11:44:04 +1100 Subject: [PATCH 67/67] fix GrillAttributeClearMenuItem for USD-21.11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian López Barrón --- grill/views/usdview.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/grill/views/usdview.py b/grill/views/usdview.py index b6100339..564b32e3 100644 --- a/grill/views/usdview.py +++ b/grill/views/usdview.py @@ -259,8 +259,17 @@ def RunCommand(self): def IsEnabled(self): # Usd.Attribute.Clear operates only on specs with an existing authored default value at the current edit target + try: + # USD>=24.11 + spec_getter = Usd.EditTarget.GetAttributeSpecForScenePath + except AttributeError: + # USD<24.11 + # Usd.EditTarget.GetSpecForScenePath did not work on earlier usd versions. It was fixed in 24.11. + def spec_getter(edit_target, path): + return edit_target.GetLayer().GetAttributeAtPath(edit_target.MapToSpecPath(path)) + return super().IsEnabled() and any( - (spec := attr.GetStage().GetEditTarget().GetAttributeSpecForScenePath(attr.GetPath())) and spec.default + (spec := spec_getter(attr.GetStage().GetEditTarget(), attr.GetPath())) and spec.default for attr in self._attributes )