API Basics for designing a "manage edits" editor for USD

I’m interested in potentially building a small Qt tool which would allow to basically visualize the changes made by a USD layer and potentially just picking which changes to remove - I have a feeling there might be some basic functions in the USD api that I would mostly be looking at.

What USD concepts should I be focusing on? Would I be traversing the USD stage’s prims and their prim.GetPrimStack() and from there how would I be ‘deleting’ any edits made in a layer?

I understand this is an insanely broad question to answer. I’m just hoping that there might be some good examples or pointers on how to start out finding the relevant edits and how one could say, remove opinions on a specific property in a layer, etc.

Conceptually it’d be something similar to Maya’s Reference Editor’s “List edits…”


Related

1 Like

I think you’re on the right track.

Essentially each primspec on a given layer makes up the final prim on the stage. So removing the primspec from the composition arcs would in effect remove their edit contribution

I think usdview has some inspection tooling that shows you what arcs constitute the final result.

Thanks! It does indeed seem like prim.GetPrimStack is the entry point to getting all ‘opinions’ on the primitive.

from pxr import Sdf

symbol = {
    Sdf.SpecifierDef: "Def  ",     # Defines a concrete prim
    Sdf.SpecifierOver: "Over ",    # Overrides an existing prim
    Sdf.SpecifierClass: "Class ",  # Define an abstract prim
}

prims = stage.TraverseAll()
for prim in prims:
    for prim_spec in prim.GetPrimStack():
        print(f"layer: {prim_spec.layer.identifier}")
        print(f"{symbol[prim_spec.specifier]} {prim_spec.path}")
        attributes = prim_spec.attributes
        for attr in attributes:
            print(f"       - {attr.name} -> {attr.default}")

(Of course the above will be slow for very large scenes)

This would e.g. print something like:

layer: path/to/data/asset.usd
Def   /root
layer: path/to/look.usd
Over  /root
layer: path/to/data/model.usd
Over  /root
layer: path/to/look.usd
Over  /root/Cube1
layer: path/to/data/model.usd
Def   /root/Cube1
layer: path/to/look.usd
Def   /mtl
layer: path/to/look.usd
Def   /mtl/UsdPreviewSurface1
       - outputs:surface -> None
layer: path/to/look.usd
Def   /mtl/UsdPreviewSurface1/UsdPreviewSurface1
       - info:id -> UsdPreviewSurface
       - outputs:surface -> None

Note that the above example does not print relationships - those are accessible through prim_spec.relationships (or GetRelationships() in C++) - so let’s add those.

And with some slight modification to not show anything that doesn’t directly specify an opinion on the prim itself using SdfSpec.IsInert() we get to:

from pxr import Sdf

symbol = {
    Sdf.SpecifierDef: "Def  ",     # Defines a concrete prim
    Sdf.SpecifierOver: "Over ",    # Overrides an existing prim
    Sdf.SpecifierClass: "Class ",  # Define an abstract prim
}

prims = stage.TraverseAll()
for prim in prims:
    for prim_spec in prim.GetPrimStack():
        
        if prim_spec.IsInert(ignoreChildren=True):
            continue
        
        print(f"{symbol[prim_spec.specifier]} {prim_spec.path}".ljust(60, " ") + f"<-- {prim_spec.layer.identifier}")
        attributes = prim_spec.attributes
        for attr in attributes:
            print(f"       - {attr.name} -> {attr.default}")
        for relationship in prim_spec.relationships:
            target_paths = relationship.targetPathList
            print(f"       > {relationship.name} -- {target_paths}")

Printing:

Def   /root                                                 <-- path/to/asset.usd
Over  /root/Cube1                                           <-- path/to/look.usd
       > material:binding -- [/mtl/UsdPreviewSurface1]
Def   /root/Cube1                                           <-- path/to/model.usd
Def   /mtl                                                  <-- path/to/look.usd
Def   /mtl/UsdPreviewSurface1                               <-- path/to/look.usd
       - outputs:surface -> None
Def   /mtl/UsdPreviewSurface1/UsdPreviewSurface1            <-- path/to/look.usd
       - info:id -> UsdPreviewSurface
       - outputs:surface -> None

Again, these are just simple debug tests to learn about the USD API but might help others on their way.


Question: How to remove authored opinions / prim specs?

The question now becomes how do I tweak these opinions / prim specs and thereby actually adjust the contents in the layers. So Questions:

  • I want to remove an attribute/property opinion in a specific layer. How would I do so?
  • I want to remove a prim definition. How would I do so?

