Skip to main content

agg_rust/
vcgen_dash.rs

1//! Dash vertex generator.
2//!
3//! Port of `agg_vcgen_dash.h` / `agg_vcgen_dash.cpp` — generates
4//! dashed lines from a continuous center-line path.
5
6use crate::array::{shorten_path, VertexDist, VertexSequence};
7use crate::basics::{
8    get_close_flag, is_move_to, is_vertex, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP,
9};
10
11const MAX_DASHES: usize = 32;
12
13// ============================================================================
14// VcgenDash
15// ============================================================================
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18enum Status {
19    Initial,
20    Ready,
21    Polyline,
22    Stop,
23}
24
25/// Dash vertex generator.
26///
27/// Maintains a dash pattern (up to 16 dash/gap pairs) and generates
28/// dashed segments from a continuous path.
29///
30/// Port of C++ `vcgen_dash`.
31pub struct VcgenDash {
32    dashes: [f64; MAX_DASHES],
33    total_dash_len: f64,
34    num_dashes: usize,
35    dash_start: f64,
36    shorten: f64,
37    curr_dash_start: f64,
38    curr_dash: usize,
39    curr_rest: f64,
40    v1_idx: usize,
41    v2_idx: usize,
42    src_vertices: VertexSequence,
43    closed: u32,
44    status: Status,
45    src_vertex: usize,
46}
47
48impl VcgenDash {
49    pub fn new() -> Self {
50        Self {
51            dashes: [0.0; MAX_DASHES],
52            total_dash_len: 0.0,
53            num_dashes: 0,
54            dash_start: 0.0,
55            shorten: 0.0,
56            curr_dash_start: 0.0,
57            curr_dash: 0,
58            curr_rest: 0.0,
59            v1_idx: 0,
60            v2_idx: 0,
61            src_vertices: VertexSequence::new(),
62            closed: 0,
63            status: Status::Initial,
64            src_vertex: 0,
65        }
66    }
67
68    pub fn remove_all_dashes(&mut self) {
69        self.total_dash_len = 0.0;
70        self.num_dashes = 0;
71        self.curr_dash_start = 0.0;
72        self.curr_dash = 0;
73    }
74
75    pub fn add_dash(&mut self, dash_len: f64, gap_len: f64) {
76        if self.num_dashes < MAX_DASHES {
77            self.total_dash_len += dash_len + gap_len;
78            self.dashes[self.num_dashes] = dash_len;
79            self.num_dashes += 1;
80            self.dashes[self.num_dashes] = gap_len;
81            self.num_dashes += 1;
82        }
83    }
84
85    pub fn dash_start(&mut self, ds: f64) {
86        self.dash_start = ds;
87        self.calc_dash_start(ds.abs());
88    }
89
90    fn calc_dash_start(&mut self, mut ds: f64) {
91        self.curr_dash = 0;
92        self.curr_dash_start = 0.0;
93        while ds > 0.0 {
94            if ds > self.dashes[self.curr_dash] {
95                ds -= self.dashes[self.curr_dash];
96                self.curr_dash += 1;
97                self.curr_dash_start = 0.0;
98                if self.curr_dash >= self.num_dashes {
99                    self.curr_dash = 0;
100                }
101            } else {
102                self.curr_dash_start = ds;
103                ds = 0.0;
104            }
105        }
106    }
107
108    pub fn set_shorten(&mut self, s: f64) {
109        self.shorten = s;
110    }
111
112    pub fn shorten(&self) -> f64 {
113        self.shorten
114    }
115
116    // Vertex Generator Interface
117    pub fn remove_all(&mut self) {
118        self.status = Status::Initial;
119        self.src_vertices.remove_all();
120        self.closed = 0;
121    }
122
123    pub fn add_vertex(&mut self, x: f64, y: f64, cmd: u32) {
124        self.status = Status::Initial;
125        if is_move_to(cmd) {
126            self.src_vertices.modify_last(VertexDist::new(x, y));
127        } else if is_vertex(cmd) {
128            self.src_vertices.add(VertexDist::new(x, y));
129        } else {
130            self.closed = get_close_flag(cmd);
131        }
132    }
133
134    // Vertex Source Interface
135    pub fn rewind(&mut self, _path_id: u32) {
136        if self.status == Status::Initial {
137            self.src_vertices.close(self.closed != 0);
138            shorten_path(&mut self.src_vertices, self.shorten, self.closed);
139        }
140        self.status = Status::Ready;
141        self.src_vertex = 0;
142    }
143
144    pub fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
145        // C++ uses while(!is_stop(cmd)) with switch/case fallthrough.
146        // We use loop{match} with the same semantics: states either return
147        // or fall through to the next iteration.
148        let mut cmd = PATH_CMD_MOVE_TO;
149        loop {
150            if crate::basics::is_stop(cmd) {
151                break;
152            }
153            match self.status {
154                Status::Initial => {
155                    self.rewind(0);
156                    // fall through to Ready
157                }
158                Status::Ready => {
159                    if self.num_dashes < 2 || self.src_vertices.size() < 2 {
160                        cmd = PATH_CMD_STOP;
161                        continue; // re-check while condition (is_stop → break)
162                    }
163                    self.status = Status::Polyline;
164                    self.src_vertex = 1;
165                    self.v1_idx = 0;
166                    self.v2_idx = 1;
167                    self.curr_rest = self.src_vertices[0].dist;
168                    *x = self.src_vertices[0].x;
169                    *y = self.src_vertices[0].y;
170                    if self.dash_start >= 0.0 {
171                        self.calc_dash_start(self.dash_start);
172                    }
173                    return PATH_CMD_MOVE_TO;
174                }
175                Status::Polyline => {
176                    let dash_rest = self.dashes[self.curr_dash] - self.curr_dash_start;
177
178                    cmd = if (self.curr_dash & 1) != 0 {
179                        PATH_CMD_MOVE_TO
180                    } else {
181                        PATH_CMD_LINE_TO
182                    };
183
184                    let v1 = self.src_vertices[self.v1_idx];
185                    let v2 = self.src_vertices[self.v2_idx];
186
187                    if self.curr_rest > dash_rest {
188                        self.curr_rest -= dash_rest;
189                        self.curr_dash += 1;
190                        if self.curr_dash >= self.num_dashes {
191                            self.curr_dash = 0;
192                        }
193                        self.curr_dash_start = 0.0;
194                        *x = v2.x - (v2.x - v1.x) * self.curr_rest / v1.dist;
195                        *y = v2.y - (v2.y - v1.y) * self.curr_rest / v1.dist;
196                    } else {
197                        self.curr_dash_start += self.curr_rest;
198                        *x = v2.x;
199                        *y = v2.y;
200                        self.src_vertex += 1;
201                        self.v1_idx = self.v2_idx;
202                        self.curr_rest = self.src_vertices[self.v1_idx].dist;
203                        if self.closed != 0 {
204                            if self.src_vertex > self.src_vertices.size() {
205                                self.status = Status::Stop;
206                            } else {
207                                self.v2_idx = if self.src_vertex >= self.src_vertices.size() {
208                                    0
209                                } else {
210                                    self.src_vertex
211                                };
212                            }
213                        } else if self.src_vertex >= self.src_vertices.size() {
214                            self.status = Status::Stop;
215                        } else {
216                            self.v2_idx = self.src_vertex;
217                        }
218                    }
219                    return cmd;
220                }
221                Status::Stop => {
222                    cmd = PATH_CMD_STOP;
223                    continue; // re-check while condition (is_stop → break)
224                }
225            }
226        }
227        PATH_CMD_STOP
228    }
229}
230
231impl Default for VcgenDash {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237impl crate::conv_adaptor_vcgen::VcgenGenerator for VcgenDash {
238    fn remove_all(&mut self) {
239        self.remove_all();
240    }
241    fn add_vertex(&mut self, x: f64, y: f64, cmd: u32) {
242        self.add_vertex(x, y, cmd);
243    }
244    fn rewind(&mut self, path_id: u32) {
245        self.rewind(path_id);
246    }
247    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
248        self.vertex(x, y)
249    }
250}
251
252// ============================================================================
253// Tests
254// ============================================================================
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::basics::{is_stop, is_vertex, PATH_CMD_MOVE_TO};
260
261    fn collect_gen_vertices(gen: &mut VcgenDash) -> Vec<(f64, f64, u32)> {
262        gen.rewind(0);
263        let mut result = Vec::new();
264        loop {
265            let (mut x, mut y) = (0.0, 0.0);
266            let cmd = gen.vertex(&mut x, &mut y);
267            if is_stop(cmd) {
268                break;
269            }
270            result.push((x, y, cmd));
271        }
272        result
273    }
274
275    #[test]
276    fn test_new_defaults() {
277        let gen = VcgenDash::new();
278        assert!((gen.shorten() - 0.0).abs() < 1e-10);
279    }
280
281    #[test]
282    fn test_empty_produces_stop() {
283        let mut gen = VcgenDash::new();
284        gen.add_dash(10.0, 5.0);
285        let verts = collect_gen_vertices(&mut gen);
286        assert!(verts.is_empty());
287    }
288
289    #[test]
290    fn test_no_dashes_produces_stop() {
291        let mut gen = VcgenDash::new();
292        // No dashes added
293        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
294        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
295        let verts = collect_gen_vertices(&mut gen);
296        assert!(verts.is_empty(), "No dashes → no output");
297    }
298
299    #[test]
300    fn test_basic_dash_pattern() {
301        let mut gen = VcgenDash::new();
302        gen.add_dash(20.0, 10.0); // 20px dash, 10px gap
303        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
304        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
305
306        let verts = collect_gen_vertices(&mut gen);
307        assert!(!verts.is_empty(), "Should produce dash vertices");
308
309        // First vertex should be a move_to at the start
310        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
311        assert!((verts[0].0 - 0.0).abs() < 1e-10);
312    }
313
314    #[test]
315    fn test_dash_has_gaps() {
316        let mut gen = VcgenDash::new();
317        gen.add_dash(20.0, 10.0);
318        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
319        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
320
321        let verts = collect_gen_vertices(&mut gen);
322
323        // Should have multiple move_to commands (one per dash segment)
324        let move_count = verts.iter().filter(|v| v.2 == PATH_CMD_MOVE_TO).count();
325        assert!(
326            move_count >= 2,
327            "Expected multiple dash segments, got {} move_to",
328            move_count
329        );
330    }
331
332    #[test]
333    fn test_dash_vertices_on_line() {
334        let mut gen = VcgenDash::new();
335        gen.add_dash(25.0, 10.0);
336        gen.add_vertex(0.0, 50.0, PATH_CMD_MOVE_TO);
337        gen.add_vertex(100.0, 50.0, PATH_CMD_LINE_TO);
338
339        let verts = collect_gen_vertices(&mut gen);
340
341        // All vertices should be on y=50 for a horizontal line
342        for v in &verts {
343            if is_vertex(v.2) {
344                assert!((v.1 - 50.0).abs() < 1e-10, "y={} should be 50", v.1);
345            }
346        }
347    }
348
349    #[test]
350    fn test_dash_rewind_replay() {
351        let mut gen = VcgenDash::new();
352        gen.add_dash(15.0, 5.0);
353        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
354        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
355
356        let v1 = collect_gen_vertices(&mut gen);
357        let v2 = collect_gen_vertices(&mut gen);
358        assert_eq!(v1.len(), v2.len());
359    }
360
361    #[test]
362    fn test_remove_all() {
363        let mut gen = VcgenDash::new();
364        gen.add_dash(10.0, 5.0);
365        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
366        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
367        gen.remove_all();
368        let verts = collect_gen_vertices(&mut gen);
369        assert!(verts.is_empty());
370    }
371
372    #[test]
373    fn test_remove_all_dashes() {
374        let mut gen = VcgenDash::new();
375        gen.add_dash(10.0, 5.0);
376        gen.remove_all_dashes();
377        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
378        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
379        let verts = collect_gen_vertices(&mut gen);
380        assert!(verts.is_empty(), "Removed dashes → no output");
381    }
382
383    #[test]
384    fn test_dash_start_offset() {
385        let mut gen1 = VcgenDash::new();
386        gen1.add_dash(20.0, 10.0);
387        gen1.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
388        gen1.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
389        let v1 = collect_gen_vertices(&mut gen1);
390
391        let mut gen2 = VcgenDash::new();
392        gen2.add_dash(20.0, 10.0);
393        gen2.dash_start(15.0); // offset by 15
394        gen2.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
395        gen2.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
396        let v2 = collect_gen_vertices(&mut gen2);
397
398        // Different dash start should produce different vertices
399        assert_ne!(
400            v1.len(),
401            v2.len(),
402            "Different dash start should change vertex count"
403        );
404    }
405
406    #[test]
407    fn test_multiple_dash_gaps() {
408        let mut gen = VcgenDash::new();
409        gen.add_dash(10.0, 5.0);
410        gen.add_dash(20.0, 5.0);
411        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
412        gen.add_vertex(200.0, 0.0, PATH_CMD_LINE_TO);
413
414        let verts = collect_gen_vertices(&mut gen);
415        assert!(!verts.is_empty());
416
417        // Count line_to segments to verify pattern is applied
418        let line_count = verts.iter().filter(|v| v.2 == PATH_CMD_LINE_TO).count();
419        assert!(
420            line_count >= 3,
421            "Expected multiple line segments, got {}",
422            line_count
423        );
424    }
425
426    #[test]
427    fn test_shorten_setter() {
428        let mut gen = VcgenDash::new();
429        gen.set_shorten(5.0);
430        assert!((gen.shorten() - 5.0).abs() < 1e-10);
431    }
432
433    #[test]
434    fn test_diagonal_dash() {
435        let mut gen = VcgenDash::new();
436        gen.add_dash(10.0, 5.0);
437        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
438        gen.add_vertex(100.0, 100.0, PATH_CMD_LINE_TO);
439
440        let verts = collect_gen_vertices(&mut gen);
441        assert!(!verts.is_empty());
442
443        // Vertices should lie on the diagonal line y=x
444        for v in &verts {
445            if is_vertex(v.2) {
446                assert!(
447                    (v.0 - v.1).abs() < 1e-10,
448                    "Point ({}, {}) should be on y=x diagonal",
449                    v.0,
450                    v.1
451                );
452            }
453        }
454    }
455}