From f04c5c8c36381c373d42579a414b7907dff17ea4 Mon Sep 17 00:00:00 2001 From: aki Date: Wed, 9 Sep 2026 17:55:56 +0800 Subject: [PATCH] fix(robotics): enable collider for mesh-bearing URDF collision links The URDF importer fills `shapeFile`/shape transforms for mesh-bearing geometry but leaves `colliderType` at the prefab default (ColliderType::None). The resulting bot has no collision representation: mochi contact queries (TotalContactForce / ContactPoints) are refused ("Contact queries are only supported for actors with contact sample points") and no contact is generated with other actors. Set ColliderType::Auto when a collision mesh resolves, so imported URDF bots participate in contact. Links without a mesh collision keep the default unchanged. Regression tests: mesh collision -> Auto, primitive-only collision -> None. --- .../src/utils/urdf_import_utils.cpp | 3 + .../test/urdf_mesh_resolution_test.cpp | 67 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/superdex_robotics/src/utils/urdf_import_utils.cpp b/superdex_robotics/src/utils/urdf_import_utils.cpp index c816e9ee..3bf5245a 100644 --- a/superdex_robotics/src/utils/urdf_import_utils.cpp +++ b/superdex_robotics/src/utils/urdf_import_utils.cpp @@ -481,6 +481,9 @@ void PopulateFromParsedModel( ParsedMesh const& mesh = *parsedLink.collision; if (auto const resolved = ResolveMeshPath(mesh.filename, meshBasePath)) { link->shapeFile = DynamicString{*resolved}; + // Enable the collider for mesh-bearing links: ColliderType::None is + // the prefab default, which leaves the robot contactless. + link->colliderType = ColliderType::Auto; } if (meshRefs != nullptr) { meshRefs->links[iLink].collision = DynamicString{mesh.filename}; diff --git a/superdex_robotics/test/urdf_mesh_resolution_test.cpp b/superdex_robotics/test/urdf_mesh_resolution_test.cpp index 3eb82dad..ae4236c0 100644 --- a/superdex_robotics/test/urdf_mesh_resolution_test.cpp +++ b/superdex_robotics/test/urdf_mesh_resolution_test.cpp @@ -69,6 +69,37 @@ class UrdfMeshResolutionTest : public testing::Test { )"; } + // Same, but the mesh is the geometry instead of . + static std::string SingleMeshCollisionUrdf(std::string_view packageMeshUri) { + return std::string(R"( + + + + + + + + + +)"; + } + + // Primitive-only collision geometry (no mesh). + static std::string SinglePrimitiveCollisionUrdf() { + return std::string(R"( + + + + + + + + + +)"); + } + mochi::TempDirCleanup _tempDirCleanup = mochi::CreateTempDirectory("urdf_mesh_resolution_test", ExpectOK{}); fs::path _tempDir = _tempDirCleanup.Path(); @@ -164,3 +195,39 @@ TEST_F(UrdfMeshResolutionTest, UnresolvableMeshPreservesRawReference) { std::string(meshRefs.links[0].visual.c_str()), "package://ur_description/meshes/base.dae"); EXPECT_TRUE(meshRefs.links[0].collision.empty()); } + +// A mesh-bearing must enable the collider: ColliderType::None is the +// prefab default, which left imported robots contactless (contact queries and +// contact forces unavailable). +TEST_F(UrdfMeshResolutionTest, MeshCollisionEnablesColliderType) { + auto const packageDir = _tempDir / "ur_description"; + auto const urdfPath = packageDir / "urdf" / "robot.urdf"; + auto const meshPath = packageDir / "meshes" / "base.dae"; + + WriteFileText(meshPath, "dummy"); + WriteFileText( + urdfPath, SingleMeshCollisionUrdf("package://ur_description/meshes/base.dae")); + + BotPrefab const prefab = LoadBotPrefabFromUrdfFile(urdfPath.string(), ExpectOK{}); + + ASSERT_FALSE(prefab.links.empty()); + EXPECT_FALSE(prefab.links[0].shapeFile.empty()); + EXPECT_EQ(static_cast(prefab.links[0].colliderType), + static_cast(ColliderType::Auto)); +} + +// Primitive-only geometry keeps the prefab default: the field is not +// touched and the bot stays contactless. +TEST_F(UrdfMeshResolutionTest, PrimitiveCollisionKeepsDefaultColliderType) { + auto const packageDir = _tempDir / "some_other_dir"; + auto const urdfPath = packageDir / "urdf" / "robot.urdf"; + + WriteFileText(urdfPath, SinglePrimitiveCollisionUrdf()); + + BotPrefab const prefab = LoadBotPrefabFromUrdfFile(urdfPath.string(), ExpectOK{}); + + ASSERT_FALSE(prefab.links.empty()); + EXPECT_TRUE(prefab.links[0].shapeFile.empty()); + EXPECT_EQ(static_cast(prefab.links[0].colliderType), + static_cast(ColliderType::None)); +}