Skip to main content

agg_rust/
trans_double_path.rs

1//! Double-path coordinate transformation.
2//!
3//! Port of `agg_trans_double_path.h` + `agg_trans_double_path.cpp`.
4//! Maps coordinates between two paths: x → distance along paths,
5//! y → interpolation between path1 and path2.
6//!
7//! Copyright (c) 2025. BSD-3-Clause License.
8
9use crate::array::{VertexDist, VertexSequence};
10use crate::basics::{is_move_to, is_stop, is_vertex, VertexSource};
11use crate::span_interpolator_linear::Transformer;
12
13/// Status of the path building state machine.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15enum Status {
16    Initial,
17    MakingPath,
18    Ready,
19}
20
21/// Double-path coordinate transformation.
22///
23/// Stores two paths as sequences of vertices with cumulative distances.
24/// `transform()` maps x → distance along path1 (and path2 scaled proportionally),
25/// y → linear interpolation between the two paths based on `base_height`.
26/// Used for text-between-two-curves effects.
27pub struct TransDoublePath {
28    src_vertices1: VertexSequence,
29    src_vertices2: VertexSequence,
30    base_length: f64,
31    base_height: f64,
32    kindex1: f64,
33    kindex2: f64,
34    status1: Status,
35    status2: Status,
36    preserve_x_scale: bool,
37}
38
39impl TransDoublePath {
40    pub fn new() -> Self {
41        Self {
42            src_vertices1: VertexSequence::new(),
43            src_vertices2: VertexSequence::new(),
44            base_length: 0.0,
45            base_height: 1.0,
46            kindex1: 0.0,
47            kindex2: 0.0,
48            status1: Status::Initial,
49            status2: Status::Initial,
50            preserve_x_scale: true,
51        }
52    }
53
54    pub fn set_base_length(&mut self, v: f64) {
55        self.base_length = v;
56    }
57
58    pub fn base_length(&self) -> f64 {
59        self.base_length
60    }
61
62    pub fn set_base_height(&mut self, v: f64) {
63        self.base_height = v;
64    }
65
66    pub fn base_height(&self) -> f64 {
67        self.base_height
68    }
69
70    pub fn set_preserve_x_scale(&mut self, f: bool) {
71        self.preserve_x_scale = f;
72    }
73
74    pub fn preserve_x_scale(&self) -> bool {
75        self.preserve_x_scale
76    }
77
78    pub fn reset(&mut self) {
79        self.src_vertices1.remove_all();
80        self.src_vertices2.remove_all();
81        self.kindex1 = 0.0;
82        self.kindex2 = 0.0;
83        self.status1 = Status::Initial;
84        self.status2 = Status::Initial;
85    }
86
87    pub fn move_to1(&mut self, x: f64, y: f64) {
88        if self.status1 == Status::Initial {
89            self.src_vertices1.modify_last(VertexDist::new(x, y));
90            self.status1 = Status::MakingPath;
91        } else {
92            self.line_to1(x, y);
93        }
94    }
95
96    pub fn line_to1(&mut self, x: f64, y: f64) {
97        if self.status1 == Status::MakingPath {
98            self.src_vertices1.add(VertexDist::new(x, y));
99        }
100    }
101
102    pub fn move_to2(&mut self, x: f64, y: f64) {
103        if self.status2 == Status::Initial {
104            self.src_vertices2.modify_last(VertexDist::new(x, y));
105            self.status2 = Status::MakingPath;
106        } else {
107            self.line_to2(x, y);
108        }
109    }
110
111    pub fn line_to2(&mut self, x: f64, y: f64) {
112        if self.status2 == Status::MakingPath {
113            self.src_vertices2.add(VertexDist::new(x, y));
114        }
115    }
116
117    /// Build both paths from two VertexSources and finalize.
118    pub fn add_paths<VS1: VertexSource, VS2: VertexSource>(
119        &mut self,
120        vs1: &mut VS1,
121        vs2: &mut VS2,
122        path1_id: u32,
123        path2_id: u32,
124    ) {
125        let (mut x, mut y) = (0.0, 0.0);
126
127        vs1.rewind(path1_id);
128        loop {
129            let cmd = vs1.vertex(&mut x, &mut y);
130            if is_stop(cmd) {
131                break;
132            }
133            if is_move_to(cmd) {
134                self.move_to1(x, y);
135            } else if is_vertex(cmd) {
136                self.line_to1(x, y);
137            }
138        }
139
140        vs2.rewind(path2_id);
141        loop {
142            let cmd = vs2.vertex(&mut x, &mut y);
143            if is_stop(cmd) {
144                break;
145            }
146            if is_move_to(cmd) {
147                self.move_to2(x, y);
148            } else if is_vertex(cmd) {
149                self.line_to2(x, y);
150            }
151        }
152
153        self.finalize_paths();
154    }
155
156    /// Finalize both paths — compute cumulative distances and prepare for transform.
157    pub fn finalize_paths(&mut self) {
158        if self.status1 == Status::MakingPath
159            && self.src_vertices1.size() > 1
160            && self.status2 == Status::MakingPath
161            && self.src_vertices2.size() > 1
162        {
163            self.kindex1 = Self::finalize_path(&mut self.src_vertices1);
164            self.kindex2 = Self::finalize_path(&mut self.src_vertices2);
165            self.status1 = Status::Ready;
166            self.status2 = Status::Ready;
167        }
168    }
169
170    /// Total length of path 1 (or base_length if set).
171    pub fn total_length1(&self) -> f64 {
172        if self.base_length >= 1e-10 {
173            return self.base_length;
174        }
175        if self.status1 == Status::Ready {
176            self.src_vertices1[self.src_vertices1.size() - 1].dist
177        } else {
178            0.0
179        }
180    }
181
182    /// Total length of path 2 (or base_length if set).
183    pub fn total_length2(&self) -> f64 {
184        if self.base_length >= 1e-10 {
185            return self.base_length;
186        }
187        if self.status2 == Status::Ready {
188            self.src_vertices2[self.src_vertices2.size() - 1].dist
189        } else {
190            0.0
191        }
192    }
193
194    // -- Internal helpers --
195
196    /// Finalize a single path: merge tiny trailing segments, convert to cumulative distances.
197    fn finalize_path(vertices: &mut VertexSequence) -> f64 {
198        vertices.close(false);
199
200        if vertices.size() > 2 {
201            let n = vertices.size();
202            if vertices[n - 2].dist * 10.0 < vertices[n - 3].dist {
203                let d = vertices[n - 3].dist + vertices[n - 2].dist;
204                let last = vertices[n - 1];
205                vertices[n - 2] = last;
206                vertices.remove_last();
207                let idx = vertices.size() - 2;
208                vertices[idx].dist = d;
209            }
210        }
211
212        let mut dist = 0.0;
213        for i in 0..vertices.size() {
214            let d = vertices[i].dist;
215            vertices[i].dist = dist;
216            dist += d;
217        }
218
219        (vertices.size() - 1) as f64 / dist
220    }
221
222    /// Transform a point along a single path with a given kx scaling factor.
223    /// This is the inner helper used by both path1 and path2 transforms.
224    fn transform1(
225        &self,
226        vertices: &VertexSequence,
227        kindex: f64,
228        kx: f64,
229        x: &mut f64,
230        y: &mut f64,
231    ) {
232        let x1;
233        let y1;
234        let dx;
235        let dy;
236        let d;
237        let dd;
238
239        *x *= kx;
240
241        if *x < 0.0 {
242            // Extrapolation on the left
243            x1 = vertices[0].x;
244            y1 = vertices[0].y;
245            dx = vertices[1].x - x1;
246            dy = vertices[1].y - y1;
247            dd = vertices[1].dist - vertices[0].dist;
248            d = *x;
249        } else if *x > vertices[vertices.size() - 1].dist {
250            // Extrapolation on the right
251            let i = vertices.size() - 2;
252            let j = vertices.size() - 1;
253            x1 = vertices[j].x;
254            y1 = vertices[j].y;
255            dx = x1 - vertices[i].x;
256            dy = y1 - vertices[i].y;
257            dd = vertices[j].dist - vertices[i].dist;
258            d = *x - vertices[j].dist;
259        } else {
260            // Interpolation
261            let mut i = 0usize;
262            let mut j = vertices.size() - 1;
263
264            if self.preserve_x_scale {
265                loop {
266                    if j - i <= 1 {
267                        break;
268                    }
269                    let k = (i + j) >> 1;
270                    if *x < vertices[k].dist {
271                        j = k;
272                    } else {
273                        i = k;
274                    }
275                }
276                dd = vertices[j].dist - vertices[i].dist;
277                d = *x - vertices[i].dist;
278            } else {
279                let fi = *x * kindex;
280                i = fi as usize;
281                j = i + 1;
282                dd = vertices[j].dist - vertices[i].dist;
283                d = (fi - i as f64) * dd;
284            }
285
286            x1 = vertices[i].x;
287            y1 = vertices[i].y;
288            dx = vertices[j].x - x1;
289            dy = vertices[j].y - y1;
290        }
291
292        *x = x1 + dx * d / dd;
293        *y = y1 + dy * d / dd;
294    }
295}
296
297impl Default for TransDoublePath {
298    fn default() -> Self {
299        Self::new()
300    }
301}
302
303impl Transformer for TransDoublePath {
304    fn transform(&self, x: &mut f64, y: &mut f64) {
305        if self.status1 != Status::Ready || self.status2 != Status::Ready {
306            return;
307        }
308
309        if self.base_length > 1e-10 {
310            *x *= self.src_vertices1[self.src_vertices1.size() - 1].dist / self.base_length;
311        }
312
313        let mut x1 = *x;
314        let mut y1 = *y;
315        let mut x2 = *x;
316        let mut y2 = *y;
317
318        let dd = self.src_vertices2[self.src_vertices2.size() - 1].dist
319            / self.src_vertices1[self.src_vertices1.size() - 1].dist;
320
321        self.transform1(&self.src_vertices1, self.kindex1, 1.0, &mut x1, &mut y1);
322        self.transform1(&self.src_vertices2, self.kindex2, dd, &mut x2, &mut y2);
323
324        *x = x1 + *y * (x2 - x1) / self.base_height;
325        *y = y1 + *y * (y2 - y1) / self.base_height;
326    }
327}
328
329// ============================================================================
330// Tests
331// ============================================================================
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_parallel_horizontal_paths() {
339        let mut tdp = TransDoublePath::new();
340        tdp.set_base_height(20.0);
341
342        // Path 1: y=0
343        tdp.move_to1(0.0, 0.0);
344        tdp.line_to1(100.0, 0.0);
345
346        // Path 2: y=20
347        tdp.move_to2(0.0, 20.0);
348        tdp.line_to2(100.0, 20.0);
349
350        tdp.finalize_paths();
351
352        assert!((tdp.total_length1() - 100.0).abs() < 1e-10);
353        assert!((tdp.total_length2() - 100.0).abs() < 1e-10);
354
355        // Point on path1 (y=0): should map to path1
356        let (mut x, mut y) = (50.0, 0.0);
357        tdp.transform(&mut x, &mut y);
358        assert!((x - 50.0).abs() < 1e-10);
359        assert!((y - 0.0).abs() < 1e-10);
360
361        // Point halfway between (y=base_height/2=10): should interpolate midway
362        let (mut x, mut y) = (50.0, 10.0);
363        tdp.transform(&mut x, &mut y);
364        assert!((x - 50.0).abs() < 1e-10);
365        assert!((y - 10.0).abs() < 1e-10);
366
367        // Point on path2 (y=base_height=20): should map to path2
368        let (mut x, mut y) = (50.0, 20.0);
369        tdp.transform(&mut x, &mut y);
370        assert!((x - 50.0).abs() < 1e-10);
371        assert!((y - 20.0).abs() < 1e-10);
372    }
373
374    #[test]
375    fn test_diverging_paths() {
376        let mut tdp = TransDoublePath::new();
377        tdp.set_base_height(10.0);
378
379        // Path 1: y=0
380        tdp.move_to1(0.0, 0.0);
381        tdp.line_to1(100.0, 0.0);
382
383        // Path 2: y=0 to y=50 (diverging)
384        tdp.move_to2(0.0, 0.0);
385        tdp.line_to2(100.0, 50.0);
386
387        tdp.finalize_paths();
388
389        // At x=50, y=0 → on path1 → (50, 0)
390        let (mut x, mut y) = (50.0, 0.0);
391        tdp.transform(&mut x, &mut y);
392        assert!((x - 50.0).abs() < 1e-6);
393        assert!((y - 0.0).abs() < 1e-6);
394    }
395
396    #[test]
397    fn test_base_length() {
398        let mut tdp = TransDoublePath::new();
399        tdp.set_base_height(10.0);
400        tdp.set_base_length(50.0);
401
402        tdp.move_to1(0.0, 0.0);
403        tdp.line_to1(100.0, 0.0);
404        tdp.move_to2(0.0, 10.0);
405        tdp.line_to2(100.0, 10.0);
406        tdp.finalize_paths();
407
408        assert!((tdp.total_length1() - 50.0).abs() < 1e-10);
409    }
410
411    #[test]
412    fn test_not_ready_is_noop() {
413        let tdp = TransDoublePath::new();
414        let (mut x, mut y) = (50.0, 25.0);
415        tdp.transform(&mut x, &mut y);
416        assert!((x - 50.0).abs() < 1e-10);
417        assert!((y - 25.0).abs() < 1e-10);
418    }
419
420    #[test]
421    fn test_add_paths_from_vertex_sources() {
422        use crate::path_storage::PathStorage;
423
424        let mut tdp = TransDoublePath::new();
425        tdp.set_base_height(20.0);
426
427        let mut p1 = PathStorage::new();
428        p1.move_to(0.0, 0.0);
429        p1.line_to(100.0, 0.0);
430
431        let mut p2 = PathStorage::new();
432        p2.move_to(0.0, 20.0);
433        p2.line_to(100.0, 20.0);
434
435        tdp.add_paths(&mut p1, &mut p2, 0, 0);
436
437        assert!((tdp.total_length1() - 100.0).abs() < 1e-10);
438        assert!((tdp.total_length2() - 100.0).abs() < 1e-10);
439    }
440}