Skip to main content

agg_rust/
trans_single_path.rs

1//! Single-path coordinate transformation.
2//!
3//! Port of `agg_trans_single_path.h` + `agg_trans_single_path.cpp`.
4//! Maps coordinates along a path: x → distance along path, y → perpendicular offset.
5
6use crate::array::{VertexDist, VertexSequence};
7use crate::basics::{is_move_to, is_stop, is_vertex, VertexSource};
8use crate::span_interpolator_linear::Transformer;
9
10/// Status of the path building state machine.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum Status {
13    Initial,
14    MakingPath,
15    Ready,
16}
17
18/// Single-path coordinate transformation.
19///
20/// Stores a path as a sequence of vertices with cumulative distances.
21/// `transform()` maps x → distance-along-path, y → perpendicular offset.
22/// Used by `ConvTransform` with `ConvSegmentator` for text-on-path effects.
23pub struct TransSinglePath {
24    src_vertices: VertexSequence,
25    base_length: f64,
26    kindex: f64,
27    status: Status,
28    preserve_x_scale: bool,
29}
30
31impl TransSinglePath {
32    pub fn new() -> Self {
33        Self {
34            src_vertices: VertexSequence::new(),
35            base_length: 0.0,
36            kindex: 0.0,
37            status: Status::Initial,
38            preserve_x_scale: true,
39        }
40    }
41
42    pub fn base_length(&self) -> f64 {
43        self.base_length
44    }
45
46    pub fn set_base_length(&mut self, v: f64) {
47        self.base_length = v;
48    }
49
50    pub fn preserve_x_scale(&self) -> bool {
51        self.preserve_x_scale
52    }
53
54    pub fn set_preserve_x_scale(&mut self, f: bool) {
55        self.preserve_x_scale = f;
56    }
57
58    pub fn reset(&mut self) {
59        self.src_vertices.remove_all();
60        self.kindex = 0.0;
61        self.status = Status::Initial;
62    }
63
64    pub fn move_to(&mut self, x: f64, y: f64) {
65        if self.status == Status::Initial {
66            self.src_vertices.modify_last(VertexDist::new(x, y));
67            self.status = Status::MakingPath;
68        } else {
69            self.line_to(x, y);
70        }
71    }
72
73    pub fn line_to(&mut self, x: f64, y: f64) {
74        if self.status == Status::MakingPath {
75            self.src_vertices.add(VertexDist::new(x, y));
76        }
77    }
78
79    /// Build the path from a VertexSource.
80    pub fn add_path<VS: VertexSource>(&mut self, vs: &mut VS, path_id: u32) {
81        let mut x = 0.0;
82        let mut y = 0.0;
83
84        vs.rewind(path_id);
85        loop {
86            let cmd = vs.vertex(&mut x, &mut y);
87            if is_stop(cmd) {
88                break;
89            }
90            if is_move_to(cmd) {
91                self.move_to(x, y);
92            } else if is_vertex(cmd) {
93                self.line_to(x, y);
94            }
95        }
96        self.finalize_path();
97    }
98
99    /// Finalize the path — compute cumulative distances and prepare for transform.
100    pub fn finalize_path(&mut self) {
101        if self.status != Status::MakingPath || self.src_vertices.size() <= 1 {
102            return;
103        }
104
105        self.src_vertices.close(false);
106
107        if self.src_vertices.size() > 2 {
108            let n = self.src_vertices.size();
109            // If the second-to-last segment is very short compared to the one before it,
110            // merge the last two vertices.
111            if self.src_vertices[n - 2].dist * 10.0 < self.src_vertices[n - 3].dist {
112                let d = self.src_vertices[n - 3].dist + self.src_vertices[n - 2].dist;
113                let last = self.src_vertices[n - 1];
114                self.src_vertices[n - 2] = last;
115                self.src_vertices.remove_last();
116                let idx = self.src_vertices.size() - 2;
117                self.src_vertices[idx].dist = d;
118            }
119        }
120
121        // Convert per-segment distances to cumulative distances.
122        let mut dist = 0.0;
123        for i in 0..self.src_vertices.size() {
124            let d = self.src_vertices[i].dist;
125            self.src_vertices[i].dist = dist;
126            dist += d;
127        }
128
129        self.kindex = (self.src_vertices.size() - 1) as f64 / dist;
130        self.status = Status::Ready;
131    }
132
133    /// Total length of the path (or base_length if set).
134    pub fn total_length(&self) -> f64 {
135        if self.base_length >= 1e-10 {
136            return self.base_length;
137        }
138        if self.status == Status::Ready {
139            self.src_vertices[self.src_vertices.size() - 1].dist
140        } else {
141            0.0
142        }
143    }
144}
145
146impl Transformer for TransSinglePath {
147    fn transform(&self, x: &mut f64, y: &mut f64) {
148        if self.status != Status::Ready {
149            return;
150        }
151
152        let n = self.src_vertices.size();
153        let total_dist = self.src_vertices[n - 1].dist;
154
155        if self.base_length > 1e-10 {
156            *x *= total_dist / self.base_length;
157        }
158
159        let x1;
160        let y1;
161        let dx;
162        let dy;
163        let d;
164        let dd;
165
166        if *x < 0.0 {
167            // Extrapolation on the left
168            x1 = self.src_vertices[0].x;
169            y1 = self.src_vertices[0].y;
170            dx = self.src_vertices[1].x - x1;
171            dy = self.src_vertices[1].y - y1;
172            dd = self.src_vertices[1].dist - self.src_vertices[0].dist;
173            d = *x;
174        } else if *x > total_dist {
175            // Extrapolation on the right
176            let i = n - 2;
177            let j = n - 1;
178            x1 = self.src_vertices[j].x;
179            y1 = self.src_vertices[j].y;
180            dx = x1 - self.src_vertices[i].x;
181            dy = y1 - self.src_vertices[i].y;
182            dd = self.src_vertices[j].dist - self.src_vertices[i].dist;
183            d = *x - self.src_vertices[j].dist;
184        } else {
185            // Interpolation — binary search for segment
186            let mut i = 0usize;
187            let mut j = n - 1;
188
189            if self.preserve_x_scale {
190                loop {
191                    if j - i <= 1 {
192                        break;
193                    }
194                    let k = (i + j) >> 1;
195                    if *x < self.src_vertices[k].dist {
196                        j = k;
197                    } else {
198                        i = k;
199                    }
200                }
201                dd = self.src_vertices[j].dist - self.src_vertices[i].dist;
202                d = *x - self.src_vertices[i].dist;
203            } else {
204                let fi = *x * self.kindex;
205                i = fi as usize;
206                j = i + 1;
207                dd = self.src_vertices[j].dist - self.src_vertices[i].dist;
208                d = (fi - i as f64) * dd;
209            }
210
211            x1 = self.src_vertices[i].x;
212            y1 = self.src_vertices[i].y;
213            dx = self.src_vertices[j].x - x1;
214            dy = self.src_vertices[j].y - y1;
215        }
216
217        let x2 = x1 + dx * d / dd;
218        let y2 = y1 + dy * d / dd;
219        *x = x2 - *y * dy / dd;
220        *y = y2 + *y * dx / dd;
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_straight_line_path() {
230        let mut tsp = TransSinglePath::new();
231        tsp.move_to(0.0, 0.0);
232        tsp.line_to(100.0, 0.0);
233        tsp.finalize_path();
234
235        assert!((tsp.total_length() - 100.0).abs() < 1e-10);
236
237        // Midpoint of path, zero offset
238        let (mut x, mut y) = (50.0, 0.0);
239        tsp.transform(&mut x, &mut y);
240        assert!((x - 50.0).abs() < 1e-10);
241        assert!((y - 0.0).abs() < 1e-10);
242
243        // Midpoint with perpendicular offset
244        let (mut x, mut y) = (50.0, 10.0);
245        tsp.transform(&mut x, &mut y);
246        assert!((x - 50.0).abs() < 1e-10);
247        assert!((y - 10.0).abs() < 1e-10);
248    }
249
250    #[test]
251    fn test_diagonal_path() {
252        let mut tsp = TransSinglePath::new();
253        tsp.move_to(0.0, 0.0);
254        tsp.line_to(100.0, 100.0);
255        tsp.finalize_path();
256
257        let expected_len = (100.0_f64 * 100.0 + 100.0 * 100.0).sqrt();
258        assert!((tsp.total_length() - expected_len).abs() < 1e-10);
259    }
260
261    #[test]
262    fn test_base_length_scaling() {
263        let mut tsp = TransSinglePath::new();
264        tsp.move_to(0.0, 0.0);
265        tsp.line_to(200.0, 0.0);
266        tsp.finalize_path();
267        tsp.set_base_length(100.0);
268
269        // x=50 with base_length=100 maps to x=100 on the 200-unit path
270        let (mut x, mut y) = (50.0, 0.0);
271        tsp.transform(&mut x, &mut y);
272        assert!((x - 100.0).abs() < 1e-10);
273        assert!((y - 0.0).abs() < 1e-10);
274    }
275
276    #[test]
277    fn test_extrapolation_left() {
278        let mut tsp = TransSinglePath::new();
279        tsp.move_to(10.0, 0.0);
280        tsp.line_to(110.0, 0.0);
281        tsp.finalize_path();
282
283        // x=-10 is before the path start
284        let (mut x, mut y) = (-10.0, 0.0);
285        tsp.transform(&mut x, &mut y);
286        // Should extrapolate backwards along first segment direction
287        assert!((x - 0.0).abs() < 1e-10);
288        assert!((y - 0.0).abs() < 1e-10);
289    }
290
291    #[test]
292    fn test_extrapolation_right() {
293        let mut tsp = TransSinglePath::new();
294        tsp.move_to(0.0, 0.0);
295        tsp.line_to(100.0, 0.0);
296        tsp.finalize_path();
297
298        // x=110 is past the path end
299        let (mut x, mut y) = (110.0, 0.0);
300        tsp.transform(&mut x, &mut y);
301        assert!((x - 110.0).abs() < 1e-10);
302        assert!((y - 0.0).abs() < 1e-10);
303    }
304
305    #[test]
306    fn test_multi_segment_path() {
307        let mut tsp = TransSinglePath::new();
308        tsp.move_to(0.0, 0.0);
309        tsp.line_to(50.0, 0.0);
310        tsp.line_to(50.0, 50.0);
311        tsp.finalize_path();
312
313        // Total length = 50 + 50 = 100
314        assert!((tsp.total_length() - 100.0).abs() < 1e-10);
315
316        // x=25 is in first segment (horizontal)
317        let (mut x, mut y) = (25.0, 0.0);
318        tsp.transform(&mut x, &mut y);
319        assert!((x - 25.0).abs() < 1e-10);
320        assert!((y - 0.0).abs() < 1e-10);
321
322        // x=75 is in second segment (vertical)
323        let (mut x, mut y) = (75.0, 0.0);
324        tsp.transform(&mut x, &mut y);
325        assert!((x - 50.0).abs() < 1e-10);
326        assert!((y - 25.0).abs() < 1e-10);
327    }
328
329    #[test]
330    fn test_no_preserve_x_scale() {
331        let mut tsp = TransSinglePath::new();
332        tsp.set_preserve_x_scale(false);
333        tsp.move_to(0.0, 0.0);
334        tsp.line_to(100.0, 0.0);
335        tsp.finalize_path();
336
337        let (mut x, mut y) = (50.0, 0.0);
338        tsp.transform(&mut x, &mut y);
339        assert!((x - 50.0).abs() < 1e-10);
340    }
341
342    #[test]
343    fn test_not_ready_is_noop() {
344        let tsp = TransSinglePath::new();
345        let (mut x, mut y) = (50.0, 25.0);
346        tsp.transform(&mut x, &mut y);
347        assert!((x - 50.0).abs() < 1e-10);
348        assert!((y - 25.0).abs() < 1e-10);
349    }
350}