And here’s a quick prototype example code to duplicate a prim, flatten its opinions using UsdUtils
stitch commands and then put that flattened prim spec into the new layer.
Prototype 1: Duplicate prim with UsdUtils.StitchInfo
In this case I use UsdUtils.StitchInfo
to solely merge specs however there are some caveats to be aware of:
- This does not stitch any childrens and their opinions, even though the initial call to
CopySpec
will copy over children prims from that stack. You might want to either remove all children from the copied spec or do something different there so you also transfer children and apply their stitched specs
- Unfortunately this requires a temporary layer because the logic of the
StitchInfo
checks whether the spec paths are equal - which I suppose is solely a “safety check” but due to that we can’t stitch into a duplicated prim that will of course require its own unique name.
from pxr import Sdf, Usd, UsdUtils, UsdGeom
def unique_name(stage: Usd.Stage, prim_path: Sdf.Path) -> Sdf.Path:
"""Return Sdf.Path that is unique under the current composed stage.
Note that this technically does not ensure that the Sdf.Path does not
exist in any of the layers, e.g. it could be defined within a currently
unselected variant or a muted layer.
"""
src = prim_path.pathString.rstrip("123456789")
i = 1
while stage.GetPrimAtPath(prim_path):
prim_path = Sdf.Path(f"{src}{i}")
i += 1
return prim_path
def duplicate_prim(prim: Usd.Prim) -> list[Sdf.Path]:
"""Duplicate prim flattened into current edit target"""
path = prim.GetPath()
stage = prim.GetStage()
stack = prim.GetPrimStack()
initial_spec = stack.pop()
# Copy spec to a temp layer where we use stitch utiltiies
# to merge the opinions of multiple layers of the original
# prim's PrimStack so we have a 'merged opinion' on one
# layer
tmp_layer = Sdf.Layer.CreateAnonymous()
Sdf.CreatePrimInLayer(tmp_layer, path.GetParentPath()) # ensure parent exists
if not Sdf.CopySpec(initial_spec.layer, path, tmp_layer, path):
raise RuntimeError("No spec copied")
# Add the additional opinions that make the original prim stack
new_spec = tmp_layer.GetPrimAtPath(path)
for spec in stack:
# Note that this does not copy over opinions to prim specs
# nor does it copy over children. It doesn't seem UsdUtils.StitchValueFn
# is exposed to Python so we also couldn't make it copy over new children
# with a custom call to `Sdf.CopySpec`
UsdUtils.StitchInfo(new_spec, spec)
# Now copy the flattened spec to the target "duplicate"
target = stage.GetEditTarget()
target_layer = target.GetLayer()
new_path = unique_name(stage, path)
new_path = target.MapToSpecPath(new_path)
Sdf.CopySpec(tmp_layer, path, target_layer, new_path)
return new_path
# Create a stage with an Xform prim
stage = Usd.Stage.CreateInMemory()
prim = stage.DefinePrim("/root/a", "Xform")
# Now add an opinion in another layer
layer = Sdf.Layer.CreateAnonymous()
prim_spec = Sdf.CreatePrimInLayer(layer, "/root/a")
attr_spec = Sdf.AttributeSpec(prim_spec, "my_custom_attr", Sdf.ValueTypeNames.Double)
attr_spec.default = 10
attr_spec.interpolation = UsdGeom.Tokens.constant
# Sublayer the path ontot the stage
root_layer = stage.GetRootLayer()
root_layer.subLayerPaths.append(layer.identifier)
# Debugging: Make sure the attribute appears on the composed stage
prim = stage.GetPrimAtPath("/root/a")
assert prim.GetAttribute("my_custom_attr").Get() == 10
# Duplicate things
new_prim_path = duplicate_prim(prim)
# Ensure the new prim is also as we want it, of Xform type with the custom attribute
# and that the opinions appear in a single prim spec (single layer)
new_prim = stage.GetPrimAtPath("/root/a1")
assert new_prim.IsValid(), "New prim must exist"
print(stage.ExportToString())
assert new_prim.GetAttribute("my_custom_attr").Get() == 10
assert new_prim.GetTypeName() == "Xform"
assert len(new_prim.GetPrimStack()) == 1, "Must be on a single layer"
assert new_prim.GetPrimStack()[0].layer == stage.GetEditTarget().GetLayer(), "New prim must be on the current edit target layer"
Prototype 2: Duplicate prim with UsdUtils.StitchLayers
And here’s an example that does clone its children by just using a temporary Sdf.Layer
per spec in the prim’s stack and then doing a full stitch layers:
from pxr import Sdf, Usd, UsdUtils, UsdGeom
def unique_name(stage: Usd.Stage, prim_path: Sdf.Path) -> Sdf.Path:
"""Return Sdf.Path that is unique under the current composed stage.
Note that this technically does not ensure that the Sdf.Path does not
exist in any of the layers, e.g. it could be defined within a currently
unselected variant or a muted layer.
"""
src = prim_path.pathString.rstrip("123456789")
i = 1
while stage.GetPrimAtPath(prim_path):
prim_path = Sdf.Path(f"{src}{i}")
i += 1
return prim_path
def duplicate_prim(prim: Usd.Prim) -> list[Sdf.Path]:
"""Duplicate prim flattened into current edit target"""
path = prim.GetPath()
stage = prim.GetStage()
# Copy spec to a temp layer where we use stitch utiltiies
# to merge the opinions of multiple layers of the original
# prim's PrimStack so we have a 'merged opinion' on one
# layer
tmp_layers = []
for spec in prim.GetPrimStack():
tmp_layer = Sdf.Layer.CreateAnonymous()
Sdf.CreatePrimInLayer(tmp_layer, path.GetParentPath())
if not Sdf.CopySpec(spec.layer, path, tmp_layer, path):
raise RuntimeError("No spec copied")
tmp_layers.append(tmp_layer)
# Add the additional opinions that make the original prim stack
new_spec = tmp_layer.GetPrimAtPath(path)
full_layer = Sdf.Layer.CreateAnonymous()
for tmp_layer in tmp_layers:
UsdUtils.StitchLayers(full_layer, tmp_layer)
# Now copy the flattened spec to the target "duplicate"
target = stage.GetEditTarget()
target_layer = target.GetLayer()
new_path = unique_name(stage, path)
new_path = target.MapToSpecPath(new_path)
Sdf.CopySpec(full_layer, path, target_layer, new_path)
return new_path
# Create a stage with an Xform prim
stage = Usd.Stage.CreateInMemory()
prim = stage.DefinePrim("/root/a", "Xform")
prim = stage.DefinePrim("/root/a/child_in_rootlayer", "Xform")
# Now add an opinion in another layer
layer = Sdf.Layer.CreateAnonymous()
prim_spec = Sdf.CreatePrimInLayer(layer, "/root/a")
attr_spec = Sdf.AttributeSpec(prim_spec, "my_custom_attr", Sdf.ValueTypeNames.Double)
attr_spec.default = 10
attr_spec.interpolation = UsdGeom.Tokens.constant
spec = Sdf.CreatePrimInLayer(layer, "/root/a/child_in_sublayer")
spec.specifier = Sdf.SpecifierDef
spec.typeName = "Xform"
# Sublayer the path ontot the stage
root_layer = stage.GetRootLayer()
root_layer.subLayerPaths.append(layer.identifier)
# Debugging: Make sure the attribute appears on the composed stage
prim = stage.GetPrimAtPath("/root/a")
assert prim.GetAttribute("my_custom_attr").Get() == 10
assert stage.GetPrimAtPath("/root/a/child_in_rootlayer")
assert stage.GetPrimAtPath("/root/a/child_in_sublayer")
# Duplicate things
new_prim_path = duplicate_prim(prim)
# Ensure the new prim is also as we want it, of Xform type with the custom attribute
# and that the opinions appear in a single prim spec (single layer)
new_prim = stage.GetPrimAtPath("/root/a1")
assert new_prim.IsValid(), "New prim must exist"
print(stage.ExportToString())
assert new_prim.GetAttribute("my_custom_attr").Get() == 10
assert new_prim.GetTypeName() == "Xform"
assert len(new_prim.GetPrimStack()) == 1, "Must be on a single layer"
assert new_prim.GetPrimStack()[0].layer == stage.GetEditTarget().GetLayer(), "New prim must be on the current edit target layer"
assert stage.GetPrimAtPath("/root/a1/child_in_rootlayer")
assert stage.GetPrimAtPath("/root/a1/child_in_sublayer")