Skip to main content

agg_rust/
vcgen_stroke.rs

1//! Stroke vertex generator.
2//!
3//! Port of `agg_vcgen_stroke.h` / `agg_vcgen_stroke.cpp` — generates
4//! a stroked outline from a center-line path using `MathStroke`.
5
6use crate::array::{shorten_path, VertexDist, VertexSequence};
7use crate::basics::{
8    get_close_flag, is_move_to, is_vertex, PointD, PATH_CMD_END_POLY, PATH_CMD_LINE_TO,
9    PATH_CMD_MOVE_TO, PATH_CMD_STOP, PATH_FLAGS_CCW, PATH_FLAGS_CLOSE, PATH_FLAGS_CW,
10};
11use crate::math_stroke::{InnerJoin, LineCap, LineJoin, MathStroke};
12
13// ============================================================================
14// VcgenStroke
15// ============================================================================
16
17#[derive(Debug, Clone, Copy, PartialEq)]
18enum Status {
19    Initial,
20    Ready,
21    Cap1,
22    Cap2,
23    Outline1,
24    CloseFirst,
25    Outline2,
26    OutVertices,
27    EndPoly1,
28    EndPoly2,
29    Stop,
30}
31
32/// Stroke vertex generator.
33///
34/// Accumulates source path vertices, then generates a stroked outline
35/// using `MathStroke` for join/cap calculations.
36///
37/// Port of C++ `vcgen_stroke`.
38pub struct VcgenStroke {
39    stroker: MathStroke,
40    src_vertices: VertexSequence,
41    out_vertices: Vec<PointD>,
42    shorten: f64,
43    closed: u32,
44    status: Status,
45    prev_status: Status,
46    src_vertex: usize,
47    out_vertex: usize,
48}
49
50impl VcgenStroke {
51    pub fn new() -> Self {
52        Self {
53            stroker: MathStroke::new(),
54            src_vertices: VertexSequence::new(),
55            out_vertices: Vec::new(),
56            shorten: 0.0,
57            closed: 0,
58            status: Status::Initial,
59            prev_status: Status::Initial,
60            src_vertex: 0,
61            out_vertex: 0,
62        }
63    }
64
65    // Parameter forwarding to MathStroke
66    pub fn set_line_cap(&mut self, lc: LineCap) {
67        self.stroker.set_line_cap(lc);
68    }
69    pub fn line_cap(&self) -> LineCap {
70        self.stroker.line_cap()
71    }
72
73    pub fn set_line_join(&mut self, lj: LineJoin) {
74        self.stroker.set_line_join(lj);
75    }
76    pub fn line_join(&self) -> LineJoin {
77        self.stroker.line_join()
78    }
79
80    pub fn set_inner_join(&mut self, ij: InnerJoin) {
81        self.stroker.set_inner_join(ij);
82    }
83    pub fn inner_join(&self) -> InnerJoin {
84        self.stroker.inner_join()
85    }
86
87    pub fn set_width(&mut self, w: f64) {
88        self.stroker.set_width(w);
89    }
90    pub fn width(&self) -> f64 {
91        self.stroker.width()
92    }
93
94    pub fn set_miter_limit(&mut self, ml: f64) {
95        self.stroker.set_miter_limit(ml);
96    }
97    pub fn miter_limit(&self) -> f64 {
98        self.stroker.miter_limit()
99    }
100
101    pub fn set_miter_limit_theta(&mut self, t: f64) {
102        self.stroker.set_miter_limit_theta(t);
103    }
104
105    pub fn set_inner_miter_limit(&mut self, ml: f64) {
106        self.stroker.set_inner_miter_limit(ml);
107    }
108    pub fn inner_miter_limit(&self) -> f64 {
109        self.stroker.inner_miter_limit()
110    }
111
112    pub fn set_approximation_scale(&mut self, s: f64) {
113        self.stroker.set_approximation_scale(s);
114    }
115    pub fn approximation_scale(&self) -> f64 {
116        self.stroker.approximation_scale()
117    }
118
119    pub fn set_shorten(&mut self, s: f64) {
120        self.shorten = s;
121    }
122    pub fn shorten(&self) -> f64 {
123        self.shorten
124    }
125
126    // Vertex Generator Interface
127    pub fn remove_all(&mut self) {
128        self.src_vertices.remove_all();
129        self.closed = 0;
130        self.status = Status::Initial;
131    }
132
133    pub fn add_vertex(&mut self, x: f64, y: f64, cmd: u32) {
134        self.status = Status::Initial;
135        if is_move_to(cmd) {
136            self.src_vertices.modify_last(VertexDist::new(x, y));
137        } else if is_vertex(cmd) {
138            self.src_vertices.add(VertexDist::new(x, y));
139        } else {
140            self.closed = get_close_flag(cmd);
141        }
142    }
143
144    // Vertex Source Interface
145    pub fn rewind(&mut self, _path_id: u32) {
146        if self.status == Status::Initial {
147            self.src_vertices.close(self.closed != 0);
148            shorten_path(&mut self.src_vertices, self.shorten, self.closed);
149            if self.src_vertices.size() < 3 {
150                self.closed = 0;
151            }
152        }
153        self.status = Status::Ready;
154        self.src_vertex = 0;
155        self.out_vertex = 0;
156    }
157
158    pub fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
159        // cmd persists across states within one call; initialized per the C++ pattern.
160        // In C++, break exits the switch (not the while), so the while loop continues.
161        // We use loop{match} where each arm either returns or falls through to the
162        // next iteration.
163        let mut cmd = PATH_CMD_LINE_TO;
164        loop {
165            match self.status {
166                Status::Initial => {
167                    self.rewind(0);
168                    // fall through to Ready
169                }
170                Status::Ready => {
171                    if self.src_vertices.size() < 2 + (self.closed != 0) as usize {
172                        return PATH_CMD_STOP;
173                    }
174                    self.status = if self.closed != 0 {
175                        Status::Outline1
176                    } else {
177                        Status::Cap1
178                    };
179                    cmd = PATH_CMD_MOVE_TO;
180                    self.src_vertex = 0;
181                    self.out_vertex = 0;
182                    // continue loop to Cap1/Outline1
183                }
184                Status::Cap1 => {
185                    let v0 = *self.src_vertices.curr(0);
186                    let v1 = *self.src_vertices.curr(1);
187                    self.stroker
188                        .calc_cap(&mut self.out_vertices, &v0, &v1, v0.dist);
189                    self.src_vertex = 1;
190                    self.prev_status = Status::Outline1;
191                    self.status = Status::OutVertices;
192                    self.out_vertex = 0;
193                    // continue loop to OutVertices
194                }
195                Status::Cap2 => {
196                    let n = self.src_vertices.size();
197                    let v0 = *self.src_vertices.curr(n - 1);
198                    let v1 = *self.src_vertices.curr(n - 2);
199                    self.stroker
200                        .calc_cap(&mut self.out_vertices, &v0, &v1, v1.dist);
201                    self.prev_status = Status::Outline2;
202                    self.status = Status::OutVertices;
203                    self.out_vertex = 0;
204                    // continue loop to OutVertices
205                }
206                Status::Outline1 => {
207                    if self.closed != 0 {
208                        if self.src_vertex >= self.src_vertices.size() {
209                            self.prev_status = Status::CloseFirst;
210                            self.status = Status::EndPoly1;
211                            continue; // to EndPoly1
212                        }
213                    } else if self.src_vertex >= self.src_vertices.size() - 1 {
214                        self.status = Status::Cap2;
215                        continue; // to Cap2
216                    }
217                    let v_prev = *self.src_vertices.prev(self.src_vertex);
218                    let v_curr = *self.src_vertices.curr(self.src_vertex);
219                    let v_next = *self.src_vertices.next(self.src_vertex);
220                    self.stroker.calc_join(
221                        &mut self.out_vertices,
222                        &v_prev,
223                        &v_curr,
224                        &v_next,
225                        v_prev.dist,
226                        v_curr.dist,
227                    );
228                    self.src_vertex += 1;
229                    self.prev_status = self.status;
230                    self.status = Status::OutVertices;
231                    self.out_vertex = 0;
232                    // continue loop to OutVertices
233                }
234                Status::CloseFirst => {
235                    self.status = Status::Outline2;
236                    cmd = PATH_CMD_MOVE_TO;
237                    // fall through to Outline2
238                }
239                Status::Outline2 => {
240                    if self.src_vertex <= (self.closed == 0) as usize {
241                        self.status = Status::EndPoly2;
242                        self.prev_status = Status::Stop;
243                        continue; // to EndPoly2
244                    }
245                    self.src_vertex -= 1;
246                    let v_next = *self.src_vertices.next(self.src_vertex);
247                    let v_curr = *self.src_vertices.curr(self.src_vertex);
248                    let v_prev = *self.src_vertices.prev(self.src_vertex);
249                    self.stroker.calc_join(
250                        &mut self.out_vertices,
251                        &v_next,
252                        &v_curr,
253                        &v_prev,
254                        v_curr.dist,
255                        v_prev.dist,
256                    );
257                    self.prev_status = self.status;
258                    self.status = Status::OutVertices;
259                    self.out_vertex = 0;
260                    // continue loop to OutVertices
261                }
262                Status::OutVertices => {
263                    if self.out_vertex >= self.out_vertices.len() {
264                        self.status = self.prev_status;
265                        // continue loop to prev_status
266                    } else {
267                        let c = self.out_vertices[self.out_vertex];
268                        self.out_vertex += 1;
269                        *x = c.x;
270                        *y = c.y;
271                        return cmd;
272                    }
273                }
274                Status::EndPoly1 => {
275                    self.status = self.prev_status;
276                    return PATH_CMD_END_POLY | PATH_FLAGS_CLOSE | PATH_FLAGS_CCW;
277                }
278                Status::EndPoly2 => {
279                    self.status = self.prev_status;
280                    return PATH_CMD_END_POLY | PATH_FLAGS_CLOSE | PATH_FLAGS_CW;
281                }
282                Status::Stop => {
283                    return PATH_CMD_STOP;
284                }
285            }
286        }
287    }
288}
289
290impl Default for VcgenStroke {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296impl crate::conv_adaptor_vcgen::VcgenGenerator for VcgenStroke {
297    fn remove_all(&mut self) {
298        self.remove_all();
299    }
300    fn add_vertex(&mut self, x: f64, y: f64, cmd: u32) {
301        self.add_vertex(x, y, cmd);
302    }
303    fn rewind(&mut self, path_id: u32) {
304        self.rewind(path_id);
305    }
306    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
307        self.vertex(x, y)
308    }
309}
310
311// ============================================================================
312// Tests
313// ============================================================================
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use crate::basics::is_stop;
319
320    fn collect_gen_vertices(gen: &mut VcgenStroke) -> Vec<(f64, f64, u32)> {
321        gen.rewind(0);
322        let mut result = Vec::new();
323        loop {
324            let (mut x, mut y) = (0.0, 0.0);
325            let cmd = gen.vertex(&mut x, &mut y);
326            if is_stop(cmd) {
327                break;
328            }
329            result.push((x, y, cmd));
330        }
331        result
332    }
333
334    #[test]
335    fn test_new_defaults() {
336        let gen = VcgenStroke::new();
337        assert!((gen.width() - 1.0).abs() < 1e-10);
338        assert_eq!(gen.line_cap(), LineCap::Butt);
339        assert_eq!(gen.line_join(), LineJoin::Miter);
340    }
341
342    #[test]
343    fn test_empty_produces_stop() {
344        let mut gen = VcgenStroke::new();
345        let verts = collect_gen_vertices(&mut gen);
346        assert!(verts.is_empty());
347    }
348
349    #[test]
350    fn test_single_segment_open() {
351        let mut gen = VcgenStroke::new();
352        gen.set_width(10.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 verts = collect_gen_vertices(&mut gen);
357        // Should produce a stroked rectangle (open path with two caps)
358        assert!(
359            verts.len() >= 4,
360            "Expected at least 4 vertices, got {}",
361            verts.len()
362        );
363        // First vertex should be move_to
364        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
365    }
366
367    #[test]
368    fn test_closed_triangle() {
369        let mut gen = VcgenStroke::new();
370        gen.set_width(4.0);
371        gen.add_vertex(10.0, 10.0, PATH_CMD_MOVE_TO);
372        gen.add_vertex(50.0, 10.0, PATH_CMD_LINE_TO);
373        gen.add_vertex(30.0, 40.0, PATH_CMD_LINE_TO);
374        gen.add_vertex(0.0, 0.0, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
375
376        let verts = collect_gen_vertices(&mut gen);
377        // Closed polygon produces two outline loops (inner + outer)
378        assert!(
379            verts.len() >= 6,
380            "Expected at least 6 vertices for closed triangle stroke, got {}",
381            verts.len()
382        );
383    }
384
385    #[test]
386    fn test_width_setter() {
387        let mut gen = VcgenStroke::new();
388        gen.set_width(5.0);
389        assert!((gen.width() - 5.0).abs() < 1e-10);
390    }
391
392    #[test]
393    fn test_line_cap_setter() {
394        let mut gen = VcgenStroke::new();
395        gen.set_line_cap(LineCap::Round);
396        assert_eq!(gen.line_cap(), LineCap::Round);
397    }
398
399    #[test]
400    fn test_line_join_setter() {
401        let mut gen = VcgenStroke::new();
402        gen.set_line_join(LineJoin::Round);
403        assert_eq!(gen.line_join(), LineJoin::Round);
404    }
405
406    #[test]
407    fn test_shorten() {
408        let mut gen = VcgenStroke::new();
409        gen.set_shorten(5.0);
410        assert!((gen.shorten() - 5.0).abs() < 1e-10);
411    }
412
413    #[test]
414    fn test_rewind_resets() {
415        let mut gen = VcgenStroke::new();
416        gen.set_width(10.0);
417        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
418        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
419
420        let verts1 = collect_gen_vertices(&mut gen);
421        // Rewind and collect again — should be the same
422        gen.rewind(0);
423        let mut verts2 = Vec::new();
424        loop {
425            let (mut x, mut y) = (0.0, 0.0);
426            let cmd = gen.vertex(&mut x, &mut y);
427            if is_stop(cmd) {
428                break;
429            }
430            verts2.push((x, y, cmd));
431        }
432        assert_eq!(verts1.len(), verts2.len());
433    }
434
435    #[test]
436    fn test_remove_all() {
437        let mut gen = VcgenStroke::new();
438        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
439        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
440        gen.remove_all();
441        let verts = collect_gen_vertices(&mut gen);
442        assert!(verts.is_empty());
443    }
444
445    #[test]
446    fn test_round_cap_produces_more_vertices() {
447        let mut butt = VcgenStroke::new();
448        butt.set_width(20.0);
449        butt.set_line_cap(LineCap::Butt);
450        butt.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
451        butt.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
452        let butt_verts = collect_gen_vertices(&mut butt);
453
454        let mut round = VcgenStroke::new();
455        round.set_width(20.0);
456        round.set_line_cap(LineCap::Round);
457        round.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
458        round.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
459        let round_verts = collect_gen_vertices(&mut round);
460
461        // Round caps produce more vertices than butt caps
462        assert!(
463            round_verts.len() > butt_verts.len(),
464            "Round ({}) should have more vertices than butt ({})",
465            round_verts.len(),
466            butt_verts.len()
467        );
468    }
469
470    #[test]
471    fn test_horizontal_line_stroke_y_extent() {
472        let mut gen = VcgenStroke::new();
473        gen.set_width(10.0); // half-width = 5
474        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
475        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
476
477        let verts = collect_gen_vertices(&mut gen);
478        let max_y = verts.iter().map(|v| v.1).fold(f64::MIN, f64::max);
479        let min_y = verts.iter().map(|v| v.1).fold(f64::MAX, f64::min);
480
481        // Stroke should extend ~5 units above and below
482        assert!(max_y >= 4.5, "Max y={} should be >= 4.5", max_y);
483        assert!(min_y <= -4.5, "Min y={} should be <= -4.5", min_y);
484    }
485}