Skip to main content

deke_linear/
path.rs

1//! Stage A — condition a raw Cartesian polyline into smooth, arc-length
2//! parameterised runs.
3//!
4//! The polyline is split at *sharp* corners (turn angle above a threshold) into
5//! runs that start and stop at rest. Inside a run, shallow corners are smoothed
6//! by a Catmull–Rom [`squiggle::Spline`] through the vertices (it stays straight on
7//! collinear stretches and bows through a shallow turn).
8//! Orientation is carried as a unit quaternion slerped between the run's
9//! vertices, keyed on arc length. Position geometry runs in `f32` (squiggle); poses
10//! are returned in `f64` for the kinematics downstream.
11
12use deke_types::glam::{DAffine3, DQuat, DVec3};
13use glam::Vec3;
14use squiggle::{Segmented, Spline};
15
16use crate::constraints::PathConditioning;
17use crate::error::LinearError;
18
19/// One arc-length-parameterised Cartesian segment with no sharp corners.
20#[derive(Clone, Debug)]
21pub struct CartesianRun {
22    spline: Spline,
23    ts: Vec<f32>,
24    ss: Vec<f32>,
25    vtx_s: Vec<f64>,
26    vtx_q: Vec<DQuat>,
27    length: f64,
28}
29
30impl CartesianRun {
31    /// Total Cartesian arc length of the run (metres).
32    pub fn length(&self) -> f64 {
33        self.length
34    }
35
36    /// Pose at arc length `s ∈ [0, length]`.
37    pub fn eval(&self, s: f64) -> DAffine3 {
38        let s = s.clamp(0.0, self.length);
39        let t = self.t_at_s(s as f32);
40        let p = self.spline.point(t);
41        let pos = DVec3::new(p.x as f64, p.y as f64, p.z as f64);
42        let rot = self.orientation_at(s);
43        DAffine3::from_rotation_translation(rot, pos)
44    }
45
46    fn t_at_s(&self, s: f32) -> f32 {
47        let s = s.clamp(0.0, *self.ss.last().unwrap_or(&0.0));
48        match self.ss.binary_search_by(|v| v.partial_cmp(&s).unwrap()) {
49            Ok(i) => self.ts[i],
50            Err(i) => {
51                if i == 0 {
52                    return self.ts[0];
53                }
54                if i >= self.ss.len() {
55                    return *self.ts.last().unwrap();
56                }
57                let (s0, s1) = (self.ss[i - 1], self.ss[i]);
58                let (t0, t1) = (self.ts[i - 1], self.ts[i]);
59                let f = if s1 > s0 { (s - s0) / (s1 - s0) } else { 0.0 };
60                t0 + (t1 - t0) * f
61            }
62        }
63    }
64
65    fn orientation_at(&self, s: f64) -> DQuat {
66        if self.vtx_q.len() == 1 {
67            return self.vtx_q[0];
68        }
69        let mut k = 0usize;
70        while k + 2 < self.vtx_s.len() && s > self.vtx_s[k + 1] {
71            k += 1;
72        }
73        let (s0, s1) = (self.vtx_s[k], self.vtx_s[k + 1]);
74        let f = if s1 > s0 {
75            ((s - s0) / (s1 - s0)).clamp(0.0, 1.0)
76        } else {
77            0.0
78        };
79        self.vtx_q[k].slerp(self.vtx_q[k + 1], f)
80    }
81}
82
83/// Split `poses` at sharp corners and condition each run.
84pub fn condition(
85    poses: &[DAffine3],
86    cfg: &PathConditioning,
87) -> Result<Vec<CartesianRun>, LinearError> {
88    if poses.len() < 2 {
89        return Err(LinearError::TooFewPoses(poses.len()));
90    }
91
92    let p: Vec<DVec3> = poses.iter().map(|t| t.translation).collect();
93
94    let mut bounds = vec![0usize];
95    for i in 1..poses.len() - 1 {
96        if turn_angle(&p, i).is_some_and(|a| a > cfg.sharp_corner_angle) {
97            bounds.push(i);
98        }
99    }
100    bounds.push(poses.len() - 1);
101
102    let mut runs = Vec::with_capacity(bounds.len() - 1);
103    for (run_idx, w) in bounds.windows(2).enumerate() {
104        runs.push(build_run(&poses[w[0]..=w[1]], run_idx)?);
105    }
106    Ok(runs)
107}
108
109fn turn_angle(p: &[DVec3], i: usize) -> Option<f64> {
110    let a = (p[i] - p[i - 1]).normalize_or_zero();
111    let b = (p[i + 1] - p[i]).normalize_or_zero();
112    if a == DVec3::ZERO || b == DVec3::ZERO {
113        return None;
114    }
115    Some(a.dot(b).clamp(-1.0, 1.0).acos())
116}
117
118fn build_run(poses: &[DAffine3], run_idx: usize) -> Result<CartesianRun, LinearError> {
119    // Drop consecutive coincident vertices so the spline and slerp stay well-formed.
120    let mut keep: Vec<usize> = vec![0];
121    for i in 1..poses.len() {
122        let prev = poses[*keep.last().unwrap()].translation;
123        if poses[i].translation.distance(prev) > 1e-9 {
124            keep.push(i);
125        }
126    }
127    if keep.len() < 2 {
128        return Err(LinearError::DegenerateRun { run: run_idx });
129    }
130
131    let knots: Vec<Vec3> = keep
132        .iter()
133        .map(|&i| {
134            let t = poses[i].translation;
135            Vec3::new(t.x as f32, t.y as f32, t.z as f32)
136        })
137        .collect();
138    let spline = Spline::new(knots.clone());
139
140    let mut vtx_q: Vec<DQuat> = keep
141        .iter()
142        .map(|&i| DQuat::from_mat3(&poses[i].matrix3).normalize())
143        .collect();
144    // Make consecutive quaternions take the short path so slerp never flips.
145    for k in 1..vtx_q.len() {
146        if vtx_q[k - 1].dot(vtx_q[k]) < 0.0 {
147            vtx_q[k] = -vtx_q[k];
148        }
149    }
150
151    let seg = spline.segment_count().max(1);
152    let m = (seg * 24).max(64);
153    let mut ts = Vec::with_capacity(m + 1);
154    let mut ss = Vec::with_capacity(m + 1);
155    let mut prev = spline.point(0.0);
156    let mut acc = 0.0f32;
157    ts.push(0.0);
158    ss.push(0.0);
159    for i in 1..=m {
160        let t = i as f32 / m as f32;
161        let pt = spline.point(t);
162        acc += (pt - prev).length();
163        prev = pt;
164        ts.push(t);
165        ss.push(acc);
166    }
167    let length = acc as f64;
168    if length < 1e-9 {
169        return Err(LinearError::DegenerateRun { run: run_idx });
170    }
171
172    // Arc length at each retained vertex: each is a knot at t = index / seg.
173    let mut vtx_s: Vec<f64> = (0..knots.len())
174        .map(|i| {
175            let t = i as f32 / seg as f32;
176            s_at_t(&ts, &ss, t) as f64
177        })
178        .collect();
179    vtx_s[0] = 0.0;
180    *vtx_s.last_mut().unwrap() = length;
181
182    Ok(CartesianRun {
183        spline,
184        ts,
185        ss,
186        vtx_s,
187        vtx_q,
188        length,
189    })
190}
191
192fn s_at_t(ts: &[f32], ss: &[f32], t: f32) -> f32 {
193    let t = t.clamp(0.0, 1.0);
194    match ts.binary_search_by(|v| v.partial_cmp(&t).unwrap()) {
195        Ok(i) => ss[i],
196        Err(i) => {
197            if i == 0 {
198                return ss[0];
199            }
200            if i >= ts.len() {
201                return *ss.last().unwrap();
202            }
203            let (t0, t1) = (ts[i - 1], ts[i]);
204            let (s0, s1) = (ss[i - 1], ss[i]);
205            let f = if t1 > t0 { (t - t0) / (t1 - t0) } else { 0.0 };
206            s0 + (s1 - s0) * f
207        }
208    }
209}