Skip to main content

agg_rust/
vcgen_contour.rs

1//! Contour vertex generator.
2//!
3//! Port of `agg_vcgen_contour.h` / `agg_vcgen_contour.cpp` — generates
4//! an offset contour from a closed polygon path using `MathStroke`.
5
6use crate::array::{VertexDist, VertexSequence};
7use crate::basics::{
8    get_close_flag, get_orientation, is_ccw, is_end_poly, is_move_to, is_oriented, is_vertex,
9    PointD, PATH_CMD_END_POLY, PATH_CMD_LINE_TO, PATH_CMD_MOVE_TO, PATH_CMD_STOP, PATH_FLAGS_CCW,
10    PATH_FLAGS_CLOSE, PATH_FLAGS_NONE,
11};
12use crate::math::calc_polygon_area_vd;
13use crate::math_stroke::{InnerJoin, LineCap, LineJoin, MathStroke};
14
15// ============================================================================
16// VcgenContour
17// ============================================================================
18
19#[derive(Debug, Clone, Copy, PartialEq)]
20enum Status {
21    Initial,
22    Ready,
23    Outline,
24    OutVertices,
25    EndPoly,
26    Stop,
27}
28
29/// Contour vertex generator.
30///
31/// Generates an offset contour (inset or outset) from a closed polygon.
32/// Uses `MathStroke` for join calculations.
33///
34/// Port of C++ `vcgen_contour`.
35pub struct VcgenContour {
36    stroker: MathStroke,
37    width: f64,
38    src_vertices: VertexSequence,
39    out_vertices: Vec<PointD>,
40    status: Status,
41    src_vertex: usize,
42    out_vertex: usize,
43    closed: u32,
44    orientation: u32,
45    auto_detect: bool,
46}
47
48impl VcgenContour {
49    pub fn new() -> Self {
50        Self {
51            stroker: MathStroke::new(),
52            width: 1.0,
53            src_vertices: VertexSequence::new(),
54            out_vertices: Vec::new(),
55            status: Status::Initial,
56            src_vertex: 0,
57            out_vertex: 0,
58            closed: 0,
59            orientation: 0,
60            auto_detect: false,
61        }
62    }
63
64    // Parameter forwarding to MathStroke
65    pub fn set_line_cap(&mut self, lc: LineCap) {
66        self.stroker.set_line_cap(lc);
67    }
68    pub fn line_cap(&self) -> LineCap {
69        self.stroker.line_cap()
70    }
71
72    pub fn set_line_join(&mut self, lj: LineJoin) {
73        self.stroker.set_line_join(lj);
74    }
75    pub fn line_join(&self) -> LineJoin {
76        self.stroker.line_join()
77    }
78
79    pub fn set_inner_join(&mut self, ij: InnerJoin) {
80        self.stroker.set_inner_join(ij);
81    }
82    pub fn inner_join(&self) -> InnerJoin {
83        self.stroker.inner_join()
84    }
85
86    pub fn set_width(&mut self, w: f64) {
87        self.width = w;
88        self.stroker.set_width(w);
89    }
90    pub fn width(&self) -> f64 {
91        self.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_auto_detect_orientation(&mut self, v: bool) {
120        self.auto_detect = v;
121    }
122    pub fn auto_detect_orientation(&self) -> bool {
123        self.auto_detect
124    }
125
126    // Generator interface
127    pub fn remove_all(&mut self) {
128        self.src_vertices.remove_all();
129        self.closed = 0;
130        self.orientation = 0;
131        self.status = Status::Initial;
132    }
133
134    pub fn add_vertex(&mut self, x: f64, y: f64, cmd: u32) {
135        self.status = Status::Initial;
136        if is_move_to(cmd) {
137            self.src_vertices.modify_last(VertexDist::new(x, y));
138        } else if is_vertex(cmd) {
139            self.src_vertices.add(VertexDist::new(x, y));
140        } else if is_end_poly(cmd) {
141            self.closed = get_close_flag(cmd);
142            if self.orientation == PATH_FLAGS_NONE {
143                self.orientation = get_orientation(cmd);
144            }
145        }
146    }
147
148    // Vertex Source Interface
149    pub fn rewind(&mut self, _path_id: u32) {
150        if self.status == Status::Initial {
151            self.src_vertices.close(true);
152            if self.auto_detect && !is_oriented(self.orientation) {
153                let verts: Vec<VertexDist> = (0..self.src_vertices.size())
154                    .map(|i| self.src_vertices[i])
155                    .collect();
156                self.orientation = if calc_polygon_area_vd(&verts) > 0.0 {
157                    PATH_FLAGS_CCW
158                } else {
159                    crate::basics::PATH_FLAGS_CW
160                };
161            }
162            if is_oriented(self.orientation) {
163                self.stroker.set_width(if is_ccw(self.orientation) {
164                    self.width
165                } else {
166                    -self.width
167                });
168            }
169        }
170        self.status = Status::Ready;
171        self.src_vertex = 0;
172    }
173
174    pub fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
175        // C++ uses while(!is_stop(cmd)) with switch/case fallthrough.
176        // Rust: loop{match} where arms fall through or return.
177        let mut cmd = PATH_CMD_LINE_TO;
178        loop {
179            match self.status {
180                Status::Initial => {
181                    self.rewind(0);
182                    // fall through to Ready
183                }
184                Status::Ready => {
185                    if self.src_vertices.size() < 2 + (self.closed != 0) as usize {
186                        return PATH_CMD_STOP;
187                    }
188                    self.status = Status::Outline;
189                    cmd = PATH_CMD_MOVE_TO;
190                    self.src_vertex = 0;
191                    self.out_vertex = 0;
192                    // fall through to Outline
193                }
194                Status::Outline => {
195                    if self.src_vertex >= self.src_vertices.size() {
196                        self.status = Status::EndPoly;
197                        continue;
198                    }
199                    // Copy to locals to avoid borrow conflicts
200                    let v_prev = *self.src_vertices.prev(self.src_vertex);
201                    let v_curr = *self.src_vertices.curr(self.src_vertex);
202                    let v_next = *self.src_vertices.next(self.src_vertex);
203                    self.stroker.calc_join(
204                        &mut self.out_vertices,
205                        &v_prev,
206                        &v_curr,
207                        &v_next,
208                        v_prev.dist,
209                        v_curr.dist,
210                    );
211                    self.src_vertex += 1;
212                    self.status = Status::OutVertices;
213                    self.out_vertex = 0;
214                    // fall through to OutVertices
215                }
216                Status::OutVertices => {
217                    if self.out_vertex >= self.out_vertices.len() {
218                        self.status = Status::Outline;
219                        // continue loop back to Outline
220                    } else {
221                        let c = self.out_vertices[self.out_vertex];
222                        self.out_vertex += 1;
223                        *x = c.x;
224                        *y = c.y;
225                        return cmd;
226                    }
227                }
228                Status::EndPoly => {
229                    if self.closed == 0 {
230                        return PATH_CMD_STOP;
231                    }
232                    self.status = Status::Stop;
233                    return PATH_CMD_END_POLY | PATH_FLAGS_CLOSE | PATH_FLAGS_CCW;
234                }
235                Status::Stop => {
236                    return PATH_CMD_STOP;
237                }
238            }
239        }
240    }
241}
242
243impl Default for VcgenContour {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249impl crate::conv_adaptor_vcgen::VcgenGenerator for VcgenContour {
250    fn remove_all(&mut self) {
251        self.remove_all();
252    }
253    fn add_vertex(&mut self, x: f64, y: f64, cmd: u32) {
254        self.add_vertex(x, y, cmd);
255    }
256    fn rewind(&mut self, path_id: u32) {
257        self.rewind(path_id);
258    }
259    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
260        self.vertex(x, y)
261    }
262}
263
264// ============================================================================
265// Tests
266// ============================================================================
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::basics::{is_stop, PATH_FLAGS_CLOSE};
272
273    fn collect_gen_vertices(gen: &mut VcgenContour) -> Vec<(f64, f64, u32)> {
274        gen.rewind(0);
275        let mut result = Vec::new();
276        loop {
277            let (mut x, mut y) = (0.0, 0.0);
278            let cmd = gen.vertex(&mut x, &mut y);
279            if is_stop(cmd) {
280                break;
281            }
282            result.push((x, y, cmd));
283        }
284        result
285    }
286
287    #[test]
288    fn test_new_defaults() {
289        let gen = VcgenContour::new();
290        assert!((gen.width() - 1.0).abs() < 1e-10);
291        assert!(!gen.auto_detect_orientation());
292    }
293
294    #[test]
295    fn test_empty_produces_stop() {
296        let mut gen = VcgenContour::new();
297        let verts = collect_gen_vertices(&mut gen);
298        assert!(verts.is_empty());
299    }
300
301    #[test]
302    fn test_closed_square_contour() {
303        let mut gen = VcgenContour::new();
304        gen.set_width(5.0);
305        gen.set_auto_detect_orientation(true);
306
307        // CCW square
308        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
309        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
310        gen.add_vertex(100.0, 100.0, PATH_CMD_LINE_TO);
311        gen.add_vertex(0.0, 100.0, PATH_CMD_LINE_TO);
312        gen.add_vertex(0.0, 0.0, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
313
314        let verts = collect_gen_vertices(&mut gen);
315        assert!(
316            verts.len() >= 4,
317            "Expected at least 4 contour vertices, got {}",
318            verts.len()
319        );
320        // First should be move_to
321        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
322    }
323
324    #[test]
325    fn test_contour_expands_ccw_polygon() {
326        let mut gen = VcgenContour::new();
327        gen.set_width(10.0);
328        gen.set_auto_detect_orientation(true);
329
330        // CCW triangle
331        gen.add_vertex(50.0, 10.0, PATH_CMD_MOVE_TO);
332        gen.add_vertex(90.0, 90.0, PATH_CMD_LINE_TO);
333        gen.add_vertex(10.0, 90.0, PATH_CMD_LINE_TO);
334        gen.add_vertex(0.0, 0.0, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
335
336        let verts = collect_gen_vertices(&mut gen);
337
338        // Contour should be larger than the original — check bounds
339        let max_x = verts
340            .iter()
341            .filter(|v| is_vertex(v.2))
342            .map(|v| v.0)
343            .fold(f64::MIN, f64::max);
344        let min_x = verts
345            .iter()
346            .filter(|v| is_vertex(v.2))
347            .map(|v| v.0)
348            .fold(f64::MAX, f64::min);
349
350        assert!(max_x > 90.0, "Max x={} should exceed original 90", max_x);
351        assert!(
352            min_x < 10.0,
353            "Min x={} should be less than original 10",
354            min_x
355        );
356    }
357
358    #[test]
359    fn test_width_setter() {
360        let mut gen = VcgenContour::new();
361        gen.set_width(7.5);
362        assert!((gen.width() - 7.5).abs() < 1e-10);
363    }
364
365    #[test]
366    fn test_remove_all() {
367        let mut gen = VcgenContour::new();
368        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
369        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
370        gen.add_vertex(100.0, 100.0, PATH_CMD_LINE_TO);
371        gen.add_vertex(0.0, 0.0, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
372        gen.remove_all();
373        let verts = collect_gen_vertices(&mut gen);
374        assert!(verts.is_empty());
375    }
376
377    #[test]
378    fn test_rewind_replay() {
379        let mut gen = VcgenContour::new();
380        gen.set_width(5.0);
381        gen.set_auto_detect_orientation(true);
382        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
383        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
384        gen.add_vertex(50.0, 80.0, PATH_CMD_LINE_TO);
385        gen.add_vertex(0.0, 0.0, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
386
387        let v1 = collect_gen_vertices(&mut gen);
388        let v2 = collect_gen_vertices(&mut gen);
389        assert_eq!(v1.len(), v2.len());
390    }
391
392    #[test]
393    fn test_auto_detect_orientation() {
394        let mut gen = VcgenContour::new();
395        gen.set_auto_detect_orientation(true);
396        assert!(gen.auto_detect_orientation());
397        gen.set_auto_detect_orientation(false);
398        assert!(!gen.auto_detect_orientation());
399    }
400
401    #[test]
402    fn test_line_join_setter() {
403        let mut gen = VcgenContour::new();
404        gen.set_line_join(LineJoin::Round);
405        assert_eq!(gen.line_join(), LineJoin::Round);
406    }
407
408    #[test]
409    fn test_open_path_no_contour() {
410        let mut gen = VcgenContour::new();
411        gen.set_width(5.0);
412        // Open path (no close flag)
413        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
414        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
415        gen.add_vertex(100.0, 100.0, PATH_CMD_LINE_TO);
416
417        let verts = collect_gen_vertices(&mut gen);
418        // Contour generator needs a closed polygon; open path should produce
419        // vertices but end_poly check will return stop if not closed
420        // (the C++ code returns stop if !closed in EndPoly state)
421        // Just verify it doesn't crash
422        let _ = verts;
423    }
424
425    #[test]
426    fn test_end_poly_emitted_for_closed() {
427        let mut gen = VcgenContour::new();
428        gen.set_width(5.0);
429        gen.set_auto_detect_orientation(true);
430
431        // CCW triangle
432        gen.add_vertex(0.0, 0.0, PATH_CMD_MOVE_TO);
433        gen.add_vertex(100.0, 0.0, PATH_CMD_LINE_TO);
434        gen.add_vertex(50.0, 80.0, PATH_CMD_LINE_TO);
435        gen.add_vertex(0.0, 0.0, PATH_CMD_END_POLY | PATH_FLAGS_CLOSE);
436
437        gen.rewind(0);
438        let mut found_end_poly = false;
439        loop {
440            let (mut x, mut y) = (0.0, 0.0);
441            let cmd = gen.vertex(&mut x, &mut y);
442            if is_stop(cmd) {
443                break;
444            }
445            if (cmd & PATH_CMD_END_POLY) == PATH_CMD_END_POLY {
446                found_end_poly = true;
447                assert_ne!(cmd & PATH_FLAGS_CLOSE, 0, "end_poly should have close flag");
448            }
449        }
450        assert!(found_end_poly, "Closed contour should emit end_poly");
451    }
452}