@@ -310,212 +310,214 @@ struct ModelKinematicStructure
310310 }
311311 return ;
312312 }
313-
314- mjsJoint *joint{nullptr };
315- mjsJoint *joint2{nullptr };
316- // It is possible to apply joint forces using `qfrc_applied`, but this
317- // makes it harder to retrieve the last applied forces on a joint when
318- // implementing GetJoint. Instead, we use actuators and `mjData::ctrl`.
319- // This allows us to use the same interface for setting velocity servo
320- // commands as well.
321- mjsActuator *actuator{nullptr };
322- if (sdfJoint->Type () == ::sdf::JointType::PRISMATIC )
323- {
324- joint = mjs_addJoint (child, nullptr );
325- joint->type = mjJNT_SLIDE;
326- const auto *sdfAxis = sdfJoint->Axis (0 );
327- convertJointAxis (sdfAxis, joint->axis );
328- copyStandardJointAxisProperties (joint, sdfAxis);
329- }
330- else if (sdfJoint->Type () == ::sdf::JointType::REVOLUTE )
331- {
332- joint = mjs_addJoint (child, nullptr );
333- joint->type = mjJNT_HINGE;
334- const auto *sdfAxis = sdfJoint->Axis (0 );
335- convertJointAxis (sdfAxis, joint->axis );
336- copyStandardJointAxisProperties (joint, sdfAxis);
337- }
338- else if (sdfJoint->Type () == ::sdf::JointType::BALL )
313+ else
339314 {
340- joint = mjs_addJoint (child, nullptr );
341- joint->type = mjJNT_BALL;
342- const auto *sdfAxis = sdfJoint->Axis (0 );
343- if (sdfAxis)
315+ mjsJoint *joint{nullptr };
316+ mjsJoint *joint2{nullptr };
317+ // It is possible to apply joint forces using `qfrc_applied`, but this
318+ // makes it harder to retrieve the last applied forces on a joint when
319+ // implementing GetJoint. Instead, we use actuators and `mjData::ctrl`.
320+ // This allows us to use the same interface for setting velocity servo
321+ // commands as well.
322+ mjsActuator *actuator{nullptr };
323+ if (sdfJoint->Type () == ::sdf::JointType::PRISMATIC )
344324 {
325+ joint = mjs_addJoint (child, nullptr );
326+ joint->type = mjJNT_SLIDE;
327+ const auto *sdfAxis = sdfJoint->Axis (0 );
345328 convertJointAxis (sdfAxis, joint->axis );
346329 copyStandardJointAxisProperties (joint, sdfAxis);
347- // For ball joints, the first range parameter should always be set to
348- // zero.
349- if (joint->limited && std::abs (joint->range [0 ]) > 0.0 )
350- {
351- gzwarn << " MuJoCo requires the lower joint position limit of ball "
352- " joints to be zero.\n " ;
353- joint->range [0 ] = 0 ;
354- }
355330 }
356- }
357- else if (sdfJoint->Type () == ::sdf::JointType::SCREW )
358- {
359- // Screw joints in MuJoCo are modeled by coupling a hinge joint
360- // (rotation) and a slide joint (translation) along the same axis on
361- // the child body using a joint equality constraint (mjEQ_JOINT).
362- // We store the hinge joint (`joint`) as the primary joint in
363- // JointInfo. This matches DART's choice of using the rotational DOF
364- // as the primary, ensuring both physics plugins expose consistent
365- // angular units (radians and rad/s) for screw joints across the
366- // gz-physics API.
367- // Like the universal joint, `joint` and `joint2` are compiled
368- // contiguously on the same child body.
369- joint = mjs_addJoint (child, nullptr );
370- joint->type = mjJNT_HINGE;
371- const auto *sdfAxis1 = sdfJoint->Axis (0 );
372- if (sdfAxis1)
331+ else if (sdfJoint->Type () == ::sdf::JointType::REVOLUTE )
373332 {
374- convertJointAxis (sdfAxis1, joint->axis );
375- copyStandardJointAxisProperties (joint, sdfAxis1);
376- // Disable independent position limits on the primary rotational
377- // hinge joint. In MuJoCo's soft constraint solver, enforcing limits
378- // on the hinge joint when coupled with a small thread pitch causes
379- // premature solver clamping and massive numerical damping. By mapping
380- // position limits purely onto the translational slide joint, we
381- // ensure exact kinematic limit enforcement without solver resistance.
382- joint->limited = false ;
333+ joint = mjs_addJoint (child, nullptr );
334+ joint->type = mjJNT_HINGE;
335+ const auto *sdfAxis = sdfJoint->Axis (0 );
336+ convertJointAxis (sdfAxis, joint->axis );
337+ copyStandardJointAxisProperties (joint, sdfAxis);
383338 }
384-
385- joint2 = mjs_addJoint (child, nullptr );
386- joint2->type = mjJNT_SLIDE;
387- if (sdfAxis1)
339+ else if (sdfJoint->Type () == ::sdf::JointType::BALL )
388340 {
389- convertJointAxis (sdfAxis1, joint2->axis );
390- // We only copy position limits and range to the secondary slide
391- // joint. All passive dynamics (damping, frictionloss, stiffness) and
392- // actuator effort limits are enforced purely on the primary
393- // rotational hinge joint to avoid double-counting and physical unit
394- // mismatches.
395- joint2->limited = static_cast <int >(!std::isinf (sdfAxis1->Lower ()) &&
396- !std::isinf (sdfAxis1->Upper ()));
397- if (joint2->limited )
341+ joint = mjs_addJoint (child, nullptr );
342+ joint->type = mjJNT_BALL;
343+ const auto *sdfAxis = sdfJoint->Axis (0 );
344+ if (sdfAxis)
398345 {
399- const double pitch =
400- convertScrewThreadPitch (sdfJoint->ScrewThreadPitch ());
401- joint2->range [0 ] = sdfAxis1->Lower () * pitch;
402- joint2->range [1 ] = sdfAxis1->Upper () * pitch;
346+ convertJointAxis (sdfAxis, joint->axis );
347+ copyStandardJointAxisProperties (joint, sdfAxis);
348+ // For ball joints, the first range parameter should always be set to
349+ // zero.
350+ if (joint->limited && std::abs (joint->range [0 ]) > 0.0 )
351+ {
352+ gzwarn << " MuJoCo requires the lower joint position limit of ball "
353+ " joints to be zero.\n " ;
354+ joint->range [0 ] = 0 ;
355+ }
403356 }
404357 }
405- }
406- else if (sdfJoint->Type () == ::sdf::JointType::UNIVERSAL )
407- {
408- // Universal joints in MuJoCo are modeled as two hinge joints in
409- // series on the child body. We only need to keep a pointer to the
410- // first hinge joint (`joint`) in JointInfo. Since MuJoCo compiles
411- // joints on the same body contiguously in memory, all getters/setters
412- // can safely access the second joint (`joint2`)'s state data using
413- // `nq_index + 1` and `nv_index + 1` without needing to store it
414- // separately in JointInfo.
415- joint = mjs_addJoint (child, nullptr );
416- joint->type = mjJNT_HINGE;
417- const auto *sdfAxis1 = sdfJoint->Axis (0 );
418- if (sdfAxis1)
358+ else if (sdfJoint->Type () == ::sdf::JointType::SCREW )
419359 {
420- convertJointAxis (sdfAxis1, joint->axis );
421- copyStandardJointAxisProperties (joint, sdfAxis1);
360+ // Screw joints in MuJoCo are modeled by coupling a hinge joint
361+ // (rotation) and a slide joint (translation) along the same axis on
362+ // the child body using a joint equality constraint (mjEQ_JOINT).
363+ // We store the hinge joint (`joint`) as the primary joint in
364+ // JointInfo. This matches DART's choice of using the rotational DOF
365+ // as the primary, ensuring both physics plugins expose consistent
366+ // angular units (radians and rad/s) for screw joints across the
367+ // gz-physics API.
368+ // Like the universal joint, `joint` and `joint2` are compiled
369+ // contiguously on the same child body.
370+ joint = mjs_addJoint (child, nullptr );
371+ joint->type = mjJNT_HINGE;
372+ const auto *sdfAxis1 = sdfJoint->Axis (0 );
373+ if (sdfAxis1)
374+ {
375+ convertJointAxis (sdfAxis1, joint->axis );
376+ copyStandardJointAxisProperties (joint, sdfAxis1);
377+ // Disable independent position limits on the primary rotational
378+ // hinge joint. In MuJoCo's soft constraint solver, enforcing limits
379+ // on the hinge joint when coupled with a small thread pitch causes
380+ // premature solver clamping and massive numerical damping. By mapping
381+ // position limits purely onto the translational slide joint, we
382+ // ensure exact kinematic limit enforcement without solver resistance.
383+ joint->limited = false ;
384+ }
385+
386+ joint2 = mjs_addJoint (child, nullptr );
387+ joint2->type = mjJNT_SLIDE;
388+ if (sdfAxis1)
389+ {
390+ convertJointAxis (sdfAxis1, joint2->axis );
391+ // We only copy position limits and range to the secondary slide
392+ // joint. All passive dynamics (damping, frictionloss, stiffness) and
393+ // actuator effort limits are enforced purely on the primary
394+ // rotational hinge joint to avoid double-counting and physical unit
395+ // mismatches.
396+ joint2->limited = static_cast <int >(!std::isinf (sdfAxis1->Lower ()) &&
397+ !std::isinf (sdfAxis1->Upper ()));
398+ if (joint2->limited )
399+ {
400+ const double pitch =
401+ convertScrewThreadPitch (sdfJoint->ScrewThreadPitch ());
402+ joint2->range [0 ] = sdfAxis1->Lower () * pitch;
403+ joint2->range [1 ] = sdfAxis1->Upper () * pitch;
404+ }
405+ }
422406 }
407+ else if (sdfJoint->Type () == ::sdf::JointType::UNIVERSAL )
408+ {
409+ // Universal joints in MuJoCo are modeled as two hinge joints in
410+ // series on the child body. We only need to keep a pointer to the
411+ // first hinge joint (`joint`) in JointInfo. Since MuJoCo compiles
412+ // joints on the same body contiguously in memory, all getters/setters
413+ // can safely access the second joint (`joint2`)'s state data using
414+ // `nq_index + 1` and `nv_index + 1` without needing to store it
415+ // separately in JointInfo.
416+ joint = mjs_addJoint (child, nullptr );
417+ joint->type = mjJNT_HINGE;
418+ const auto *sdfAxis1 = sdfJoint->Axis (0 );
419+ if (sdfAxis1)
420+ {
421+ convertJointAxis (sdfAxis1, joint->axis );
422+ copyStandardJointAxisProperties (joint, sdfAxis1);
423+ }
423424
424- joint2 = mjs_addJoint (child, nullptr );
425- joint2->type = mjJNT_HINGE;
426- const auto *sdfAxis2 = sdfJoint->Axis (1 );
427- if (sdfAxis2)
425+ joint2 = mjs_addJoint (child, nullptr );
426+ joint2->type = mjJNT_HINGE;
427+ const auto *sdfAxis2 = sdfJoint->Axis (1 );
428+ if (sdfAxis2)
429+ {
430+ convertJointAxis (sdfAxis2, joint2->axis );
431+ copyStandardJointAxisProperties (joint2, sdfAxis2);
432+ }
433+ }
434+ else if (sdfJoint->Type () != ::sdf::JointType::FIXED )
428435 {
429- convertJointAxis (sdfAxis2, joint2->axis );
430- copyStandardJointAxisProperties (joint2, sdfAxis2);
436+ gzwarn << " Joint type " << static_cast <int >(sdfJoint->Type ())
437+ << " in joint [" << sdfJoint->Name () << " ] not supported\n " ;
438+ return ;
431439 }
432- }
433- else if (sdfJoint->Type () != ::sdf::JointType::FIXED )
434- {
435- gzwarn << " Joint type " << static_cast <int >(sdfJoint->Type ())
436- << " in joint [" << sdfJoint->Name () << " ] not supported\n " ;
437- return ;
438- }
439440
440- // Resolve the pose of the joint relative to the body with which
441- // it's associated. Note that this body is the child link of the joint
442- // in SDF terms.
443- auto jointPose = resolveSdfPose (sdfJoint->SemanticPose ());
444- mjsEquality *eq = nullptr ;
445- // Note that no joints will be created when processing a fixed joint.
446- if (joint)
447- {
448- const std::string mjJointName =
449- ::sdf::JoinName (_modelInfo->name, sdfJoint->Name ());
450- mjs_setName (joint->element, mjJointName.c_str());
451- actuator = mjs_addActuator(_spec, nullptr );
452- actuator->trntype = mjtTrn::mjTRN_JOINT;
441+ // Resolve the pose of the joint relative to the body with which
442+ // it's associated. Note that this body is the child link of the joint
443+ // in SDF terms.
444+ auto jointPose = resolveSdfPose (sdfJoint->SemanticPose ());
445+ mjsEquality *eq = nullptr ;
446+ // Note that no joints will be created when processing a fixed joint.
447+ if (joint)
448+ {
449+ const std::string mjJointName =
450+ ::sdf::JoinName (_modelInfo->name, sdfJoint->Name ());
451+ mjs_setName (joint->element, mjJointName.c_str());
452+ actuator = mjs_addActuator(_spec, nullptr );
453+ actuator->trntype = mjtTrn::mjTRN_JOINT;
453454
454- mjs_setString (actuator->target , mjJointName.c_str ());
455+ mjs_setString (actuator->target , mjJointName.c_str ());
455456
456- copyPos (jointPose.Pos (), joint->pos );
457+ copyPos (jointPose.Pos (), joint->pos );
457458
458- if (joint2)
459- {
460- // We uniquely name the second axis using getJointAxisName helper
461- // with a flat suffix. This flat name avoids indicating any
462- // Kinematic/SDF nesting (`::axis2`) while still satisfying
463- // MuJoCo's requirement that joints must be uniquely named.
464- const std::string mjJointName2 = getJointAxisName (mjJointName, 1 );
465- mjs_setName (joint2->element , mjJointName2.c_str ());
466- mjsActuator *actuator2 = mjs_addActuator (_spec, nullptr );
467- actuator2->trntype = mjtTrn::mjTRN_JOINT;
468- mjs_setString (actuator2->target , mjJointName2.c_str ());
469-
470- copyPos (jointPose.Pos (), joint2->pos );
471-
472- // If this is a screw joint, couple the slide and hinge axes using
473- // a joint equality constraint (mjEQ_JOINT) with the specified pitch.
474- if (sdfJoint->Type () == ::sdf::JointType::SCREW )
459+ if (joint2)
475460 {
476- eq = mjs_addEquality (_spec, nullptr );
477- eq->type = mjEQ_JOINT;
478- eq->active = 1 ;
479- mjs_setString (eq->name1 , mjJointName2.c_str ());
480- mjs_setString (eq->name2 , mjJointName.c_str ());
481-
482- // dif = pos[1] - ref[1] = hinge_pos - hinge_ref
483- // cpos = pos[0] - ref[0] - data[0] - data[1]*dif = 0
484- // enforces: slide_pos - slide_ref =
485- // data[1] * (hinge_pos - hinge_ref)
486- // where data[1] = pitch (meters/rad) = ScrewThreadPitch/2pi
487- std::fill (std::begin (eq->data ), std::end (eq->data ), 0.0 );
488- eq->data [1 ] =
489- convertScrewThreadPitch (sdfJoint->ScrewThreadPitch ());
461+ // We uniquely name the second axis using getJointAxisName helper
462+ // with a flat suffix. This flat name avoids indicating any
463+ // Kinematic/SDF nesting (`::axis2`) while still satisfying
464+ // MuJoCo's requirement that joints must be uniquely named.
465+ const std::string mjJointName2 = getJointAxisName (mjJointName, 1 );
466+ mjs_setName (joint2->element , mjJointName2.c_str ());
467+ mjsActuator *actuator2 = mjs_addActuator (_spec, nullptr );
468+ actuator2->trntype = mjtTrn::mjTRN_JOINT;
469+ mjs_setString (actuator2->target , mjJointName2.c_str ());
470+
471+ copyPos (jointPose.Pos (), joint2->pos );
472+
473+ // If this is a screw joint, couple the slide and hinge axes using
474+ // a joint equality constraint (mjEQ_JOINT) with the specified pitch.
475+ if (sdfJoint->Type () == ::sdf::JointType::SCREW )
476+ {
477+ eq = mjs_addEquality (_spec, nullptr );
478+ eq->type = mjEQ_JOINT;
479+ eq->active = 1 ;
480+ mjs_setString (eq->name1 , mjJointName2.c_str ());
481+ mjs_setString (eq->name2 , mjJointName.c_str ());
482+
483+ // dif = pos[1] - ref[1] = hinge_pos - hinge_ref
484+ // cpos = pos[0] - ref[0] - data[0] - data[1]*dif = 0
485+ // enforces: slide_pos - slide_ref =
486+ // data[1] * (hinge_pos - hinge_ref)
487+ // where data[1] = pitch (meters/rad) = ScrewThreadPitch/2pi
488+ std::fill (std::begin (eq->data ), std::end (eq->data ), 0.0 );
489+ eq->data [1 ] =
490+ convertScrewThreadPitch (sdfJoint->ScrewThreadPitch ());
491+ }
490492 }
491493 }
492- }
493- auto jointInfo =
494- std::make_shared<JointInfo>(_base.GetNextEntity (), _modelInfo);
495- jointInfo->name = sdfJoint->Name ();
496- jointInfo->joint = joint;
497- jointInfo->childBody = child;
498- jointInfo->actuator = actuator;
499- jointInfo->worldInfo = worldInfo;
500- if (sdfJoint->Type () == ::sdf::JointType::SCREW )
501- {
502- jointInfo->screwConstraintSpec = eq;
503- }
504- if (sdfJoint->Type () == ::sdf::JointType::BALL )
505- {
506- jointInfo->worldInfo ->ballJointPositionsCache .push_back (std::nullopt );
507- jointInfo->ballJointCacheIndex =
508- jointInfo->worldInfo ->ballJointPositionsCache .size () - 1 ;
509- }
494+ auto jointInfo =
495+ std::make_shared<JointInfo>(_base.GetNextEntity (), _modelInfo);
496+ jointInfo->name = sdfJoint->Name ();
497+ jointInfo->joint = joint;
498+ jointInfo->childBody = child;
499+ jointInfo->actuator = actuator;
500+ jointInfo->worldInfo = worldInfo;
501+ if (sdfJoint->Type () == ::sdf::JointType::SCREW )
502+ {
503+ jointInfo->screwConstraintSpec = eq;
504+ }
505+ if (sdfJoint->Type () == ::sdf::JointType::BALL )
506+ {
507+ jointInfo->worldInfo ->ballJointPositionsCache .push_back (std::nullopt );
508+ jointInfo->ballJointCacheIndex =
509+ jointInfo->worldInfo ->ballJointPositionsCache .size () - 1 ;
510+ }
510511
511- auto jointSite = mjs_addSite (child, nullptr );
512- copyPos (jointPose.Pos (), jointSite->pos );
513- copyQuat (jointPose.Rot (), jointSite->quat );
514- _base.frames [jointInfo->entityId ] =
515- std::make_shared<FrameInfo>(jointSite, worldInfo);
512+ auto jointSite = mjs_addSite (child, nullptr );
513+ copyPos (jointPose.Pos (), jointSite->pos );
514+ copyQuat (jointPose.Rot (), jointSite->quat );
515+ _base.frames [jointInfo->entityId ] =
516+ std::make_shared<FrameInfo>(jointSite, worldInfo);
516517
517- _modelInfo->joints .AddEntity (jointInfo->entityId , jointInfo,
518- jointInfo->name , _modelInfo->entityId );
518+ _modelInfo->joints .AddEntity (jointInfo->entityId , jointInfo,
519+ jointInfo->name , _modelInfo->entityId );
520+ }
519521 }
520522
521523 // / \brief Resolve the pose of a target link (specified by its model name and
0 commit comments