Skip to main content

agg_rust/
conv_curve.rs

1//! Curve flattening converter.
2//!
3//! Port of `agg_conv_curve.h` — converts `PATH_CMD_CURVE3` and `PATH_CMD_CURVE4`
4//! commands into sequences of `line_to` vertices by approximating the curves
5//! with straight line segments.
6
7use crate::basics::{is_stop, VertexSource, PATH_CMD_CURVE3, PATH_CMD_CURVE4, PATH_CMD_LINE_TO};
8use crate::curves::{Curve3, Curve4};
9
10// ============================================================================
11// ConvCurve
12// ============================================================================
13
14/// Curve flattening converter.
15///
16/// Wraps a `VertexSource` and replaces `curve3`/`curve4` commands with
17/// sequences of `line_to` vertices computed by the `Curve3`/`Curve4` classes.
18///
19/// Port of C++ `conv_curve<VertexSource>`.
20pub struct ConvCurve<VS: VertexSource> {
21    source: VS,
22    last_x: f64,
23    last_y: f64,
24    curve3: Curve3,
25    curve4: Curve4,
26}
27
28impl<VS: VertexSource> ConvCurve<VS> {
29    pub fn new(source: VS) -> Self {
30        Self {
31            source,
32            last_x: 0.0,
33            last_y: 0.0,
34            curve3: Curve3::new(),
35            curve4: Curve4::new(),
36        }
37    }
38
39    pub fn source(&self) -> &VS {
40        &self.source
41    }
42
43    pub fn source_mut(&mut self) -> &mut VS {
44        &mut self.source
45    }
46
47    /// Set the approximation scale for both curve types.
48    pub fn set_approximation_scale(&mut self, s: f64) {
49        self.curve3.set_approximation_scale(s);
50        self.curve4.set_approximation_scale(s);
51    }
52
53    pub fn approximation_scale(&self) -> f64 {
54        self.curve4.approximation_scale()
55    }
56
57    /// Set the angle tolerance for both curve types.
58    pub fn set_angle_tolerance(&mut self, v: f64) {
59        self.curve3.set_angle_tolerance(v);
60        self.curve4.set_angle_tolerance(v);
61    }
62
63    pub fn angle_tolerance(&self) -> f64 {
64        self.curve4.angle_tolerance()
65    }
66
67    /// Set the cusp limit (curve4 only — curve3 does not support cusp limit).
68    pub fn set_cusp_limit(&mut self, v: f64) {
69        self.curve4.set_cusp_limit(v);
70    }
71
72    pub fn cusp_limit(&self) -> f64 {
73        self.curve4.cusp_limit()
74    }
75}
76
77impl<VS: VertexSource> VertexSource for ConvCurve<VS> {
78    fn rewind(&mut self, path_id: u32) {
79        self.source.rewind(path_id);
80        self.last_x = 0.0;
81        self.last_y = 0.0;
82        self.curve3.reset();
83        self.curve4.reset();
84    }
85
86    fn vertex(&mut self, x: &mut f64, y: &mut f64) -> u32 {
87        // First check if curve3 has pending vertices
88        if !is_stop(self.curve3.vertex(x, y)) {
89            self.last_x = *x;
90            self.last_y = *y;
91            return PATH_CMD_LINE_TO;
92        }
93
94        // Then check if curve4 has pending vertices
95        if !is_stop(self.curve4.vertex(x, y)) {
96            self.last_x = *x;
97            self.last_y = *y;
98            return PATH_CMD_LINE_TO;
99        }
100
101        // Read next source vertex
102        let mut cmd = self.source.vertex(x, y);
103
104        match cmd {
105            PATH_CMD_CURVE3 => {
106                // Read the endpoint (control point is in x,y)
107                let (mut end_x, mut end_y) = (0.0, 0.0);
108                self.source.vertex(&mut end_x, &mut end_y);
109
110                self.curve3
111                    .init(self.last_x, self.last_y, *x, *y, end_x, end_y);
112
113                // First vertex() call returns move_to (skip it)
114                self.curve3.vertex(x, y);
115                // Second call is the first curve vertex
116                self.curve3.vertex(x, y);
117                cmd = PATH_CMD_LINE_TO;
118            }
119            PATH_CMD_CURVE4 => {
120                // Read the second control point and endpoint
121                let (mut ct2_x, mut ct2_y) = (0.0, 0.0);
122                let (mut end_x, mut end_y) = (0.0, 0.0);
123                self.source.vertex(&mut ct2_x, &mut ct2_y);
124                self.source.vertex(&mut end_x, &mut end_y);
125
126                self.curve4
127                    .init(self.last_x, self.last_y, *x, *y, ct2_x, ct2_y, end_x, end_y);
128
129                // First vertex() call returns move_to (skip it)
130                self.curve4.vertex(x, y);
131                // Second call is the first curve vertex
132                self.curve4.vertex(x, y);
133                cmd = PATH_CMD_LINE_TO;
134            }
135            _ => {}
136        }
137
138        self.last_x = *x;
139        self.last_y = *y;
140        cmd
141    }
142}
143
144// ============================================================================
145// Tests
146// ============================================================================
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::basics::PATH_CMD_MOVE_TO;
152    use crate::path_storage::PathStorage;
153
154    fn collect_vertices<VS: VertexSource>(vs: &mut VS) -> Vec<(f64, f64, u32)> {
155        let mut result = Vec::new();
156        vs.rewind(0);
157        loop {
158            let (mut x, mut y) = (0.0, 0.0);
159            let cmd = vs.vertex(&mut x, &mut y);
160            if is_stop(cmd) {
161                break;
162            }
163            result.push((x, y, cmd));
164        }
165        result
166    }
167
168    #[test]
169    fn test_no_curves_passthrough() {
170        let mut path = PathStorage::new();
171        path.move_to(10.0, 20.0);
172        path.line_to(30.0, 40.0);
173        path.line_to(50.0, 60.0);
174
175        let mut cc = ConvCurve::new(path);
176        let verts = collect_vertices(&mut cc);
177        assert_eq!(verts.len(), 3);
178        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
179        assert_eq!(verts[1].2, PATH_CMD_LINE_TO);
180        assert!((verts[0].0 - 10.0).abs() < 1e-10);
181        assert!((verts[2].0 - 50.0).abs() < 1e-10);
182    }
183
184    #[test]
185    fn test_curve3_flattening() {
186        let mut path = PathStorage::new();
187        path.move_to(0.0, 0.0);
188        path.curve3(50.0, 100.0, 100.0, 0.0);
189
190        let mut cc = ConvCurve::new(path);
191        let verts = collect_vertices(&mut cc);
192
193        // Should have move_to + multiple line_to vertices
194        assert!(
195            verts.len() > 2,
196            "Expected multiple vertices, got {}",
197            verts.len()
198        );
199        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
200        // All subsequent should be line_to
201        for v in &verts[1..] {
202            assert_eq!(v.2, PATH_CMD_LINE_TO);
203        }
204        // First vertex is the start point
205        assert!((verts[0].0).abs() < 1e-10);
206        assert!((verts[0].1).abs() < 1e-10);
207        // Last vertex should be near the endpoint
208        let last = verts.last().unwrap();
209        assert!((last.0 - 100.0).abs() < 1.0, "End x={}", last.0);
210        assert!((last.1).abs() < 1.0, "End y={}", last.1);
211    }
212
213    #[test]
214    fn test_curve4_flattening() {
215        let mut path = PathStorage::new();
216        path.move_to(0.0, 0.0);
217        path.curve4(33.0, 100.0, 66.0, 100.0, 100.0, 0.0);
218
219        let mut cc = ConvCurve::new(path);
220        let verts = collect_vertices(&mut cc);
221
222        assert!(
223            verts.len() > 2,
224            "Expected multiple vertices, got {}",
225            verts.len()
226        );
227        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
228        for v in &verts[1..] {
229            assert_eq!(v.2, PATH_CMD_LINE_TO);
230        }
231        // Last vertex near endpoint
232        let last = verts.last().unwrap();
233        assert!((last.0 - 100.0).abs() < 1.0, "End x={}", last.0);
234    }
235
236    #[test]
237    fn test_mixed_lines_and_curves() {
238        let mut path = PathStorage::new();
239        path.move_to(0.0, 0.0);
240        path.line_to(50.0, 0.0);
241        path.curve3(75.0, 50.0, 100.0, 0.0);
242        path.line_to(150.0, 0.0);
243
244        let mut cc = ConvCurve::new(path);
245        let verts = collect_vertices(&mut cc);
246
247        // Should have: move_to, line_to(50,0), curve3 flattened, line_to(150,0)
248        assert!(
249            verts.len() > 4,
250            "Expected > 4 vertices, got {}",
251            verts.len()
252        );
253        assert_eq!(verts[0].2, PATH_CMD_MOVE_TO);
254    }
255
256    #[test]
257    fn test_approximation_scale() {
258        let path = PathStorage::new();
259        let mut cc = ConvCurve::new(path);
260        cc.set_approximation_scale(2.0);
261        assert!((cc.approximation_scale() - 2.0).abs() < 1e-10);
262    }
263
264    #[test]
265    fn test_rewind_resets() {
266        let mut path = PathStorage::new();
267        path.move_to(0.0, 0.0);
268        path.curve3(50.0, 100.0, 100.0, 0.0);
269
270        let mut cc = ConvCurve::new(path);
271        let verts1 = collect_vertices(&mut cc);
272        let verts2 = collect_vertices(&mut cc);
273        assert_eq!(verts1.len(), verts2.len());
274    }
275
276    #[test]
277    fn test_empty_path() {
278        let path = PathStorage::new();
279        let mut cc = ConvCurve::new(path);
280        let verts = collect_vertices(&mut cc);
281        assert_eq!(verts.len(), 0);
282    }
283
284    #[test]
285    fn test_source_access() {
286        let path = PathStorage::new();
287        let cc = ConvCurve::new(path);
288        let _ = cc.source();
289    }
290
291    #[test]
292    fn test_angle_tolerance_and_cusp_limit() {
293        let path = PathStorage::new();
294        let mut cc = ConvCurve::new(path);
295        cc.set_angle_tolerance(0.5);
296        assert!((cc.angle_tolerance() - 0.5).abs() < 1e-10);
297        cc.set_cusp_limit(2.0);
298        assert!((cc.cusp_limit() - 2.0).abs() < 1e-10);
299    }
300}