robot_description_builder/material/
descriptor.rs

1/* DOC TODO:
2 o Internal Module Doc
3 - MaterialDescriptor
4*/
5use crate::identifiers::GroupIDChanger;
6
7use super::{data::MaterialData, Material};
8
9/// A descriptor for a future `Material`.
10///
11/// A [`MaterialDescriptor`] is used to construct a [`Material`].
12///
13/// A `Descriptor` is a smiliar idea as a `Builder`,
14///  but there is an important difference between which has to be adressed with a differen name.
15///
16/// A `Builder` would always construct a new instance of a struct, in this project meaning it cannot be used twice in the same [`KinematicTree`](crate::KinematicTree).
17/// Since using a `Builder` twice would result in two exactly the same objects.
18///
19/// A `Descriptor` on the other hand, first checks if there already exists an instance which matches it description, in this case a [`Material`].
20/// If not the case a new instance ([`Material`]) is constructed and added to the index.
21/// If an instance already exists, which exactly matches the description, then the 'new' [`Material`] will refer to the pre-existing data.
22///
23/// This is desirable in the case of [Materials](Material), since they are often reused.
24/// This could also allow for changing a [`Material`] and the other used of it changing withit.
25///
26/// # OLD STUFF TODO: UPDATE
27/// When a `MaterialDescriptor` is constructed for a specific `KinematicDataTee`, the following steps happen:
28///  1. Check if the description of the `MaterialDescriptor` matches a pre-existing `Material` already in the tree.
29///     - If the a `Material` matches the description, the reference to that material is returned.
30///     - If no `Material` matches the desctiption, a new `Material` is constructed and inserted to the `material_index` of the `KinematicDataTree` and the reference is returned.
31///     - If only the `name` of the `Material` matches, an error is raised.
32#[derive(Debug, PartialEq, Clone)]
33pub struct MaterialDescriptor {
34	name: Option<String>,
35	data: MaterialData,
36}
37
38impl MaterialDescriptor {
39	/// Creates a new [`MaterialDescriptor`] with a solid color (rgba)
40	///
41	/// The `red`, `green`, `blue` and `alpha` fields expect a value between 0 and 1.
42	///
43	/// # Example
44	///
45	/// ```
46	/// # use robot_description_builder::material::MaterialDescriptor;
47	/// MaterialDescriptor::new_color(1., 0.4, 0.6, 0.5)
48	/// # ;
49	/// ```
50	pub fn new_color(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
51		MaterialDescriptor {
52			name: None,
53			data: MaterialData::Color(red, green, blue, alpha),
54		}
55	}
56
57	/// Creates a new [`MaterialDescriptor`] with a solid color (rgb).
58	///
59	/// The `red`, `green`, `blue` fields expect a value between 0 and 1.
60	///
61	/// # Example
62	///
63	/// ```
64	/// # use robot_description_builder::material::MaterialDescriptor;
65	/// MaterialDescriptor::new_rgb(1., 0.4, 0.6)
66	/// # ;
67	/// ```
68	pub fn new_rgb(red: f32, green: f32, blue: f32) -> Self {
69		MaterialDescriptor {
70			name: None,
71			data: MaterialData::Color(red, green, blue, 1.),
72		}
73	}
74
75	/// Creates a new [`MaterialDescriptor`] with a texture.
76	///
77	/// `texture_path` should be a valid package path (e.g. `"package://NAME_OF_PACKAGE/path/{texture}"`). You are on your own here.
78	///
79	/// # Example
80	///
81	/// ```
82	/// # use robot_description_builder::material::MaterialDescriptor;
83	/// MaterialDescriptor::new_texture("package://robot_description/textures/example_texture.png")
84	/// # ;
85	/// ```
86	pub fn new_texture(texture_path: impl Into<String>) -> Self {
87		MaterialDescriptor {
88			name: None,
89			data: MaterialData::Texture(texture_path.into()),
90		}
91	}
92
93	/// Creates a new [`MaterialDescriptor`] from a pre-existing [`MaterialData`].
94	pub(crate) fn new_data(data: MaterialData) -> Self {
95		MaterialDescriptor { name: None, data }
96	}
97
98	/// Adds a `name` to the [`MaterialDescriptor`], so it can later be used as a referenced [`Material`].
99	///
100	/// # Important
101	/// When a named [`Material`] is used, it needs to be the same as all materials with the same name.
102	/// Otherwise, problems will arise later down the line.
103	///
104	/// # Example
105	///
106	/// ```
107	/// # use robot_description_builder::material::MaterialDescriptor;
108	/// MaterialDescriptor::new_rgb(0.5, 1., 0.5).named("soft-green")
109	/// # ;
110	/// ```
111	pub fn named(mut self, name: impl Into<String>) -> Self {
112		self.name = Some(name.into());
113		self
114	}
115
116	/// Builds a [`Material`] from the [`MaterialDescriptor`].
117	pub(crate) fn build(self) -> Material {
118		match self.name {
119			Some(name) => Material::new_named_uninited(name, self.data),
120			None => Material::new_unnamed(self.data),
121		}
122	}
123}
124
125/// Non-builder methods
126impl MaterialDescriptor {
127	/// Gets the optional of the [`MaterialDescriptor`] as a optional reference.
128	pub fn name(&self) -> Option<&String> {
129		self.name.as_ref()
130	}
131
132	/// Gets a reference to the [`MaterialData`] of the [`MaterialDescriptor`].
133	pub fn data(&self) -> &MaterialData {
134		&self.data
135	}
136}
137
138impl GroupIDChanger for MaterialDescriptor {
139	unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str) {
140		if let Some(name) = self.name.as_mut() {
141			name.change_group_id_unchecked(new_group_id);
142		}
143	}
144
145	fn apply_group_id(&mut self) {
146		if let Some(name) = self.name.as_mut() {
147			name.apply_group_id();
148		}
149	}
150}
151
152#[cfg(test)]
153mod tests {
154	use super::MaterialDescriptor;
155	use test_log::test;
156
157	mod group_id_changer {
158		use super::{test, MaterialDescriptor};
159		use crate::identifiers::{GroupIDChanger, GroupIDError};
160
161		#[inline]
162		fn test_change_group_id_unchecked(
163			material_builder: MaterialDescriptor,
164			new_group_id: &str,
165			final_name: Option<&str>,
166		) {
167			let mut material_builder = material_builder;
168			unsafe {
169				material_builder.change_group_id_unchecked(new_group_id);
170			}
171			assert_eq!(
172				material_builder.name,
173				final_name.and_then(|final_name| Some(final_name.to_owned()))
174			)
175		}
176
177		#[test]
178		fn change_group_id_unchecked_no_name() {
179			// Valid
180			test_change_group_id_unchecked(
181				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.),
182				"R04",
183				None,
184			);
185			test_change_group_id_unchecked(MaterialDescriptor::new_rgb(1., 1., 0.), "C064w", None);
186			test_change_group_id_unchecked(
187				MaterialDescriptor::new_texture("package://some/texture/path/text.texture"),
188				"Yellow",
189				None,
190			);
191
192			// Invalid
193			test_change_group_id_unchecked(
194				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.),
195				"[[R04",
196				None,
197			);
198			test_change_group_id_unchecked(
199				MaterialDescriptor::new_rgb(1., 1., 0.),
200				"C064w]]",
201				None,
202			);
203			test_change_group_id_unchecked(
204				MaterialDescriptor::new_texture("package://some/texture/path/text.texture"),
205				"",
206				None,
207			);
208		}
209
210		#[test]
211		fn change_group_id_unchecked_with_name() {
212			// name with field and Valid
213			test_change_group_id_unchecked(
214				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_[[L01]]_mat"),
215				"R04",
216				Some("Leg_[[R04]]_mat"),
217			);
218			test_change_group_id_unchecked(
219				MaterialDescriptor::new_rgb(1., 1., 0.).named("rgb_[[dsd]]_dsdadavj,hnmn b v"),
220				"C064w",
221				Some("rgb_[[C064w]]_dsdadavj,hnmn b v"),
222			);
223			test_change_group_id_unchecked(
224				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
225					.named("SomeCoolTexture[[GroupID]]"),
226				"Yellow",
227				Some("SomeCoolTexture[[Yellow]]"),
228			);
229
230			// Named with field and Invalid
231			test_change_group_id_unchecked(
232				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_[[L01]]_mat"),
233				"[[R04",
234				Some("Leg_[[[[R04]]_mat"),
235			);
236			test_change_group_id_unchecked(
237				MaterialDescriptor::new_rgb(1., 1., 0.).named("[[CADcs]]SomeColor"),
238				"C064w]]",
239				Some("[[C064w]]]]SomeColor"),
240			);
241			test_change_group_id_unchecked(
242				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
243					.named("SomeCoolTexture[[GroupID]]"),
244				"",
245				Some("SomeCoolTexture[[]]"),
246			);
247			// name without field and Valid
248			test_change_group_id_unchecked(
249				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_L01_mat"),
250				"R04",
251				Some("Leg_L01_mat"),
252			);
253			test_change_group_id_unchecked(
254				MaterialDescriptor::new_rgb(1., 1., 0.).named("rgb_[\\[dsd]\\]_dsdadavj,hnmn b v"),
255				"C064w",
256				Some("rgb_[\\[dsd]\\]_dsdadavj,hnmn b v"),
257			);
258			test_change_group_id_unchecked(
259				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
260					.named("SomeCoolTexture[\\[GroupID]]"),
261				"Yellow",
262				Some("SomeCoolTexture[\\[GroupID]]"),
263			);
264
265			// Named without field and Invalid
266			test_change_group_id_unchecked(
267				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_L01_mat"),
268				"[[R04",
269				Some("Leg_L01_mat"),
270			);
271			test_change_group_id_unchecked(
272				MaterialDescriptor::new_rgb(1., 1., 0.).named("[[CADcs]\\]SomeColor"),
273				"C064w]]",
274				Some("[[CADcs]\\]SomeColor"),
275			);
276			test_change_group_id_unchecked(
277				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
278					.named("SomeCoolTexture_GroupID_"),
279				"",
280				Some("SomeCoolTexture_GroupID_"),
281			);
282		}
283
284		#[inline]
285		fn test_change_group_id(
286			material_builder: MaterialDescriptor,
287			new_group_id: &str,
288			change_result: Result<(), GroupIDError>,
289			final_name: Option<&str>,
290		) {
291			let mut material_builder = material_builder;
292			assert_eq!(
293				material_builder.change_group_id(new_group_id),
294				change_result
295			);
296			assert_eq!(
297				material_builder.name,
298				final_name.and_then(|final_name| Some(final_name.to_owned()))
299			)
300		}
301
302		#[test]
303		fn change_group_id_no_name() {
304			// Valid
305			test_change_group_id(
306				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.),
307				"R04",
308				Ok(()),
309				None,
310			);
311			test_change_group_id(
312				MaterialDescriptor::new_rgb(1., 1., 0.),
313				"C064w",
314				Ok(()),
315				None,
316			);
317			test_change_group_id(
318				MaterialDescriptor::new_texture("package://some/texture/path/text.texture"),
319				"Yellow",
320				Ok(()),
321				None,
322			);
323
324			// Invalid
325			test_change_group_id(
326				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.),
327				"[[R04",
328				Err(GroupIDError::new_open("[[R04")),
329				None,
330			);
331			test_change_group_id(
332				MaterialDescriptor::new_rgb(1., 1., 0.),
333				"C064w]]",
334				Err(GroupIDError::new_close("C064w]]")),
335				None,
336			);
337			test_change_group_id(
338				MaterialDescriptor::new_texture("package://some/texture/path/text.texture"),
339				"",
340				Err(GroupIDError::new_empty()),
341				None,
342			);
343		}
344
345		#[test]
346		fn change_group_id_with_name() {
347			// name with field and Valid
348			test_change_group_id(
349				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_[[L01]]_mat"),
350				"R04",
351				Ok(()),
352				Some("Leg_[[R04]]_mat"),
353			);
354			test_change_group_id(
355				MaterialDescriptor::new_rgb(1., 1., 0.).named("rgb_[[dsd]]_dsdadavj,hnmn b v"),
356				"C064w",
357				Ok(()),
358				Some("rgb_[[C064w]]_dsdadavj,hnmn b v"),
359			);
360			test_change_group_id(
361				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
362					.named("SomeCoolTexture[[GroupID]]"),
363				"Yellow",
364				Ok(()),
365				Some("SomeCoolTexture[[Yellow]]"),
366			);
367
368			// Named with field and Invalid
369			test_change_group_id(
370				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_[[L01]]_mat"),
371				"[[R04",
372				Err(GroupIDError::new_open("[[R04")),
373				Some("Leg_[[L01]]_mat"),
374			);
375			test_change_group_id(
376				MaterialDescriptor::new_rgb(1., 1., 0.).named("[[CADcs]]SomeColor"),
377				"C064w]]",
378				Err(GroupIDError::new_close("C064w]]")),
379				Some("[[CADcs]]SomeColor"),
380			);
381			test_change_group_id(
382				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
383					.named("SomeCoolTexture[[GroupID]]"),
384				"",
385				Err(GroupIDError::new_empty()),
386				Some("SomeCoolTexture[[GroupID]]"),
387			);
388			// name without field and Valid
389			test_change_group_id(
390				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_L01_mat"),
391				"R04",
392				Ok(()),
393				Some("Leg_L01_mat"),
394			);
395			test_change_group_id(
396				MaterialDescriptor::new_rgb(1., 1., 0.).named("rgb_[\\[dsd]\\]_dsdadavj,hnmn b v"),
397				"C064w",
398				Ok(()),
399				Some("rgb_[\\[dsd]\\]_dsdadavj,hnmn b v"),
400			);
401			test_change_group_id(
402				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
403					.named("SomeCoolTexture[\\[GroupID]]"),
404				"Yellow",
405				Ok(()),
406				Some("SomeCoolTexture[\\[GroupID]]"),
407			);
408
409			// Named without field and Invalid
410			test_change_group_id(
411				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_L01_mat"),
412				"[[R04",
413				Err(GroupIDError::new_open("[[R04")),
414				Some("Leg_L01_mat"),
415			);
416			test_change_group_id(
417				MaterialDescriptor::new_rgb(1., 1., 0.).named("[[CADcs]\\]SomeColor"),
418				"C064w]]",
419				Err(GroupIDError::new_close("C064w]]")),
420				Some("[[CADcs]\\]SomeColor"),
421			);
422			test_change_group_id(
423				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
424					.named("SomeCoolTexture_GroupID_"),
425				"",
426				Err(GroupIDError::new_empty()),
427				Some("SomeCoolTexture_GroupID_"),
428			);
429		}
430
431		#[inline]
432		fn test_apply_group_id(material_builder: MaterialDescriptor, final_name: Option<&str>) {
433			let mut material_builder = material_builder;
434			material_builder.apply_group_id();
435			assert_eq!(
436				material_builder.name,
437				final_name.and_then(|final_name| Some(final_name.to_owned()))
438			)
439		}
440
441		#[test]
442		fn apply_group_id_no_name() {
443			test_apply_group_id(MaterialDescriptor::new_color(1., 0.5, 0.25, 0.), None);
444			test_apply_group_id(MaterialDescriptor::new_rgb(1., 1., 0.), None);
445			test_apply_group_id(
446				MaterialDescriptor::new_texture("package://some/texture/path/text.texture"),
447				None,
448			);
449		}
450
451		#[test]
452		fn apply_group_id_with_name() {
453			// name with field and Valid
454			test_apply_group_id(
455				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_[[L01]]_mat"),
456				Some("Leg_L01_mat"),
457			);
458			test_apply_group_id(
459				MaterialDescriptor::new_rgb(1., 1., 0.).named("rgb_[[dsd]]_dsdadavj,hnmn b v"),
460				Some("rgb_dsd_dsdadavj,hnmn b v"),
461			);
462			test_apply_group_id(
463				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
464					.named("SomeCoolTexture[[GroupID]]"),
465				Some("SomeCoolTextureGroupID"),
466			);
467
468			// name with field and Valid and escpaed
469			test_apply_group_id(
470				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_[\\[[[L01]]_mat]\\]"),
471				Some("Leg_[[L01_mat]]"),
472			);
473			test_apply_group_id(
474				MaterialDescriptor::new_rgb(1., 1., 0.)
475					.named("rgb_[[dsd]]_d[\\[sdadavj]\\],hnmn b v"),
476				Some("rgb_dsd_d[[sdadavj]],hnmn b v"),
477			);
478			test_apply_group_id(
479				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
480					.named("SomeCoolTexture[[Gro[\\[upID]]"),
481				Some("SomeCoolTextureGro[[upID"),
482			);
483
484			// This one is has too many opening and closign brackets
485			test_apply_group_id(
486				MaterialDescriptor::new_rgb(1., 1., 0.)
487					.named("rgb_[[dsd]]_d[[sdadavj]\\],hnmn b v"),
488				Some("rgb_[[dsd]]_d[[sdadavj]\\],hnmn b v"),
489			);
490
491			// name without field and Valid
492			test_apply_group_id(
493				MaterialDescriptor::new_color(1., 0.5, 0.25, 0.).named("Leg_L01_mat"),
494				Some("Leg_L01_mat"),
495			);
496			test_apply_group_id(
497				MaterialDescriptor::new_rgb(1., 1., 0.).named("rgb_[\\[dsd]\\]_dsdadavj,hnmn b v"),
498				Some("rgb_[[dsd]]_dsdadavj,hnmn b v"),
499			);
500			test_apply_group_id(
501				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
502					.named("SomeCoolTexture[\\[GroupID"),
503				Some("SomeCoolTexture[[GroupID"),
504			);
505
506			// This one is has not one of the required amounts of correct brackets
507			test_apply_group_id(
508				MaterialDescriptor::new_texture("package://some/texture/path/text.texture")
509					.named("SomeCoolTexture[\\[GroupID]]"),
510				Some("SomeCoolTexture[\\[GroupID]]"),
511			);
512		}
513	}
514}