1use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5use std::collections::{HashMap, HashSet};
6
7use crate::ModelError;
8
9#[derive(Clone, Debug)]
11pub struct Structure {
12 document: Value,
13 links: Vec<Link>,
14 joints: Vec<Joint>,
15}
16
17#[derive(Clone, Debug)]
19pub struct Link {
20 name: String,
21}
22
23#[derive(Clone, Debug)]
25pub struct Joint {
26 name: String,
27 kind: JointKind,
28 origin: Pose,
29 parent: String,
30 child: String,
31 axis: [f64; 3],
32}
33
34#[derive(Clone, Copy, Debug)]
36pub struct Pose {
37 xyz: [f64; 3],
38 rpy: [f64; 3],
39}
40
41#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44pub enum JointKind {
45 Revolute,
46 Continuous,
47 Prismatic,
48 Fixed,
49 Floating,
50 Planar,
51 Spherical,
52}
53
54impl Structure {
55 pub fn links(&self) -> impl ExactSizeIterator<Item = &Link> {
57 self.links.iter()
58 }
59
60 pub fn joints(&self) -> impl ExactSizeIterator<Item = &Joint> {
62 self.joints.iter()
63 }
64
65 #[must_use]
67 pub fn link(&self, id: &str) -> Option<&Link> {
68 self.links.iter().find(|link| link.name == id)
69 }
70
71 #[must_use]
73 pub fn joint(&self, id: &str) -> Option<&Joint> {
74 self.joints.iter().find(|joint| joint.name == id)
75 }
76
77 pub(crate) fn validate(&self) -> Result<(), ModelError> {
78 validate_unique(self.links.iter().map(Link::name), "link")?;
79 validate_unique(self.joints.iter().map(Joint::name), "joint")?;
80 let link_names = self.links.iter().map(Link::name).collect::<HashSet<_>>();
81 let mut children = HashSet::new();
82 let mut parent_by_child = HashMap::new();
83 for joint in &self.joints {
84 if !link_names.contains(joint.parent()) {
85 return Err(ModelError::Invalid(format!(
86 "joint '{}' references unknown parent link '{}'",
87 joint.name(),
88 joint.parent()
89 )));
90 }
91 if !link_names.contains(joint.child()) {
92 return Err(ModelError::Invalid(format!(
93 "joint '{}' references unknown child link '{}'",
94 joint.name(),
95 joint.child()
96 )));
97 }
98 if !children.insert(joint.child()) {
99 return Err(ModelError::Invalid(format!(
100 "link '{}' is the child of multiple joints",
101 joint.child()
102 )));
103 }
104 if joint.parent() == joint.child() {
105 return Err(ModelError::Invalid(format!(
106 "joint '{}' cannot use '{}' as both parent and child",
107 joint.name(),
108 joint.parent()
109 )));
110 }
111 parent_by_child.insert(joint.child(), joint.parent());
112 }
113 let roots = self
114 .links
115 .iter()
116 .map(Link::name)
117 .filter(|link| !children.contains(link))
118 .collect::<Vec<_>>();
119 if roots.len() != 1 {
120 return Err(ModelError::Invalid(format!(
121 "structure must have exactly one root link, found {}",
122 roots.len()
123 )));
124 }
125 for link in &self.links {
126 let mut seen = HashSet::new();
127 let mut current = Some(link.name());
128 while let Some(link_id) = current {
129 if !seen.insert(link_id) {
130 return Err(ModelError::Invalid(format!(
131 "structure contains a joint cycle involving '{link_id}'"
132 )));
133 }
134 current = parent_by_child.get(link_id).copied();
135 }
136 }
137 if roots[0] != "base_footprint" {
138 return Err(ModelError::Invalid(format!(
139 "structure root link must be 'base_footprint', found '{}'",
140 roots[0]
141 )));
142 }
143 let base_joint = self
144 .joints
145 .iter()
146 .find(|joint| joint.child() == "base_link")
147 .ok_or_else(|| {
148 ModelError::Invalid(
149 "structure must attach 'base_link' under 'base_footprint' with a fixed joint"
150 .to_string(),
151 )
152 })?;
153 if base_joint.parent() != "base_footprint" || base_joint.kind() != JointKind::Fixed {
154 return Err(ModelError::Invalid(
155 "structure must attach 'base_link' directly under 'base_footprint' with a fixed joint"
156 .to_string(),
157 ));
158 }
159 Ok(())
160 }
161
162 pub(crate) fn from_compiler_value(document: Value) -> Result<Self, ModelError> {
163 validate_document(&document)?;
164 let summary: Summary = serde_json::from_value(document.clone())
165 .map_err(|error| ModelError::Invalid(error.to_string()))?;
166 let structure = Self {
167 document,
168 links: summary
169 .links
170 .into_iter()
171 .map(|link| Link { name: link.name })
172 .collect(),
173 joints: summary
174 .joints
175 .into_iter()
176 .map(|joint| Joint {
177 name: joint.name,
178 kind: joint.kind,
179 origin: Pose {
180 xyz: joint.origin.xyz,
181 rpy: joint.origin.rpy,
182 },
183 parent: joint.parent,
184 child: joint.child,
185 axis: joint.axis,
186 })
187 .collect(),
188 };
189 structure.validate()?;
190 Ok(structure)
191 }
192}
193
194impl Link {
195 #[must_use]
196 pub fn name(&self) -> &str {
197 &self.name
198 }
199}
200
201impl Joint {
202 #[must_use]
203 pub fn name(&self) -> &str {
204 &self.name
205 }
206 #[must_use]
207 pub const fn kind(&self) -> JointKind {
208 self.kind
209 }
210 #[must_use]
211 pub const fn origin(&self) -> Pose {
212 self.origin
213 }
214 #[must_use]
215 pub fn parent(&self) -> &str {
216 &self.parent
217 }
218 #[must_use]
219 pub fn child(&self) -> &str {
220 &self.child
221 }
222 #[must_use]
223 pub const fn axis(&self) -> [f64; 3] {
224 self.axis
225 }
226}
227
228impl Pose {
229 #[must_use]
230 pub const fn xyz(self) -> [f64; 3] {
231 self.xyz
232 }
233 #[must_use]
234 pub const fn rpy(self) -> [f64; 3] {
235 self.rpy
236 }
237}
238
239impl Serialize for Structure {
240 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
241 self.document.serialize(serializer)
242 }
243}
244
245impl<'de> Deserialize<'de> for Structure {
246 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
247 let document = Value::deserialize(deserializer)?;
248 Self::from_compiler_value(document).map_err(serde::de::Error::custom)
249 }
250}
251
252#[derive(Deserialize)]
253#[serde(deny_unknown_fields)]
254struct Summary {
255 #[serde(rename = "name")]
256 _name: String,
257 links: Vec<LinkSummary>,
258 joints: Vec<JointSummary>,
259 #[serde(rename = "materials")]
260 _materials: Vec<Value>,
261}
262
263#[derive(Deserialize)]
264#[serde(deny_unknown_fields)]
265struct LinkSummary {
266 name: String,
267 #[serde(rename = "inertial")]
268 _inertial: Value,
269 #[serde(rename = "visuals")]
270 _visuals: Vec<Value>,
271 #[serde(rename = "collisions")]
272 _collisions: Vec<Value>,
273}
274
275#[derive(Deserialize)]
276#[serde(deny_unknown_fields)]
277struct JointSummary {
278 name: String,
279 kind: JointKind,
280 origin: PoseSummary,
281 parent: String,
282 child: String,
283 axis: [f64; 3],
284 #[serde(rename = "limit")]
285 _limit: Value,
286 #[serde(rename = "calibration")]
287 _calibration: Option<Value>,
288 #[serde(rename = "dynamics")]
289 _dynamics: Option<Value>,
290 #[serde(rename = "mimic")]
291 _mimic: Option<Value>,
292 #[serde(rename = "safety")]
293 _safety: Option<Value>,
294}
295
296#[derive(Deserialize)]
297#[serde(deny_unknown_fields)]
298struct PoseSummary {
299 xyz: [f64; 3],
300 rpy: [f64; 3],
301}
302
303fn validate_document(value: &Value) -> Result<(), ModelError> {
304 let root = object(value, "structure")?;
305 exact(root, &["name", "links", "joints", "materials"], "structure")?;
306 for link in array(root, "links")? {
307 validate_link(link)?;
308 }
309 for joint in array(root, "joints")? {
310 validate_joint(joint)?;
311 }
312 for material in array(root, "materials")? {
313 validate_material(material)?;
314 }
315 Ok(())
316}
317
318fn validate_link(value: &Value) -> Result<(), ModelError> {
319 let map = object(value, "link")?;
320 exact(map, &["name", "inertial", "visuals", "collisions"], "link")?;
321 validate_inertial(required(map, "inertial")?)?;
322 for visual in array(map, "visuals")? {
323 let visual = object(visual, "visual")?;
324 exact(
325 visual,
326 &["name", "origin", "geometry", "material"],
327 "visual",
328 )?;
329 validate_pose(required(visual, "origin")?)?;
330 validate_geometry(required(visual, "geometry")?)?;
331 if let Some(material) = visual.get("material").filter(|value| !value.is_null()) {
332 validate_material(material)?;
333 }
334 }
335 for collision in array(map, "collisions")? {
336 let collision = object(collision, "collision")?;
337 exact(collision, &["name", "origin", "geometry"], "collision")?;
338 validate_pose(required(collision, "origin")?)?;
339 validate_geometry(required(collision, "geometry")?)?;
340 }
341 Ok(())
342}
343
344fn validate_inertial(value: &Value) -> Result<(), ModelError> {
345 let map = object(value, "inertial")?;
346 exact(map, &["origin", "mass_kg", "inertia"], "inertial")?;
347 validate_pose(required(map, "origin")?)?;
348 exact(
349 object(required(map, "inertia")?, "inertia")?,
350 &["ixx", "ixy", "ixz", "iyy", "iyz", "izz"],
351 "inertia",
352 )
353}
354
355fn validate_joint(value: &Value) -> Result<(), ModelError> {
356 let map = object(value, "joint")?;
357 exact(
358 map,
359 &[
360 "name",
361 "kind",
362 "origin",
363 "parent",
364 "child",
365 "axis",
366 "limit",
367 "calibration",
368 "dynamics",
369 "mimic",
370 "safety",
371 ],
372 "joint",
373 )?;
374 validate_pose(required(map, "origin")?)?;
375 exact(
376 object(required(map, "limit")?, "joint limit")?,
377 &["lower", "upper", "effort", "velocity"],
378 "joint limit",
379 )?;
380 optional_exact(map, "calibration", &["rising", "falling"])?;
381 optional_exact(map, "dynamics", &["damping", "friction"])?;
382 optional_exact(map, "mimic", &["joint", "multiplier", "offset"])?;
383 optional_exact(
384 map,
385 "safety",
386 &[
387 "soft_lower_limit",
388 "soft_upper_limit",
389 "k_position",
390 "k_velocity",
391 ],
392 )
393}
394
395fn validate_material(value: &Value) -> Result<(), ModelError> {
396 exact(
397 object(value, "material")?,
398 &["name", "color", "texture"],
399 "material",
400 )
401}
402
403fn validate_pose(value: &Value) -> Result<(), ModelError> {
404 exact(object(value, "pose")?, &["xyz", "rpy"], "pose")
405}
406
407fn validate_geometry(value: &Value) -> Result<(), ModelError> {
408 let map = object(value, "geometry")?;
409 let kind = map
410 .get("kind")
411 .and_then(Value::as_str)
412 .ok_or_else(|| ModelError::Invalid("geometry.kind must be a string".into()))?;
413 let fields: &[&str] = match kind {
414 "box" => &["kind", "size"],
415 "cylinder" | "capsule" => &["kind", "radius", "length"],
416 "sphere" => &["kind", "radius"],
417 "mesh" => &["kind", "filename", "scale"],
418 _ => {
419 return Err(ModelError::Invalid(format!(
420 "unsupported geometry kind '{kind}'"
421 )));
422 }
423 };
424 exact(map, fields, "geometry")
425}
426
427fn optional_exact(map: &Map<String, Value>, key: &str, fields: &[&str]) -> Result<(), ModelError> {
428 if let Some(value) = map.get(key).filter(|value| !value.is_null()) {
429 exact(object(value, key)?, fields, key)?;
430 }
431 Ok(())
432}
433
434fn object<'a>(value: &'a Value, label: &str) -> Result<&'a Map<String, Value>, ModelError> {
435 value
436 .as_object()
437 .ok_or_else(|| ModelError::Invalid(format!("{label} must be an object")))
438}
439
440fn required<'a>(map: &'a Map<String, Value>, key: &str) -> Result<&'a Value, ModelError> {
441 map.get(key)
442 .ok_or_else(|| ModelError::Invalid(format!("missing required field '{key}'")))
443}
444
445fn array<'a>(map: &'a Map<String, Value>, key: &str) -> Result<&'a [Value], ModelError> {
446 required(map, key)?
447 .as_array()
448 .map(Vec::as_slice)
449 .ok_or_else(|| ModelError::Invalid(format!("'{key}' must be an array")))
450}
451
452fn exact(map: &Map<String, Value>, allowed: &[&str], label: &str) -> Result<(), ModelError> {
453 if let Some(key) = map.keys().find(|key| !allowed.contains(&key.as_str())) {
454 return Err(ModelError::Invalid(format!(
455 "{label} contains unknown field '{key}'"
456 )));
457 }
458 Ok(())
459}
460
461fn validate_unique<'a>(
462 names: impl Iterator<Item = &'a str>,
463 label: &str,
464) -> Result<(), ModelError> {
465 let mut seen = HashSet::new();
466 for name in names {
467 if !seen.insert(name) {
468 return Err(ModelError::Invalid(format!(
469 "duplicate {label} identity '{name}'"
470 )));
471 }
472 }
473 Ok(())
474}