diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 4d6a4d050..9ff4c58c6 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -65,6 +65,7 @@ pybind11_add_module(sdformat SHARED src/sdf/pyNavSat.cc src/sdf/pyNoise.cc src/sdf/pyParserConfig.cc + src/sdf/pyParticleEmitter.cc src/sdf/pyPbr.cc src/sdf/pyPlane.cc src/sdf/pyPlugin.cc @@ -124,6 +125,7 @@ if (BUILD_TESTING) pyNoise_TEST pyNavSat_TEST pyParserConfig_TEST + pyParticleEmitter_TEST pyPbr_TEST pyPlane_TEST pyPlugin_TEST diff --git a/python/src/sdf/_ignition_sdformat_pybind11.cc b/python/src/sdf/_ignition_sdformat_pybind11.cc index 85051adcb..da7aeb3f4 100644 --- a/python/src/sdf/_ignition_sdformat_pybind11.cc +++ b/python/src/sdf/_ignition_sdformat_pybind11.cc @@ -45,6 +45,7 @@ #include "pyNavSat.hh" #include "pyNoise.hh" #include "pyParserConfig.hh" +#include "pyParticleEmitter.hh" #include "pyPbr.hh" #include "pyPlane.hh" #include "pyPlugin.hh" @@ -93,6 +94,7 @@ PYBIND11_MODULE(sdformat, m) { sdf::python::defineNoise(m); sdf::python::defineODE(m); sdf::python::defineParserConfig(m); + sdf::python::defineParticleEmitter(m); sdf::python::definePbr(m); sdf::python::definePbrWorkflow(m); sdf::python::definePlane(m); diff --git a/python/src/sdf/pyParticleEmitter.cc b/python/src/sdf/pyParticleEmitter.cc new file mode 100644 index 000000000..0e9873c57 --- /dev/null +++ b/python/src/sdf/pyParticleEmitter.cc @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * 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. + */ + +#include "pyParticleEmitter.hh" + +#include + +#include "sdf/ParserConfig.hh" + +#include "sdf/ParticleEmitter.hh" + +using namespace pybind11::literals; + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ +///////////////////////////////////////////////// +void defineParticleEmitter(pybind11::object module) +{ + pybind11::class_ particleEmitterModule(module, "ParticleEmitter"); + particleEmitterModule + .def(pybind11::init<>()) + .def(pybind11::init()) + .def("name", &sdf::ParticleEmitter::Name, + "Get the name of the particle emitter.") + .def("set_name", &sdf::ParticleEmitter::SetName, + "Set the name of the particle emitter.") + .def("type", &sdf::ParticleEmitter::Type, + "Get the type of particle emitter.") + .def("set_type", + pybind11::overload_cast( + &sdf::ParticleEmitter::SetType), + "Set the type of particle emitter.") + .def("set_type", + pybind11::overload_cast( + &sdf::ParticleEmitter::SetType), + "Set the type of particle emitter.") + .def("type_str", &sdf::ParticleEmitter::TypeStr, + "Get the particle emitter type as a string.") + .def("emitting", &sdf::ParticleEmitter::Emitting, + "Get whether the particle emitter should run (emit " + "particles).") + .def("set_emitting", &sdf::ParticleEmitter::SetEmitting, + "Set whether the particle emitter is running, emitting " + "particles.") + .def("duration", &sdf::ParticleEmitter::Duration, + "Get the number of seconds the emitter is active." + "A value less than or equal to zero indicates infinite duration.") + .def("set_duration", &sdf::ParticleEmitter::SetDuration, + "Set the number of seconds the emitter is active") + .def("lifetime", &sdf::ParticleEmitter::Lifetime, + "Get the number of seconds each particle will 'live' for " + "before being destroyed.") + .def("set_lifetime", &sdf::ParticleEmitter::SetLifetime, + "Set the number of seconds each particle will 'live' for.") + .def("rate", &sdf::ParticleEmitter::Rate, + "Get the number of particles per second that should be emitted.") + .def("set_rate", &sdf::ParticleEmitter::SetRate, + "Set the number of particles per second that should be emitted.") + .def("scale_rate", &sdf::ParticleEmitter::ScaleRate, + "Get the amount by which to scale the particles in both x " + "and y direction per second.") + .def("set_scale_rate", &sdf::ParticleEmitter::SetScaleRate, + "Set the amount by which to scale the particles in both x " + "and y direction per second.") + .def("min_velocity", &sdf::ParticleEmitter::MinVelocity, + "Get the minimum velocity for each particle.") + .def("set_min_velocity", &sdf::ParticleEmitter::SetMinVelocity, + "Set the minimum velocity for each particle.") + .def("max_velocity", &sdf::ParticleEmitter::MaxVelocity, + "Get the maximum velocity for each particle.") + .def("set_max_velocity", &sdf::ParticleEmitter::SetMaxVelocity, + "Set the maximum velocity for each particle.") + .def("size", &sdf::ParticleEmitter::Size, + "Get the size of the emitter where the particles are sampled.") + .def("set_size", &sdf::ParticleEmitter::SetSize, + "Set the size of the emitter where the particles are sampled.") + .def("particle_size", &sdf::ParticleEmitter::ParticleSize, + "Get the size of a particle in meters.") + .def("set_particle_size", &sdf::ParticleEmitter::SetParticleSize, + "Set the size of a particle in meters.") + .def("color_start", &sdf::ParticleEmitter::ColorStart, + "Gets the starting color for all particles emitted.") + .def("set_color_start", &sdf::ParticleEmitter::SetColorStart, + "Set the starting color for all particles emitted.") + .def("color_end", &sdf::ParticleEmitter::ColorEnd, + "Get the end color for all particles emitted.") + .def("set_color_end", &sdf::ParticleEmitter::SetColorEnd, + "Set the end color for all particles emitted.") + .def("color_range_image", &sdf::ParticleEmitter::ColorRangeImage, + "Get the path to the color image used as an affector.") + .def("set_color_range_image", &sdf::ParticleEmitter::SetColorRangeImage, + "Set the path to the color image used as an affector.") + .def("topic", &sdf::ParticleEmitter::Topic, + "Get the topic used to update the particle emitter properties.") + .def("set_topic", &sdf::ParticleEmitter::SetTopic, + "Set the topic used to update the particle emitter properties.") + .def("scatter_ratio", &sdf::ParticleEmitter::ScatterRatio, + "Get the particle scatter ratio. This is used to determine the " + "ratio of particles that will be detected by sensors.") + .def("set_scatter_ratio", &sdf::ParticleEmitter::SetScatterRatio, + "Set the particle scatter ratio. This is used to determine the " + "ratio of particles that will be detected by sensors.") + .def("raw_pose", &sdf::ParticleEmitter::RawPose, + "Get the pose of the camer. This is the pose of the ParticleEmitter " + "as specified in SDF ( ... " + ").") + .def("set_raw_pose", &sdf::ParticleEmitter::SetRawPose, + "Set the pose of the ParticleEmitter.") + .def("pose_relative_to", &sdf::ParticleEmitter::PoseRelativeTo, + "Get the name of the coordinate frame relative to which this " + "object's pose is expressed. An empty value indicates that the frame " + "is relative to the parent link.") + .def("set_pose_relative_to", &sdf::ParticleEmitter::SetPoseRelativeTo, + "Set the name of the coordinate frame relative to which this " + "object's pose is expressed. An empty value indicates that the frame " + "is relative to the parent link.") + .def("semantic_pose", &sdf::ParticleEmitter::SemanticPose, + "Get SemanticPose object of this object to aid in resolving " + "poses.") + .def("material", &sdf::ParticleEmitter::Material, + pybind11::return_value_policy::reference_internal, + "Get a pointer to the emitter's material properties. This can " + "be a None if material properties have not been set.") + .def("set_material", &sdf::ParticleEmitter::SetMaterial, + "Set the emitter's material.") + .def("file_path", &sdf::ParticleEmitter::FilePath, + "The path to the file where this element was loaded from.") + .def("set_file_path", &sdf::ParticleEmitter::SetFilePath, + "Set the path to the file where this element was loaded from.") + .def("__copy__", [](const sdf::ParticleEmitter &self) { + return sdf::ParticleEmitter(self); + }) + .def("__deepcopy__", [](const sdf::ParticleEmitter &self, pybind11::dict) { + return sdf::ParticleEmitter(self); + }, "memo"_a); + + pybind11::enum_(particleEmitterModule, "ParticleEmitterType") + .value("POINT", sdf::ParticleEmitterType::POINT) + .value("BOX", sdf::ParticleEmitterType::BOX) + .value("CYLINDER", sdf::ParticleEmitterType::CYLINDER) + .value("ELLIPSOID", sdf::ParticleEmitterType::ELLIPSOID); +} +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf diff --git a/python/src/sdf/pyParticleEmitter.hh b/python/src/sdf/pyParticleEmitter.hh new file mode 100644 index 000000000..f71f5282d --- /dev/null +++ b/python/src/sdf/pyParticleEmitter.hh @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * 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 SDFORMAT_PYTHON_PARTICLEEMITTER_HH_ +#define SDFORMAT_PYTHON_PARTICLEEMITTER_HH_ + +#include + +#include "sdf/ParticleEmitter.hh" + +#include "sdf/config.hh" + +namespace sdf +{ +// Inline bracket to help doxygen filtering. +inline namespace SDF_VERSION_NAMESPACE { +namespace python +{ +/// Define a pybind11 wrapper for an sdf::ParticleEmitter +/** + * \param[in] module a pybind11 module to add the definition to + */ +void defineParticleEmitter(pybind11::object module); +} // namespace python +} // namespace SDF_VERSION_NAMESPACE +} // namespace sdf + +#endif // SDFORMAT_PYTHON_PARTICLEEMITTER_HH_ diff --git a/python/test/pyParticleEmitter_TEST.py b/python/test/pyParticleEmitter_TEST.py new file mode 100644 index 000000000..0a324f113 --- /dev/null +++ b/python/test/pyParticleEmitter_TEST.py @@ -0,0 +1,121 @@ +# Copyright (C) 2022 Open Source Robotics Foundation + +# 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. + +import copy +from ignition.math import Color, Pose3d, Vector3d, Helpers +from sdformat import ParticleEmitter +import unittest + + +class ParticleEmitterTEST(unittest.TestCase): + + + def test_default_construction(self): + emitter = ParticleEmitter() + + self.assertFalse(emitter.name()) + + emitter.set_name("test_emitter") + self.assertEqual(emitter.name(), "test_emitter") + + self.assertEqual("point", emitter.type_str()) + self.assertEqual(ParticleEmitter.ParticleEmitterType.POINT, emitter.type()) + self.assertTrue(emitter.set_type("box")) + self.assertEqual("box", emitter.type_str()) + self.assertEqual(ParticleEmitter.ParticleEmitterType.BOX, emitter.type()) + emitter.set_type(ParticleEmitter.ParticleEmitterType.CYLINDER) + self.assertEqual("cylinder", emitter.type_str()) + + self.assertTrue(emitter.emitting()) + emitter.set_emitting(False) + self.assertFalse(emitter.emitting()) + + self.assertAlmostEqual(0.0, emitter.duration()) + emitter.set_duration(10.0) + self.assertAlmostEqual(10.0, emitter.duration()) + + self.assertAlmostEqual(5.0, emitter.lifetime()) + emitter.set_lifetime(22.0) + self.assertAlmostEqual(22.0, emitter.lifetime()) + emitter.set_lifetime(-1.0) + self.assertAlmostEqual(Helpers.MIN_D, emitter.lifetime()) + + self.assertAlmostEqual(10.0, emitter.rate()) + emitter.set_rate(123.0) + self.assertAlmostEqual(123.0, emitter.rate()) + emitter.set_rate(-123.0) + self.assertAlmostEqual(0.0, emitter.rate()) + + self.assertAlmostEqual(0.0, emitter.scale_rate()) + emitter.set_scale_rate(1.2) + self.assertAlmostEqual(1.2, emitter.scale_rate()) + emitter.set_scale_rate(-1.2) + self.assertAlmostEqual(0.0, emitter.scale_rate()) + + self.assertAlmostEqual(1.0, emitter.min_velocity()) + emitter.set_min_velocity(12.4) + self.assertAlmostEqual(12.4, emitter.min_velocity()) + emitter.set_min_velocity(-12.4) + self.assertAlmostEqual(0.0, emitter.min_velocity()) + + self.assertAlmostEqual(1.0, emitter.max_velocity()) + emitter.set_max_velocity(20.6) + self.assertAlmostEqual(20.6, emitter.max_velocity()) + emitter.set_max_velocity(-12.4) + self.assertAlmostEqual(0.0, emitter.max_velocity()) + + self.assertEqual(Vector3d.ONE, emitter.size()) + emitter.set_size(Vector3d(3, 2, 1)) + self.assertEqual(Vector3d(3, 2, 1), emitter.size()) + emitter.set_size(Vector3d(-3, -2, -1)) + self.assertEqual(Vector3d(0, 0, 0), emitter.size()) + + self.assertEqual(Vector3d.ONE, emitter.particle_size()) + emitter.set_particle_size(Vector3d(4, 5, 6)) + self.assertEqual(Vector3d(4, 5, 6), emitter.particle_size()) + emitter.set_particle_size(Vector3d(-4, -5, -6)) + self.assertEqual(Vector3d(0, 0, 0), emitter.particle_size()) + + self.assertEqual(Color.WHITE, emitter.color_start()) + emitter.set_color_start(Color(0.1, 0.2, 0.3, 1.0)) + self.assertEqual(Color(0.1, 0.2, 0.3, 1.0), + emitter.color_start()) + + self.assertEqual(Color.WHITE, emitter.color_end()) + emitter.set_color_end(Color(0.4, 0.5, 0.6, 1.0)) + self.assertEqual(Color(0.4, 0.5, 0.6, 1.0), emitter.color_end()) + + self.assertFalse(emitter.color_range_image()) + emitter.set_color_range_image("/test/string") + self.assertEqual("/test/string", emitter.color_range_image()) + + self.assertFalse(emitter.topic()) + emitter.set_topic("/test/topic") + self.assertEqual("/test/topic", emitter.topic()) + + self.assertAlmostEqual(0.65, emitter.scatter_ratio()) + emitter.set_scatter_ratio(0.5) + self.assertAlmostEqual(0.5, emitter.scatter_ratio()) + + self.assertEqual(Pose3d.ZERO, emitter.raw_pose()) + emitter.set_raw_pose(Pose3d(1, 2, 3, 0, 0, 1.5707)) + self.assertEqual(Pose3d(1, 2, 3, 0, 0, 1.5707), emitter.raw_pose()) + + self.assertFalse(emitter.pose_relative_to()) + emitter.set_pose_relative_to("/test/relative") + self.assertEqual("/test/relative", emitter.pose_relative_to()) + + +if __name__ == '__main__': + unittest.main()