diff --git a/docs/source/conf.py b/docs/source/conf.py index ce0fbbf3..311e9822 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,6 +33,7 @@ # ones. extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.extlinks', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.inheritance_diagram', @@ -47,6 +48,7 @@ 'sphinx_inline_tabs', 'hoverxref.extension', 'sphinx.ext.autosectionlabel', + 'sphinxcontrib.doxylink', ] # Offset to play well with copybutton @@ -54,9 +56,18 @@ togglebutton_hint = " " intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), + 'usd': ('https://openusd.org/release/', None), + 'networkx': ('https://networkx.org/documentation/stable/', None), 'naming': ('https://naming.readthedocs.io/en/latest/', None), 'grill.names': ('https://grill-names.readthedocs.io/en/latest/', None) } +# extlinks = { +# 'usd': ('https://openusd.org/release/api/%s.html', '') +# } +extlinks = { + 'usdclass': ('https://openusd.org/release/api/class_usd_%s.html', '%s'), + 'usdmethod': ('https://openusd.org/release/api/class_usd_%s.html#%s', 'Usd.%s'), +} hoverxref_auto_ref = True hoverxref_default_type = 'tooltip' @@ -234,3 +245,43 @@ # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] + +_USD_TAG_URL = "https://openusd.org/release/USD.tag" +_DOXY_LINK_CACHE_NAME = 'usdcpp' +_DOXY_LINK_ROOT_DIR = "https://openusd.org/release/api/" +doxylink = { + _DOXY_LINK_CACHE_NAME: (_USD_TAG_URL, _DOXY_LINK_ROOT_DIR) +} + +def _handle_missing_usd_reference(app, env, node, contnode): + """Handle missing references by redirecting to a custom URL.""" + from docutils import nodes + + target = node['reftarget'] + if not target.startswith('pxr.'): + return None + + pxr_obj_namespace = target.removeprefix('pxr.').replace(".", "") + print("---------") + print(f"{target=}") + print(f"{pxr_obj_namespace=}") + pxr_obj_namespace = { + "UsdInitialLoadSet": "UsdStage::InitialLoadSet", # there's a level of indirection in the python bindings? + "UsdFilter": "UsdPrimCompositionQuery::Filter", # filter is a member of the query type + "UsdCompositionArc": "UsdPrimCompositionQueryArc", + "Usd_Term": "primFlags.h", + "Usd_PrimFlagsConjunction": "primFlags.h", + }.get(pxr_obj_namespace, pxr_obj_namespace) + from sphinxcontrib.doxylink import doxylink + has_explicit_title, title, part = doxylink.split_explicit_title(pxr_obj_namespace) + part = doxylink.utils.unescape(part) + url = app.env.doxylink_cache[_DOXY_LINK_CACHE_NAME]['mapping'][part] + full_url = doxylink.join(_DOXY_LINK_ROOT_DIR, url.file) + print(full_url) + return nodes.reference('', contnode.astext(), refuri=full_url) + + +def setup(app): + """Setup Sphinx to handle missing USD references. This can be removed when the USD C++ docs ship with an inventory of the USD types for python bindings.""" + app.connect("missing-reference", _handle_missing_usd_reference) + return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/grill/cook/__init__.py b/grill/cook/__init__.py index ec29d245..d89986a5 100644 --- a/grill/cook/__init__.py +++ b/grill/cook/__init__.py @@ -31,6 +31,7 @@ import itertools import contextlib import contextvars +import collections from pathlib import Path from pprint import pformat @@ -93,7 +94,7 @@ def _fetch_layer(identifier: str, context: Ar.ResolverContext) -> Sdf.Layer: return layer -def asset_identifier(path): +def asset_identifier(path: Path | str): """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. @@ -106,7 +107,7 @@ def asset_identifier(path): return str(path.relative_to(Repository.get())) -def fetch_stage(identifier: typing.Union[str, UsdAsset], 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.InitialLoadSet = 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. @@ -169,7 +170,7 @@ def define_taxon(stage: Usd.Stage, name: str, *, references: tuple[Usd.Prim] = t return prim -def itaxa(stage: Usd.Stage) -> typing.Generator[Usd.Prim]: +def itaxa(stage: Usd.Stage) -> collections.abc.Iterator[Usd.Prim]: """For the given stage, iterate existing taxa under the taxonomy hierarchy.""" return filter( lambda prim: prim.GetAssetInfoByKey(_ASSETINFO_TAXA_KEY), @@ -191,7 +192,7 @@ def _broadcast_root_path(taxon, broadcast_method, scope_path=None): return scope_path.ReplacePrefix(_CATALOGUE_ROOT_PATH, _BROADCAST_METHOD_RELPATHS[broadcast_method]) -def create_many(taxon, names, labels=tuple()) -> typing.List[Usd.Prim]: +def create_many(taxon: Usd.Prim, names: collections.abc.Iterable[str], labels: collections.abc.Iterable[str] = tuple()) -> list[Usd.Prim]: """Create a new taxon member for each of the provided names. When creating hundreds or thousands of members, this provides a considerable performance improvement over :func:`create_unit`. @@ -351,7 +352,7 @@ def unit_asset(prim: Usd.Prim) -> Sdf.Layer: return _find_layer_matching(fields, (i.layer for i in prim.GetPrimStack())) -def spawn_unit(parent, child, path=Sdf.Path.emptyPath, label=""): +def spawn_unit(parent: Usd.Prim, child: Usd.Prim, path: Sdf.Path = Sdf.Path.emptyPath, label: str = "") -> Usd.Prim: """Spawn a unit prim as a descendant of another. * Both parent and child must be existing units in the catalogue. @@ -367,7 +368,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] = ()) -> list[Usd.Prim]: """Spawn many instances of a prim unit as descendants of another. * Both parent and child must be existing units in the catalogue. @@ -407,7 +408,10 @@ def spawn_many(parent: Usd.Prim, child: Usd.Prim, paths: list[Sdf.Path], labels: with Sdf.ChangeBlock(): # Action of bringing a unit from our catalogue turns parent into an assembly only if child is a model. if child_is_model and not (parent_model := Usd.ModelAPI(parent)).IsKind(Kind.Tokens.assembly): - parent_model.SetKind(Kind.Tokens.assembly) + try: + parent_model.SetKind(Kind.Tokens.assembly) + except Exception as exc: + raise RuntimeError(f'Could not set kind to "{Kind.Tokens.assembly}" on parent model {parent_model} with current kind: "{parent_model.GetKind()}", when spawning {child} of kind "{Usd.ModelAPI(child).GetKind()}"') from exc for spawned_unit, label in zip(spawned, labels): # Use reference for the asset to: # 1. Make use of instancing as much as possible with fewer prototypes. @@ -456,7 +460,7 @@ def _get_id_fields(prim): return fields -def _find_layer_matching(tokens: typing.Mapping, layers: typing.Iterable[Sdf.Layer]) -> Sdf.Layer: +def _find_layer_matching(tokens: typing.Mapping, layers: collections.abc.Iterable[Sdf.Layer]) -> Sdf.Layer: """Find the first layer matching the given identifier tokens. :raises ValueError: If none of the given layers match the provided tokens. @@ -511,7 +515,7 @@ def _inherit_or_specialize_unit(method, context_unit): @functools.singledispatch -def taxonomy_graph(prims: Usd.Prim, url_id_prefix) -> nx.DiGraph: +def taxonomy_graph(prims: Usd.Prim, url_id_prefix: str) -> nx.DiGraph: """Get the hierarchical taxonomy representation of existing prims.""" graph = nx.DiGraph(tooltip="Taxonomy Graph") graph.graph.update( @@ -535,6 +539,6 @@ def taxonomy_graph(prims: Usd.Prim, url_id_prefix) -> nx.DiGraph: @taxonomy_graph.register(Usd.Stage) -def _(stage: Usd.Stage, url_id_prefix) -> nx.DiGraph: +def _(stage: Usd.Stage, url_id_prefix: str) -> 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 2b5c1773..14972172 100644 --- a/grill/usd/__init__.py +++ b/grill/usd/__init__.py @@ -46,7 +46,7 @@ def _pruned_prims(prim_range: Usd.PrimRange, predicate): yield prim -def common_paths(paths: typing.Iterable[Sdf.Path]) -> typing.List[Sdf.Path]: +def common_paths(paths: abc.Iterable[Sdf.Path]) -> list[Sdf.Path]: """For the given paths, get those which are the common parents.""" unique = list() for path in sorted(filter(lambda p: p and not p.IsAbsoluteRootPath(), paths)): @@ -56,12 +56,20 @@ def common_paths(paths: typing.Iterable[Sdf.Path]) -> typing.List[Sdf.Path]: return unique -def iprims(stage: Usd.Stage, root_paths: typing.Iterable[Sdf.Path] = tuple(), prune_predicate: typing.Callable = None, traverse_predicate=Usd.PrimDefaultPredicate) -> typing.Iterator[Usd.Prim]: - """Convenience function that creates a generator useful for common prim traversals. +def iprims(stage: Usd.Stage, root_paths: abc.Iterable[Sdf.Path] = tuple(), prune_predicate: abc.Callable[[Usd.Prim], bool] = None, traverse_predicate: typing.Union[Usd._Term, Usd._PrimFlagsConjunction] = None) -> abc.Iterator[Usd.Prim]: + """Convenience function that creates an iterator useful for common :ref:`glossary:stage traversal`. + + Refer to the :ref:`glossary:specifier` ins the documentation. + Refer to the :ref:`Specifier ` in the documentation. Without keyword arguments, this is the same as calling `Usd.Stage.Traverse(...)`, so use that instead when no `root_paths` or `prune_predicates` are needed. + + The remaining methods + (e.g. :code:`GetChildren()`) all use a predefined :usdcpp:`Default Predicate ` """ + if traverse_predicate is None: + traverse_predicate = Usd.PrimDefaultPredicate if root_paths: # Traverse only specific parts of the stage. root_paths = common_paths(root_paths) # Usd.PrimRange already discards invalid prims, so no need to check. @@ -76,11 +84,17 @@ def iprims(stage: Usd.Stage, root_paths: typing.Iterable[Sdf.Path] = tuple(), pr @functools.singledispatch -def edit_context(prim: Usd.Prim, /, query_filter: Usd.PrimCompositionQuery.Filter, arc_predicate: typing.Callable[[Usd.CompositionArc], bool]) -> Usd.EditContext: +def edit_context(prim: Usd.Prim, /, query_filter: Usd.PrimCompositionQuery.Filter, arc_predicate: abc.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. + Operate on a :usdclass:`prim` and return an :usdclass:`EditContext `. + + Refer to the :ref:`glossary:specifier` ins the documentation. + Refer to the :ref:`Specifier ` in the documentation. + Refer to the :ref:`Stage ` in the documentation. + .. tip:: You can try the below code snippet on ``USDView`` (or any other USD DCC application) @@ -201,6 +215,9 @@ def Sphere "child" ( } } + Returns: + :usdclass:`EditContext ` + """ # https://blogs.mathworks.com/developer/2015/03/31/dont-get-in-too-deep/ # with write.context(prim, dict(kingdom="assets")): @@ -243,8 +260,8 @@ 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: +@edit_context.register(Usd.VariantSet) +def _(variant_set, /, layer: Sdf.Layer) -> Usd.EditContext: with contextlib.suppress(Tf.ErrorException): return variant_set.GetVariantEditContext() # ----- From Pixar ----- diff --git a/setup.cfg b/setup.cfg index a092ba7c..3b81c958 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,11 +45,11 @@ include = grill.* # conda install conda-forge::graphviz # 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>=1.4.1 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 sphinxcontrib-doxylink # 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 +docs = sphinx; myst-parser; sphinx-toggleprompt; sphinx-copybutton; sphinx-togglebutton; sphinx-hoverxref>=1.4.1; sphinx_autodoc_typehints; sphinx-inline-tabs; shibuya; usd-core; sphinxcontrib-doxylink full = PySide6; usd-core; PyOpenGL