@@ -542,6 +542,77 @@ def modify_articulation_root_properties(
542542 return True
543543
544544
545+ """
546+ Fragment-writer helpers.
547+ """
548+
549+
550+ def _resolve_fragment_targets (
551+ prim_path : str ,
552+ api_type ,
553+ stage : Usd .Stage ,
554+ apply_fresh : bool ,
555+ ) -> tuple [list , bool ]:
556+ """Resolve the prims that a fragment family writer should author on.
557+
558+ This mirrors the legacy nested writers: prims under ``prim_path`` that already carry
559+ ``api_type`` are modified in place, and the search does not descend past a match, since
560+ nested applications of the same physics schema are not allowed. Only when the subtree
561+ carries no such prim is the defining API applied freshly to ``prim_path`` itself -- the
562+ bare-prim case used by the shape and mesh spawners. Instanced prims cannot be edited, so
563+ they are skipped with a warning.
564+
565+ Args:
566+ prim_path: The prim path whose subtree is searched.
567+ api_type: The defining USD API schema type (e.g. ``UsdPhysics.RigidBodyAPI``).
568+ stage: The stage containing the prim.
569+ apply_fresh: Whether to apply ``api_type`` to ``prim_path`` when the subtree contains
570+ no carrier of it.
571+
572+ Returns:
573+ A tuple ``(targets, any_skipped)`` holding the writable target prims and whether any
574+ instanced prim was skipped.
575+
576+ Raises:
577+ ValueError: When the prim path is not valid.
578+ """
579+ prim = stage .GetPrimAtPath (prim_path )
580+ if not prim .IsValid ():
581+ raise ValueError (f"Prim path '{ prim_path } ' is not valid." )
582+ # breadth-first search that stops descending at the first carrier on each branch, like the
583+ # legacy nested writers: nested applications of the same physics schema are not allowed, so
584+ # nothing below a carrier is ever a target. This keeps the search linear in the subtree size
585+ # instead of collecting every carrier and pruning afterwards. Carriers on instanced prims are
586+ # read-only and collected separately so they can be reported.
587+ targets = []
588+ skipped = []
589+ prims_queue = [prim ]
590+ index = 0
591+ while index < len (prims_queue ):
592+ candidate = prims_queue [index ]
593+ index += 1
594+ if candidate .HasAPI (api_type ):
595+ if candidate .IsInstance () or candidate .IsInstanceProxy ():
596+ skipped .append (candidate )
597+ else :
598+ targets .append (candidate )
599+ continue
600+ prims_queue += candidate .GetFilteredChildren (Usd .TraverseInstanceProxies ())
601+ if not targets and not skipped and apply_fresh :
602+ if prim .IsInstance () or prim .IsInstanceProxy ():
603+ skipped .append (prim )
604+ else :
605+ api_type .Apply (prim )
606+ targets .append (prim )
607+ if skipped :
608+ logger .warning (
609+ "Skipping %s updates on instanced prims: %s." ,
610+ api_type .__name__ ,
611+ [p .GetPath ().pathString for p in skipped ],
612+ )
613+ return targets , bool (skipped )
614+
615+
545616"""
546617Rigid body properties.
547618"""
@@ -550,34 +621,41 @@ def modify_articulation_root_properties(
550621def apply_rigid_body_properties (
551622 prim_path : str , fragments : Iterable [schemas_cfg .RigidBodyFragment ], stage : Usd .Stage | None = None
552623) -> bool :
553- """Apply a list of rigid-body fragments to a prim.
624+ """Apply a list of rigid-body fragments to the rigid bodies under a prim.
554625
555- Applies ``UsdPhysics.RigidBodyAPI`` as the implicit anchor (the defining schema for a rigid
556- body), then dispatches each fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
557- Backend fragments carry backend-specific funcs, so core never imports a backend.
626+ Existing rigid bodies in the subtree are modified in place, matching the legacy nested
627+ writer. Real assets author ``UsdPhysics.RigidBodyAPI`` on their link prims, so anchoring a
628+ fresh rigid body on the spawn prim instead would nest those links under a new body and the
629+ PhysX parser would drop the articulation's joint graph. Only when the subtree carries no
630+ rigid body is ``UsdPhysics.RigidBodyAPI`` applied to ``prim_path`` itself -- the bare-prim
631+ case used by the shape and mesh spawners. Each fragment is dispatched to every resolved
632+ target via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend fragments carry
633+ backend-specific funcs, so core never imports a backend.
558634
559635 Args:
560- prim_path: The prim path to apply the rigid-body schemas on .
636+ prim_path: The prim path whose subtree is searched for rigid bodies .
561637 fragments: An iterable of :class:`~isaaclab.sim.schemas.RigidBodyFragment` instances.
562638 stage: The stage where to find the prim. Defaults to None, in which case the current
563639 stage is used.
564640
565641 Returns:
566- True if the properties were successfully set.
642+ True if every target and fragment succeeded and no instanced prim was skipped.
643+
644+ Raises:
645+ ValueError: When the prim path is not valid.
567646 """
647+ fragments = list (fragments )
568648 if stage is None :
569649 stage = get_current_stage ()
570- prim = stage .GetPrimAtPath (prim_path )
571- # fail loudly on an invalid path (matches the legacy define_rigid_body_properties writer)
572- if not prim .IsValid ():
573- raise ValueError (f"Prim path '{ prim_path } ' is not valid." )
574- if not UsdPhysics .RigidBodyAPI (prim ):
575- UsdPhysics .RigidBodyAPI .Apply (prim )
576- # aggregate per-fragment results so a reported failure is not masked by the always-applied anchor
577- success = True
578- for cfg in fragments :
579- func = cfg .func if callable (cfg .func ) else string_to_callable (cfg .func )
580- success = bool (func (cfg , prim_path , stage )) and success
650+ targets , any_skipped = _resolve_fragment_targets (
651+ prim_path , UsdPhysics .RigidBodyAPI , stage , apply_fresh = bool (fragments )
652+ )
653+ # aggregate per-target, per-fragment results so a reported failure is not masked
654+ success = not any_skipped
655+ for target in targets :
656+ target_path = target .GetPath ().pathString
657+ for cfg in fragments :
658+ success = bool (cfg .func (cfg , target_path , stage )) and success
581659 return success
582660
583661
@@ -778,31 +856,41 @@ def modify_rigid_body_properties(
778856def apply_collision_properties (
779857 prim_path : str , fragments : Iterable [schemas_cfg .CollisionFragment ], stage : Usd .Stage | None = None
780858) -> bool :
781- """Apply a list of collision fragments to a prim.
859+ """Apply a list of collision fragments to the colliders under a prim.
782860
783- Applies ``UsdPhysics.CollisionAPI`` as the implicit anchor (the defining schema for a
784- collider), then dispatches each fragment via its
785- :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend fragments carry backend-specific
786- funcs, so core never imports a backend.
861+ Existing colliders in the subtree are modified in place, matching the legacy nested writer.
862+ Real assets author ``UsdPhysics.CollisionAPI`` on their collider prims, so anchoring a fresh
863+ collider on the spawn prim would change the scene's collision geometry. Only when the
864+ subtree carries no collider is ``UsdPhysics.CollisionAPI`` applied to ``prim_path`` itself
865+ -- the bare-prim case used by the shape and mesh spawners. Each fragment is dispatched to
866+ every resolved target via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`. Backend
867+ fragments carry backend-specific funcs, so core never imports a backend.
787868
788869 Args:
789- prim_path: The prim path to apply the collision schemas on .
870+ prim_path: The prim path whose subtree is searched for colliders .
790871 fragments: An iterable of :class:`~isaaclab.sim.schemas.CollisionFragment` instances.
791872 stage: The stage where to find the prim. Defaults to None, in which case the current
792873 stage is used.
793874
794875 Returns:
795- True if the properties were successfully set.
876+ True if every target and fragment succeeded and no instanced prim was skipped.
877+
878+ Raises:
879+ ValueError: When the prim path is not valid.
796880 """
881+ fragments = list (fragments )
797882 if stage is None :
798883 stage = get_current_stage ()
799- prim = stage .GetPrimAtPath (prim_path )
800- if not UsdPhysics .CollisionAPI (prim ):
801- UsdPhysics .CollisionAPI .Apply (prim )
802- for cfg in fragments :
803- func = cfg .func if callable (cfg .func ) else string_to_callable (cfg .func )
804- func (cfg , prim_path , stage )
805- return True
884+ targets , any_skipped = _resolve_fragment_targets (
885+ prim_path , UsdPhysics .CollisionAPI , stage , apply_fresh = bool (fragments )
886+ )
887+ # aggregate per-target, per-fragment results so a reported failure is not masked
888+ success = not any_skipped
889+ for target in targets :
890+ target_path = target .GetPath ().pathString
891+ for cfg in fragments :
892+ success = bool (cfg .func (cfg , target_path , stage )) and success
893+ return success
806894
807895
808896def define_collision_properties (
@@ -902,33 +990,36 @@ def modify_collision_properties(
902990def apply_mass_properties (
903991 prim_path : str , fragments : Iterable [schemas_cfg .MassFragment ], stage : Usd .Stage | None = None
904992) -> bool :
905- """Apply a list of mass fragments to a prim.
993+ """Apply a list of mass fragments to the mass-bearing prims under a prim.
906994
907- Applies ``UsdPhysics.MassAPI`` as the implicit anchor (the defining schema for mass properties),
908- then dispatches each fragment via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
995+ Existing mass-bearing prims in the subtree are modified in place, matching the legacy nested
996+ writer, since real assets author ``UsdPhysics.MassAPI`` on their link prims. Only when the
997+ subtree carries none is ``UsdPhysics.MassAPI`` applied to ``prim_path`` itself -- the
998+ bare-prim case used by the shape and mesh spawners. Each fragment is dispatched to every
999+ resolved target via its :attr:`~isaaclab.sim.schemas.SchemaFragment.func`.
9091000
9101001 Args:
911- prim_path: The prim path to apply the mass schemas on .
1002+ prim_path: The prim path whose subtree is searched for mass-bearing prims .
9121003 fragments: An iterable of :class:`~isaaclab.sim.schemas.MassFragment` instances.
9131004 stage: The stage where to find the prim. Defaults to None, in which case the current
9141005 stage is used.
9151006
9161007 Returns:
917- True if the properties were successfully set.
1008+ True if every target and fragment succeeded and no instanced prim was skipped.
1009+
1010+ Raises:
1011+ ValueError: When the prim path is not valid.
9181012 """
1013+ fragments = list (fragments )
9191014 if stage is None :
9201015 stage = get_current_stage ()
921- prim = stage .GetPrimAtPath (prim_path )
922- # fail loudly on an invalid path (matches the legacy define_mass_properties writer)
923- if not prim .IsValid ():
924- raise ValueError (f"Prim path '{ prim_path } ' is not valid." )
925- if not UsdPhysics .MassAPI (prim ):
926- UsdPhysics .MassAPI .Apply (prim )
927- # aggregate per-fragment results so a reported failure is not masked by the always-applied anchor
928- success = True
929- for cfg in fragments :
930- func = cfg .func if callable (cfg .func ) else string_to_callable (cfg .func )
931- success = bool (func (cfg , prim_path , stage )) and success
1016+ targets , any_skipped = _resolve_fragment_targets (prim_path , UsdPhysics .MassAPI , stage , apply_fresh = bool (fragments ))
1017+ # aggregate per-target, per-fragment results so a reported failure is not masked
1018+ success = not any_skipped
1019+ for target in targets :
1020+ target_path = target .GetPath ().pathString
1021+ for cfg in fragments :
1022+ success = bool (cfg .func (cfg , target_path , stage )) and success
9321023 return success
9331024
9341025
0 commit comments