1use std::collections::{BTreeMap, HashMap, HashSet};
15
16use crate::{Joint, JointKind, Motion};
17use geop_core_math::{
18 geop_error::{GeopError, GeopResult},
19 solvers::walk::{Take, flood},
20 union_find::UnionFind,
21};
22
23use crate::urdf::Mechanism;
24
25#[derive(Clone, Debug, PartialEq)]
27pub struct Tree {
28 pub links: Vec<LinkBodies>,
31 pub joints: Vec<TreeJoint>,
34}
35
36#[derive(Clone, Debug, PartialEq)]
39pub struct LinkBodies {
40 pub name: String,
41 pub bodies: Vec<usize>,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq)]
48pub struct TreeJoint {
49 pub joint: usize,
50 pub parent: usize,
51 pub child: usize,
52 pub forward: bool,
53}
54
55fn groups(mechanism: &Mechanism) -> Vec<usize> {
60 let count = mechanism.bodies.len();
61 let mut groups = UnionFind::new(count + 1);
62 for (b, body) in mechanism.bodies.iter().enumerate() {
63 match body.carrier {
64 Some(carrier) => groups.union(carrier + 1, b + 1),
65 None if !body.free => groups.union(0, b + 1),
66 None => {}
67 }
68 }
69 for (_, [a, b]) in &mechanism.constraints {
70 groups.union(node(*a), node(*b));
71 }
72 for joint in &mechanism.joints {
73 if joint.kind.joint == Joint::Fastened {
74 groups.union(node(joint.a.body), node(joint.b.body));
75 }
76 }
77 (0..=count).map(|node| groups.find(node)).collect()
78}
79
80fn node(body: Option<usize>) -> usize {
82 body.map_or(0, |b| b + 1)
83}
84
85pub fn tree(mechanism: &Mechanism) -> GeopResult<Tree> {
88 let count = mechanism.bodies.len();
89 let groups = groups(mechanism);
90 for joint in &mechanism.joints {
91 check_joint(&joint.name, &joint.kind)?;
92 }
93
94 let part = |b: usize| mechanism.bodies[b].name.as_str();
95 let members =
96 |group: usize| -> Vec<usize> { (0..count).filter(|&b| groups[b + 1] == group).collect() };
97 let link_named = |group: usize| -> LinkBodies {
98 let bodies = members(group);
99 let fixed = (bodies.iter()).find(|&&b| {
100 let body = &mechanism.bodies[b];
101 !body.free && body.carrier.is_none()
102 });
103 let name = match fixed.or(bodies.first()) {
104 Some(&b) => part(b).to_string(),
105 None => "base".to_string(),
106 };
107 LinkBodies { name, bodies }
108 };
109
110 let moving: Vec<usize> = (0..mechanism.joints.len())
111 .filter(|&j| mechanism.joints[j].kind.joint != Joint::Fastened)
112 .collect();
113 let ends = |j: usize| {
114 let joint = &mechanism.joints[j];
115 (groups[node(joint.a.body)], groups[node(joint.b.body)])
116 };
117 let mut tree = Tree {
118 links: vec![link_named(0)],
119 joints: Vec::new(),
120 };
121 let mut link_of_group = HashMap::from([(0, 0)]);
122 let mut group_of_link = vec![0];
123 let mut carried_by: Vec<Option<usize>> = vec![None];
125 let mut used = HashSet::new();
126 flood([0], Take::Oldest, |&link| {
129 let group = group_of_link[link];
130 let mut children = Vec::new();
131 for &j in &moving {
132 let (a, b) = ends(j);
133 if used.contains(&j) || (a != group && b != group) {
134 continue;
135 }
136 used.insert(j);
137 if a == b {
138 return Err(GeopError::new(format!(
139 "the joint {:?} joins {}, which mates already hold rigidly together: a closed loop, which URDF — a tree of links — cannot express; remove the joint or the mates",
140 mechanism.joints[j].name, tree.links[link].name
141 )));
142 }
143 let (other, forward) = if a == group { (b, true) } else { (a, false) };
144 if let Some(&reached) = link_of_group.get(&other) {
145 return Err(loop_error(mechanism, &tree, &carried_by, link, reached, j));
146 }
147 let child = tree.links.len();
148 link_of_group.insert(other, child);
149 group_of_link.push(other);
150 tree.links.push(link_named(other));
151 carried_by.push(Some(tree.joints.len()));
152 tree.joints.push(TreeJoint {
153 joint: j,
154 parent: link,
155 child,
156 forward,
157 });
158 children.push(child);
159 }
160 Ok(children)
161 })?;
162
163 let unreached: Vec<&str> = (0..count)
164 .filter(|&b| !link_of_group.contains_key(&groups[b + 1]))
165 .map(part)
166 .collect();
167 if !unreached.is_empty() {
168 let root = &tree.links[0];
169 let one = unreached.len() == 1;
170 let parts = unreached.join(", ");
171 return Err(GeopError::new(if root.bodies.is_empty() {
172 format!(
173 "no part is fixed, so {parts} {} from nothing: a URDF robot is a tree of links from one root, the fixed part — fix the part the robot stands on",
174 if one { "hangs" } else { "hang" }
175 )
176 } else {
177 format!(
178 "{parts} {} joined to the fixed part {} by neither joints nor mates: a URDF robot is one tree of links from one root — join {} to it, or fix {}",
179 if one { "is" } else { "are" },
180 root.name,
181 if one { "it" } else { "them" },
182 if one { "it" } else { "them" },
183 )
184 }));
185 }
186 Ok(tree)
187}
188
189fn check_joint(name: &str, kind: &JointKind) -> GeopResult<()> {
193 let problem = match kind.joint {
194 Joint::Revolute | Joint::Slider | Joint::Fastened => None,
195 other => Some(format!(
196 "is {}, and a URDF joint turns about one axis or slides along one, or neither: use a revolute, a slider or a fastened joint",
197 other.label().to_lowercase(),
198 )),
199 };
200 let [min, max] = match kind.joint {
201 Joint::Slider => kind.limits(Motion::Slide),
202 _ => kind.limits(Motion::Turn),
203 };
204 let problem = problem.or_else(|| match kind.joint {
205 Joint::Revolute if min.is_some() != max.is_some() => Some(format!(
206 "is limited one way only ({}), and a URDF revolute joint is limited both ways or not at all (continuous): give it both limits or neither",
207 if min.is_some() { "a min" } else { "a max" }
208 )),
209 Joint::Slider if min.is_none() || max.is_none() => Some(
210 "slides without a min and a max, and a URDF prismatic joint needs both: give the joint its travel"
211 .to_string(),
212 ),
213 _ => None,
214 });
215 match problem {
216 Some(problem) => Err(GeopError::new(format!("the joint {name:?} {problem}"))),
217 None => Ok(()),
218 }
219}
220
221pub fn check_rigid(
226 mechanism: &Mechanism,
227 tree: &Tree,
228 freedom: &BTreeMap<String, usize>,
229) -> GeopResult<()> {
230 let loose: Vec<usize> = (0..mechanism.bodies.len())
231 .filter(|&b| {
232 freedom
233 .get(&mechanism.bodies[b].name)
234 .is_some_and(|&dof| dof > 0)
235 })
236 .collect();
237 if loose.is_empty() {
238 return Ok(());
239 }
240 let link_of = |body: Option<usize>| match body {
243 Some(b) => tree.links.iter().position(|l| l.bodies.contains(&b)),
244 None => Some(0),
245 };
246 let links: HashSet<Option<usize>> = loose.iter().map(|&b| link_of(Some(b))).collect();
247 let in_links = |ends: [Option<usize>; 2]| ends.iter().any(|&e| links.contains(&link_of(e)));
248 let parts: Vec<&str> = (loose.iter())
249 .map(|&b| mechanism.bodies[b].name.as_str())
250 .collect();
251 let mut mates: Vec<&str> = (mechanism.constraints.iter())
252 .filter(|(_, ends)| in_links(*ends))
253 .map(|(name, _)| name.as_str())
254 .collect();
255 mates.extend(
256 (mechanism.joints.iter())
257 .filter(|j| j.kind.joint == Joint::Fastened && in_links([j.a.body, j.b.body]))
258 .map(|j| j.name.as_str()),
259 );
260 Err(GeopError::new(format!(
261 "{} {} held by the mates {} alone, which leave {} free to move: a URDF robot moves only at its joints — join the parts by revolute or slider joints, or hold them fully",
262 parts.join(", "),
263 if parts.len() == 1 { "is" } else { "are" },
264 mates.join(", "),
265 if parts.len() == 1 { "it" } else { "them" },
266 )))
267}
268
269fn loop_error(
273 mechanism: &Mechanism,
274 tree: &Tree,
275 carried_by: &[Option<usize>],
276 from: usize,
277 reached: usize,
278 closing: usize,
279) -> GeopError {
280 let up = |link: usize| -> Vec<usize> {
282 std::iter::successors(Some(link), |&l| {
283 carried_by[l].map(|j| tree.joints[j].parent)
284 })
285 .collect()
286 };
287 let (a, b) = (up(from), up(reached));
288 let common = a.iter().find(|l| b.contains(l)).copied().unwrap_or(0);
289 let before = |path: &[usize]| -> Vec<usize> {
290 path.iter().copied().take_while(|&l| l != common).collect()
291 };
292 let (a, b) = (before(&a), before(&b));
293 let mut links: Vec<usize> = a.clone();
294 links.push(common);
295 links.extend(b.iter().rev());
296 let mut joints: Vec<&str> = a
297 .iter()
298 .chain(&b)
299 .filter_map(|&l| carried_by[l])
300 .map(|j| mechanism.joints[tree.joints[j].joint].name.as_str())
301 .collect();
302 joints.push(mechanism.joints[closing].name.as_str());
303 joints.sort();
304 let links: Vec<&str> = links.iter().map(|&l| tree.links[l].name.as_str()).collect();
305 GeopError::new(format!(
306 "the joints {} close a loop through {}: URDF describes a tree of links, and cannot express a closed kinematic chain — remove one of these joints to export the rest",
307 joints.join(", "),
308 links.join(", ")
309 ))
310}