robot_description_builder/link/builder/
visual_builder.rs

1use nalgebra::Matrix3;
2
3use crate::{
4	identifiers::GroupIDChanger,
5	link::{
6		builder::CollisionBuilder,
7		geometry::{GeometryInterface, GeometryShapeData},
8		visual::Visual,
9	},
10	material::MaterialDescriptor,
11	transform::{Mirror, Transform},
12};
13
14/// The builder for `Visual` components.
15///
16/// The `VisualBuilder` is used to construct [`Visual`] elements of [`Links`](crate::link::Link).
17///
18/// This will configure the visual 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/// They can be added to a [`LinkBuilder`](super::LinkBuilder) while constructing a [`Link`] by calling [`add_visual`](crate::link::builder::LinkBuilder::add_visual).
25///
26/// A `VisualBuilder` can be converted to a [`CollisionBuilder`] to make defining [`Collision`](crate::link::collision::Collision) easier. <br/>
27/// **WARNING:** It is not recommended to use high-detail meshes for collision geometries, since this will slow down the collision checking process.
28/// Also, keep in mind, that some simulators only support the use of convex meshes for collisions, if at all.
29///
30/// [`Link`]: crate::link::Link
31#[derive(Debug)]
32pub struct VisualBuilder {
33	/// The [_string identifier_](crate::identifiers) or name of this visual element.
34	///
35	/// For practical purposes, it is recommended to use unique identifiers/names.
36	pub(crate) name: Option<String>,
37	/// The transform from the origin of the parent `Link` to the origin of this `Visual`.
38	///
39	/// This is the reference for the placement of the `geometry`.
40	///
41	/// In URDF this field is refered to as `<origin>`.
42	pub(crate) transform: Option<Transform>,
43	/// The geometry of this Visual element.
44	pub(crate) geometry: Box<dyn GeometryInterface + Sync + Send>,
45	/// The material of this Visual element.
46	pub(crate) material_description: Option<MaterialDescriptor>,
47}
48
49impl VisualBuilder {
50	/// Create a new [`VisualBuilder`] with the specified [`Geometry`](crate::link_data::geometry).
51	pub fn new(geometry: impl Into<Box<dyn GeometryInterface + Sync + Send>>) -> Self {
52		Self {
53			name: None,
54			transform: None,
55			geometry: geometry.into(),
56			material_description: None,
57		}
58	}
59
60	// TODO: Figure out if this will be kept [Added for easier transistion]
61	/// Create a new [`VisualBuilder`] with all fields specified.
62	pub fn new_full(
63		name: Option<String>,
64		transform: Option<Transform>,
65		geometry: impl Into<Box<dyn GeometryInterface + Sync + Send>>,
66		material_description: Option<MaterialDescriptor>,
67	) -> Self {
68		Self {
69			name,
70			transform,
71			geometry: geometry.into(),
72			material_description,
73		}
74	}
75
76	/// Sets the `name` of this `VisualBuilder`.
77	pub fn named(mut self, name: impl Into<String>) -> Self {
78		self.name = Some(name.into());
79		self
80	}
81
82	/// Specify a `transform` for this `VisualBuilder`.
83	///
84	/// The default is a no transformation (The frame of the `Visual` will be the same as the frame of the parent `Link`).
85	pub fn transformed(mut self, transform: Transform) -> Self {
86		self.transform = Some(transform);
87		self
88	}
89
90	/// Specify a `material` for this `VisualBuilder`.
91	///
92	/// The default is no material.
93	pub fn materialized(mut self, material_description: MaterialDescriptor) -> Self {
94		self.material_description = Some(material_description);
95		self
96	}
97
98	// TODO: IMPROVE DOCS
99	/// Creates a `CollisionBuilder` from this `VisualBuilder` reference by lossy conversion.
100	///
101	/// Creates a [`CollisionBuilder`] from the `VisualBuilder` by cloning the following fields:
102	///  - `name`
103	///  - `transform`
104	///  - `geometry`
105	pub fn to_collision(&self) -> CollisionBuilder {
106		CollisionBuilder {
107			name: self.name.clone(),
108			transform: self.transform,
109			geometry: self.geometry.boxed_clone(),
110		}
111	}
112
113	pub(crate) fn build(self) -> Visual {
114		let material = self
115			.material_description
116			.map(|description| description.build());
117
118		Visual {
119			name: self.name,
120			transform: self.transform,
121			geometry: self.geometry,
122			material,
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
134impl Mirror for VisualBuilder {
135	fn mirrored(&self, mirror_matrix: &Matrix3<f32>) -> Self {
136		Self {
137			name: self.name.as_ref().cloned(), // TODO: Rename?
138			transform: self
139				.transform
140				.as_ref()
141				.map(|transform| transform.mirrored(mirror_matrix)),
142			geometry: self.geometry.boxed_mirrored(mirror_matrix),
143			material_description: self.material_description.clone(),
144		}
145	}
146}
147
148/// Non-builder methods
149impl VisualBuilder {
150	/// Gets an optional reference to the `name` of this `VisualBuilder`.
151	pub fn name(&self) -> Option<&String> {
152		self.name.as_ref()
153	}
154
155	/// Gets an optional reference to the `transform` of this `VisualBuilder`.
156	pub fn transform(&self) -> Option<&Transform> {
157		self.transform.as_ref()
158	}
159
160	/// Gets a reference to the [`geometry`](crate::link_data::geometry) of this `VisualBuilder`.
161	pub fn geometry(&self) -> &Box<dyn GeometryInterface + Sync + Send> {
162		&self.geometry
163	}
164
165	/// Gets an optional reference to the [`MaterialDescriptor`](crate::material::MaterialDescriptor) of this `VisualBuilder`.
166	pub fn material(&self) -> Option<&MaterialDescriptor> {
167		self.material_description.as_ref()
168	}
169}
170
171impl GroupIDChanger for VisualBuilder {
172	unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str) {
173		if let Some(name) = self.name.as_mut() {
174			name.change_group_id_unchecked(new_group_id);
175		}
176
177		if let Some(material_builder) = self.material_description.as_mut() {
178			material_builder.change_group_id_unchecked(new_group_id);
179		}
180	}
181
182	fn apply_group_id(&mut self) {
183		if let Some(name) = self.name.as_mut() {
184			name.apply_group_id();
185		}
186
187		if let Some(material_builder) = self.material_description.as_mut() {
188			material_builder.apply_group_id();
189		}
190	}
191}
192
193impl PartialEq for VisualBuilder {
194	fn eq(&self, other: &Self) -> bool {
195		self.name == other.name
196			&& self.transform == other.transform
197			&& *self.geometry == *other.geometry
198			&& self.material_description == other.material_description
199	}
200}
201
202impl Clone for VisualBuilder {
203	fn clone(&self) -> Self {
204		Self {
205			name: self.name.clone(),
206			transform: self.transform,
207			geometry: self.geometry.boxed_clone(),
208			material_description: self.material_description.clone(),
209		}
210	}
211}
212
213#[cfg(test)]
214mod tests {
215	use super::VisualBuilder;
216	use crate::link::link_data::geometry::{BoxGeometry, CylinderGeometry, SphereGeometry};
217	use test_log::test;
218	// TODO: Write tests
219	mod group_id_changer {
220		use super::{test, BoxGeometry, CylinderGeometry, SphereGeometry, VisualBuilder};
221		use crate::identifiers::{GroupIDChanger, GroupIDError};
222
223		#[test]
224		fn change_group_id_unchecked_no_material() {
225			#[inline]
226			fn test(collision_builder: VisualBuilder, new_group_id: &str, name: Option<&str>) {
227				let mut visual_builder = collision_builder;
228				unsafe {
229					visual_builder.change_group_id_unchecked(new_group_id);
230				}
231				assert_eq!(
232					visual_builder.name,
233					name.and_then(|name| Some(name.to_owned()))
234				)
235			}
236
237			// No Name
238			test(VisualBuilder::new(BoxGeometry::new(1., 2., 3.)), "7", None);
239			test(
240				VisualBuilder::new(CylinderGeometry::new(32., 5.)),
241				"[[invalid]]",
242				None,
243			);
244			test(VisualBuilder::new(SphereGeometry::new(3.3e9)), "", None);
245
246			// Named, but no GroupID
247			test(
248				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("ThisCoolName"),
249				"7",
250				Some("ThisCoolName"),
251			);
252			test(
253				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("ADAdsadsdasdDS[]"),
254				"valid4",
255				Some("ADAdsadsdasdDS[]"),
256			);
257			test(
258				VisualBuilder::new(SphereGeometry::new(3.3e9)).named("Bal"),
259				"bol",
260				Some("Bal"),
261			);
262
263			// Named with GroupID and valid
264			test(
265				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("Leg_[[L01]]_l04_col"),
266				"7",
267				Some("Leg_[[7]]_l04_col"),
268			);
269			test(
270				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("Arm_[[B01d]]_link_0313c"),
271				"valid4",
272				Some("Arm_[[valid4]]_link_0313c"),
273			);
274			test(
275				VisualBuilder::new(SphereGeometry::new(3.3e9))
276					.named("Bal_[[F900]]_this_doesn't_matter"),
277				"G0-02",
278				Some("Bal_[[G0-02]]_this_doesn't_matter"),
279			);
280
281			// Named with GroupID and invalid
282			test(
283				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("Leg_[[L01]]_l04_col"),
284				"[[7",
285				Some("Leg_[[[[7]]_l04_col"),
286			);
287			test(
288				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("Arm_[[B01d]]_link_0313c"),
289				"[[invalid]]",
290				Some("Arm_[[[[invalid]]]]_link_0313c"),
291			);
292			test(
293				VisualBuilder::new(SphereGeometry::new(3.3e9))
294					.named("Bal_[[F900]]_this_doesn't_matter"),
295				"",
296				Some("Bal_[[]]_this_doesn't_matter"),
297			);
298		}
299
300		#[test]
301		#[ignore = "TODO"]
302		fn change_group_id_unchecked_with_material() {
303			todo!()
304		}
305
306		#[test]
307		fn change_group_id_no_material() {
308			#[inline]
309			fn test(
310				visual_builder: VisualBuilder,
311				new_group_id: &str,
312				result_change: Result<(), GroupIDError>,
313				name: Option<&str>,
314			) {
315				let mut visual_builder = visual_builder;
316				assert_eq!(visual_builder.change_group_id(new_group_id), result_change);
317				assert_eq!(
318					visual_builder.name,
319					name.and_then(|name| Some(name.to_owned()))
320				)
321			}
322
323			// No Name, valid
324			test(
325				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)),
326				"7",
327				Ok(()),
328				None,
329			);
330			test(
331				VisualBuilder::new(CylinderGeometry::new(32., 5.)),
332				"valid5",
333				Ok(()),
334				None,
335			);
336			test(
337				VisualBuilder::new(SphereGeometry::new(7.)),
338				"R04",
339				Ok(()),
340				None,
341			);
342
343			// No Name, invalid
344			test(
345				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)),
346				"7]]",
347				Err(GroupIDError::new_close("7]]")),
348				None,
349			);
350			test(
351				VisualBuilder::new(CylinderGeometry::new(32., 5.)),
352				"[[invalid]]",
353				Err(GroupIDError::new_open("[[invalid]]")),
354				None,
355			);
356			test(
357				VisualBuilder::new(SphereGeometry::new(3.3e9)),
358				"",
359				Err(GroupIDError::new_empty()),
360				None,
361			);
362
363			// Named, but no GroupID
364			test(
365				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("ThisCoolName"),
366				"7",
367				Ok(()),
368				Some("ThisCoolName"),
369			);
370			test(
371				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("ADAdsadsdasdDS[]"),
372				"valid4",
373				Ok(()),
374				Some("ADAdsadsdasdDS[]"),
375			);
376			test(
377				VisualBuilder::new(SphereGeometry::new(3.3e9)).named("Bal"),
378				"bol",
379				Ok(()),
380				Some("Bal"),
381			);
382
383			// Named, but no GroupID and invalid
384			test(
385				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("ThisCoolName"),
386				"7]]",
387				Err(GroupIDError::new_close("7]]")),
388				Some("ThisCoolName"),
389			);
390			test(
391				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("ADAdsadsdasdDS[]"),
392				"[[invalid]]",
393				Err(GroupIDError::new_open("[[invalid]]")),
394				Some("ADAdsadsdasdDS[]"),
395			);
396			test(
397				VisualBuilder::new(SphereGeometry::new(3.3e9)).named("Bal"),
398				"",
399				Err(GroupIDError::new_empty()),
400				Some("Bal"),
401			);
402
403			// Named with GroupID and valid
404			test(
405				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("Leg_[[L01]]_l04_col"),
406				"7",
407				Ok(()),
408				Some("Leg_[[7]]_l04_col"),
409			);
410			test(
411				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("Arm_[[B01d]]_link_0313c"),
412				"valid4",
413				Ok(()),
414				Some("Arm_[[valid4]]_link_0313c"),
415			);
416			test(
417				VisualBuilder::new(SphereGeometry::new(3.3e9))
418					.named("Bal_[[F900]]_this_doesn't_matter"),
419				"G0-02",
420				Ok(()),
421				Some("Bal_[[G0-02]]_this_doesn't_matter"),
422			);
423
424			// Named with GroupID and invalid
425			test(
426				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("Leg_[[L01]]_l04_col"),
427				"[[7",
428				Err(GroupIDError::new_open("[[7")),
429				Some("Leg_[[L01]]_l04_col"),
430			);
431			test(
432				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("Arm_[[B01d]]_link_0313c"),
433				"[[invalid]]",
434				Err(GroupIDError::new_open("[[invalid]]")),
435				Some("Arm_[[B01d]]_link_0313c"),
436			);
437			test(
438				VisualBuilder::new(SphereGeometry::new(3.3e9))
439					.named("Bal_[[F900]]_this_doesn't_matter"),
440				"",
441				Err(GroupIDError::new_empty()),
442				Some("Bal_[[F900]]_this_doesn't_matter"),
443			);
444		}
445
446		#[test]
447		#[ignore = "TODO"]
448		fn change_group_id_with_material() {
449			todo!()
450		}
451
452		#[test]
453		fn apply_group_id_no_material() {
454			#[inline]
455			fn test(visual_builder: VisualBuilder, name: Option<&str>) {
456				let mut collision_builder = visual_builder;
457				collision_builder.apply_group_id();
458				assert_eq!(
459					collision_builder.name,
460					name.and_then(|name| Some(name.to_owned()))
461				)
462			}
463
464			// No Name
465			test(VisualBuilder::new(BoxGeometry::new(1., 2., 3.)), None);
466			test(VisualBuilder::new(CylinderGeometry::new(32., 5.)), None);
467			test(VisualBuilder::new(SphereGeometry::new(7.)), None);
468
469			// Named, but no GroupID
470			test(
471				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("ThisCoolName"),
472				Some("ThisCoolName"),
473			);
474			test(
475				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("ADAdsadsdasdDS[]"),
476				Some("ADAdsadsdasdDS[]"),
477			);
478			test(
479				VisualBuilder::new(SphereGeometry::new(3.3e9)).named("Bal"),
480				Some("Bal"),
481			);
482
483			// Named, but escaped
484			test(
485				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("This[\\[Cool]\\]Name"),
486				Some("This[[Cool]]Name"),
487			);
488			test(
489				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("ADAdsadsdasdDS[\\[]"),
490				Some("ADAdsadsdasdDS[[]"),
491			);
492			test(
493				VisualBuilder::new(SphereGeometry::new(3.3e9)).named("Bal]\\]"),
494				Some("Bal]]"),
495			);
496
497			// Named with GroupID and valid
498			test(
499				VisualBuilder::new(BoxGeometry::new(1., 2., 3.)).named("Leg_[[L01]]_l04_col"),
500				Some("Leg_L01_l04_col"),
501			);
502			test(
503				VisualBuilder::new(CylinderGeometry::new(32., 5.)).named("Arm_[[B01d]]_link_0313c"),
504				Some("Arm_B01d_link_0313c"),
505			);
506			test(
507				VisualBuilder::new(SphereGeometry::new(3.3e9))
508					.named("Bal_[[F900]]_this_doesn't_matter"),
509				Some("Bal_F900_this_doesn't_matter"),
510			);
511
512			// Named with mixed
513			test(
514				VisualBuilder::new(BoxGeometry::new(1., 2., 3.))
515					.named("Leg_[\\[L01]\\]_[[l04]]_col"),
516				Some("Leg_[[L01]]_l04_col"),
517			);
518			test(
519				VisualBuilder::new(CylinderGeometry::new(32., 5.))
520					.named("Arm_[[B01d]\\]_[\\[link_0313c]]"),
521				Some("Arm_B01d]]_[[link_0313c"),
522			);
523			test(
524				VisualBuilder::new(SphereGeometry::new(3.3e9))
525					.named("Bal_[[F900]]_this_[\\[doesn't]\\]_matter"),
526				Some("Bal_F900_this_[[doesn't]]_matter"),
527			);
528		}
529
530		#[test]
531		#[ignore = "TODO"]
532		fn apply_group_id_with_material() {
533			todo!()
534		}
535	}
536}