Skip to main content

agg_rust/
conv_stroke.rs

1//! Stroke converter for vertex sources.
2//!
3//! Port of `agg_conv_stroke.h` — convenience wrapper that combines
4//! `ConvAdaptorVcgen` with `VcgenStroke` to stroke any vertex source.
5
6use crate::basics::VertexSource;
7use crate::conv_adaptor_vcgen::ConvAdaptorVcgen;
8use crate::math_stroke::{InnerJoin, LineCap, LineJoin};
9use crate::vcgen_stroke::VcgenStroke;
10
11// ============================================================================
12// ConvStroke
13// ============================================================================
14
15/// Stroke converter: generates a stroked outline from a center-line path.
16///
17/// Port of C++ `conv_stroke<VertexSource>`.
18pub struct ConvStroke<VS: VertexSource> {
19    base: ConvAdaptorVcgen<VS, VcgenStroke>,
20}
21
22impl<VS: VertexSource> ConvStroke<VS> {
23    pub fn new(source: VS) -> Self {
24        Self {
25            base: ConvAdaptorVcgen::new(source, VcgenStroke::new()),
26        }
27    }
28
29    // Parameter forwarding
30    pub fn set_line_cap(&mut self, lc: LineCap) {
31        self.base.generator_mut().set_line_cap(lc);
32    }
33    pub fn line_cap(&self) -> LineCap {
34        self.base.generator().line_cap()
35    }
36
37    pub fn set_line_join(&mut self, lj: LineJoin) {
38        self.base.generator_mut().set_line_join(lj);
39    }
40    pub fn line_join(&self) -> LineJoin {
41        self.base.generator().line_join()
42    }
43
44    pub fn set_inner_join(&mut self, ij: InnerJoin) {
45        self.base.generator_mut().set_inner_join(ij);
46    }
47    pub fn inner_join(&self) -> InnerJoin {
48        self.base.generator().inner_join()
49    }
50
51    pub fn set_width(&mut self, w: f64) {
52        self.base.generator_mut().set_width(w);
53    }
54    pub fn width(&self) -> f64 {
55        self.base.generator().width()
56    }
57
58    pub fn set_miter_limit(&mut self, ml: f64) {
59        self.base.generator_mut().set_miter_limit(ml);
60    }
61    pub fn miter_limit(&self) -> f64 {
62        self.base.generator().miter_limit()
63    }
64
65    pub fn set_miter_limit_theta(&mut self, t: f64) {
66        self.base.generator_mut().set_miter_limit_theta(t);
67    }
68
69    pub fn set_inner_miter_limit(&mut self, ml: f64) {
70        self.base.generator_mut().set_inner_miter_limit(ml);
71    }
72    pub fn inner_miter_limit(&self) -> f64 {
73        self.base.generator().inner_miter_limit()
74    }
75
76    pub fn set_approximation_scale(&mut self, s: f64) {
77        self.base.generator_mut().set_approximation_scale(s);
78    }
79    pub fn approximation_scale(&self) -> f64 {
80        self.base.generator().approximation_scale()
81    }
82
83    pub fn set_shorten(&mut self, s: f64) {
84        self.base.generator_mut().set_shorten(s);
85    }
86    pub fn shorten(&self) -> f64 {
87        self.base.generator().shorten()
88    }
89
90    pub fn source(&self) -> &VS {
91        self.base.source()
92    }
93
94    pub fn source_mut(&mut self) -> &mut VS {
95        self.base.source_mut()
96    }
97}
98
99impl<VS: VertexSource> VertexSource for ConvStroke<VS> {
100    fn rewind(&mut self, path_id: u32) {
101        self.base.rewind(path_id);
102    }
103
104    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
105        self.base.vertex(x, y)
106    }
107}
108
109// ============================================================================
110// Tests
111// ============================================================================
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::basics::{is_stop, is_vertex, PATH_CMD_MOVE_TO};
117    use crate::path_storage::PathStorage;
118
119    fn collect_vertices<VS: VertexSource>(vs: &mut VS) -> Vec<(f64, f64, u32)> {
120        let mut result = Vec::new();
121        vs.rewind(0);
122        loop {
123            let (mut x, mut y) = (0.0, 0.0);
124            let cmd = vs.vertex(&mut x, &mut y);
125            if is_stop(cmd) {
126                break;
127            }
128            result.push((x, y, cmd));
129        }
130        result
131    }
132
133    #[test]
134    fn test_stroke_horizontal_line() {
135        let mut path = PathStorage::new();
136        path.move_to(0.0, 0.0);
137        path.line_to(100.0, 0.0);
138
139        let mut stroke = ConvStroke::new(path);
140        stroke.set_width(10.0);
141        let verts = collect_vertices(&mut stroke);
142
143        assert!(
144            verts.len() >= 4,
145            "Expected at least 4 stroke vertices, got {}",
146            verts.len()
147        );
148        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
149    }
150
151    #[test]
152    fn test_stroke_triangle() {
153        let mut path = PathStorage::new();
154        path.move_to(10.0, 10.0);
155        path.line_to(90.0, 10.0);
156        path.line_to(50.0, 80.0);
157
158        let mut stroke = ConvStroke::new(path);
159        stroke.set_width(4.0);
160        let verts = collect_vertices(&mut stroke);
161
162        assert!(verts.len() >= 6, "Expected many stroke vertices");
163    }
164
165    #[test]
166    fn test_stroke_width() {
167        let path = PathStorage::new();
168        let mut stroke = ConvStroke::new(path);
169        stroke.set_width(5.0);
170        assert!((stroke.width() - 5.0).abs() < 1e-10);
171    }
172
173    #[test]
174    fn test_stroke_empty_path() {
175        let path = PathStorage::new();
176        let mut stroke = ConvStroke::new(path);
177        let verts = collect_vertices(&mut stroke);
178        assert!(verts.is_empty());
179    }
180
181    #[test]
182    fn test_stroke_round_cap() {
183        let mut path = PathStorage::new();
184        path.move_to(0.0, 0.0);
185        path.line_to(50.0, 0.0);
186
187        let mut stroke = ConvStroke::new(path);
188        stroke.set_width(20.0);
189        stroke.set_line_cap(LineCap::Round);
190        let verts = collect_vertices(&mut stroke);
191
192        // Round caps produce many vertices
193        assert!(
194            verts.len() > 10,
195            "Round cap stroke should have many vertices, got {}",
196            verts.len()
197        );
198    }
199
200    #[test]
201    fn test_stroke_y_extent() {
202        let mut path = PathStorage::new();
203        path.move_to(10.0, 50.0);
204        path.line_to(90.0, 50.0);
205
206        let mut stroke = ConvStroke::new(path);
207        stroke.set_width(20.0); // half-width = 10
208
209        let verts = collect_vertices(&mut stroke);
210        let max_y = verts
211            .iter()
212            .filter(|v| is_vertex(v.2))
213            .map(|v| v.1)
214            .fold(f64::MIN, f64::max);
215        let min_y = verts
216            .iter()
217            .filter(|v| is_vertex(v.2))
218            .map(|v| v.1)
219            .fold(f64::MAX, f64::min);
220
221        assert!(max_y >= 59.0, "Max y={} should be >= 59", max_y);
222        assert!(min_y <= 41.0, "Min y={} should be <= 41", min_y);
223    }
224
225    #[test]
226    fn test_stroke_rewind_replay() {
227        let mut path = PathStorage::new();
228        path.move_to(0.0, 0.0);
229        path.line_to(100.0, 0.0);
230
231        let mut stroke = ConvStroke::new(path);
232        stroke.set_width(4.0);
233        let v1 = collect_vertices(&mut stroke);
234        let v2 = collect_vertices(&mut stroke);
235        assert_eq!(v1.len(), v2.len());
236    }
237
238    #[test]
239    fn test_stroke_line_join_round() {
240        let mut path = PathStorage::new();
241        path.move_to(0.0, 0.0);
242        path.line_to(50.0, 0.0);
243        path.line_to(50.0, 50.0);
244
245        let mut miter = ConvStroke::new(&mut path);
246        miter.set_width(10.0);
247        miter.set_line_join(LineJoin::Miter);
248        let miter_verts = collect_vertices(&mut miter);
249
250        let mut round = ConvStroke::new(&mut path);
251        round.set_width(10.0);
252        round.set_line_join(LineJoin::Round);
253        let round_verts = collect_vertices(&mut round);
254
255        // Round join produces more vertices than miter
256        assert!(
257            round_verts.len() >= miter_verts.len(),
258            "Round ({}) should have >= vertices than miter ({})",
259            round_verts.len(),
260            miter_verts.len()
261        );
262    }
263}