@@ -1801,6 +1801,63 @@ TEST_F(SensorTest, TactileSkipTangents) {
18011801 EXPECT_EQ (nonzero_count, 2 ) << " Expected 2 taxels in contact" ;
18021802}
18031803
1804+ // Test tactile sensor cutoff attribute, engine clamping, and round-trip XML save/load
1805+ TEST_F (SensorTest, TactileCutoff) {
1806+ constexpr char xml[] = R"(
1807+ <mujoco>
1808+ <option>
1809+ <flag multiccd="enable"/>
1810+ </option>
1811+ <asset>
1812+ <mesh name="sensor_mesh" builtin="sphere" params="0"/>
1813+ </asset>
1814+ <worldbody>
1815+ <body pos="0 0 1">
1816+ <freejoint/>
1817+ <geom name="sensor_geom" type="mesh" mesh="sensor_mesh"/>
1818+ </body>
1819+ <body>
1820+ <geom type="box" size=".7 .7 .3"/>
1821+ </body>
1822+ </worldbody>
1823+ <sensor>
1824+ <tactile geom="sensor_geom" mesh="sensor_mesh" cutoff="0.05"/>
1825+ </sensor>
1826+ </mujoco>
1827+ )" ;
1828+ char error[1024 ];
1829+ MjModelPtr model = LoadModelFromString (xml, error, sizeof (error));
1830+ ASSERT_THAT (model.get (), NotNull ()) << error;
1831+ ASSERT_GT (model->nsensordata , 0 ) << " No sensor data allocated" ;
1832+ EXPECT_MJTNUM_EQ (model->sensor_cutoff [0 ], 0.05 );
1833+
1834+ MjDataPtr data = MakeData (model);
1835+
1836+ // Compute collisions and sensors at t=0
1837+ mj_forward (model.get (), data.get ());
1838+
1839+ int ntaxel = model->nsensordata / 3 ;
1840+ int nonzero_count = 0 ;
1841+ for (int i = 0 ; i < ntaxel; i++) {
1842+ if (data->sensordata [i] != 0 ) {
1843+ nonzero_count++;
1844+ // Without cutoff, penetration is ~0.2; with cutoff=0.05, it must be clamped to 0.05
1845+ EXPECT_NEAR (data->sensordata [i], 0.05 , MjTol (1e-6 , 1e-4 ))
1846+ << " Penetration depth at taxel " << i << " should be clamped to cutoff" ;
1847+ }
1848+ }
1849+ EXPECT_EQ (nonzero_count, 2 ) << " Expected 2 taxels in contact" ;
1850+
1851+ // Verify XML round-trip preserves cutoff without schema errors
1852+ std::string saved_xml = SaveAndReadXml (model.get ());
1853+ EXPECT_THAT (saved_xml, HasSubstr (" cutoff=\" 0.05\" " ));
1854+ EXPECT_THAT (saved_xml, Not (HasSubstr (" noise" )));
1855+
1856+ MjModelPtr reloaded = LoadModelFromString (saved_xml.c_str (), error, sizeof (error));
1857+ ASSERT_THAT (reloaded.get (), NotNull ()) << error;
1858+ EXPECT_MJTNUM_EQ (reloaded->sensor_cutoff [0 ], 0.05 );
1859+ }
1860+
18041861// insidesite uses subtree_com for massless flex parent bodies
18051862TEST_F (SensorTest, InsideSiteFlexBody) {
18061863 static constexpr char xml[] = R"(
0 commit comments