1use crate::model::{Kinematics, Machine, MachineKind, Point3, RigidTransform, RotaryAxis};
22use serde::{Deserialize, Serialize};
23use std::f64::consts::PI;
24
25pub mod library;
26pub mod post;
27pub mod sequence;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum Carrier {
32 Table,
34 Head,
36}
37
38#[derive(Clone, Debug, PartialEq)]
40pub struct Joint {
41 pub name: String,
43 pub direction: Point3,
45 pub origin: Point3,
48 pub limits_deg: Option<[f64; 2]>,
50 pub carrier: Carrier,
51}
52
53pub use crate::model::Link;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum ChainKind {
60 Mill3,
62 Mill4,
64 TableTable,
66 HeadHead,
68 HeadTable,
70}
71
72#[derive(Clone, Debug, PartialEq)]
74pub struct Chain {
75 pub kind: ChainKind,
76 pub joints: Vec<Joint>,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct KinematicsOptions {
82 pub tcp: bool,
86 pub singular_cone_deg: f64,
89 pub cos_eps: f64,
92}
93
94impl Default for KinematicsOptions {
95 fn default() -> Self {
96 Self { tcp: true, singular_cone_deg: 0.5, cos_eps: 1e-6 }
97 }
98}
99
100impl KinematicsOptions {
101 pub fn validate(&self) -> Result<(), KinematicsError> {
102 if !self.singular_cone_deg.is_finite() || self.singular_cone_deg < 0.0 || self.singular_cone_deg >= 90.0 {
103 return Err(KinematicsError::InvalidInput("singular cone must be in [0, 90) degrees".into()));
104 }
105 if !self.cos_eps.is_finite() || self.cos_eps < 0.0 || self.cos_eps > 1e-3 {
106 return Err(KinematicsError::InvalidInput("cosine tolerance must be in [0, 1e-3]".into()));
107 }
108 Ok(())
109 }
110}
111
112#[derive(Clone, Debug, PartialEq)]
114pub struct Pose {
115 pub linear: Point3,
117 pub angles_deg: Vec<f64>,
120 pub branch: u8,
122 pub singular: bool,
124}
125
126#[derive(Clone, Debug, PartialEq)]
127pub enum KinematicsError {
128 InvalidMachine(String),
130 InvalidInput(String),
131 InvalidAxis,
133 Unreachable { axis: Point3 },
135 LimitViolation { index: usize, joint: String },
137}
138
139impl std::fmt::Display for KinematicsError {
140 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141 match self {
142 KinematicsError::InvalidMachine(m) => write!(f, "machine: {m}"),
143 KinematicsError::InvalidInput(m) => write!(f, "kinematics: {m}"),
144 KinematicsError::InvalidAxis => write!(f, "tool axis must be a finite non-zero vector"),
145 KinematicsError::Unreachable { axis } => {
146 write!(f, "tool axis [{}, {}, {}] is unreachable on this machine", axis.x, axis.y, axis.z)
147 }
148 KinematicsError::LimitViolation { index, joint } => {
149 write!(f, "CL point {index} needs rotary {joint} outside its limits")
150 }
151 }
152 }
153}
154
155impl std::error::Error for KinematicsError {}
156
157impl From<KinematicsError> for crate::CamError {
158 fn from(e: KinematicsError) -> Self {
159 crate::CamError::InvalidInput(e.to_string())
160 }
161}
162
163pub fn rotate(d: Point3, theta: f64, v: Point3) -> Point3 {
169 let (s, c) = theta.sin_cos();
170 v.scale(c).add(d.cross(v).scale(s)).add(d.scale(d.dot(v) * (1.0 - c)))
171}
172
173pub fn rotate_about(o: Point3, d: Point3, theta: f64, x: Point3) -> Point3 {
175 o.add(rotate(d, theta, x.sub(o)))
176}
177
178fn signed_angle(d: Point3, x: Point3, y: Point3) -> f64 {
181 (x.cross(y).dot(d)).atan2(x.dot(y))
182}
183
184fn perp(d: Point3, v: Point3) -> Point3 {
186 v.sub(d.scale(v.dot(d)))
187}
188
189fn wrap_pi(a: f64) -> f64 {
191 let mut r = a % (2.0 * PI);
192 if r <= -PI {
193 r += 2.0 * PI;
194 } else if r > PI {
195 r -= 2.0 * PI;
196 }
197 r
198}
199
200pub fn wrap_deg(a: f64) -> f64 {
202 let mut r = a % 360.0;
203 if r <= -180.0 {
204 r += 360.0;
205 } else if r > 180.0 {
206 r -= 360.0;
207 }
208 r
209}
210
211#[derive(Clone, Copy, Debug, PartialEq)]
213pub struct TwoAxisSolution {
214 pub alpha: f64,
216 pub beta: f64,
218 pub singular: bool,
219}
220
221pub fn two_axis(
228 d_a: Point3,
229 d_b: Point3,
230 u: Point3,
231 w: Point3,
232 prev: [f64; 2],
233 cone: f64,
234 cos_eps: f64,
235) -> Vec<TwoAxisSolution> {
236 let ua = u.dot(d_a);
239 let ab = d_a.dot(d_b);
240 let a = u.dot(d_b) - ua * ab;
241 let b = d_a.cross(u).dot(d_b);
242 let c = ua * ab;
243 let k = w.dot(d_b);
244 let r = a.hypot(b);
247 let sin_cone = cone.sin().max(0.0);
248 let mut alphas: Vec<(f64, bool)> = Vec::new();
249 if r <= sin_cone.max(cos_eps) {
250 if (k - c).abs() > r + cos_eps {
251 return Vec::new();
252 }
253 alphas.push((prev[0], true));
254 } else {
255 let phi = b.atan2(a);
256 let ratio = (k - c) / r;
257 if ratio > 1.0 + cos_eps || ratio < -1.0 - cos_eps {
258 return Vec::new();
259 }
260 let delta = ratio.clamp(-1.0, 1.0).acos();
261 alphas.push((wrap_pi(phi + delta), false));
262 if delta > 0.0 {
263 alphas.push((wrap_pi(phi - delta), false));
264 }
265 }
266 let w_perp = perp(d_b, w);
267 let w_singular = w_perp.length() <= sin_cone.max(cos_eps);
268 let mut out = Vec::new();
269 for (alpha, alpha_free) in alphas {
270 let u1 = rotate(d_a, alpha, u);
271 let u_perp = perp(d_b, u1);
272 let (beta, beta_free) = if w_singular || u_perp.length() <= 1e-12 {
273 (prev[1], true)
274 } else {
275 (wrap_pi(signed_angle(d_b, u_perp, w_perp)), false)
276 };
277 out.push(TwoAxisSolution { alpha, beta, singular: alpha_free || beta_free });
278 }
279 out
280}
281
282pub fn one_axis(d: Point3, u: Point3, w: Point3, prev: f64, cone: f64, cos_eps: f64) -> Option<(f64, bool)> {
285 if (u.dot(d) - w.dot(d)).abs() > cos_eps {
286 return None;
287 }
288 let up = perp(d, u);
289 let wp = perp(d, w);
290 if wp.length() <= cone.sin().max(cos_eps) || up.length() <= cos_eps {
291 return Some((prev, true));
292 }
293 Some((wrap_pi(signed_angle(d, up, wp)), false))
294}
295
296fn joint(axis: &RotaryAxis, carrier: Carrier, wcs_origin: Point3) -> Result<Joint, KinematicsError> {
301 axis.validate().map_err(KinematicsError::InvalidMachine)?;
302 let direction = axis.direction.normalized().ok_or_else(|| {
303 KinematicsError::InvalidMachine(format!("rotary axis {} has a zero direction", axis.name))
304 })?;
305 let origin = match carrier {
306 Carrier::Table => axis.origin.sub(wcs_origin),
307 Carrier::Head => axis.origin,
308 };
309 Ok(Joint { name: axis.name.clone(), direction, origin, limits_deg: axis.limits_deg, carrier })
310}
311
312impl Chain {
313 pub fn from_machine(machine: &Machine, wcs_origin: Point3) -> Result<Chain, KinematicsError> {
316 machine.validate().map_err(KinematicsError::InvalidMachine)?;
317 if !wcs_origin.is_finite() {
318 return Err(KinematicsError::InvalidMachine("work offset origin is not finite".into()));
319 }
320 let chain = match &machine.kind {
321 MachineKind::Mill3 => Chain { kind: ChainKind::Mill3, joints: Vec::new() },
322 MachineKind::Lathe => {
323 return Err(KinematicsError::InvalidMachine("lathe kinematics are not a milling chain".into()))
324 }
325 MachineKind::Mill4 { rotary } => {
326 Chain { kind: ChainKind::Mill4, joints: vec![joint(rotary, Carrier::Table, wcs_origin)?] }
327 }
328 MachineKind::MillTurn { c_axis, .. } => {
331 Chain { kind: ChainKind::Mill4, joints: vec![joint(c_axis, Carrier::Table, wcs_origin)?] }
332 }
333 MachineKind::Mill5 { kinematics } => match kinematics {
334 Kinematics::TableTable { primary, secondary } => Chain {
335 kind: ChainKind::TableTable,
336 joints: vec![
337 joint(primary, Carrier::Table, wcs_origin)?,
338 joint(secondary, Carrier::Table, wcs_origin)?,
339 ],
340 },
341 Kinematics::HeadHead { primary, secondary } => Chain {
342 kind: ChainKind::HeadHead,
343 joints: vec![
344 joint(primary, Carrier::Head, wcs_origin)?,
345 joint(secondary, Carrier::Head, wcs_origin)?,
346 ],
347 },
348 Kinematics::HeadTable { head, table } => Chain {
349 kind: ChainKind::HeadTable,
350 joints: vec![joint(head, Carrier::Head, wcs_origin)?, joint(table, Carrier::Table, wcs_origin)?],
351 },
352 },
353 };
354 if chain.joints.len() == 2 {
355 let (a, b) = (chain.joints[0].direction, chain.joints[1].direction);
356 if a.cross(b).length() <= 1e-9 {
357 return Err(KinematicsError::InvalidMachine("the two rotary axes are parallel".into()));
358 }
359 if chain.joints[0].name == chain.joints[1].name {
360 return Err(KinematicsError::InvalidMachine("the two rotary axes share a name".into()));
361 }
362 }
363 Ok(chain)
364 }
365
366 pub fn mill3() -> Chain {
368 Chain { kind: ChainKind::Mill3, joints: Vec::new() }
369 }
370
371 pub fn rotary_count(&self) -> usize {
372 self.joints.len()
373 }
374
375 pub fn joint_names(&self) -> Vec<&str> {
377 self.joints.iter().map(|j| j.name.as_str()).collect()
378 }
379
380 fn check_angles(&self, angles_deg: &[f64]) -> Result<(), KinematicsError> {
381 if angles_deg.len() != self.joints.len() {
382 return Err(KinematicsError::InvalidInput(format!(
383 "expected {} rotary angles, got {}",
384 self.joints.len(),
385 angles_deg.len()
386 )));
387 }
388 if angles_deg.iter().any(|a| !a.is_finite()) {
389 return Err(KinematicsError::InvalidInput("rotary angle is not finite".into()));
390 }
391 Ok(())
392 }
393
394 fn joint_transform(j: &Joint, theta: f64) -> RigidTransform {
396 let r = RigidTransform::axis_angle(j.direction, theta, Point3::ZERO).unwrap_or_else(|_| RigidTransform::identity());
397 let shift = j.origin.sub(r.apply_vector(j.origin));
398 RigidTransform { rotation: r.rotation, translation: shift }
399 }
400
401 pub fn link_transform(&self, link: Link, angles_deg: &[f64], linear: Point3) -> Result<RigidTransform, KinematicsError> {
405 self.check_angles(angles_deg)?;
406 let th: Vec<f64> = angles_deg.iter().map(|a| a.to_radians()).collect();
407 let j = &self.joints;
408 let table_joints: Vec<usize> = (0..j.len()).filter(|&k| j[k].carrier == Carrier::Table).collect();
409 let head_joints: Vec<usize> = (0..j.len()).filter(|&k| j[k].carrier == Carrier::Head).collect();
410 let compose = |ids: &[usize]| -> RigidTransform {
412 let mut t = RigidTransform::identity();
413 for &k in ids.iter().rev() {
414 t = t.then(&Self::joint_transform(&j[k], th[k]));
416 }
417 t
418 };
419 let shift = RigidTransform::translation(linear);
420 Ok(match link {
421 Link::Base => RigidTransform::identity(),
422 Link::Part => compose(&table_joints),
423 Link::Tool => compose(&head_joints).then(&shift),
424 Link::Joint { index: k } => {
425 if k >= j.len() {
426 return Err(KinematicsError::InvalidInput(format!("link joint {k} does not exist")));
427 }
428 match j[k].carrier {
429 Carrier::Table => {
430 let upto: Vec<usize> = table_joints.iter().copied().take_while(|&i| i <= k).collect();
431 compose(&upto)
432 }
433 Carrier::Head => {
434 let upto: Vec<usize> = head_joints.iter().copied().take_while(|&i| i <= k).collect();
435 compose(&upto).then(&shift)
436 }
437 }
438 }
439 })
440 }
441
442 pub fn part_to_world(&self, angles_deg: &[f64], x: Point3) -> Point3 {
444 let j = &self.joints;
445 match self.kind {
446 ChainKind::Mill3 | ChainKind::HeadHead => x,
447 ChainKind::Mill4 => rotate_about(j[0].origin, j[0].direction, angles_deg[0].to_radians(), x),
448 ChainKind::TableTable => {
449 let inner = rotate_about(j[1].origin, j[1].direction, angles_deg[1].to_radians(), x);
450 rotate_about(j[0].origin, j[0].direction, angles_deg[0].to_radians(), inner)
451 }
452 ChainKind::HeadTable => rotate_about(j[1].origin, j[1].direction, angles_deg[1].to_radians(), x),
453 }
454 }
455
456 pub fn world_to_part(&self, angles_deg: &[f64], x: Point3) -> Point3 {
458 let j = &self.joints;
459 match self.kind {
460 ChainKind::Mill3 | ChainKind::HeadHead => x,
461 ChainKind::Mill4 => rotate_about(j[0].origin, j[0].direction, -angles_deg[0].to_radians(), x),
462 ChainKind::TableTable => {
463 let outer = rotate_about(j[0].origin, j[0].direction, -angles_deg[0].to_radians(), x);
464 rotate_about(j[1].origin, j[1].direction, -angles_deg[1].to_radians(), outer)
465 }
466 ChainKind::HeadTable => rotate_about(j[1].origin, j[1].direction, -angles_deg[1].to_radians(), x),
467 }
468 }
469
470 pub fn part_dir_to_world(&self, angles_deg: &[f64], v: Point3) -> Point3 {
472 let j = &self.joints;
473 match self.kind {
474 ChainKind::Mill3 | ChainKind::HeadHead => v,
475 ChainKind::Mill4 => rotate(j[0].direction, angles_deg[0].to_radians(), v),
476 ChainKind::TableTable => {
477 let inner = rotate(j[1].direction, angles_deg[1].to_radians(), v);
478 rotate(j[0].direction, angles_deg[0].to_radians(), inner)
479 }
480 ChainKind::HeadTable => rotate(j[1].direction, angles_deg[1].to_radians(), v),
481 }
482 }
483
484 pub fn tool_axis_world(&self, angles_deg: &[f64]) -> Point3 {
486 let j = &self.joints;
487 match self.kind {
488 ChainKind::Mill3 | ChainKind::Mill4 | ChainKind::TableTable => Point3::Z,
489 ChainKind::HeadHead => {
490 let inner = rotate(j[1].direction, angles_deg[1].to_radians(), Point3::Z);
491 rotate(j[0].direction, angles_deg[0].to_radians(), inner)
492 }
493 ChainKind::HeadTable => rotate(j[0].direction, angles_deg[0].to_radians(), Point3::Z),
494 }
495 }
496
497 pub fn head_offset(&self, angles_deg: &[f64]) -> Point3 {
500 let j = &self.joints;
501 match self.kind {
502 ChainKind::Mill3 | ChainKind::Mill4 | ChainKind::TableTable => Point3::ZERO,
503 ChainKind::HeadHead => {
504 let (t1, t2) = (angles_deg[0].to_radians(), angles_deg[1].to_radians());
505 let o1 = j[0].origin;
506 let o2 = j[1].origin;
507 let inner = o2.sub(rotate(j[1].direction, t2, o2)).sub(o1);
508 o1.add(rotate(j[0].direction, t1, inner))
509 }
510 ChainKind::HeadTable => {
511 let o = j[0].origin;
512 o.sub(rotate(j[0].direction, angles_deg[0].to_radians(), o))
513 }
514 }
515 }
516
517 pub fn forward(&self, pose: &Pose, options: &KinematicsOptions) -> Result<(Point3, Point3), KinematicsError> {
519 self.check_angles(&pose.angles_deg)?;
520 if !pose.linear.is_finite() {
521 return Err(KinematicsError::InvalidInput("linear position is not finite".into()));
522 }
523 let axis_w = self.tool_axis_world(&pose.angles_deg);
524 let axis = self.world_dir_to_part(&pose.angles_deg, axis_w);
525 let tip = if options.tcp {
526 pose.linear
527 } else {
528 let tip_w = pose.linear.add(self.head_offset(&pose.angles_deg));
529 self.world_to_part(&pose.angles_deg, tip_w)
530 };
531 Ok((tip, axis))
532 }
533
534 pub fn world_dir_to_part(&self, angles_deg: &[f64], v: Point3) -> Point3 {
536 let j = &self.joints;
537 match self.kind {
538 ChainKind::Mill3 | ChainKind::HeadHead => v,
539 ChainKind::Mill4 => rotate(j[0].direction, -angles_deg[0].to_radians(), v),
540 ChainKind::TableTable => {
541 let outer = rotate(j[0].direction, -angles_deg[0].to_radians(), v);
542 rotate(j[1].direction, -angles_deg[1].to_radians(), outer)
543 }
544 ChainKind::HeadTable => rotate(j[1].direction, -angles_deg[1].to_radians(), v),
545 }
546 }
547
548 pub fn linear_for(&self, tip: Point3, angles_deg: &[f64], options: &KinematicsOptions) -> Point3 {
550 if options.tcp {
551 tip
552 } else {
553 self.part_to_world(angles_deg, tip).sub(self.head_offset(angles_deg))
554 }
555 }
556
557 pub fn inverse(
563 &self,
564 tip: Point3,
565 axis: Option<Point3>,
566 previous: Option<&[f64]>,
567 options: &KinematicsOptions,
568 ) -> Result<Vec<Pose>, KinematicsError> {
569 options.validate()?;
570 if !tip.is_finite() {
571 return Err(KinematicsError::InvalidInput("tool tip is not finite".into()));
572 }
573 let a = match axis {
574 None => Point3::Z,
575 Some(v) => v.normalized().ok_or(KinematicsError::InvalidAxis)?,
576 };
577 let prev: Vec<f64> = match previous {
578 Some(p) => {
579 self.check_angles(p)?;
580 p.iter().map(|d| d.to_radians()).collect()
581 }
582 None => vec![0.0; self.joints.len()],
583 };
584 let cone = options.singular_cone_deg.to_radians();
585 let eps = options.cos_eps;
586 let unreachable = || KinematicsError::Unreachable { axis: a };
587 let j = &self.joints;
588 let mut raw: Vec<(Vec<f64>, bool)> = Vec::new();
589 match self.kind {
590 ChainKind::Mill3 => {
591 if 1.0 - a.dot(Point3::Z) > eps {
592 return Err(unreachable());
593 }
594 raw.push((Vec::new(), false));
595 }
596 ChainKind::Mill4 => {
597 let (theta, singular) =
598 one_axis(j[0].direction, a, Point3::Z, prev[0], cone, eps).ok_or_else(unreachable)?;
599 raw.push((vec![theta], singular));
600 }
601 ChainKind::TableTable => {
602 for s in two_axis(j[1].direction, j[0].direction, a, Point3::Z, [prev[1], prev[0]], cone, eps) {
604 raw.push((vec![s.beta, s.alpha], s.singular));
605 }
606 }
607 ChainKind::HeadHead => {
608 for s in two_axis(j[1].direction, j[0].direction, Point3::Z, a, [prev[1], prev[0]], cone, eps) {
610 raw.push((vec![s.beta, s.alpha], s.singular));
611 }
612 }
613 ChainKind::HeadTable => {
614 for s in two_axis(j[0].direction, j[1].direction, Point3::Z, a, [prev[0], -prev[1]], cone, eps) {
616 raw.push((vec![s.alpha, -s.beta], s.singular));
617 }
618 }
619 }
620 if raw.is_empty() {
621 return Err(unreachable());
622 }
623 let mut poses = Vec::with_capacity(raw.len());
624 for (branch, (angles, singular)) in raw.into_iter().enumerate() {
625 let angles_deg: Vec<f64> = angles.iter().map(|r| wrap_deg(r.to_degrees())).collect();
626 let linear = self.linear_for(tip, &angles_deg, options);
627 poses.push(Pose { linear, angles_deg, branch: branch as u8, singular });
628 }
629 Ok(poses)
630 }
631}
632