robot_description_builder/link/
visual.rs

1#[cfg(feature = "xml")]
2use quick_xml::{events::attributes::Attribute, name::QName};
3
4#[cfg(feature = "urdf")]
5use crate::to_rdf::to_urdf::ToURDF;
6use crate::{
7	identifiers::GroupID,
8	link::{builder::VisualBuilder, geometry::GeometryInterface},
9	link_data::geometry::GeometryShapeData,
10	material::Material,
11	transform::Transform,
12};
13
14/// A `Visual` geometry for a `Link`.
15///
16/// This struct holds one of (the many) Visual geometries for the associated [`Link`].
17/// It can be constructed via the [`VisualBuilder`] (accessable via the [`builder`](Self::builder) method) and [added while building the `Link`](crate::link::builder::LinkBuilder::add_visual).
18/// It contains the following data:
19/// - **[`geometry`](crate::link_data::geometry)**: The geometry used for visualization.
20/// - **[`material`](crate::material)** (Optional): The material is used to control the appearance of the `geometry`.
21/// - **[`transform`](crate::Transform)** (Optional): The transform from the [`Link`] frame to the `geometry`.
22/// - **`name`** (Optional): The [_string identifier_](crate::identifiers) (or name) of this visual element. For practical purposes, it is recommended to use unique identifiers/names.
23///
24/// [`Link`]: crate::link::Link
25#[derive(Debug)]
26pub struct Visual {
27	// TODO: Add export option which generates name.
28	/// The [_string identifier_](crate::identifiers) or name of this visual element.
29	///
30	/// For practical purposes, it is recommended to use unique identifiers/names.
31	pub(crate) name: Option<String>,
32	/// The transform from the origin of the parent `Link` to the origin of this `Visual`.
33	///
34	/// This is the reference for the placement of the `geometry`.
35	///
36	/// In URDF this field is refered to as `<origin>`.
37	pub(crate) transform: Option<Transform>,
38	/// The geometry of this Visual element.
39	pub(crate) geometry: Box<dyn GeometryInterface + Sync + Send>,
40	/// The material of this Visual element.
41	pub(crate) material: Option<Material>,
42}
43
44impl Visual {
45	/// Create a new [`VisualBuilder`] with the specified [`geometry`](crate::link_data::geometry).
46	pub fn builder(geometry: impl Into<Box<dyn GeometryInterface + Sync + Send>>) -> VisualBuilder {
47		VisualBuilder::new(geometry)
48	}
49
50	// TODO: Is docthe test helpfull?
51	/// Gets an optional reference to the name of this `Visual`.
52	///
53	/// # Example
54	/// Unwraps are hidden for brevity.
55	/// ```rust
56	/// # use robot_description_builder::{
57	/// #     link_data::{geometry::SphereGeometry, Visual},
58	/// #     linkbuilding::{LinkBuilder, VisualBuilder},
59	/// #     KinematicInterface,
60	/// # };
61	/// let vis: VisualBuilder = Visual::builder(SphereGeometry::new(1.));
62	/// let tree = LinkBuilder::new("example-1")
63	///     .add_visual(vis.clone())
64	///     .build_tree();
65	///
66	/// assert_eq!(
67	///     tree.get_root_link()
68	///         .read()
69	/// #       .unwrap()
70	///         .visuals()
71	///         .first()
72	/// #       .unwrap()
73	///         .name(),
74	///     None
75	/// );
76	///
77	/// let tree = LinkBuilder::new("example-2")
78	///     .add_visual(vis.named("Some Name"))
79	///     .build_tree();
80	///
81	/// assert_eq!(
82	///     tree.get_root_link()
83	///         .read()
84	/// #       .unwrap()
85	///         .visuals()
86	///         .first()
87	/// #       .unwrap()
88	///         .name(),
89	///     Some(&"Some Name".to_owned())
90	/// )
91	/// ```
92	pub fn name(&self) -> Option<&String> {
93		self.name.as_ref()
94	}
95
96	/// Gets an optional reference to the `transform` of this `Visual`.
97	pub fn transform(&self) -> Option<&Transform> {
98		self.transform.as_ref()
99	}
100
101	/// Gets a reference to the `geometry` of this `Visual`.
102	pub fn geometry(&self) -> &Box<dyn GeometryInterface + Sync + Send> {
103		&self.geometry
104	}
105
106	/// Gets an optional reference to the [`material`](crate::material::Material) of this `Visual`.
107	pub fn material(&self) -> Option<&Material> {
108		self.material.as_ref()
109	}
110
111	/// Gets an optional mutable reference to the [`material`](crate::material::Material) of this `Visual`.
112	pub(crate) fn material_mut(&mut self) -> Option<&mut Material> {
113		self.material.as_mut()
114	}
115
116	/// Recreates the [`VisualBuilder`], which was used to create this `Visual`.
117	pub fn rebuild(&self) -> VisualBuilder {
118		VisualBuilder {
119			name: self.name.clone(),
120			transform: self.transform,
121			geometry: self.geometry.boxed_clone(),
122			material_description: self.material.as_ref().map(Material::describe),
123		}
124	}
125
126	pub(crate) fn get_geometry_data(&self) -> GeometryShapeData {
127		GeometryShapeData {
128			transform: self.transform.unwrap_or_default(),
129			geometry: self.geometry.shape_container(),
130		}
131	}
132}
133
134#[cfg(feature = "urdf")]
135impl ToURDF for Visual {
136	fn to_urdf(
137		&self,
138		writer: &mut quick_xml::Writer<std::io::Cursor<Vec<u8>>>,
139		urdf_config: &crate::to_rdf::to_urdf::URDFConfig,
140	) -> Result<(), quick_xml::Error> {
141		let mut element = writer.create_element("visual");
142		if let Some(name) = self.name() {
143			element = element.with_attribute(Attribute {
144				key: QName(b"name"),
145				value: name.display().as_bytes().into(),
146			});
147		}
148		element.write_inner_content(|writer| -> quick_xml::Result<()> {
149			// Could make this with `get_geometry_data``
150			if let Some(transform) = self.transform() {
151				transform.to_urdf(writer, urdf_config)?
152			}
153
154			self.geometry()
155				.shape_container()
156				.to_urdf(writer, urdf_config)?;
157			if let Some(material) = self.material() {
158				material.to_urdf(writer, urdf_config)?;
159			}
160			Ok(())
161		})?;
162
163		Ok(())
164	}
165}
166
167impl PartialEq for Visual {
168	fn eq(&self, other: &Self) -> bool {
169		self.name == other.name
170			&& self.transform == other.transform
171			&& *self.geometry == *other.geometry
172			&& match (&self.material, &other.material) {
173				(None, None) => true,
174				(Some(own_material), Some(other_material)) => own_material == other_material,
175				_ => false,
176			}
177	}
178}
179
180impl Clone for Visual {
181	fn clone(&self) -> Self {
182		Self {
183			name: self.name.clone(),
184			transform: self.transform,
185			geometry: self.geometry.boxed_clone(),
186			material: self.material.clone(),
187		}
188	}
189}
190
191#[cfg(test)]
192mod tests {
193	use std::f32::consts::PI;
194	use test_log::test;
195
196	use crate::{
197		link::{
198			builder::VisualBuilder,
199			geometry::{BoxGeometry, CylinderGeometry, SphereGeometry},
200			visual::Visual,
201		},
202		transform::Transform,
203	};
204
205	#[cfg(feature = "urdf")]
206	mod to_urdf {
207		use super::{test, *};
208		use crate::{
209			material::MaterialDescriptor,
210			to_rdf::to_urdf::{ToURDF, URDFConfig, URDFMaterialReferences},
211		};
212		use std::io::Seek;
213
214		fn test_to_urdf_visual(visual: VisualBuilder, result: String, urdf_config: &URDFConfig) {
215			let mut writer = quick_xml::Writer::new(std::io::Cursor::new(Vec::new()));
216			assert!(visual.build().to_urdf(&mut writer, urdf_config).is_ok());
217
218			writer.get_mut().rewind().unwrap();
219
220			assert_eq!(
221				std::io::read_to_string(writer.into_inner()).unwrap(),
222				result
223			)
224		}
225
226		#[test]
227		fn no_name_no_origin_no_material() {
228			test_to_urdf_visual(
229				Visual::builder(BoxGeometry::new(1.0, 2.0, 3.0)),
230				String::from(r#"<visual><geometry><box size="1 2 3"/></geometry></visual>"#),
231				&URDFConfig::default(),
232			);
233		}
234
235		#[test]
236		fn name_no_origin_no_material() {
237			test_to_urdf_visual(
238				Visual::builder(CylinderGeometry::new(9., 6.258)).named("myLink_vis"),
239				String::from(
240					r#"<visual name="myLink_vis"><geometry><cylinder radius="9" length="6.258"/></geometry></visual>"#,
241				),
242				&URDFConfig::default(),
243			);
244		}
245
246		#[test]
247		fn no_name_origin_no_material() {
248			test_to_urdf_visual(
249				Visual::builder(SphereGeometry::new(3.))
250					.transformed(Transform::new((4., 6.78, 1.), (PI, 2. * PI, 0.))),
251				String::from(
252					r#"<visual><origin xyz="4 6.78 1" rpy="3.1415927 6.2831855 0"/><geometry><sphere radius="3"/></geometry></visual>"#,
253				),
254				&URDFConfig::default(),
255			);
256		}
257
258		#[test]
259		fn no_name_no_origin_material() {
260			test_to_urdf_visual(
261				Visual::builder(CylinderGeometry::new(4.5, 75.35)).materialized(
262					MaterialDescriptor::new_color(0.5, 0.55, 0.6, 1.).named("material_name"),
263				),
264				String::from(
265					r#"<visual><geometry><cylinder radius="4.5" length="75.35"/></geometry><material name="material_name"><color rgba="0.5 0.55 0.6 1"/></material></visual>"#,
266				),
267				&URDFConfig {
268					material_references: URDFMaterialReferences::OnlyMultiUseMaterials,
269					..Default::default()
270				},
271			);
272		}
273
274		#[test]
275		fn name_origin_material() {
276			test_to_urdf_visual(
277				Visual::builder(CylinderGeometry::new(4.5, 75.35))
278					.named("some_col")
279					.transformed(Transform::new_translation(5.4, 9.1, 7.8))
280					.materialized(MaterialDescriptor::new_color(0.75, 0.5, 1., 1.)),
281				String::from(
282					r#"<visual name="some_col"><origin xyz="5.4 9.1 7.8"/><geometry><cylinder radius="4.5" length="75.35"/></geometry><material><color rgba="0.75 0.5 1 1"/></material></visual>"#,
283				),
284				&URDFConfig::default(),
285			);
286		}
287	}
288}