6161 _deformable_import_element_collision_filters ,
6262 _deformable_remap_collapsed ,
6363)
64- from .import_usd_deformable_cable import _deformable_import_cable , _deformable_prepare_cable_topology
64+ from .import_usd_deformable_cable import (
65+ _deformable_import_cable ,
66+ _deformable_prepare_cable_topology ,
67+ _read_cable_attachment_endpoint ,
68+ )
6569from .import_usd_deformable_cloth import _deformable_import_cloth
6670from .import_usd_deformable_utils import (
6771 _LOADABLE_VISUAL_TYPE_NAMES_LOWER ,
@@ -2824,6 +2828,45 @@ def _resolve_contact_attr(key, _prim=prim):
28242828 # This allows us to parse orphan joints (joints not included in any articulation)
28252829 # even when articulations are present in the USD.
28262830 processed_joints : set [str ] = set ()
2831+
2832+ cable_attachment_target_body_paths : set [str ] = set ()
2833+ if _deformable_prims .cables and _deformable_prims .attachments :
2834+ cable_topology : dict [str , tuple [int , bool ]] = {}
2835+ for cable_prim in _deformable_prims .cables :
2836+ curves = UsdGeom .BasisCurves (cable_prim )
2837+ vertex_counts = curves .GetCurveVertexCountsAttr ().Get () or []
2838+ if len (vertex_counts ) != 1 :
2839+ continue
2840+ cable_topology [str (cable_prim .GetPath ())] = (
2841+ int (vertex_counts [0 ]),
2842+ curves .GetWrapAttr ().Get () == UsdGeom .Tokens .periodic ,
2843+ )
2844+
2845+ attachments_by_cable : dict [str , list [Usd .Prim ]] = {}
2846+ for attachment_prim in _deformable_prims .attachments :
2847+ cable_path = _get_first_target (attachment_prim , "physics:src0" )
2848+ if cable_path in cable_topology :
2849+ attachments_by_cable .setdefault (cable_path , []).append (attachment_prim )
2850+
2851+ for cable_path , attachment_prims in attachments_by_cable .items ():
2852+ if len (attachment_prims ) != 1 :
2853+ continue
2854+ attachment_prim = attachment_prims [0 ]
2855+ point_count , closed = cable_topology [cable_path ]
2856+ if _read_cable_attachment_endpoint (attachment_prim , deformable_read , point_count , closed ) is None :
2857+ continue
2858+ target_path = _get_first_target (attachment_prim , "physics:src1" )
2859+ target_prim = stage .GetPrimAtPath (target_path )
2860+ if not target_prim or not target_prim .IsValid ():
2861+ continue
2862+ current_prim = target_prim
2863+ while current_prim and current_prim .IsValid ():
2864+ current_path = str (current_prim .GetPath ())
2865+ if current_path in body_specs :
2866+ cable_attachment_target_body_paths .add (current_path )
2867+ break
2868+ current_prim = current_prim .GetParent ()
2869+
28272870 authored_articulation_root_paths = [
28282871 str (prim .GetPath ())
28292872 for prim in Usd .PrimRange (stage .GetPrimAtPath (root_path ), Usd .TraverseInstanceProxies ())
@@ -2837,10 +2880,20 @@ def _resolve_contact_attr(key, _prim=prim):
28372880 if UsdPhysics .ObjectType .Articulation in ret_dict :
28382881 paths , articulation_descs = ret_dict [UsdPhysics .ObjectType .Articulation ]
28392882
2883+ articulation_entries = list (zip (paths , articulation_descs , strict = False ))
2884+ target_articulations = [
2885+ index
2886+ for index , (_path , desc ) in enumerate (articulation_entries )
2887+ if cable_attachment_target_body_paths .intersection (str (body ) for body in desc .articulatedBodies )
2888+ ]
2889+ if len (target_articulations ) == 1 :
2890+ target_articulation = articulation_entries .pop (target_articulations [0 ])
2891+ articulation_entries .append (target_articulation )
2892+
28402893 articulation_id = builder .articulation_count
28412894 parent_prim = None
28422895 body_data = {}
2843- for path , desc in zip ( paths , articulation_descs , strict = False ) :
2896+ for path , desc in articulation_entries :
28442897 if warn_invalid_desc (path , desc ):
28452898 continue
28462899 articulation_path = str (path )
@@ -4492,11 +4545,26 @@ def _aggregate_recorded_mass_properties(body_path: str, body_density: float | No
44924545 else :
44934546 bodies_to_articulate = new_bodies
44944547
4495- if bodies_to_articulate :
4548+ cable_attachment_target_bodies = [
4549+ path_body_map [path ] for path in cable_attachment_target_body_paths if path in path_body_map
4550+ ]
4551+ if len (cable_attachment_target_bodies ) == 1 :
4552+ cable_attachment_target_body = cable_attachment_target_bodies [0 ]
4553+ bodies_before_cables = (
4554+ [cable_attachment_target_body ] if cable_attachment_target_body in bodies_to_articulate else []
4555+ )
4556+ bodies_after_cables = [body_id for body_id in bodies_to_articulate if body_id != cable_attachment_target_body ]
4557+ else :
4558+ bodies_before_cables = bodies_to_articulate
4559+ bodies_after_cables = []
4560+
4561+ def add_base_articulations (body_ids : list [int ]) -> None :
4562+ if not body_ids :
4563+ return
44964564 if parent_body != - 1 :
44974565 # When parent_body is specified, manually add joints to floating bodies with correct parent
44984566 joint_children = set (builder .joint_child )
4499- for body_id in bodies_to_articulate :
4567+ for body_id in body_ids :
45004568 if body_id in joint_children :
45014569 continue # Already has a joint
45024570 if builder .body_mass [body_id ] <= 0 :
@@ -4519,7 +4587,7 @@ def _aggregate_recorded_mass_properties(body_path: str, body_density: float | No
45194587 )
45204588 else :
45214589 joint_children = set (builder .joint_child )
4522- for body_id in bodies_to_articulate :
4590+ for body_id in body_ids :
45234591 if body_id in joint_children :
45244592 continue
45254593 if builder .body_mass [body_id ] <= 0 :
@@ -4544,6 +4612,10 @@ def _aggregate_recorded_mass_properties(body_path: str, body_density: float | No
45444612 else :
45454613 builder .add_articulation ([joint_id ], label = body_path )
45464614
4615+ # Articulation joints occupy a contiguous range. Create the attached body's base joint
4616+ # immediately before the cable joints, then add unrelated base joints after the cable.
4617+ add_base_articulations (bodies_before_cables )
4618+
45474619 def initialize_free_joint_velocities () -> None :
45484620 imported_bodies = set (path_body_map .values ())
45494621 for joint_id , joint_type in enumerate (builder .joint_type ):
@@ -4576,8 +4648,8 @@ def initialize_free_joint_velocities() -> None:
45764648 qd_start = builder .joint_qd_start [joint_id ]
45774649 builder .joint_qd [qd_start : qd_start + 6 ] = [* linear_velocity , * angular_velocity ]
45784650
4579- # Build deformables after rigid bodies, collider-mass computation, and the floating-body
4580- # base-joint pass so physical attachment targets and their root joints already exist.
4651+ # Build deformables after rigid bodies and collider-mass computation so physical attachment
4652+ # targets already exist.
45814653 # Volume deformables (TetMesh -> soft body). PhysicsVolumeDeformableSimAPI (or a
45824654 # PhysicsDeformableBodyAPI) opts into the mass precedence; a bare TetMesh stays legacy.
45834655 # Mass precedence (proposal): per-point physics:masses > body mass > body density
@@ -4656,6 +4728,8 @@ def initialize_free_joint_velocities() -> None:
46564728 if _filter_prim and _filter_prim .IsValid ():
46574729 _collect_filtered_pairs (_filter_prim )
46584730
4731+ add_base_articulations (bodies_after_cables )
4732+
46594733 def _resolve_collision_shape_ids (path : str ) -> tuple [list [int ], str | None ]:
46604734 """Resolve a filtered-pair endpoint to Newton shape indices, or an unsupported reason.
46614735
0 commit comments