It seems SdfLayer has some remove methods that take PrimSpecs or PropertySpecs, like SdfLayer.RemovePrimIfInert but they seem focused on the “if inert” cases. How do I actually remove a particular Attribute Spec opinion or a Prim Spec opinion.

I can find remove_prim_specin the omniverse docs but how to that natively through USD?

Hi @BigRoyNL ,
Agreed with @dhruvgovil that your approach seems suited to what you’re trying to do. The Sdf Layer and Spec API’s are wrapped to python in a “pythonic” idiom, where enumerable things (like prim children and properties) are exposed like containers, which can be “directly” mutated. So to remove a spec, you just del it from its parent. In https://github.com/PixarAnimationStudios/OpenUSD/blob/dev/pxr/usd/sdf/testenv/testSdfAttribute.py#L440 , see the three occurences of del for how to remove three types of specs (I highlighted a PropertySpec removal).

Cheers,
–spiff

That sounds neat - I’ll need to give that a go tomorrow!

In the meantime I had tried this which somewhat works but likely is not the way to do it

def remove_spec(spec):     
    if spec.permission != Sdf.PermissionPublic:
        raise RuntimeError(f"Skipping due to permission {spec.name} {spec.permission}")
    
    for key in spec.ListInfoKeys():
        spec.ClearInfo(key)
    
    layer = spec.layer
    if spec:
        layer.ScheduleRemoveIfInert(spec)

Which if done in a Sdf.ChangeBlock() works quite well but there are some specs that disallow editing the info (in particular relationships like targetlists seem to fail) and thus I assume that might have been a bit too magical hehe. There are likely many reasons why I should NOT be doing this like that.

ClearInfo() is perfectly valid for micro-surgery, which is sometimes what you want! But yeah, for removing a whole spec, del the spec.

So this does work:

Remove all authored properties of a Sdf.PrimSpec in Python

This removes all authored opinions on any properties of a Sdf.PrimSpec. Relationships and Attributes are both properties so this will remove both.

for property in prim_spec.properties:
    del prim_spec.properties[property.name]

Remove all authored attributes or relationships of a Sdf.PrimSpec in Python

Remove all attribute opinions in this Sdf.PrimSpec:

for attribute in prim_spec.attributes:
    del prim_spec.attributes[attribute.name]

Remove all relationship opinions in this Sdf.PrimSpec:

for relationship in prim_spec.relationships:
    del prim_spec.relationships[relationship.name]

Remove authored Sdf.PrimSpec in Python

However, then now the question becomes. With this same logic, how do I delete the full PrimSpec itself?
For root prims it seems I would be able to do:

del layer.rootPrims[prim.name]

But how do I do it for a nested prim spec that is not the root prim. Here you go:

for child_prim_spec in prim_spec.nameChildren:
    del prim_spec.nameChildren[child_prim_spec.name]

From the above examples it seems we’ll always need to remove a spec from its parent and know which ‘view’ to remove it from, e.g.

def remove_spec(spec):    
    if spec.expired:
        return
        
    if isinstance(spec, Sdf.PrimSpec):
        # PrimSpec
        parent = prim_spec.nameParent
        if parent:
            view = parent.nameChildren
        else:
            # Assume PrimSpec is root prim
            view = spec.layer.rootPrims
        del view[spec.name]
        
    elif isinstance(spec, Sdf.PropertySpec):
        # Relationship and Attribute specs
        del spec.owner.properties[spec.name]
    else:
        raise TypeError(f"Unsupported spec type: {spec}")

Can we generalize this more?

Would still be nice if there’d be an easier way to do this in Python. Any pointers @spiff to improve this logic? Is there a more universal way to just say: Take this Sdf.Spec and remove it.

I’ve done a very quick and dirty “prototype” UI as a Manage Layer edits editor for USD using Qt.
In this case prototyped with PySide2 in Maya so picked colors in the treeview assume the UI has a dark color scheme, e.g. like the one inherited from Maya.

Here’s a public gist with my the USD Manage Layer Edits prototype. Again, it’s quick and dirty.

Update: Added auto-refresh for changes on the UsdStage using Tf.Notice.

Thanks for calling attention to this, @BigRoyNL . We agree it’s a PITA, and in fact it’s been causing grief in the implementation of namespace editing :sweat_smile:

I’m filing a task internally, and we’ll figure out whether a method on SdfLayer or a free-floating function like SdfCopySpec makes more sense. Probably won’t see it till 24.02 earliest, though.

1 Like