6767 ) from exc
6868
6969from isaaclab .cloner import ClonePlan
70+ from isaaclab .cloner import query as clone_query
7071from isaaclab .renderers import BaseRenderer , RenderBufferKind , RenderBufferSpec
7172from isaaclab .sim import SimulationContext
7273from isaaclab .utils .warp .warp_math import convert_camera_frame_orientation_convention_wp
@@ -433,7 +434,7 @@ def prepare_stage(self, stage: Any, num_envs: int) -> None:
433434 create_scene_partition_attributes (stage , num_envs )
434435
435436 # Composed scales must be read while the full stage is still live, before export trims it.
436- self ._capture_object_scales (stage )
437+ self ._capture_object_scales (stage , self . _clone_plan )
437438
438439 # The clone plan already identifies every source row. Keep those rows independent so
439440 # backend bindings for dynamic assets retain the paths they were compiled against.
@@ -444,7 +445,7 @@ def prepare_stage(self, stage: Any, num_envs: int) -> None:
444445 keep_env_roots = not self ._use_ovstage ,
445446 )
446447
447- def _capture_object_scales (self , stage : Any ) -> None :
448+ def _capture_object_scales (self , stage : Any , plan : ClonePlan ) -> None :
448449 """Record composed world scales of scaled environment prims before the stage is exported.
449450
450451 The per-frame object transform write rebuilds each body's matrix from a Newton
@@ -453,11 +454,12 @@ def _capture_object_scales(self, stage: Any) -> None:
453454 stage is still live, lets :meth:`_create_object_scale_array` fold it back in.
454455
455456 Only paths whose scale deviates from unit are stored. Scales found under clone-plan source
456- paths are projected onto their active destinations because OVRTX creates those prims only
457- after the host stage is exported.
457+ paths are projected through the cloner query boundary because OVRTX creates their active
458+ destinations only after the host stage is exported.
458459
459460 Args:
460461 stage: The live USD stage, before per-environment trimming and export.
462+ plan: Validated plan describing the active prototype-to-clone relation.
461463 """
462464 self ._object_scales_by_path .clear ()
463465
@@ -477,24 +479,12 @@ def _capture_object_scales(self, stage: Any) -> None:
477479 self ._object_scales_by_path [str (prim .GetPath ())] = scale
478480
479481 # OVRTX creates non-source rows after this stage is exported, so those destination prims
480- # cannot be traversed above. The clone plan is the authority for their path correspondence.
481- clone_plan = self ._clone_plan
482- if clone_plan is None or clone_plan .env_ids is None :
483- return
484- captured_scales = tuple (self ._object_scales_by_path .items ())
485- env_ids = clone_plan .env_ids .detach ().cpu ()
486- clone_mask = clone_plan .clone_mask .detach ().cpu ()
487- for row , (source , destination ) in enumerate (zip (clone_plan .sources , clone_plan .destinations , strict = True )):
488- source_root = source .rstrip ("/" )
489- source_scales = [
490- (path , scale )
491- for path , scale in captured_scales
492- if path == source_root or path .startswith (f"{ source_root } /" )
493- ]
494- for env_id in env_ids [clone_mask [row ]].tolist ():
495- target_root = destination .format (int (env_id )).rstrip ("/" )
496- for path , scale in source_scales :
497- self ._object_scales_by_path [f"{ target_root } { path .removeprefix (source_root )} " ] = scale
482+ # cannot be traversed above. Clone queries retain the plan's nearest-owner semantics.
483+ for source_path , scale in tuple (self ._object_scales_by_path .items ()):
484+ for env_id in clone_query .path_env_ids (plan , source_path ):
485+ clone_path = clone_query .path_to_clone (plan , source_path , env_id )
486+ assert clone_path is not None
487+ self ._object_scales_by_path .setdefault (clone_path , scale )
498488
499489 def _create_object_scale_array (self , object_paths : list [str ]) -> wp .array :
500490 """Build the device scale array aligned with the Newton body binding order.
0 commit comments