Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions source/isaaclab/docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ Changed
module directory before resolution.


4.2.3 (2026-02-25)
~~~~~~~~~~~~~~~~~~

Fixed
^^^^^

* Fixed :func:`~isaaclab.cloner.usd_replicate` and :func:`~isaaclab.cloner.physx_replicate`
skipping ``Sdf.CopySpec`` when the source and destination paths are identical (self-copy),
avoiding a redundant and potentially destructive USD spec overwrite.


4.2.2 (2026-02-26)
~~~~~~~~~~~~~~~~~~

Expand Down
5 changes: 4 additions & 1 deletion source/isaaclab/isaaclab/cloner/cloner_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,10 @@ def dp_depth(template: str) -> int:
for wid in target_envs.tolist():
dp = tmpl.format(wid)
Sdf.CreatePrimInLayer(rl, dp)
Sdf.CopySpec(rl, Sdf.Path(src), rl, Sdf.Path(dp))
if src == dp:
pass # self-copy: CreatePrimInLayer already ensures it exists; CopySpec would be destructive
else:
Sdf.CopySpec(rl, Sdf.Path(src), rl, Sdf.Path(dp))

if positions is not None or quaternions is not None:
ps = rl.GetPrimAtPath(dp)
Expand Down
34 changes: 22 additions & 12 deletions source/isaaclab/isaaclab/sim/utils/prims.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,21 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs):
else:
source_prim_paths = [root_path]

# resolve prim paths for spawning
prim_spawn_path = prim_path.replace(".*", "0")
# Build a prototype prim path to spawn once, then copy to ALL matching parents.
#
# Octi: Leaf note wild card and root not wild card should be treated differently:
# (A) ".*" in root_path e.g. /World/Origin_0.*/CameraSensor
# source_prim_paths holds ALL matching parent prims already in the stage.
# We spawn the child once at source_prim_paths[0] as the prototype, then
# Sdf.CopySpec it to every remaining parent so every parent ends up with
# the child prim.
#
# (B) ".*" in asset_path only e.g. /World/template/Object/proto_asset_.*
# No matching prims exist yet; source_prim_paths == [root_path] (one entry).
# Replacing ".*" → "0" in asset_path gives the intended name proto_asset_0.
# No copy step runs because there is only one parent.
#
prim_spawn_path = f"{source_prim_paths[0]}/{asset_path.replace('.*', '0')}"
# spawn single instance
prim = func(prim_spawn_path, cfg, *args, **kwargs)
# set the prim visibility
Expand Down Expand Up @@ -698,16 +711,13 @@ def wrapper(prim_path: str | Sdf.Path, cfg: SpawnerCfg, *args, **kwargs):
_schemas.activate_contact_sensors(prim_spawn_path)
# clone asset using cloner API
if len(source_prim_paths) > 1:
# lazy import to avoid circular import
from isaaclab.cloner import usd_replicate

formattable_path = f"{root_path.replace('.*', '{}')}/{asset_path}"
usd_replicate(
stage=stage,
sources=[formattable_path.format(0)],
destinations=[formattable_path],
env_ids=torch.arange(len(source_prim_paths)),
)
sanitized_asset = asset_path.replace(".*", "0")
rl = stage.GetRootLayer()
with Sdf.ChangeBlock():
for src_parent in source_prim_paths[1:]:
dest_path = f"{src_parent}/{sanitized_asset}"
Sdf.CreatePrimInLayer(rl, dest_path)
Sdf.CopySpec(rl, Sdf.Path(prim_spawn_path), rl, Sdf.Path(dest_path))
# return the source prim
return prim

Expand Down
Loading