From 4d2d9696886d5bc6990b83ba7b12504e3aa8e082 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 29 May 2026 14:03:04 +0800 Subject: [PATCH 01/21] Add ns component Signed-off-by: C88-YQ <1409947012@qq.com> --- include/gz/sim/components/Namespace.hh | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 include/gz/sim/components/Namespace.hh diff --git a/include/gz/sim/components/Namespace.hh b/include/gz/sim/components/Namespace.hh new file mode 100644 index 0000000000..e03f239a2f --- /dev/null +++ b/include/gz/sim/components/Namespace.hh @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2026 Jiayi Cai + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ +#ifndef GZ_SIM_COMPONENTS_NAMESPACE_HH_ +#define GZ_SIM_COMPONENTS_NAMESPACE_HH_ + +#include +#include +#include +#include +#include + +namespace gz +{ +namespace sim +{ +// Inline bracket to help doxygen filtering. +inline namespace GZ_SIM_VERSION_NAMESPACE { +namespace components +{ + /// \brief This component holds an entity's namespace. + using Namespace = Component; + GZ_SIM_REGISTER_COMPONENT("gz_sim_components.Namespace", Namespace) +} +} +} +} + +#endif From fb964abafd3363758a0ea0431fb085abab30b3f4 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 29 May 2026 15:26:23 +0800 Subject: [PATCH 02/21] Create ns component when creating entity Signed-off-by: C88-YQ <1409947012@qq.com> --- src/SdfEntityCreator.cc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/SdfEntityCreator.cc b/src/SdfEntityCreator.cc index 4e5fe9110d..0515c9ed55 100644 --- a/src/SdfEntityCreator.cc +++ b/src/SdfEntityCreator.cc @@ -70,6 +70,7 @@ #include "gz/sim/components/Material.hh" #include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/NavSat.hh" #include "gz/sim/components/ParentEntity.hh" #include "gz/sim/components/ParentLinkName.hh" @@ -264,6 +265,9 @@ void SdfEntityCreator::CreateEntities(const sdf::World *_world, this->dataPtr->ecm->CreateComponent(_worldEntity, components::Name(_world->Name())); + this->dataPtr->ecm->CreateComponent(_worldEntity, + components::Namespace(_world->Namespace())); + // Gravity this->dataPtr->ecm->CreateComponent(_worldEntity, components::Gravity(_world->Gravity())); @@ -526,6 +530,8 @@ Entity SdfEntityCreator::CreateEntities(const sdf::Model *_model, components::Pose(ResolveSdfPose(_model->SemanticPose()))); this->dataPtr->ecm->CreateComponent(modelEntity, components::Name(_model->Name())); + this->dataPtr->ecm->CreateComponent(modelEntity, + components::Namespace(_model->Namespace())); bool isStatic = _model->Static() || _staticParent; this->dataPtr->ecm->CreateComponent(modelEntity, components::Static(isStatic)); @@ -1057,6 +1063,8 @@ Entity SdfEntityCreator::CreateEntities(const sdf::ParticleEmitter *_emitter) components::Pose(ResolveSdfPose(_emitter->SemanticPose()))); this->dataPtr->ecm->CreateComponent(emitterEntity, components::Name(_emitter->Name())); + this->dataPtr->ecm->CreateComponent(emitterEntity, + components::Namespace(_emitter->Namespace())); return emitterEntity; } @@ -1123,6 +1131,8 @@ Entity SdfEntityCreator::CreateEntities(const sdf::Sensor *_sensor) components::Pose(ResolveSdfPose(_sensor->SemanticPose()))); this->dataPtr->ecm->CreateComponent(sensorEntity, components::Name(_sensor->Name())); + this->dataPtr->ecm->CreateComponent(sensorEntity, + components::Namespace(_sensor->Namespace())); if (_sensor->Type() == sdf::SensorType::CAMERA) { From 0908f9eb448f64e15657e3c9bdb27053b0d4227e Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 29 May 2026 15:31:55 +0800 Subject: [PATCH 03/21] Add ns helper function Signed-off-by: C88-YQ <1409947012@qq.com> --- include/gz/sim/Util.hh | 17 ++++++++++++++ src/Util.cc | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/include/gz/sim/Util.hh b/include/gz/sim/Util.hh index 96665464c3..7c5461c811 100644 --- a/include/gz/sim/Util.hh +++ b/include/gz/sim/Util.hh @@ -68,6 +68,23 @@ namespace gz const EntityComponentManager &_ecm, const std::string &_delim = "/", bool _includePrefix = true); + /// \brief Helper function to check whether any entity has a non-empty + /// namespace component. + /// \param[in] _ecm Immutable reference to ECM. + /// \return True if any entity has a non-empty namespace. + bool GZ_SIM_VISIBLE hasNamespace( + const EntityComponentManager &_ecm); + + /// \brief Helper function to generate the full scoped namespace of an entity, + /// including namespaces inherited from all parent entities. + /// \param[in] _ecm Immutable reference to ECM. + /// \param[in] _entity Entity to get the scoped namespace for. + /// \param[in] _delim Delimiter to put between namespaces, defaults to "/". + /// \return Scoped namespace, or empty string if no namespace is found. + std::string GZ_SIM_VISIBLE scopedNamespace( + const EntityComponentManager &_ecm, const Entity &_entity, + const std::string &_delim = "/"); + /// \brief Helper function to get an entity given its scoped name. /// The scope may start at any level by default. For example, in this /// hierarchy: diff --git a/src/Util.cc b/src/Util.cc index b97790cb34..cd7ddfb81d 100644 --- a/src/Util.cc +++ b/src/Util.cc @@ -48,6 +48,7 @@ #include "gz/sim/components/Link.hh" #include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/ParentEntity.hh" #include "gz/sim/components/ParticleEmitter.hh" #include "gz/sim/components/Projector.hh" @@ -184,6 +185,58 @@ std::string scopedName(const Entity &_entity, return result; } +////////////////////////////////////////////////// +bool hasNamespace(const EntityComponentManager &_ecm) +{ + const auto &entities = _ecm.Entities().Vertices(); + + for ( const auto &entity : entities) + { + const auto ns = _ecm.Component(entity.first); + if (ns && !ns->Data().empty()) + return true; + } + return false; +} + +////////////////////////////////////////////////// +std::string scopedNamespace(const EntityComponentManager &_ecm, + const Entity &_entity, const std::string &_delim) +{ + std::vector namespaces; + + auto entity = _entity; + while (entity != kNullEntity) + { + const auto ns = _ecm.Component(entity); + if (ns && !ns->Data().empty()) + { + std::string nsStr = ns->Data(); + const auto begin = nsStr.find_first_not_of('/'); + if (begin != std::string::npos) + { + const auto end = nsStr.find_last_not_of('/'); + nsStr = nsStr.substr(begin, end - begin + 1); + + namespaces.push_back(nsStr); + } + } + + const auto parentEntity = _ecm.Component(entity); + if (!parentEntity) + break; + entity = parentEntity->Data(); + } + + std::reverse(namespaces.begin(), namespaces.end()); + std::string result; + for (const auto &ns : namespaces) + { + result += _delim + ns; + } + return result; +} + ////////////////////////////////////////////////// std::string normalizePluginName(const std::string &_name) { From c826ea7d843c721e74c18e27ff5d9e64c3d3f94a Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 5 Jun 2026 12:45:01 +0800 Subject: [PATCH 04/21] Add test for hasNamespace & scopedNamespace Signed-off-by: C88-YQ <1409947012@qq.com> --- src/Util_TEST.cc | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/Util_TEST.cc b/src/Util_TEST.cc index 08ae2cd847..598b4024b8 100644 --- a/src/Util_TEST.cc +++ b/src/Util_TEST.cc @@ -35,6 +35,7 @@ #include "gz/sim/components/Link.hh" #include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/ParentEntity.hh" #include "gz/sim/components/ParticleEmitter.hh" #include "gz/sim/components/Projector.hh" @@ -239,6 +240,62 @@ TEST_F(UtilTest, ScopedName) EXPECT_EQ(kNullEntity, sim::worldEntity(kNullEntity, ecm)); } +///////////////////////////////////////////////// +TEST_F(UtilTest, HasNamespace) +{ + EntityComponentManager ecm; + + EXPECT_FALSE(hasNamespace(ecm)); + + ecm.CreateEntity(); + EXPECT_FALSE(hasNamespace(ecm)); + + auto entityWithEmptyNamespace = ecm.CreateEntity(); + ecm.CreateComponent(entityWithEmptyNamespace, components::Namespace("")); + EXPECT_FALSE(hasNamespace(ecm)); + + auto entityWithNamespace = ecm.CreateEntity(); + ecm.CreateComponent(entityWithNamespace, components::Namespace("robot")); + EXPECT_TRUE(hasNamespace(ecm)); +} + +///////////////////////////////////////////////// +TEST_F(UtilTest, ScopedNamespace) +{ + EntityComponentManager ecm; + + auto worldEntity = ecm.CreateEntity(); + ecm.CreateComponent(worldEntity, components::Namespace("world_ns/")); + + auto modelEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelEntity, components::Namespace("model_ns")); + ecm.CreateComponent(modelEntity, components::ParentEntity(worldEntity)); + + auto nestedModelEntity = ecm.CreateEntity(); + ecm.CreateComponent(nestedModelEntity, components::ParentEntity(modelEntity)); + + auto linkEntity = ecm.CreateEntity(); + ecm.CreateComponent(linkEntity, components::Namespace("//link_ns//")); + ecm.CreateComponent(linkEntity, components::ParentEntity(nestedModelEntity)); + + auto entityWithSlashesNamespace = ecm.CreateEntity(); + ecm.CreateComponent(entityWithSlashesNamespace, + components::Namespace("///")); + ecm.CreateComponent(entityWithSlashesNamespace, + components::ParentEntity(linkEntity)); + + EXPECT_EQ("/world_ns", scopedNamespace(ecm, worldEntity)); + EXPECT_EQ("/world_ns/model_ns", scopedNamespace(ecm, modelEntity)); + EXPECT_EQ("/world_ns/model_ns", scopedNamespace(ecm, nestedModelEntity)); + EXPECT_EQ("/world_ns/model_ns/link_ns", scopedNamespace(ecm, linkEntity)); + EXPECT_EQ("::world_ns::model_ns::link_ns", + scopedNamespace(ecm, linkEntity, "::")); + EXPECT_EQ("/world_ns/model_ns/link_ns", + scopedNamespace(ecm, entityWithSlashesNamespace)); + + EXPECT_TRUE(scopedNamespace(ecm, kNullEntity).empty()); +} + ///////////////////////////////////////////////// TEST_F(UtilTest, EntitiesFromScopedName) { From e881c213f726f072b67d7ca29d3394196c7ff58f Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 5 Jun 2026 12:47:18 +0800 Subject: [PATCH 05/21] Pass codecheck Signed-off-by: C88-YQ <1409947012@qq.com> --- src/Util.cc | 4 ++-- src/Util_TEST.cc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Util.cc b/src/Util.cc index cd7ddfb81d..38741638ea 100644 --- a/src/Util.cc +++ b/src/Util.cc @@ -204,7 +204,7 @@ std::string scopedNamespace(const EntityComponentManager &_ecm, const Entity &_entity, const std::string &_delim) { std::vector namespaces; - + auto entity = _entity; while (entity != kNullEntity) { @@ -217,7 +217,7 @@ std::string scopedNamespace(const EntityComponentManager &_ecm, { const auto end = nsStr.find_last_not_of('/'); nsStr = nsStr.substr(begin, end - begin + 1); - + namespaces.push_back(nsStr); } } diff --git a/src/Util_TEST.cc b/src/Util_TEST.cc index 598b4024b8..fade174cb3 100644 --- a/src/Util_TEST.cc +++ b/src/Util_TEST.cc @@ -283,7 +283,7 @@ TEST_F(UtilTest, ScopedNamespace) components::Namespace("///")); ecm.CreateComponent(entityWithSlashesNamespace, components::ParentEntity(linkEntity)); - + EXPECT_EQ("/world_ns", scopedNamespace(ecm, worldEntity)); EXPECT_EQ("/world_ns/model_ns", scopedNamespace(ecm, modelEntity)); EXPECT_EQ("/world_ns/model_ns", scopedNamespace(ecm, nestedModelEntity)); From 380862294568f255440be18598ba8e675110474a Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 5 Jun 2026 12:56:48 +0800 Subject: [PATCH 06/21] Remove the namespace attribute of world & sensor & particleEmitter Signed-off-by: C88-YQ <1409947012@qq.com> --- src/SdfEntityCreator.cc | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/SdfEntityCreator.cc b/src/SdfEntityCreator.cc index 0515c9ed55..d06f164968 100644 --- a/src/SdfEntityCreator.cc +++ b/src/SdfEntityCreator.cc @@ -265,9 +265,6 @@ void SdfEntityCreator::CreateEntities(const sdf::World *_world, this->dataPtr->ecm->CreateComponent(_worldEntity, components::Name(_world->Name())); - this->dataPtr->ecm->CreateComponent(_worldEntity, - components::Namespace(_world->Namespace())); - // Gravity this->dataPtr->ecm->CreateComponent(_worldEntity, components::Gravity(_world->Gravity())); @@ -1063,8 +1060,6 @@ Entity SdfEntityCreator::CreateEntities(const sdf::ParticleEmitter *_emitter) components::Pose(ResolveSdfPose(_emitter->SemanticPose()))); this->dataPtr->ecm->CreateComponent(emitterEntity, components::Name(_emitter->Name())); - this->dataPtr->ecm->CreateComponent(emitterEntity, - components::Namespace(_emitter->Namespace())); return emitterEntity; } @@ -1131,8 +1126,6 @@ Entity SdfEntityCreator::CreateEntities(const sdf::Sensor *_sensor) components::Pose(ResolveSdfPose(_sensor->SemanticPose()))); this->dataPtr->ecm->CreateComponent(sensorEntity, components::Name(_sensor->Name())); - this->dataPtr->ecm->CreateComponent(sensorEntity, - components::Namespace(_sensor->Namespace())); if (_sensor->Type() == sdf::SensorType::CAMERA) { From f1eea9c47eb94dd75824ca5b78adc818fe6ec77c Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 5 Jun 2026 13:27:20 +0800 Subject: [PATCH 07/21] Add test for SdfEntityCreator Signed-off-by: C88-YQ <1409947012@qq.com> --- src/SdfEntityCreator_TEST.cc | 25 ++++++++++++++++++++----- test/worlds/lights.sdf | 2 +- test/worlds/shapes.sdf | 4 ++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/SdfEntityCreator_TEST.cc b/src/SdfEntityCreator_TEST.cc index e7f7527dc4..1af906ded8 100644 --- a/src/SdfEntityCreator_TEST.cc +++ b/src/SdfEntityCreator_TEST.cc @@ -43,6 +43,7 @@ #include "gz/sim/components/Material.hh" #include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/ParentEntity.hh" #include "gz/sim/components/ParentLinkName.hh" #include "gz/sim/components/Physics.hh" @@ -100,6 +101,7 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) EXPECT_TRUE(this->ecm.HasComponentType(components::Visual::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Light::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Name::typeId)); + EXPECT_TRUE(this->ecm.HasComponentType(components::Namespace::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::ParentEntity::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Geometry::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Material::typeId)); @@ -149,17 +151,20 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) this->ecm.Each( + components::Name, + components::Namespace>( [&](const Entity &_entity, const components::Model *_model, const components::Pose *_pose, const components::ParentEntity *_parent, - const components::Name *_name)->bool + const components::Name *_name, + const components::Namespace *_ns)->bool { EXPECT_NE(nullptr, _model); EXPECT_NE(nullptr, _pose); EXPECT_NE(nullptr, _parent); EXPECT_NE(nullptr, _name); + EXPECT_NE(nullptr, _ns); modelCount++; @@ -171,6 +176,7 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) EXPECT_EQ(math::Pose3d(1, 2, 3, 0, 0, 1), _pose->Data()); EXPECT_EQ("box", _name->Data()); + EXPECT_EQ("", _ns->Data()); boxModelEntity = _entity; } else if (modelCount == 2) @@ -178,6 +184,7 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) EXPECT_EQ(math::Pose3d(-1, -2, -3, 0, 0, 1), _pose->Data()); EXPECT_EQ("cylinder", _name->Data()); + EXPECT_EQ("cylinder", _ns->Data()); cylModelEntity = _entity; } else if (modelCount == 3) @@ -185,6 +192,7 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) EXPECT_EQ(math::Pose3d(0, 0, 0, 0, 0, 1), _pose->Data()); EXPECT_EQ("sphere", _name->Data()); + EXPECT_EQ("sphere_ns", _ns->Data()); sphModelEntity = _entity; } else if (modelCount == 4) @@ -192,6 +200,7 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) EXPECT_EQ(math::Pose3d(-4, -5, -6, 0, 0, 1), _pose->Data()); EXPECT_EQ("capsule", _name->Data()); + EXPECT_EQ("", _ns->Data()); capModelEntity = _entity; } else if (modelCount == 5) @@ -199,6 +208,7 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) EXPECT_EQ(math::Pose3d(4, 5, 6, 0, 0, 1), _pose->Data()); EXPECT_EQ("ellipsoid", _name->Data()); + EXPECT_EQ("", _ns->Data()); ellipModelEntity = _entity; } return true; @@ -693,7 +703,7 @@ TEST_F(SdfEntityCreatorTest, CreateLights) unsigned int worldCount{0}; Entity worldEntity = kNullEntity; this->ecm.Each( + components::Name>( [&](const Entity &_entity, const components::World *_world, const components::Name *_name)->bool @@ -718,17 +728,20 @@ TEST_F(SdfEntityCreatorTest, CreateLights) this->ecm.Each( + components::Name, + components::Namespace>( [&](const Entity &_entity, const components::Model *_model, const components::Pose *_pose, const components::ParentEntity *_parent, - const components::Name *_name)->bool + const components::Name *_name, + const components::Namespace *_ns)->bool { EXPECT_NE(nullptr, _model); EXPECT_NE(nullptr, _pose); EXPECT_NE(nullptr, _parent); EXPECT_NE(nullptr, _name); + EXPECT_NE(nullptr, _ns); modelCount++; @@ -738,6 +751,7 @@ TEST_F(SdfEntityCreatorTest, CreateLights) EXPECT_EQ(math::Pose3d(0, 0, 0, 0, 0, 0), _pose->Data()); EXPECT_EQ("sphere", _name->Data()); + EXPECT_EQ("sphere", _ns->Data()); sphModelEntity = _entity; return true; @@ -986,6 +1000,7 @@ TEST_F(SdfEntityCreatorTest, CreateJointEntities) EXPECT_TRUE(this->ecm.HasComponentType(components::ParentEntity::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Pose::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Name::typeId)); + EXPECT_TRUE(this->ecm.HasComponentType(components::Namespace::typeId)); const sdf::Model *model = root.WorldByIndex(0)->ModelByIndex(1); diff --git a/test/worlds/lights.sdf b/test/worlds/lights.sdf index a3843c8c86..9a2782af4e 100644 --- a/test/worlds/lights.sdf +++ b/test/worlds/lights.sdf @@ -59,7 +59,7 @@ false - + 0 0.0 0.0 0 0 0 diff --git a/test/worlds/shapes.sdf b/test/worlds/shapes.sdf index 8fd01f57eb..4a784030ac 100644 --- a/test/worlds/shapes.sdf +++ b/test/worlds/shapes.sdf @@ -80,7 +80,7 @@ - + -1 -2 -3 0 0 1 0.2 0.2 0.2 0 0 0 @@ -123,7 +123,7 @@ - + 0 0 0 0 0 1 0.3 0.3 0.3 0 0 0 From 6126f27a79ad8f8adddaf86b8b5c842ab8893d9b Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 5 Jun 2026 16:43:36 +0800 Subject: [PATCH 08/21] Add ns support to diff_drive Signed-off-by: C88-YQ <1409947012@qq.com> --- src/systems/diff_drive/DiffDrive.cc | 65 +++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/src/systems/diff_drive/DiffDrive.cc b/src/systems/diff_drive/DiffDrive.cc index b65af3fce6..f65d02d347 100644 --- a/src/systems/diff_drive/DiffDrive.cc +++ b/src/systems/diff_drive/DiffDrive.cc @@ -332,13 +332,33 @@ void DiffDrive::Configure(const Entity &_entity, this->dataPtr->odom.SetWheelParams(this->dataPtr->wheelSeparation, this->dataPtr->wheelRadius, this->dataPtr->wheelRadius); + // Generate namespace + std::string ns; + if (hasNamespace(_ecm)) + { + ns = scopedNamespace(_ecm, this->dataPtr->model.Entity()); + } + // Subscribe to commands std::vector topics; if (_sdf->HasElement("topic")) { - topics.push_back(_sdf->Get("topic")); + std::string topicName = _sdf->Get("topic"); + if (!topicName.empty()) + { + if (topicName.front() != '/') + { + topicName = ns + "/" + topicName; + } + else + { + topicName = ns + topicName; + } + } + topics.push_back(topicName); } - topics.push_back("/model/" + this->dataPtr->model.Name(_ecm) + "/cmd_vel"); + topics.push_back( + ns + "/model/" + this->dataPtr->model.Name(_ecm) + "/cmd_vel"); auto topic = validTopic(topics); this->dataPtr->node.Subscribe(topic, &DiffDrivePrivate::OnCmdVel, @@ -347,7 +367,7 @@ void DiffDrive::Configure(const Entity &_entity, // Subscribe to enable/disable std::vector enableTopics; enableTopics.push_back( - "/model/" + this->dataPtr->model.Name(_ecm) + "/enable"); + ns + "/model/" + this->dataPtr->model.Name(_ecm) + "/enable"); auto enableTopic = validTopic(enableTopics); if (!enableTopic.empty()) @@ -360,19 +380,48 @@ void DiffDrive::Configure(const Entity &_entity, std::vector odomTopics; if (_sdf->HasElement("odom_topic")) { - odomTopics.push_back(_sdf->Get("odom_topic")); + std::string odomTopicName = _sdf->Get("odom_topic"); + if (!odomTopicName.empty()) + { + if (odomTopicName.front() != '/') + { + odomTopicName = ns + "/" + odomTopicName; + } + else + { + odomTopicName = ns + odomTopicName; + } + } + odomTopics.push_back(odomTopicName); } - odomTopics.push_back("/model/" + this->dataPtr->model.Name(_ecm) + + odomTopics.push_back(ns + "/model/" + this->dataPtr->model.Name(_ecm) + "/odometry"); auto odomTopic = validTopic(odomTopics); this->dataPtr->odomPub = this->dataPtr->node.Advertise( odomTopic); - std::string tfTopic{"/model/" + this->dataPtr->model.Name(_ecm) + - "/tf"}; + std::vector tfTopics; if (_sdf->HasElement("tf_topic")) - tfTopic = _sdf->Get("tf_topic"); + { + std::string tfTopicName = _sdf->Get("tf_topic"); + if (!tfTopicName.empty()) + { + if (tfTopicName.front() != '/') + { + tfTopicName = ns + "/" + tfTopicName; + } + else + { + tfTopicName = ns + tfTopicName; + } + } + tfTopics.push_back(tfTopicName); + } + tfTopics.push_back(ns + "/model/" + this->dataPtr->model.Name(_ecm) + + "/tf"); + auto tfTopic = validTopic(tfTopics); + this->dataPtr->tfPub = this->dataPtr->node.Advertise( tfTopic); From 703cb12562d017724bda1b5204cd9fe90e24a737 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 5 Jun 2026 16:44:07 +0800 Subject: [PATCH 09/21] Add test for ns support in diff_drive Signed-off-by: C88-YQ <1409947012@qq.com> --- test/integration/diff_drive_system.cc | 152 +++++++++++++++ test/worlds/diff_drive_ns.sdf | 258 ++++++++++++++++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 test/worlds/diff_drive_ns.sdf diff --git a/test/integration/diff_drive_system.cc b/test/integration/diff_drive_system.cc index 7094d13fb4..7c52adb70f 100644 --- a/test/integration/diff_drive_system.cc +++ b/test/integration/diff_drive_system.cc @@ -758,6 +758,158 @@ TEST_P(DiffDriveTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Pose_VCustomTfTopic)) EXPECT_EQ(5u, odomPosesCount); } +///////////////////////////////////////////////// +TEST_P(DiffDriveTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(NamespaceTopic)) +{ + // Start server + ServerConfig serverConfig; + serverConfig.SetSdfFile(std::string(PROJECT_SOURCE_PATH) + + "/test/worlds/diff_drive_ns.sdf"); + + Server server(serverConfig); + EXPECT_FALSE(server.Running()); + EXPECT_FALSE(*server.Running(0)); + + server.SetUpdatePeriod(0ns); + + // Create a system that records the vehicle poses + test::Relay testSystem; + + std::vector poses; + testSystem.OnPostUpdate([&poses](const UpdateInfo &, + const EntityComponentManager &_ecm) + { + auto id = _ecm.EntityByComponents( + components::Model(), + components::Name("vehicle_nested")); + EXPECT_NE(kNullEntity, id); + + auto poseComp = _ecm.Component(id); + ASSERT_NE(nullptr, poseComp); + + poses.push_back(poseComp->Data()); + }); + server.AddSystem(testSystem.systemPtr); + + // Run server and check that vehicle didn't move + server.Run(true, 1000, false); + + EXPECT_EQ(1000u, poses.size()); + + for (const auto &pose : poses) + { + EXPECT_EQ(poses[0], pose); + } + + // Publish command and check that vehicle moved + double odomPeriod{1.0}; + double odomLastMsgTime{1.0}; + std::vector odomPoses; + std::function odomCb = + [&](const msgs::Odometry &_msg) + { + ASSERT_TRUE(_msg.has_header()); + ASSERT_TRUE(_msg.header().has_stamp()); + + double msgTime = + static_cast(_msg.header().stamp().sec()) + + static_cast(_msg.header().stamp().nsec()) * 1e-9; + + EXPECT_DOUBLE_EQ(msgTime, odomLastMsgTime + odomPeriod); + odomLastMsgTime = msgTime; + + odomPoses.push_back(msgs::Convert(_msg.pose())); + }; + + std::vector tfPoses; + std::function tfCb = + [&](const msgs::Pose_V &_msg) + { + ASSERT_TRUE(_msg.pose(0).has_header()); + ASSERT_TRUE(_msg.pose(0).header().has_stamp()); + + ASSERT_GT(_msg.pose(0).header().data_size(), 1); + + EXPECT_STREQ(_msg.pose(0).header().data(0).key().c_str(), "frame_id"); + EXPECT_STREQ( + _msg.pose(0).header().data(0).value().Get(0).c_str(), + "vehicle_nested/odom"); + + EXPECT_STREQ( + _msg.pose(0).header().data(1).key().c_str(), "child_frame_id"); + EXPECT_STREQ( + _msg.pose(0).header().data(1).value().Get(0).c_str(), + "vehicle_nested/chassis"); + + tfPoses.push_back(msgs::Convert(_msg.pose(0))); + }; + + transport::Node node; + auto pub = node.Advertise("/vehicle/nested_ns/cmd_vel"); + node.Subscribe("/vehicle/nested_ns/odom", odomCb); + node.Subscribe("/vehicle/nested_ns/model/vehicle_nested/tf", tfCb); + + msgs::Twist msg; + msgs::Set(msg.mutable_linear(), math::Vector3d(0.5, 0, 0)); + msgs::Set(msg.mutable_angular(), math::Vector3d(0.0, 0, 0.0)); + + pub.Publish(msg); + + server.Run(true, 3000, false); + + // Poses for 4s + EXPECT_EQ(4000u, poses.size()); + + // Disable controller + auto pub_enable = node.Advertise( + "/vehicle/nested_ns/model/vehicle_nested/enable"); + + msgs::Boolean msg_enable; + msg_enable.set_data(false); + + pub_enable.Publish(msg_enable); + + // Run for 2s and expect no movement + server.Run(true, 2000, false); + + EXPECT_EQ(6000u, poses.size()); + + // Re-enable controller + msg_enable.set_data(true); + + pub_enable.Publish(msg_enable); + + pub.Publish(msg); + + // Run for 2s and expect movement again + server.Run(true, 2000, false); + + EXPECT_EQ(8000u, poses.size()); + + int sleep = 0; + int maxSleep = 70; + for (; (odomPoses.size() < 7 || tfPoses.size() < 7) && sleep < maxSleep; + ++sleep) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + EXPECT_NE(maxSleep, sleep); + + // Odom and TF for 7s + ASSERT_FALSE(odomPoses.empty()); + EXPECT_EQ(7u, odomPoses.size()); + ASSERT_FALSE(tfPoses.empty()); + EXPECT_EQ(7u, tfPoses.size()); + + EXPECT_LT(poses[0].Pos().X(), poses[3999].Pos().X()); + + // Should no be moving from 5s to 6s (stopped at 3s and time to slow down) + EXPECT_NEAR(poses[4999].Pos().X(), poses[5999].Pos().X(), tol); + + // Should be moving from 6s to 8s + EXPECT_LT(poses[5999].Pos().X(), poses[7999].Pos().X()); +} + // Run multiple times INSTANTIATE_TEST_SUITE_P(ServerRepeat, DiffDriveTest, ::testing::Range(1, 2)); diff --git a/test/worlds/diff_drive_ns.sdf b/test/worlds/diff_drive_ns.sdf new file mode 100644 index 0000000000..e20d16d079 --- /dev/null +++ b/test/worlds/diff_drive_ns.sdf @@ -0,0 +1,258 @@ + + + + + + 0.001 + 0 + + + + + + true + 0 0 10 0 0 0 + 1 1 1 1 + 0.5 0.5 0.5 1 + -0.5 0.1 -0.9 + + + + true + + + + + 0 0 1 + 100 100 + + + + + + + 0 0 1 + 100 100 + + + + 0.8 0.8 0.8 1 + 0.8 0.8 0.8 1 + 0.8 0.8 0.8 1 + + + + + + + + + + + 0.01 0.01 0.01 + + + + + + + 0 0 0.325 0 -0 0 + + + -0.151427 -0 0.175 0 -0 0 + + 1.14395 + + 0.126164 + 0 + 0 + 0.416519 + 0 + 0.481014 + + + + + + 2.01142 1 0.568726 + + + + 0.5 0.5 1.0 1 + 0.5 0.5 1.0 1 + 0.0 0.0 1.0 1 + + + + + + 2.01142 1 0.568726 + + + + + + + 0.554283 0.625029 -0.025 -1.5707 0 0 + + 2 + + 0.145833 + 0 + 0 + 0.145833 + 0 + 0.125 + + + + + + 0.3 + + + + 0.2 0.2 0.2 1 + 0.2 0.2 0.2 1 + 0.2 0.2 0.2 1 + + + + + + 0.3 + + + + + + + 0.554282 -0.625029 -0.025 -1.5707 0 0 + + 2 + + 0.145833 + 0 + 0 + 0.145833 + 0 + 0.125 + + + + + + 0.3 + + + + 0.2 0.2 0.2 1 + 0.2 0.2 0.2 1 + 0.2 0.2 0.2 1 + + + + + + 0.3 + + + + + + + -0.957138 -0 -0.125 0 -0 0 + + 1 + + 0.1 + 0 + 0 + 0.1 + 0 + 0.1 + + + + + + 0.2 + + + + 0.2 0.2 0.2 1 + 0.2 0.2 0.2 1 + 0.2 0.2 0.2 1 + + + + + + 0.2 + + + + + + + chassis + left_wheel + + 0 0 1 + + -1.79769e+308 + 1.79769e+308 + + + + + + chassis + right_wheel + + 0 0 1 + + -1.79769e+308 + 1.79769e+308 + + + + + + chassis + caster + + + + left_wheel_joint + right_wheel_joint + 1.25 + 0.3 + /cmd_vel + odom + + 1 + -1 + 2 + -2 + 0.5 + -0.5 + 1 + -1 + 1 + + + + + + + + + + From 67539dbcdcee4f224998aaf3fb45bf4cfa1d5d1f Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Mon, 8 Jun 2026 21:43:59 +0800 Subject: [PATCH 10/21] Only prepend namespace to relative topic names Signed-off-by: C88-YQ <1409947012@qq.com> --- src/systems/diff_drive/DiffDrive.cc | 32 ++++++++++----------------- test/integration/diff_drive_system.cc | 6 ++--- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/systems/diff_drive/DiffDrive.cc b/src/systems/diff_drive/DiffDrive.cc index f65d02d347..44c647e272 100644 --- a/src/systems/diff_drive/DiffDrive.cc +++ b/src/systems/diff_drive/DiffDrive.cc @@ -334,9 +334,11 @@ void DiffDrive::Configure(const Entity &_entity, // Generate namespace std::string ns; + std::string defaultPrefix = "/model/" + this->dataPtr->model.Name(_ecm); if (hasNamespace(_ecm)) { ns = scopedNamespace(_ecm, this->dataPtr->model.Entity()); + defaultPrefix = ns; } // Subscribe to commands @@ -346,19 +348,16 @@ void DiffDrive::Configure(const Entity &_entity, std::string topicName = _sdf->Get("topic"); if (!topicName.empty()) { + // Only prepend namespace to relative topic names. + // Absolute topic names (starting with '/') are left unchanged. if (topicName.front() != '/') { topicName = ns + "/" + topicName; } - else - { - topicName = ns + topicName; - } } topics.push_back(topicName); } - topics.push_back( - ns + "/model/" + this->dataPtr->model.Name(_ecm) + "/cmd_vel"); + topics.push_back(defaultPrefix + "/cmd_vel"); auto topic = validTopic(topics); this->dataPtr->node.Subscribe(topic, &DiffDrivePrivate::OnCmdVel, @@ -366,8 +365,7 @@ void DiffDrive::Configure(const Entity &_entity, // Subscribe to enable/disable std::vector enableTopics; - enableTopics.push_back( - ns + "/model/" + this->dataPtr->model.Name(_ecm) + "/enable"); + enableTopics.push_back(defaultPrefix + "/enable"); auto enableTopic = validTopic(enableTopics); if (!enableTopic.empty()) @@ -383,19 +381,16 @@ void DiffDrive::Configure(const Entity &_entity, std::string odomTopicName = _sdf->Get("odom_topic"); if (!odomTopicName.empty()) { + // Only prepend namespace to relative topic names. + // Absolute topic names (starting with '/') are left unchanged. if (odomTopicName.front() != '/') { odomTopicName = ns + "/" + odomTopicName; } - else - { - odomTopicName = ns + odomTopicName; - } } odomTopics.push_back(odomTopicName); } - odomTopics.push_back(ns + "/model/" + this->dataPtr->model.Name(_ecm) + - "/odometry"); + odomTopics.push_back(defaultPrefix + "/odometry"); auto odomTopic = validTopic(odomTopics); this->dataPtr->odomPub = this->dataPtr->node.Advertise( @@ -407,19 +402,16 @@ void DiffDrive::Configure(const Entity &_entity, std::string tfTopicName = _sdf->Get("tf_topic"); if (!tfTopicName.empty()) { + // Only prepend namespace to relative topic names. + // Absolute topic names (starting with '/') are left unchanged. if (tfTopicName.front() != '/') { tfTopicName = ns + "/" + tfTopicName; } - else - { - tfTopicName = ns + tfTopicName; - } } tfTopics.push_back(tfTopicName); } - tfTopics.push_back(ns + "/model/" + this->dataPtr->model.Name(_ecm) + - "/tf"); + tfTopics.push_back(defaultPrefix + "/tf"); auto tfTopic = validTopic(tfTopics); this->dataPtr->tfPub = this->dataPtr->node.Advertise( diff --git a/test/integration/diff_drive_system.cc b/test/integration/diff_drive_system.cc index 7c52adb70f..077063c1ee 100644 --- a/test/integration/diff_drive_system.cc +++ b/test/integration/diff_drive_system.cc @@ -845,9 +845,9 @@ TEST_P(DiffDriveTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(NamespaceTopic)) }; transport::Node node; - auto pub = node.Advertise("/vehicle/nested_ns/cmd_vel"); + auto pub = node.Advertise("/cmd_vel"); node.Subscribe("/vehicle/nested_ns/odom", odomCb); - node.Subscribe("/vehicle/nested_ns/model/vehicle_nested/tf", tfCb); + node.Subscribe("/vehicle/nested_ns/tf", tfCb); msgs::Twist msg; msgs::Set(msg.mutable_linear(), math::Vector3d(0.5, 0, 0)); @@ -862,7 +862,7 @@ TEST_P(DiffDriveTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(NamespaceTopic)) // Disable controller auto pub_enable = node.Advertise( - "/vehicle/nested_ns/model/vehicle_nested/enable"); + "/vehicle/nested_ns/enable"); msgs::Boolean msg_enable; msg_enable.set_data(false); From 8a8fcf8d9f7e3665d0855cfbdc558c1182836903 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Thu, 11 Jun 2026 20:47:42 +0800 Subject: [PATCH 11/21] Add namespace support to entity creation services Signed-off-by: C88-YQ <1409947012@qq.com> --- include/gz/sim/EntityComponentManager.hh | 11 +- src/EntityComponentManager.cc | 37 ++++- src/SimulationRunner.cc | 3 +- src/systems/user_commands/UserCommands.cc | 159 ++++++++++++++++++++-- 4 files changed, 187 insertions(+), 23 deletions(-) diff --git a/include/gz/sim/EntityComponentManager.hh b/include/gz/sim/EntityComponentManager.hh index 73a1355d72..787c5bde8c 100644 --- a/include/gz/sim/EntityComponentManager.hh +++ b/include/gz/sim/EntityComponentManager.hh @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -105,6 +106,7 @@ namespace gz /// \param[in] _name The name that should be given to the cloned entity. /// Set this to an empty string if the cloned entity name should be /// auto-generated to something unique. + /// \param[in] _ns The namespace that should be given to the cloned entity. /// \param[in] _allowRename True if _name can be modified to be a unique /// name if it isn't already a unique name. False if _name cannot be /// modified to be a unique name. If _allowRename is set to False, and @@ -117,7 +119,9 @@ namespace gz /// cloned. /// \sa Clone public: Entity Clone(Entity _entity, Entity _parent, - const std::string &_name, bool _allowRename); + const std::string &_name, + const std::optional &_ns, + bool _allowRename); /// \brief Get the number of entities on the server. /// \return Entity count. @@ -371,13 +375,16 @@ namespace gz /// \param[in] _entity The entity to clone. /// \param[in] _parent The parent of the cloned entity. /// \param[in] _name The name that should be given to the cloned entity. + /// \param[in] _ns The namespace that should be given to the cloned entity. /// \param[in] _allowRename True if _name can be modified to be a unique /// name if it isn't already a unique name. False if _name cannot be /// modified to be a unique name. /// \return The cloned entity. kNullEntity is returned if cloning failed. /// \sa Clone private: Entity CloneImpl(Entity _entity, Entity _parent, - const std::string &_name, bool _allowRename); + const std::string &_name, + const std::optional &_ns, + bool _allowRename); /// \brief A version of Each() that doesn't use a cache. The cached /// version, Each(), is preferred. diff --git a/src/EntityComponentManager.cc b/src/EntityComponentManager.cc index 3a301a5241..00a1e39c27 100644 --- a/src/EntityComponentManager.cc +++ b/src/EntityComponentManager.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -39,7 +40,9 @@ #include "gz/sim/components/Factory.hh" #include "gz/sim/components/Joint.hh" #include "gz/sim/components/Link.hh" +#include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/ParentEntity.hh" #include "gz/sim/components/ParentLinkName.hh" #include "gz/sim/components/Recreate.hh" @@ -394,7 +397,8 @@ Entity EntityComponentManagerPrivate::CreateEntityImplementation(Entity _entity) ///////////////////////////////////////////////// Entity EntityComponentManager::Clone(Entity _entity, Entity _parent, - const std::string &_name, bool _allowRename) + const std::string &_name, const std::optional &_ns, + bool _allowRename) { // Clear maps so they're populated for the entity being cloned this->dataPtr->oldToClonedCanonicalLink.clear(); @@ -402,7 +406,7 @@ Entity EntityComponentManager::Clone(Entity _entity, Entity _parent, this->dataPtr->originalToClonedLink.clear(); this->dataPtr->clonedToOriginalJointLinks.clear(); - auto clonedEntity = this->CloneImpl(_entity, _parent, _name, _allowRename); + auto clonedEntity = this->CloneImpl(_entity, _parent, _name, _ns, _allowRename); if (kNullEntity != clonedEntity) { @@ -455,7 +459,8 @@ Entity EntityComponentManager::Clone(Entity _entity, Entity _parent, ///////////////////////////////////////////////// Entity EntityComponentManager::CloneImpl(Entity _entity, Entity _parent, - const std::string &_name, bool _allowRename) + const std::string &_name, const std::optional &_ns, + bool _allowRename) { auto uniqueNameGenerated = false; @@ -529,13 +534,32 @@ Entity EntityComponentManager::CloneImpl(Entity _entity, Entity _parent, } this->CreateComponent(clonedEntity, components::Name(clonedName)); + if (nullptr != this->Component(_entity)) + { + std::string ns; + if (_ns.has_value()) + { + ns = _ns.value(); + } + else + { + // If the namespace is not provided, use the original entity's namespace + // if it exists. Otherwise, use an empty string as the namespace for the + // cloned entity. + auto originalNsComp = this->Component(_entity); + ns = originalNsComp ? originalNsComp->Data() : ""; + } + this->CreateComponent(clonedEntity, components::Namespace(ns)); + } + // copy all components from _entity to clonedEntity for (const auto &type : this->ComponentTypes(_entity)) { - // skip the Name and ParentEntity components since those were already + // skip the Name, Namespace and ParentEntity components since those were already // handled above if ((type == components::Name::typeId) || - (type == components::ParentEntity::typeId)) + (type == components::ParentEntity::typeId) || + (type == components::Namespace::typeId)) continue; auto originalComp = this->ComponentImplementation(_entity, type); @@ -641,8 +665,9 @@ Entity EntityComponentManager::CloneImpl(Entity _entity, Entity _parent, name = nameComp->Data(); } } + auto clonedChild = this->CloneImpl(childEntity, clonedEntity, name, - _allowRename); + std::nullopt, _allowRename); if (kNullEntity == clonedChild) { gzerr << "Cloning child entity [" << childEntity << "] failed.\n"; diff --git a/src/SimulationRunner.cc b/src/SimulationRunner.cc index 8de37f5894..e85dcff1ef 100644 --- a/src/SimulationRunner.cc +++ b/src/SimulationRunner.cc @@ -44,6 +44,7 @@ #include "gz/sim/Constants.hh" #include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/Sensor.hh" #include "gz/sim/components/Visual.hh" #include "gz/sim/components/World.hh" @@ -1465,7 +1466,7 @@ void SimulationRunner::ProcessRecreateEntitiesCreate() { // set allowRenaming to false so the entities keep their original name Entity clonedEntity = this->entityCompMgr.Clone(ent, - parentComp->Data(), nameComp->Data(), false); + parentComp->Data(), nameComp->Data(), std::nullopt, false); // remove the Recreate component so they do not get recreated again in the // next iteration diff --git a/src/systems/user_commands/UserCommands.cc b/src/systems/user_commands/UserCommands.cc index 7d1dfcb41a..01de162e5f 100644 --- a/src/systems/user_commands/UserCommands.cc +++ b/src/systems/user_commands/UserCommands.cc @@ -20,6 +20,7 @@ #include "UserCommands.hh" #include #include +#include #ifdef _MSC_VER #pragma warning(push) @@ -36,6 +37,8 @@ #include #include #include +#include +#include #include #include #include @@ -165,18 +168,43 @@ class CreateCommand : public UserCommandBase public: CreateCommand(msgs::EntityFactory *_msg, std::shared_ptr &_iface); + /// \brief Constructor + /// \param[in] _msg Factory message. + /// \param[in] _iface Pointer to user commands interface with namespace. + public: CreateCommand(msgs::EntityFactoryWithNs *_msg, + std::shared_ptr &_iface); + /// \brief Constructor overload that takes a vector of Factory messages /// \param[in] _msg Vector of Factory message. /// \param[in] _iface Pointer to user commands interface. public: CreateCommand(msgs::EntityFactory_V *_msg, std::shared_ptr &_iface); + /// \brief Constructor overload that takes a vector of Factory messages + /// \param[in] _msg Vector of Factory message. + /// \param[in] _iface Pointer to user commands interface with namespace. + public: CreateCommand(msgs::EntityFactoryWithNs_V *_msg, + std::shared_ptr &_iface); + // Documentation inherited public: bool Execute() final; /// \brief Actual implementation that creates entities from message. /// \param[in] Factory message that specifies the entity to create. - private: bool CreateFromMsg(const msgs::EntityFactory &_createMsg); + /// \param[in] _ns Optional namespace to apply to the created entity. + private: bool CreateFromMsg(const msgs::EntityFactory &_createMsg, + const std::optional &_ns = std::nullopt); + + /// \brief Actual implementation that creates entities from message. + /// \param[in] Factory message that specifies the entity to create. + private: bool CreateFromMsg(const msgs::EntityFactoryWithNs &_createMsg); + + /// \brief Helper function to copy data from EntityFactoryWithNs message to + /// EntityFactory message. + /// \param[in] _sourceMsg EntityFactoryWithNs message to copy data from. + /// \param[in] _targetMsg EntityFactory message to copy data to. + private: void CopyData(const msgs::EntityFactoryWithNs &_sourceMsg, + msgs::EntityFactory &_targetMsg); }; /// \brief Command to remove an entity from simulation. @@ -607,8 +635,12 @@ void UserCommands::Configure(const Entity &_entity, // Create service this->dataPtr->AdvertiseService( "/world/" + validWorldName + "/create"); + this->dataPtr->AdvertiseService( + "/world/" + validWorldName + "/create_with_ns"); this->dataPtr->AdvertiseService( "/world/" + validWorldName + "/create_multiple", "Create"); + this->dataPtr->AdvertiseService( + "/world/" + validWorldName + "/create_with_ns_multiple", "Create"); // Remove service this->dataPtr->AdvertiseService( @@ -820,33 +852,58 @@ CreateCommand::CreateCommand(msgs::EntityFactory *_msg, { } +////////////////////////////////////////////////// +CreateCommand::CreateCommand(msgs::EntityFactoryWithNs *_msg, + std::shared_ptr &_iface) + : UserCommandBase(_msg, _iface) +{ +} + ////////////////////////////////////////////////// CreateCommand::CreateCommand(msgs::EntityFactory_V *_msg, std::shared_ptr &_iface) : UserCommandBase(_msg, _iface) { } + +////////////////////////////////////////////////// +CreateCommand::CreateCommand(msgs::EntityFactoryWithNs_V *_msg, + std::shared_ptr &_iface) + : UserCommandBase(_msg, _iface) +{ +} + ////////////////////////////////////////////////// bool CreateCommand::Execute() { - auto createMsg = dynamic_cast(this->msg); - if (nullptr != createMsg) + if (auto createMsg = dynamic_cast(this->msg)) { return this->CreateFromMsg(*createMsg); } - else + else if (auto createMsgV = dynamic_cast(this->msg)) { // It could also be an EntityFactory_V - auto createMsgV = dynamic_cast(this->msg); - if (nullptr != createMsgV) + bool result = true; + for (const auto &msgItem : createMsgV->data()) { - bool result = true; - for (const auto &msgItem : createMsgV->data()) - { - result = result && this->CreateFromMsg(msgItem); - } - return result; + result = result && this->CreateFromMsg(msgItem); } + return result; + } + else if (auto createMsgWithNs = dynamic_cast(this->msg)) + { + // It could also be an EntityFactoryWithNs + return this->CreateFromMsg(*createMsgWithNs); + } + else if (auto createMsgWithNsV = dynamic_cast(this->msg)) + { + // It could also be an EntityFactoryWithNs_V + bool result = true; + for (const auto &msgItem : createMsgWithNsV->data()) + { + result = result && this->CreateFromMsg(msgItem); + } + return result; } gzerr << "Internal error, null create message" << std::endl; @@ -854,7 +911,8 @@ bool CreateCommand::Execute() } ////////////////////////////////////////////////// -bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg) +bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg, + const std::optional &_ns) { // Load SDF sdf::Root root; @@ -900,7 +958,7 @@ bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg) { auto parentEntity = parentComp->Data(); clonedEntity = this->iface->ecm->Clone(entityToClone, - parentEntity, _createMsg.name(), _createMsg.allow_renaming()); + parentEntity, _createMsg.name(), _ns, _createMsg.allow_renaming()); validClone = kNullEntity != clonedEntity; } } @@ -1025,6 +1083,10 @@ bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg) { auto model = *root.Model(); model.SetName(desiredName); + if (_ns.has_value()) + { + model.SetNamespace(_ns.value()); + } entity = this->iface->creator->CreateEntitiesWithoutLoadingPlugins(&model); } else if (isLight && isRoot) @@ -1116,6 +1178,75 @@ bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg) return true; } +////////////////////////////////////////////////// +bool CreateCommand::CreateFromMsg(const msgs::EntityFactoryWithNs &_createMsg) +{ + msgs::EntityFactory baseMsg; + this->CopyData(_createMsg, baseMsg); + + std::optional ns = std::nullopt; + if(_createMsg.has_ns()) + { + ns = _createMsg.ns().data(); + } + + return this->CreateFromMsg(baseMsg, ns); +} + +////////////////////////////////////////////////// +void CreateCommand::CopyData(const msgs::EntityFactoryWithNs &_sourceMsg, + msgs::EntityFactory &_targetMsg) +{ + _targetMsg.Clear(); + + if (_sourceMsg.has_header()) + { + _targetMsg.mutable_header()->CopyFrom(_sourceMsg.header()); + } + + switch (_sourceMsg.from_case()) + { + case msgs::EntityFactoryWithNs::kSdf: + _targetMsg.set_sdf(_sourceMsg.sdf()); + break; + + case msgs::EntityFactoryWithNs::kSdfFilename: + _targetMsg.set_sdf_filename(_sourceMsg.sdf_filename()); + break; + + case msgs::EntityFactoryWithNs::kModel: + _targetMsg.mutable_model()->CopyFrom(_sourceMsg.model()); + break; + + case msgs::EntityFactoryWithNs::kLight: + _targetMsg.mutable_light()->CopyFrom(_sourceMsg.light()); + break; + + case msgs::EntityFactoryWithNs::kCloneName: + _targetMsg.set_clone_name(_sourceMsg.clone_name()); + break; + + case msgs::EntityFactoryWithNs::FROM_NOT_SET: + default: + break; + } + + if (_sourceMsg.has_pose()) + { + _targetMsg.mutable_pose()->CopyFrom(_sourceMsg.pose()); + } + + _targetMsg.set_name(_sourceMsg.name()); + _targetMsg.set_allow_renaming(_sourceMsg.allow_renaming()); + _targetMsg.set_relative_to(_sourceMsg.relative_to()); + + if (_sourceMsg.has_spherical_coordinates()) + { + _targetMsg.mutable_spherical_coordinates()->CopyFrom( + _sourceMsg.spherical_coordinates()); + } +} + ////////////////////////////////////////////////// RemoveCommand::RemoveCommand(msgs::Entity *_msg, std::shared_ptr &_iface) From 87553b54913159a8853c25633383958dfd457ec9 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Thu, 11 Jun 2026 20:48:47 +0800 Subject: [PATCH 12/21] Add tests for namespace support in entity creation services Signed-off-by: C88-YQ <1409947012@qq.com> --- src/EntityComponentManager_TEST.cc | 40 ++++++++---- test/integration/user_commands.cc | 98 +++++++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 22 deletions(-) diff --git a/src/EntityComponentManager_TEST.cc b/src/EntityComponentManager_TEST.cc index a9735be8b7..7688076228 100644 --- a/src/EntityComponentManager_TEST.cc +++ b/src/EntityComponentManager_TEST.cc @@ -30,6 +30,7 @@ #include "gz/sim/components/Joint.hh" #include "gz/sim/components/Link.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/ParentEntity.hh" #include "gz/sim/components/ParentLinkName.hh" #include "gz/sim/components/Pose.hh" @@ -2814,11 +2815,13 @@ TEST_P(EntityComponentManagerFixture, Entity topLevelEntity = manager.CreateEntity(); manager.CreateComponent(topLevelEntity, components::Name("topLevelEntity")); + manager.CreateComponent(topLevelEntity, components::Namespace("topLevelNs")); manager.CreateComponent(topLevelEntity, IntComponent(123)); manager.CreateComponent(topLevelEntity, StringComponent("string0")); Entity childEntity1 = manager.CreateEntity(); manager.CreateComponent(childEntity1, components::Name("childEntity1")); + manager.CreateComponent(childEntity1, components::Namespace("childNs1")); manager.CreateComponent(childEntity1, components::ParentEntity(topLevelEntity)); manager.CreateComponent(childEntity1, IntComponent(456)); @@ -2827,11 +2830,14 @@ TEST_P(EntityComponentManagerFixture, Entity grandChildEntity1 = manager.CreateEntity(); manager.CreateComponent(grandChildEntity1, components::Name("grandChildEntity1")); + manager.CreateComponent(grandChildEntity1, + components::Namespace("grandChildNs1")); manager.CreateComponent(grandChildEntity1, components::ParentEntity(childEntity1)); Entity childEntity2 = manager.CreateEntity(); manager.CreateComponent(childEntity2, components::Name("childEntity2")); + manager.CreateComponent(childEntity2, components::Namespace("childNs2")); manager.CreateComponent(childEntity2, components::ParentEntity(topLevelEntity)); manager.CreateComponent(childEntity2, IntComponent(789)); @@ -2859,6 +2865,8 @@ TEST_P(EntityComponentManagerFixture, components::ParentEntity::typeId)); CompareEntityComponents(manager, topLevelEntity, _clonedEntity, false); + CompareEntityComponents(manager, topLevelEntity, + _clonedEntity, true); CompareEntityComponents(manager, topLevelEntity, _clonedEntity, true); CompareEntityComponents(manager, topLevelEntity, @@ -2869,7 +2877,7 @@ TEST_P(EntityComponentManagerFixture, // clone the topLevelEntity auto clonedTopLevelEntity = - manager.Clone(topLevelEntity, kNullEntity, "", allowRename); + manager.Clone(topLevelEntity, kNullEntity, "", std::nullopt, allowRename); EXPECT_EQ(8u, manager.EntityCount()); clonedEntities.insert(clonedTopLevelEntity); validateTopLevelClone(clonedTopLevelEntity); @@ -2887,6 +2895,8 @@ TEST_P(EntityComponentManagerFixture, EXPECT_EQ(clonedTopLevelEntity, parentComp->Data()); CompareEntityComponents(manager, _clonedChild, _originalChild, false); + CompareEntityComponents(manager, _clonedChild, + _originalChild, true); CompareEntityComponents(manager, _clonedChild, _originalChild, true); CompareEntityComponents(manager, _clonedChild, @@ -2894,13 +2904,15 @@ TEST_P(EntityComponentManagerFixture, }; auto validateGrandChildClone = - [&](const Entity _clonedEntity, bool _sameParent) + [&](const Entity _clonedEntity, bool _sameNs, bool _sameParent) { EXPECT_NE(kNullEntity, _clonedEntity); EXPECT_EQ(manager.ComponentTypes(_clonedEntity), manager.ComponentTypes(grandChildEntity1)); CompareEntityComponents(manager, _clonedEntity, grandChildEntity1, false); + CompareEntityComponents(manager, _clonedEntity, + grandChildEntity1, _sameNs); CompareEntityComponents(manager, _clonedEntity, grandChildEntity1, _sameParent); EXPECT_TRUE(manager.EntitiesByComponents( @@ -2930,7 +2942,7 @@ TEST_P(EntityComponentManagerFixture, ASSERT_EQ(1u, clonedGrandChildren.size()); clonedEntities.insert(clonedGrandChildren[0]); - validateGrandChildClone(clonedGrandChildren[0], false); + validateGrandChildClone(clonedGrandChildren[0], true, false); auto parentComp = manager.Component(clonedGrandChildren[0]); ASSERT_NE(nullptr, parentComp); @@ -2950,31 +2962,33 @@ TEST_P(EntityComponentManagerFixture, EXPECT_TRUE(comparedToOriginalChild); } - // clone a child entity + // clone a child entity with a namespace provided auto grandChildParentComp = manager.Component(grandChildEntity1); ASSERT_NE(nullptr, grandChildParentComp); auto clonedGrandChildEntity = manager.Clone(grandChildEntity1, - grandChildParentComp->Data(), "", allowRename); + grandChildParentComp->Data(), "", "clonedGrandChildNs", allowRename); EXPECT_EQ(9u, manager.EntityCount()); clonedEntities.insert(clonedGrandChildEntity); - validateGrandChildClone(clonedGrandChildEntity, true); + validateGrandChildClone(clonedGrandChildEntity, false, true); - // Try cloning an entity with a name that already exists, but allow renaming. - // This should succeed and generate a cloned entity with a unique name. + // Try cloning an entity with a name that already exists, but allow renaming + // and without a namespace provided. + // This should succeed and generate a cloned entity with a unique name + // and a same namespace. const auto existingName = "grandChildEntity1"; EXPECT_NE(kNullEntity, manager.EntityByComponents(components::Name(existingName))); auto renamedClonedEntity = manager.Clone(grandChildEntity1, - grandChildParentComp->Data(), existingName, allowRename); + grandChildParentComp->Data(), existingName, std::nullopt, allowRename); EXPECT_EQ(10u, manager.EntityCount()); clonedEntities.insert(clonedGrandChildEntity); - validateGrandChildClone(renamedClonedEntity, true); + validateGrandChildClone(renamedClonedEntity, true, true); // Try cloning an entity with a name that already exists, without allowing // renaming. This should fail since entities should have unique names. auto failedClonedEntity = manager.Clone(grandChildEntity1, - grandChildParentComp->Data(), existingName, noAllowRename); + grandChildParentComp->Data(), existingName, std::nullopt, noAllowRename); EXPECT_EQ(10u, manager.EntityCount()); EXPECT_EQ(kNullEntity, failedClonedEntity); @@ -3010,7 +3024,7 @@ TEST_P(EntityComponentManagerFixture, // clone a joint that has a parent and child link. auto clonedParentModelEntity = manager.Clone(parentModelEntity, kNullEntity, - "", true); + "", std::nullopt, true); ASSERT_NE(kNullEntity, clonedParentModelEntity); // We just cloned a model with two links and a joint, a total of 4 new // entities. @@ -3062,7 +3076,7 @@ TEST_P(EntityComponentManagerFixture, // try to clone an entity that does not exist EXPECT_EQ(kNullEntity, manager.Clone(kNullEntity, topLevelEntity, "", - allowRename)); + std::nullopt, allowRename)); EXPECT_EQ(18u, manager.EntityCount()); } diff --git a/test/integration/user_commands.cc b/test/integration/user_commands.cc index 1f12bc95cd..3585d946fa 100644 --- a/test/integration/user_commands.cc +++ b/test/integration/user_commands.cc @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include "gz/sim/components/Material.hh" #include "gz/sim/components/Model.hh" #include "gz/sim/components/Name.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/components/Physics.hh" #include "gz/sim/components/Pose.hh" #include "gz/sim/components/VisualCmd.hh" @@ -130,7 +132,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) // SDF strings auto modelStr = std::string("") + "" + - "" + + "" + "" + "" + "1.0" + @@ -172,7 +174,8 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) // Check entity has not been created yet EXPECT_EQ(kNullEntity, ecm->EntityByComponents(components::Model(), - components::Name("spawned_model"))); + components::Name("spawned_model"), + components::Namespace("spawned_model/ns"))); // Run an iteration and check it was created server.Run(true, 1, false); @@ -187,7 +190,8 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) entityCount = ecm->EntityCount(); auto model = ecm->EntityByComponents(components::Model(), - components::Name("spawned_model")); + components::Name("spawned_model"), + components::Namespace("spawned_model/ns")); EXPECT_NE(kNullEntity, model); auto poseComp = ecm->Component(model); @@ -232,7 +236,8 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) entityCount = ecm->EntityCount(); model = ecm->EntityByComponents(components::Model(), - components::Name("spawned_model_0")); + components::Name("spawned_model_0"), + components::Namespace("spawned_model_0/ns")); EXPECT_NE(kNullEntity, model); // Spawn with a different name @@ -255,7 +260,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) entityCount = ecm->EntityCount(); model = ecm->EntityByComponents(components::Model(), - components::Name("banana")); + components::Name("banana"), components::Namespace("banana/ns")); EXPECT_NE(kNullEntity, model); // Spawn a light @@ -319,9 +324,9 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) // Check neither exists yet EXPECT_EQ(kNullEntity, ecm->EntityByComponents(components::Model(), - components::Name("acerola"))); + components::Name("acerola"), components::Namespace("acerola/ns"))); EXPECT_EQ(kNullEntity, ecm->EntityByComponents(components::Model(), - components::Name("coconut"))); + components::Name("coconut"), components::Namespace("coconut/ns"))); EXPECT_EQ(entityCount, ecm->EntityCount()); // Run an iteration and check both models were created @@ -343,9 +348,9 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) entityCount = ecm->EntityCount(); EXPECT_NE(kNullEntity, ecm->EntityByComponents(components::Model(), - components::Name("acerola"))); + components::Name("acerola"), components::Namespace("acerola/ns"))); EXPECT_NE(kNullEntity, ecm->EntityByComponents(components::Model(), - components::Name("coconut"))); + components::Name("coconut"), components::Namespace("coconut/ns"))); // Try to spawn 2 entities at once req.Clear(); @@ -407,9 +412,84 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) EXPECT_TRUE(requestData.response.data()); } EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + entityCount = ecm->EntityCount(); EXPECT_NE(kNullEntity, ecm->EntityByComponents(components::Model(), components::Name("test_model"))); + + // Spawn a model with namespace + msgs::EntityFactoryWithNs reqWithNs; + reqWithNs.set_sdf(modelStr); + reqWithNs.set_name("spawned_model_with_ns"); + reqWithNs.mutable_ns()->set_data("test_ns"); + + std::string serviceWithNs{"/world/empty/create_with_ns/blocking"}; + auto requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); + + // Run an iteration and check it was created with namespace + server.Run(true, 1, false); + { + auto requestData = requestWithNsFuture.get(); + EXPECT_TRUE(requestData.retval); + EXPECT_TRUE(requestData.result); + EXPECT_TRUE(requestData.response.data()); + } + + EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + entityCount = ecm->EntityCount(); + + model = ecm->EntityByComponents(components::Model(), + components::Name("spawned_model_with_ns"), + components::Namespace("test_ns")); + EXPECT_NE(kNullEntity, model); + + // Spawn a model with empty namespace + reqWithNs.Clear(); + reqWithNs.set_sdf(modelStr); + reqWithNs.set_name("spawned_model_with_empty_ns"); + reqWithNs.mutable_ns()->set_data(""); + + requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); + + // Run an iteration and check it was created with namespace + server.Run(true, 1, false); + { + auto requestData = requestWithNsFuture.get(); + EXPECT_TRUE(requestData.retval); + EXPECT_TRUE(requestData.result); + EXPECT_TRUE(requestData.response.data()); + } + + EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + entityCount = ecm->EntityCount(); + + model = ecm->EntityByComponents(components::Model(), + components::Name("spawned_model_with_empty_ns"), + components::Namespace("")); + EXPECT_NE(kNullEntity, model); + + // Spawn a model without namespace provided + reqWithNs.Clear(); + reqWithNs.set_sdf(modelStr); + reqWithNs.set_name("spawned_model_without_ns"); + + requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); + + // Run an iteration and check it was created with namespace + server.Run(true, 1, false); + { + auto requestData = requestWithNsFuture.get(); + EXPECT_TRUE(requestData.retval); + EXPECT_TRUE(requestData.result); + EXPECT_TRUE(requestData.response.data()); + } + + EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + + model = ecm->EntityByComponents(components::Model(), + components::Name("spawned_model_without_ns"), + components::Namespace("spawned_model_without_ns/ns")); + EXPECT_NE(kNullEntity, model); } ///////////////////////////////////////////////// From 7f93c879a9c60ceaeba160810ea560b49cd4a7bc Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 12 Jun 2026 10:39:18 +0800 Subject: [PATCH 13/21] Adapt namespace support for gz-msgs Signed-off-by: C88-YQ <1409947012@qq.com> --- src/systems/user_commands/UserCommands.cc | 69 ++--------------------- test/integration/user_commands.cc | 4 +- 2 files changed, 6 insertions(+), 67 deletions(-) diff --git a/src/systems/user_commands/UserCommands.cc b/src/systems/user_commands/UserCommands.cc index 01de162e5f..d7baa2a771 100644 --- a/src/systems/user_commands/UserCommands.cc +++ b/src/systems/user_commands/UserCommands.cc @@ -198,13 +198,6 @@ class CreateCommand : public UserCommandBase /// \brief Actual implementation that creates entities from message. /// \param[in] Factory message that specifies the entity to create. private: bool CreateFromMsg(const msgs::EntityFactoryWithNs &_createMsg); - - /// \brief Helper function to copy data from EntityFactoryWithNs message to - /// EntityFactory message. - /// \param[in] _sourceMsg EntityFactoryWithNs message to copy data from. - /// \param[in] _targetMsg EntityFactory message to copy data to. - private: void CopyData(const msgs::EntityFactoryWithNs &_sourceMsg, - msgs::EntityFactory &_targetMsg); }; /// \brief Command to remove an entity from simulation. @@ -1085,7 +1078,7 @@ bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg, model.SetName(desiredName); if (_ns.has_value()) { - model.SetNamespace(_ns.value()); + model.SetRawNamespace(_ns.value()); } entity = this->iface->creator->CreateEntitiesWithoutLoadingPlugins(&model); } @@ -1182,71 +1175,17 @@ bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg, bool CreateCommand::CreateFromMsg(const msgs::EntityFactoryWithNs &_createMsg) { msgs::EntityFactory baseMsg; - this->CopyData(_createMsg, baseMsg); + baseMsg.ParseFromString(_createMsg.SerializeAsString()); std::optional ns = std::nullopt; - if(_createMsg.has_ns()) + if(_createMsg.has_namespace_()) { - ns = _createMsg.ns().data(); + ns = _createMsg.namespace_().data(); } return this->CreateFromMsg(baseMsg, ns); } -////////////////////////////////////////////////// -void CreateCommand::CopyData(const msgs::EntityFactoryWithNs &_sourceMsg, - msgs::EntityFactory &_targetMsg) -{ - _targetMsg.Clear(); - - if (_sourceMsg.has_header()) - { - _targetMsg.mutable_header()->CopyFrom(_sourceMsg.header()); - } - - switch (_sourceMsg.from_case()) - { - case msgs::EntityFactoryWithNs::kSdf: - _targetMsg.set_sdf(_sourceMsg.sdf()); - break; - - case msgs::EntityFactoryWithNs::kSdfFilename: - _targetMsg.set_sdf_filename(_sourceMsg.sdf_filename()); - break; - - case msgs::EntityFactoryWithNs::kModel: - _targetMsg.mutable_model()->CopyFrom(_sourceMsg.model()); - break; - - case msgs::EntityFactoryWithNs::kLight: - _targetMsg.mutable_light()->CopyFrom(_sourceMsg.light()); - break; - - case msgs::EntityFactoryWithNs::kCloneName: - _targetMsg.set_clone_name(_sourceMsg.clone_name()); - break; - - case msgs::EntityFactoryWithNs::FROM_NOT_SET: - default: - break; - } - - if (_sourceMsg.has_pose()) - { - _targetMsg.mutable_pose()->CopyFrom(_sourceMsg.pose()); - } - - _targetMsg.set_name(_sourceMsg.name()); - _targetMsg.set_allow_renaming(_sourceMsg.allow_renaming()); - _targetMsg.set_relative_to(_sourceMsg.relative_to()); - - if (_sourceMsg.has_spherical_coordinates()) - { - _targetMsg.mutable_spherical_coordinates()->CopyFrom( - _sourceMsg.spherical_coordinates()); - } -} - ////////////////////////////////////////////////// RemoveCommand::RemoveCommand(msgs::Entity *_msg, std::shared_ptr &_iface) diff --git a/test/integration/user_commands.cc b/test/integration/user_commands.cc index 3585d946fa..eb0e332e61 100644 --- a/test/integration/user_commands.cc +++ b/test/integration/user_commands.cc @@ -421,7 +421,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) msgs::EntityFactoryWithNs reqWithNs; reqWithNs.set_sdf(modelStr); reqWithNs.set_name("spawned_model_with_ns"); - reqWithNs.mutable_ns()->set_data("test_ns"); + reqWithNs.mutable_namespace_()->set_data("test_ns"); std::string serviceWithNs{"/world/empty/create_with_ns/blocking"}; auto requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); @@ -447,7 +447,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) reqWithNs.Clear(); reqWithNs.set_sdf(modelStr); reqWithNs.set_name("spawned_model_with_empty_ns"); - reqWithNs.mutable_ns()->set_data(""); + reqWithNs.mutable_namespace_()->set_data(""); requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); From 99214ab795a44709871a00a2fa770538fc110451 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Wed, 17 Jun 2026 22:38:42 +0800 Subject: [PATCH 14/21] Avoid creating Namespace component without explicit namespace Signed-off-by: C88-YQ <1409947012@qq.com> --- src/SdfEntityCreator.cc | 7 +++++-- src/SdfEntityCreator_TEST.cc | 21 +++++++++------------ test/worlds/shapes.sdf | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/SdfEntityCreator.cc b/src/SdfEntityCreator.cc index d06f164968..b588b1ff21 100644 --- a/src/SdfEntityCreator.cc +++ b/src/SdfEntityCreator.cc @@ -527,8 +527,11 @@ Entity SdfEntityCreator::CreateEntities(const sdf::Model *_model, components::Pose(ResolveSdfPose(_model->SemanticPose()))); this->dataPtr->ecm->CreateComponent(modelEntity, components::Name(_model->Name())); - this->dataPtr->ecm->CreateComponent(modelEntity, - components::Namespace(_model->Namespace())); + if (_model->Namespace().has_value()) + { + this->dataPtr->ecm->CreateComponent(modelEntity, + components::Namespace(_model->Namespace().value())); + } bool isStatic = _model->Static() || _staticParent; this->dataPtr->ecm->CreateComponent(modelEntity, components::Static(isStatic)); diff --git a/src/SdfEntityCreator_TEST.cc b/src/SdfEntityCreator_TEST.cc index 2dcda9df59..f002a8d875 100644 --- a/src/SdfEntityCreator_TEST.cc +++ b/src/SdfEntityCreator_TEST.cc @@ -101,7 +101,6 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) EXPECT_TRUE(this->ecm.HasComponentType(components::Visual::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Light::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Name::typeId)); - EXPECT_TRUE(this->ecm.HasComponentType(components::Namespace::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::ParentEntity::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Geometry::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Material::typeId)); @@ -151,20 +150,19 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) this->ecm.Each( + components::Name>( [&](const Entity &_entity, const components::Model *_model, const components::Pose *_pose, const components::ParentEntity *_parent, - const components::Name *_name, - const components::Namespace *_ns)->bool + const components::Name *_name)->bool { EXPECT_NE(nullptr, _model); EXPECT_NE(nullptr, _pose); EXPECT_NE(nullptr, _parent); EXPECT_NE(nullptr, _name); - EXPECT_NE(nullptr, _ns); + + const auto ns = this->ecm.Component(_entity); modelCount++; @@ -175,35 +173,35 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) { EXPECT_EQ(math::Pose3d(1, 2, 3, 0, 0, 1), _pose->Data()); - EXPECT_EQ("", _ns->Data()); + EXPECT_EQ(nullptr, ns); boxModelEntity = _entity; } else if (_name->Data() == "cylinder") { EXPECT_EQ(math::Pose3d(-1, -2, -3, 0, 0, 1), _pose->Data()); - EXPECT_EQ("cylinder", _ns->Data()); + EXPECT_EQ("cylinder", ns->Data()); cylModelEntity = _entity; } else if (_name->Data() == "sphere") { EXPECT_EQ(math::Pose3d(0, 0, 0, 0, 0, 1), _pose->Data()); - EXPECT_EQ("sphere_ns", _ns->Data()); + EXPECT_EQ("sphere_ns", ns->Data()); sphModelEntity = _entity; } else if (_name->Data() == "capsule") { EXPECT_EQ(math::Pose3d(-4, -5, -6, 0, 0, 1), _pose->Data()); - EXPECT_EQ("", _ns->Data()); + EXPECT_EQ("ns", ns->Data()); capModelEntity = _entity; } else if (_name->Data() == "ellipsoid") { EXPECT_EQ(math::Pose3d(4, 5, 6, 0, 0, 1), _pose->Data()); - EXPECT_EQ("", _ns->Data()); + EXPECT_EQ(nullptr, ns); ellipModelEntity = _entity; } return true; @@ -985,7 +983,6 @@ TEST_F(SdfEntityCreatorTest, CreateJointEntities) EXPECT_TRUE(this->ecm.HasComponentType(components::ParentEntity::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Pose::typeId)); EXPECT_TRUE(this->ecm.HasComponentType(components::Name::typeId)); - EXPECT_TRUE(this->ecm.HasComponentType(components::Namespace::typeId)); const sdf::Model *model = root.WorldByIndex(0)->ModelByIndex(1); diff --git a/test/worlds/shapes.sdf b/test/worlds/shapes.sdf index 4a784030ac..23dd8ab645 100644 --- a/test/worlds/shapes.sdf +++ b/test/worlds/shapes.sdf @@ -123,7 +123,7 @@ - + 0 0 0 0 0 1 0.3 0.3 0.3 0 0 0 @@ -166,7 +166,7 @@ - + -4 -5 -6 0 0 1 0.5 0.5 0.5 0 0 0 From 68b174b16f2f387edcbb0834209539b81d38f88d Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Wed, 17 Jun 2026 22:41:37 +0800 Subject: [PATCH 15/21] Update UserCommands test for explicit namespace component creation Signed-off-by: C88-YQ <1409947012@qq.com> --- test/integration/user_commands.cc | 98 ++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/test/integration/user_commands.cc b/test/integration/user_commands.cc index eb0e332e61..0476f75a19 100644 --- a/test/integration/user_commands.cc +++ b/test/integration/user_commands.cc @@ -144,6 +144,20 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) "" + ""; + auto modelStrWithoutNs = std::string("") + + "" + + "" + + "" + + "" + + "1.0" + + "" + + "" + + "1.0" + + "" + + "" + + "" + + ""; + auto lightStr = std::string("") + "" + "" + @@ -263,6 +277,30 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) components::Name("banana"), components::Namespace("banana/ns")); EXPECT_NE(kNullEntity, model); + // Spawn a model from SDF that doesn't define a namespace + req.Clear(); + req.set_sdf(modelStrWithoutNs); + req.set_name("orange"); + + requestDataFuture = asyncRequest(node, service, req); + + // Run an iteration and check it was created with given name + server.Run(true, 1, false); + { + auto requestData = requestDataFuture.get(); + EXPECT_TRUE(requestData.retval); + EXPECT_TRUE(requestData.result); + EXPECT_TRUE(requestData.response.data()); + } + + EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + entityCount = ecm->EntityCount(); + + model = ecm->EntityByComponents(components::Model(), + components::Name("orange")); + EXPECT_NE(kNullEntity, model); + EXPECT_EQ(nullptr, ecm->Component(model)); + // Spawn a light req.Clear(); req.set_sdf(lightStr); @@ -417,7 +455,8 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) EXPECT_NE(kNullEntity, ecm->EntityByComponents(components::Model(), components::Name("test_model"))); - // Spawn a model with namespace + // Spawn a model from SDF that defines a namespace through EntityFactoryWithNs + // with a namespace override. msgs::EntityFactoryWithNs reqWithNs; reqWithNs.set_sdf(modelStr); reqWithNs.set_name("spawned_model_with_ns"); @@ -443,7 +482,8 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) components::Namespace("test_ns")); EXPECT_NE(kNullEntity, model); - // Spawn a model with empty namespace + // Spawn a model from SDF that defines a namespace through EntityFactoryWithNs + // with an empty namespace override. reqWithNs.Clear(); reqWithNs.set_sdf(modelStr); reqWithNs.set_name("spawned_model_with_empty_ns"); @@ -468,7 +508,8 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) components::Namespace("")); EXPECT_NE(kNullEntity, model); - // Spawn a model without namespace provided + // Spawn a model from SDF that defines a namespace through EntityFactoryWithNs + // without a namespace override. reqWithNs.Clear(); reqWithNs.set_sdf(modelStr); reqWithNs.set_name("spawned_model_without_ns"); @@ -485,11 +526,62 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) } EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + entityCount = ecm->EntityCount(); model = ecm->EntityByComponents(components::Model(), components::Name("spawned_model_without_ns"), components::Namespace("spawned_model_without_ns/ns")); EXPECT_NE(kNullEntity, model); + + // Spawn a model from SDF that doesn't define a namespace through + // EntityFactoryWithNs without a namespace override. + reqWithNs.Clear(); + reqWithNs.set_sdf(modelStrWithoutNs); + reqWithNs.set_name("pineapple"); + + requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); + + // Run an iteration and check it was created with namespace + server.Run(true, 1, false); + { + auto requestData = requestWithNsFuture.get(); + EXPECT_TRUE(requestData.retval); + EXPECT_TRUE(requestData.result); + EXPECT_TRUE(requestData.response.data()); + } + + EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + entityCount = ecm->EntityCount(); + + model = ecm->EntityByComponents(components::Model(), + components::Name("pineapple")); + EXPECT_NE(kNullEntity, model); + EXPECT_EQ(nullptr, ecm->Component(model)); + + // Spawn a model from SDF that doesn't define a namespace through + // EntityFactoryWithNs with a namespace override. + reqWithNs.Clear(); + reqWithNs.set_sdf(modelStrWithoutNs ); + reqWithNs.set_name("grape"); + reqWithNs.mutable_namespace_()->set_data("test_ns"); + + requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); + + // Run an iteration and check it was created with namespace + server.Run(true, 1, false); + { + auto requestData = requestWithNsFuture.get(); + EXPECT_TRUE(requestData.retval); + EXPECT_TRUE(requestData.result); + EXPECT_TRUE(requestData.response.data()); + } + + EXPECT_EQ(entityCount + 4, ecm->EntityCount()); + + model = ecm->EntityByComponents(components::Model(), + components::Name("grape"), + components::Namespace("test_ns")); + EXPECT_NE(kNullEntity, model); } ///////////////////////////////////////////////// From 85d298f64a6b90de7c9e368c557a5bc9b423b1b7 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Thu, 18 Jun 2026 20:59:18 +0800 Subject: [PATCH 16/21] Update hasNamespace Signed-off-by: C88-YQ <1409947012@qq.com> --- src/Util.cc | 3 +-- src/Util_TEST.cc | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Util.cc b/src/Util.cc index 38741638ea..fa829099ac 100644 --- a/src/Util.cc +++ b/src/Util.cc @@ -192,8 +192,7 @@ bool hasNamespace(const EntityComponentManager &_ecm) for ( const auto &entity : entities) { - const auto ns = _ecm.Component(entity.first); - if (ns && !ns->Data().empty()) + if (_ecm.Component(entity.first)) return true; } return false; diff --git a/src/Util_TEST.cc b/src/Util_TEST.cc index fade174cb3..ab58f0e10c 100644 --- a/src/Util_TEST.cc +++ b/src/Util_TEST.cc @@ -252,7 +252,7 @@ TEST_F(UtilTest, HasNamespace) auto entityWithEmptyNamespace = ecm.CreateEntity(); ecm.CreateComponent(entityWithEmptyNamespace, components::Namespace("")); - EXPECT_FALSE(hasNamespace(ecm)); + EXPECT_TRUE(hasNamespace(ecm)); auto entityWithNamespace = ecm.CreateEntity(); ecm.CreateComponent(entityWithNamespace, components::Namespace("robot")); From 7f0667e28d18ff09452e27a20d4358a4b52427b3 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Tue, 23 Jun 2026 16:34:32 +0800 Subject: [PATCH 17/21] Always create Namespace components for model entities Signed-off-by: C88-YQ <1409947012@qq.com> --- include/gz/sim/EntityComponentManager.hh | 7 ++----- src/EntityComponentManager.cc | 21 +++++++++------------ src/EntityComponentManager_TEST.cc | 10 +++++----- src/SdfEntityCreator.cc | 5 +++++ src/SdfEntityCreator_TEST.cc | 19 ++++++++++--------- src/SimulationRunner.cc | 2 +- src/Util.cc | 3 ++- src/Util_TEST.cc | 2 +- src/systems/user_commands/UserCommands.cc | 23 ++++++++--------------- test/integration/user_commands.cc | 16 +++++++--------- 10 files changed, 50 insertions(+), 58 deletions(-) diff --git a/include/gz/sim/EntityComponentManager.hh b/include/gz/sim/EntityComponentManager.hh index 787c5bde8c..20fa933577 100644 --- a/include/gz/sim/EntityComponentManager.hh +++ b/include/gz/sim/EntityComponentManager.hh @@ -31,7 +31,6 @@ #include #include #include -#include #include #include @@ -119,8 +118,7 @@ namespace gz /// cloned. /// \sa Clone public: Entity Clone(Entity _entity, Entity _parent, - const std::string &_name, - const std::optional &_ns, + const std::string &_name, const std::string &_ns, bool _allowRename); /// \brief Get the number of entities on the server. @@ -382,8 +380,7 @@ namespace gz /// \return The cloned entity. kNullEntity is returned if cloning failed. /// \sa Clone private: Entity CloneImpl(Entity _entity, Entity _parent, - const std::string &_name, - const std::optional &_ns, + const std::string &_name, const std::string &_ns, bool _allowRename); /// \brief A version of Each() that doesn't use a cache. The cached diff --git a/src/EntityComponentManager.cc b/src/EntityComponentManager.cc index 00a1e39c27..32aba67fa3 100644 --- a/src/EntityComponentManager.cc +++ b/src/EntityComponentManager.cc @@ -28,7 +28,6 @@ #include #include #include -#include #include #include @@ -397,7 +396,7 @@ Entity EntityComponentManagerPrivate::CreateEntityImplementation(Entity _entity) ///////////////////////////////////////////////// Entity EntityComponentManager::Clone(Entity _entity, Entity _parent, - const std::string &_name, const std::optional &_ns, + const std::string &_name, const std::string &_ns, bool _allowRename) { // Clear maps so they're populated for the entity being cloned @@ -459,7 +458,7 @@ Entity EntityComponentManager::Clone(Entity _entity, Entity _parent, ///////////////////////////////////////////////// Entity EntityComponentManager::CloneImpl(Entity _entity, Entity _parent, - const std::string &_name, const std::optional &_ns, + const std::string &_name, const std::string &_ns, bool _allowRename) { auto uniqueNameGenerated = false; @@ -534,20 +533,18 @@ Entity EntityComponentManager::CloneImpl(Entity _entity, Entity _parent, } this->CreateComponent(clonedEntity, components::Name(clonedName)); - if (nullptr != this->Component(_entity)) + auto originalNsComp = this->Component(_entity); + if (nullptr != originalNsComp) { std::string ns; - if (_ns.has_value()) + if (!_ns.empty()) { - ns = _ns.value(); + ns = _ns; } else { - // If the namespace is not provided, use the original entity's namespace - // if it exists. Otherwise, use an empty string as the namespace for the - // cloned entity. - auto originalNsComp = this->Component(_entity); - ns = originalNsComp ? originalNsComp->Data() : ""; + // If the namespace is not provided, use the original entity's namespace. + ns = originalNsComp->Data(); } this->CreateComponent(clonedEntity, components::Namespace(ns)); } @@ -667,7 +664,7 @@ Entity EntityComponentManager::CloneImpl(Entity _entity, Entity _parent, } auto clonedChild = this->CloneImpl(childEntity, clonedEntity, name, - std::nullopt, _allowRename); + "", _allowRename); if (kNullEntity == clonedChild) { gzerr << "Cloning child entity [" << childEntity << "] failed.\n"; diff --git a/src/EntityComponentManager_TEST.cc b/src/EntityComponentManager_TEST.cc index 7688076228..8eb62842ba 100644 --- a/src/EntityComponentManager_TEST.cc +++ b/src/EntityComponentManager_TEST.cc @@ -2877,7 +2877,7 @@ TEST_P(EntityComponentManagerFixture, // clone the topLevelEntity auto clonedTopLevelEntity = - manager.Clone(topLevelEntity, kNullEntity, "", std::nullopt, allowRename); + manager.Clone(topLevelEntity, kNullEntity, "", "", allowRename); EXPECT_EQ(8u, manager.EntityCount()); clonedEntities.insert(clonedTopLevelEntity); validateTopLevelClone(clonedTopLevelEntity); @@ -2980,7 +2980,7 @@ TEST_P(EntityComponentManagerFixture, EXPECT_NE(kNullEntity, manager.EntityByComponents(components::Name(existingName))); auto renamedClonedEntity = manager.Clone(grandChildEntity1, - grandChildParentComp->Data(), existingName, std::nullopt, allowRename); + grandChildParentComp->Data(), existingName, "", allowRename); EXPECT_EQ(10u, manager.EntityCount()); clonedEntities.insert(clonedGrandChildEntity); validateGrandChildClone(renamedClonedEntity, true, true); @@ -2988,7 +2988,7 @@ TEST_P(EntityComponentManagerFixture, // Try cloning an entity with a name that already exists, without allowing // renaming. This should fail since entities should have unique names. auto failedClonedEntity = manager.Clone(grandChildEntity1, - grandChildParentComp->Data(), existingName, std::nullopt, noAllowRename); + grandChildParentComp->Data(), existingName, "", noAllowRename); EXPECT_EQ(10u, manager.EntityCount()); EXPECT_EQ(kNullEntity, failedClonedEntity); @@ -3024,7 +3024,7 @@ TEST_P(EntityComponentManagerFixture, // clone a joint that has a parent and child link. auto clonedParentModelEntity = manager.Clone(parentModelEntity, kNullEntity, - "", std::nullopt, true); + "", "", true); ASSERT_NE(kNullEntity, clonedParentModelEntity); // We just cloned a model with two links and a joint, a total of 4 new // entities. @@ -3076,7 +3076,7 @@ TEST_P(EntityComponentManagerFixture, // try to clone an entity that does not exist EXPECT_EQ(kNullEntity, manager.Clone(kNullEntity, topLevelEntity, "", - std::nullopt, allowRename)); + "", allowRename)); EXPECT_EQ(18u, manager.EntityCount()); } diff --git a/src/SdfEntityCreator.cc b/src/SdfEntityCreator.cc index b588b1ff21..3fb66d989e 100644 --- a/src/SdfEntityCreator.cc +++ b/src/SdfEntityCreator.cc @@ -532,6 +532,11 @@ Entity SdfEntityCreator::CreateEntities(const sdf::Model *_model, this->dataPtr->ecm->CreateComponent(modelEntity, components::Namespace(_model->Namespace().value())); } + else + { + this->dataPtr->ecm->CreateComponent(modelEntity, + components::Namespace("")); + } bool isStatic = _model->Static() || _staticParent; this->dataPtr->ecm->CreateComponent(modelEntity, components::Static(isStatic)); diff --git a/src/SdfEntityCreator_TEST.cc b/src/SdfEntityCreator_TEST.cc index f002a8d875..969d5f5937 100644 --- a/src/SdfEntityCreator_TEST.cc +++ b/src/SdfEntityCreator_TEST.cc @@ -150,19 +150,20 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) this->ecm.Each( + components::Name, + components::Namespace>( [&](const Entity &_entity, const components::Model *_model, const components::Pose *_pose, const components::ParentEntity *_parent, - const components::Name *_name)->bool + const components::Name *_name, + const components::Namespace *_ns)->bool { EXPECT_NE(nullptr, _model); EXPECT_NE(nullptr, _pose); EXPECT_NE(nullptr, _parent); EXPECT_NE(nullptr, _name); - - const auto ns = this->ecm.Component(_entity); + EXPECT_NE(nullptr, _ns); modelCount++; @@ -173,35 +174,35 @@ TEST_F(SdfEntityCreatorTest, CreateEntities) { EXPECT_EQ(math::Pose3d(1, 2, 3, 0, 0, 1), _pose->Data()); - EXPECT_EQ(nullptr, ns); + EXPECT_EQ("", _ns->Data()); boxModelEntity = _entity; } else if (_name->Data() == "cylinder") { EXPECT_EQ(math::Pose3d(-1, -2, -3, 0, 0, 1), _pose->Data()); - EXPECT_EQ("cylinder", ns->Data()); + EXPECT_EQ("cylinder", _ns->Data()); cylModelEntity = _entity; } else if (_name->Data() == "sphere") { EXPECT_EQ(math::Pose3d(0, 0, 0, 0, 0, 1), _pose->Data()); - EXPECT_EQ("sphere_ns", ns->Data()); + EXPECT_EQ("sphere_ns", _ns->Data()); sphModelEntity = _entity; } else if (_name->Data() == "capsule") { EXPECT_EQ(math::Pose3d(-4, -5, -6, 0, 0, 1), _pose->Data()); - EXPECT_EQ("ns", ns->Data()); + EXPECT_EQ("ns", _ns->Data()); capModelEntity = _entity; } else if (_name->Data() == "ellipsoid") { EXPECT_EQ(math::Pose3d(4, 5, 6, 0, 0, 1), _pose->Data()); - EXPECT_EQ(nullptr, ns); + EXPECT_EQ("", _ns->Data()); ellipModelEntity = _entity; } return true; diff --git a/src/SimulationRunner.cc b/src/SimulationRunner.cc index e85dcff1ef..bca5615c53 100644 --- a/src/SimulationRunner.cc +++ b/src/SimulationRunner.cc @@ -1466,7 +1466,7 @@ void SimulationRunner::ProcessRecreateEntitiesCreate() { // set allowRenaming to false so the entities keep their original name Entity clonedEntity = this->entityCompMgr.Clone(ent, - parentComp->Data(), nameComp->Data(), std::nullopt, false); + parentComp->Data(), nameComp->Data(), "", false); // remove the Recreate component so they do not get recreated again in the // next iteration diff --git a/src/Util.cc b/src/Util.cc index fa829099ac..38741638ea 100644 --- a/src/Util.cc +++ b/src/Util.cc @@ -192,7 +192,8 @@ bool hasNamespace(const EntityComponentManager &_ecm) for ( const auto &entity : entities) { - if (_ecm.Component(entity.first)) + const auto ns = _ecm.Component(entity.first); + if (ns && !ns->Data().empty()) return true; } return false; diff --git a/src/Util_TEST.cc b/src/Util_TEST.cc index ab58f0e10c..fade174cb3 100644 --- a/src/Util_TEST.cc +++ b/src/Util_TEST.cc @@ -252,7 +252,7 @@ TEST_F(UtilTest, HasNamespace) auto entityWithEmptyNamespace = ecm.CreateEntity(); ecm.CreateComponent(entityWithEmptyNamespace, components::Namespace("")); - EXPECT_TRUE(hasNamespace(ecm)); + EXPECT_FALSE(hasNamespace(ecm)); auto entityWithNamespace = ecm.CreateEntity(); ecm.CreateComponent(entityWithNamespace, components::Namespace("robot")); diff --git a/src/systems/user_commands/UserCommands.cc b/src/systems/user_commands/UserCommands.cc index d7baa2a771..fa30e2caaa 100644 --- a/src/systems/user_commands/UserCommands.cc +++ b/src/systems/user_commands/UserCommands.cc @@ -20,7 +20,6 @@ #include "UserCommands.hh" #include #include -#include #ifdef _MSC_VER #pragma warning(push) @@ -191,9 +190,9 @@ class CreateCommand : public UserCommandBase /// \brief Actual implementation that creates entities from message. /// \param[in] Factory message that specifies the entity to create. - /// \param[in] _ns Optional namespace to apply to the created entity. + /// \param[in] _ns Namespace to apply to the created entity. private: bool CreateFromMsg(const msgs::EntityFactory &_createMsg, - const std::optional &_ns = std::nullopt); + const std::string &_ns); /// \brief Actual implementation that creates entities from message. /// \param[in] Factory message that specifies the entity to create. @@ -871,7 +870,7 @@ bool CreateCommand::Execute() { if (auto createMsg = dynamic_cast(this->msg)) { - return this->CreateFromMsg(*createMsg); + return this->CreateFromMsg(*createMsg, ""); } else if (auto createMsgV = dynamic_cast(this->msg)) { @@ -879,7 +878,7 @@ bool CreateCommand::Execute() bool result = true; for (const auto &msgItem : createMsgV->data()) { - result = result && this->CreateFromMsg(msgItem); + result = result && this->CreateFromMsg(msgItem, ""); } return result; } @@ -905,7 +904,7 @@ bool CreateCommand::Execute() ////////////////////////////////////////////////// bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg, - const std::optional &_ns) + const std::string &_ns) { // Load SDF sdf::Root root; @@ -1076,9 +1075,9 @@ bool CreateCommand::CreateFromMsg(const msgs::EntityFactory &_createMsg, { auto model = *root.Model(); model.SetName(desiredName); - if (_ns.has_value()) + if (!_ns.empty()) { - model.SetRawNamespace(_ns.value()); + model.SetRawNamespace(_ns); } entity = this->iface->creator->CreateEntitiesWithoutLoadingPlugins(&model); } @@ -1177,13 +1176,7 @@ bool CreateCommand::CreateFromMsg(const msgs::EntityFactoryWithNs &_createMsg) msgs::EntityFactory baseMsg; baseMsg.ParseFromString(_createMsg.SerializeAsString()); - std::optional ns = std::nullopt; - if(_createMsg.has_namespace_()) - { - ns = _createMsg.namespace_().data(); - } - - return this->CreateFromMsg(baseMsg, ns); + return this->CreateFromMsg(baseMsg, _createMsg.namespace_()); } ////////////////////////////////////////////////// diff --git a/test/integration/user_commands.cc b/test/integration/user_commands.cc index 0476f75a19..7c3f91a16b 100644 --- a/test/integration/user_commands.cc +++ b/test/integration/user_commands.cc @@ -277,7 +277,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) components::Name("banana"), components::Namespace("banana/ns")); EXPECT_NE(kNullEntity, model); - // Spawn a model from SDF that doesn't define a namespace + // Spawn a model from SDF that doesn't set the namespace req.Clear(); req.set_sdf(modelStrWithoutNs); req.set_name("orange"); @@ -297,9 +297,8 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) entityCount = ecm->EntityCount(); model = ecm->EntityByComponents(components::Model(), - components::Name("orange")); + components::Name("orange"), components::Namespace("")); EXPECT_NE(kNullEntity, model); - EXPECT_EQ(nullptr, ecm->Component(model)); // Spawn a light req.Clear(); @@ -460,7 +459,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) msgs::EntityFactoryWithNs reqWithNs; reqWithNs.set_sdf(modelStr); reqWithNs.set_name("spawned_model_with_ns"); - reqWithNs.mutable_namespace_()->set_data("test_ns"); + reqWithNs.set_namespace_("test_ns"); std::string serviceWithNs{"/world/empty/create_with_ns/blocking"}; auto requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); @@ -487,7 +486,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) reqWithNs.Clear(); reqWithNs.set_sdf(modelStr); reqWithNs.set_name("spawned_model_with_empty_ns"); - reqWithNs.mutable_namespace_()->set_data(""); + reqWithNs.set_namespace_(""); requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); @@ -505,7 +504,7 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) model = ecm->EntityByComponents(components::Model(), components::Name("spawned_model_with_empty_ns"), - components::Namespace("")); + components::Namespace("spawned_model_with_empty_ns/ns")); EXPECT_NE(kNullEntity, model); // Spawn a model from SDF that defines a namespace through EntityFactoryWithNs @@ -554,16 +553,15 @@ TEST_F(UserCommandsTest, GZ_UTILS_TEST_DISABLED_ON_WIN32(Create)) entityCount = ecm->EntityCount(); model = ecm->EntityByComponents(components::Model(), - components::Name("pineapple")); + components::Name("pineapple"), components::Namespace("")); EXPECT_NE(kNullEntity, model); - EXPECT_EQ(nullptr, ecm->Component(model)); // Spawn a model from SDF that doesn't define a namespace through // EntityFactoryWithNs with a namespace override. reqWithNs.Clear(); reqWithNs.set_sdf(modelStrWithoutNs ); reqWithNs.set_name("grape"); - reqWithNs.mutable_namespace_()->set_data("test_ns"); + reqWithNs.set_namespace_("test_ns"); requestWithNsFuture = asyncRequest(node, serviceWithNs, reqWithNs); From 987c698b25ed84a948f77c970692ffc495ad2a3f Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 26 Jun 2026 12:29:15 +0800 Subject: [PATCH 18/21] Stop scoped namespace lookup at absolute namespace Signed-off-by: C88-YQ <1409947012@qq.com> --- src/Util.cc | 5 ++++ src/Util_TEST.cc | 59 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/Util.cc b/src/Util.cc index 38741638ea..78de884c63 100644 --- a/src/Util.cc +++ b/src/Util.cc @@ -212,6 +212,7 @@ std::string scopedNamespace(const EntityComponentManager &_ecm, if (ns && !ns->Data().empty()) { std::string nsStr = ns->Data(); + const bool isAbsolute = nsStr.front() == '/'; const auto begin = nsStr.find_first_not_of('/'); if (begin != std::string::npos) { @@ -220,6 +221,10 @@ std::string scopedNamespace(const EntityComponentManager &_ecm, namespaces.push_back(nsStr); } + if (isAbsolute) + { + break; + } } const auto parentEntity = _ecm.Component(entity); diff --git a/src/Util_TEST.cc b/src/Util_TEST.cc index fade174cb3..258e13d3ca 100644 --- a/src/Util_TEST.cc +++ b/src/Util_TEST.cc @@ -264,34 +264,53 @@ TEST_F(UtilTest, ScopedNamespace) { EntityComponentManager ecm; + // world + // - modelA + // - modelAA + // - modelB + // - modelBA + // - modelC + // - modelCA + auto worldEntity = ecm.CreateEntity(); ecm.CreateComponent(worldEntity, components::Namespace("world_ns/")); - auto modelEntity = ecm.CreateEntity(); - ecm.CreateComponent(modelEntity, components::Namespace("model_ns")); - ecm.CreateComponent(modelEntity, components::ParentEntity(worldEntity)); + auto modelAEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelAEntity, components::Namespace("model_a_ns")); + ecm.CreateComponent(modelAEntity, components::ParentEntity(worldEntity)); + + auto modelAAEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelAAEntity, components::Namespace("/model_aa_ns/")); + ecm.CreateComponent(modelAAEntity, components::ParentEntity(modelAEntity)); + + auto modelAAAEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelAAAEntity, components::Namespace("model_aaa_ns")); + ecm.CreateComponent(modelAAAEntity, components::ParentEntity(modelAAEntity)); - auto nestedModelEntity = ecm.CreateEntity(); - ecm.CreateComponent(nestedModelEntity, components::ParentEntity(modelEntity)); + auto modelBEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelBEntity, components::Namespace("")); + ecm.CreateComponent(modelBEntity, components::ParentEntity(worldEntity)); - auto linkEntity = ecm.CreateEntity(); - ecm.CreateComponent(linkEntity, components::Namespace("//link_ns//")); - ecm.CreateComponent(linkEntity, components::ParentEntity(nestedModelEntity)); + auto modelBAEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelBAEntity, components::Namespace("model_ba_ns")); + ecm.CreateComponent(modelBAEntity, components::ParentEntity(modelBEntity)); + + auto modelCEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelCEntity, components::Namespace("//model_c_ns//")); + ecm.CreateComponent(modelCEntity, components::ParentEntity(worldEntity)); - auto entityWithSlashesNamespace = ecm.CreateEntity(); - ecm.CreateComponent(entityWithSlashesNamespace, - components::Namespace("///")); - ecm.CreateComponent(entityWithSlashesNamespace, - components::ParentEntity(linkEntity)); + auto modelCAEntity = ecm.CreateEntity(); + ecm.CreateComponent(modelCAEntity, components::Namespace("///")); + ecm.CreateComponent(modelCAEntity, components::ParentEntity(modelCEntity)); EXPECT_EQ("/world_ns", scopedNamespace(ecm, worldEntity)); - EXPECT_EQ("/world_ns/model_ns", scopedNamespace(ecm, modelEntity)); - EXPECT_EQ("/world_ns/model_ns", scopedNamespace(ecm, nestedModelEntity)); - EXPECT_EQ("/world_ns/model_ns/link_ns", scopedNamespace(ecm, linkEntity)); - EXPECT_EQ("::world_ns::model_ns::link_ns", - scopedNamespace(ecm, linkEntity, "::")); - EXPECT_EQ("/world_ns/model_ns/link_ns", - scopedNamespace(ecm, entityWithSlashesNamespace)); + EXPECT_EQ("/world_ns/model_a_ns", scopedNamespace(ecm, modelAEntity)); + EXPECT_EQ("/model_aa_ns", scopedNamespace(ecm, modelAAEntity)); + EXPECT_EQ("/model_aa_ns/model_aaa_ns", scopedNamespace(ecm, modelAAAEntity)); + EXPECT_EQ("/world_ns", scopedNamespace(ecm, modelBEntity)); + EXPECT_EQ("/world_ns/model_ba_ns", scopedNamespace(ecm, modelBAEntity)); + EXPECT_EQ("/model_c_ns", scopedNamespace(ecm, modelCEntity)); + EXPECT_EQ("", scopedNamespace(ecm, modelCAEntity)); EXPECT_TRUE(scopedNamespace(ecm, kNullEntity).empty()); } From fdcfadc36e9a2e8d5c6af17f5774b1701fe2c5ac Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 26 Jun 2026 20:30:23 +0800 Subject: [PATCH 19/21] Add ns support to systems/imu Signed-off-by: C88-YQ <1409947012@qq.com> --- src/systems/imu/Imu.cc | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/systems/imu/Imu.cc b/src/systems/imu/Imu.cc index 90be4bf19f..5f2bd1ca69 100644 --- a/src/systems/imu/Imu.cc +++ b/src/systems/imu/Imu.cc @@ -195,12 +195,34 @@ void ImuPrivate::AddSensor( removeParentScope(scopedName(_entity, _ecm, "::", false), "::"); sdf::Sensor data = _imu->Data(); data.SetName(sensorScopedName); + // generate namespace + std::string ns; + std::string defaultPrefix = scopedName(_entity, _ecm); + if (hasNamespace(_ecm)) + { + ns = scopedNamespace(_ecm, _entity); + defaultPrefix = ns; + } + // check topic - if (data.Topic().empty()) + std::vector topics; + if (!data.Topic().empty()) { - std::string topic = scopedName(_entity, _ecm) + "/imu"; - data.SetTopic(topic); + std::string topicName = data.Topic(); + if (!topicName.empty()) + { + // Only prepend namespace to relative topic names. + // Absolute topic names (starting with '/') are left unchanged. + if (topicName.front() != '/') + { + topicName = ns+ "/" + topicName; + } + } + topics.push_back(topicName); } + topics.push_back(defaultPrefix + "/imu"); + data.SetTopic(validTopic(topics)); + std::unique_ptr sensor = this->sensorFactory.CreateSensor< sensors::ImuSensor>(data); From 728c33c490cda920224f04e75259ae70fa1df3f8 Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 26 Jun 2026 20:43:13 +0800 Subject: [PATCH 20/21] Add ns support to systems/AckermannSteering Signed-off-by: C88-YQ <1409947012@qq.com> --- .../ackermann_steering/AckermannSteering.cc | 66 ++++++++++++++----- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/src/systems/ackermann_steering/AckermannSteering.cc b/src/systems/ackermann_steering/AckermannSteering.cc index f1398c6c95..42e7d169b3 100644 --- a/src/systems/ackermann_steering/AckermannSteering.cc +++ b/src/systems/ackermann_steering/AckermannSteering.cc @@ -390,33 +390,51 @@ void AckermannSteering::Configure(const Entity &_entity, odomPer); } } + + // Generate namespace + std::string ns; + std::string defaultPrefix = "/model/" + this->dataPtr->model.Name(_ecm); + if (hasNamespace(_ecm)) + { + ns = scopedNamespace(_ecm, this->dataPtr->model.Entity()); + defaultPrefix = ns; + } + // Subscribe to commands std::vector topics; if (_sdf->HasElement("topic")) { - topics.push_back(_sdf->Get("topic")); + std::string topicName = _sdf->Get("topic"); + if (!topicName.empty()) + { + // Only prepend namespace to relative topic names. + // Absolute topic names (starting with '/') are left unchanged. + if (topicName.front() != '/') + { + topicName = ns + "/" + topicName; + } + } + topics.push_back(topicName); } else if (_sdf->HasElement("sub_topic")) { - topics.push_back("/model/" + this->dataPtr->model.Name(_ecm) + - "/" + _sdf->Get("sub_topic")); + topics.push_back(defaultPrefix + "/" + _sdf->Get("sub_topic")); } else if ((this->dataPtr->steeringOnly) && (!this->dataPtr->useActuatorMsg)) { - topics.push_back("/model/" + this->dataPtr->model.Name(_ecm) + - "/steer_angle"); + topics.push_back(defaultPrefix + "/steer_angle"); } else if ((this->dataPtr->steeringOnly) && (this->dataPtr->useActuatorMsg)) { - topics.push_back("/actuators"); + topics.push_back(defaultPrefix + "/actuators"); } else if (!this->dataPtr->steeringOnly) { - topics.push_back("/model/" + this->dataPtr->model.Name(_ecm) + "/cmd_vel"); + topics.push_back(defaultPrefix + "/cmd_vel"); } auto topic = validTopic(topics); @@ -457,14 +475,23 @@ void AckermannSteering::Configure(const Entity &_entity, std::vector odomTopics; if (_sdf->HasElement("odom_topic")) { - odomTopics.push_back(_sdf->Get("odom_topic")); + std::string odomTopicName = _sdf->Get("odom_topic"); + if (!odomTopicName.empty()) + { + // Only prepend namespace to relative topic names. + // Absolute topic names (starting with '/') are left unchanged. + if (odomTopicName.front() != '/') + { + odomTopicName = ns + "/" + odomTopicName; + } + } + odomTopics.push_back(odomTopicName); } - odomTopics.push_back("/model/" + this->dataPtr->model.Name(_ecm) + - "/odometry"); + odomTopics.push_back(defaultPrefix + "/odometry"); auto odomTopic = validTopic(odomTopics); - if (topic.empty()) + if (odomTopic.empty()) { - gzerr << "AckermannSteering plugin received invalid model name " + gzerr << "AckermannSteering plugin received invalid odom topic name " << "Failed to initialize." << std::endl; return; } @@ -475,10 +502,19 @@ void AckermannSteering::Configure(const Entity &_entity, std::vector tfTopics; if (_sdf->HasElement("tf_topic")) { - tfTopics.push_back(_sdf->Get("tf_topic")); + std::string tfTopicName = _sdf->Get("tf_topic"); + if (!tfTopicName.empty()) + { + // Only prepend namespace to relative topic names. + // Absolute topic names (starting with '/') are left unchanged. + if (tfTopicName.front() != '/') + { + tfTopicName = ns + "/" + tfTopicName; + } + } + tfTopics.push_back(tfTopicName); } - tfTopics.push_back("/model/" + this->dataPtr->model.Name(_ecm) + - "/tf"); + tfTopics.push_back(defaultPrefix + "/tf"); auto tfTopic = validTopic(tfTopics); if (tfTopic.empty()) { From 5981615ded15dc02d8201e0ee6a9f07dbcfc7a7f Mon Sep 17 00:00:00 2001 From: C88-YQ <1409947012@qq.com> Date: Fri, 26 Jun 2026 21:05:36 +0800 Subject: [PATCH 21/21] Add ns support to systems/multicopter_control/MulticopterVelocityControl Signed-off-by: C88-YQ <1409947012@qq.com> --- .../MulticopterVelocityControl.cc | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/systems/multicopter_control/MulticopterVelocityControl.cc b/src/systems/multicopter_control/MulticopterVelocityControl.cc index 3feb5f7040..a71cc77197 100644 --- a/src/systems/multicopter_control/MulticopterVelocityControl.cc +++ b/src/systems/multicopter_control/MulticopterVelocityControl.cc @@ -39,8 +39,10 @@ #include "gz/sim/components/Model.hh" #include "gz/sim/components/ParentEntity.hh" #include "gz/sim/components/World.hh" +#include "gz/sim/components/Namespace.hh" #include "gz/sim/Link.hh" #include "gz/sim/Model.hh" +#include "gz/sim/Util.hh" #include "MulticopterVelocityControl.hh" @@ -244,22 +246,39 @@ void MulticopterVelocityControl::Configure(const Entity &_entity, math::eigen3::convert(angularVelocityMean); this->noiseParameters.angularVelocityStdDev = math::eigen3::convert(angularVelocityStdDev); - - if (sdfClone->HasElement("robotNamespace")) + + const auto nsComp = + _ecm.Component(this->model.Entity()); + if (nsComp && !nsComp->Data().empty()) + { + this->robotNamespace = transport::TopicUtils::AsValidTopic( + scopedNamespace(_ecm, this->model.Entity())); + if (this->robotNamespace.empty()) + { + gzerr << "Robot namespace [" << nsComp->Data() + << "] resolved from the model namespace component is invalid." + << std::endl; + return; + } + } + else if (sdfClone->HasElement("robotNamespace")) { this->robotNamespace = transport::TopicUtils::AsValidTopic( sdfClone->Get("robotNamespace")); if (this->robotNamespace.empty()) { gzerr << "Robot namespace [" - << sdfClone->Get("robotNamespace") <<"] is invalid." + << sdfClone->Get("robotNamespace") + <<"] specified in the plugin element is invalid." << std::endl; return; } } else { - gzerr << "Please specify a robotNamespace.\n"; + gzerr << "No robotNamespace was specified. Please set either a model " + << "namespace component or the plugin element." + << std::endl; return; }