Skip to main content

oxidize_pdf/graphics/
path.rs

1#[derive(Debug, Clone, Copy, PartialEq)]
2#[repr(u8)]
3pub enum LineCap {
4    Butt = 0,
5    Round = 1,
6    Square = 2,
7}
8
9#[derive(Debug, Clone, Copy, PartialEq)]
10#[repr(u8)]
11pub enum LineJoin {
12    Miter = 0,
13    Round = 1,
14    Bevel = 2,
15}
16
17/// Winding rule for determining path interior
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum WindingRule {
20    /// Non-zero winding rule (default)
21    NonZero,
22    /// Even-odd winding rule
23    EvenOdd,
24}
25
26pub struct PathBuilder {
27    commands: Vec<PathCommand>,
28}
29
30/// Path construction commands
31#[derive(Debug, Clone)]
32pub enum PathCommand {
33    /// Move to position
34    MoveTo { x: f64, y: f64 },
35    /// Draw line to position
36    LineTo { x: f64, y: f64 },
37    /// Draw cubic Bézier curve
38    CurveTo {
39        x1: f64,
40        y1: f64,
41        x2: f64,
42        y2: f64,
43        x3: f64,
44        y3: f64,
45    },
46    /// Draw rectangle
47    Rectangle {
48        x: f64,
49        y: f64,
50        width: f64,
51        height: f64,
52    },
53    /// Close current path
54    ClosePath,
55}
56
57impl Default for PathBuilder {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63#[allow(dead_code)]
64impl PathBuilder {
65    pub fn new() -> Self {
66        Self {
67            commands: Vec::new(),
68        }
69    }
70
71    pub fn move_to(mut self, x: f64, y: f64) -> Self {
72        self.commands.push(PathCommand::MoveTo { x, y });
73        self
74    }
75
76    pub fn line_to(mut self, x: f64, y: f64) -> Self {
77        self.commands.push(PathCommand::LineTo { x, y });
78        self
79    }
80
81    pub fn curve_to(mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self {
82        self.commands.push(PathCommand::CurveTo {
83            x1,
84            y1,
85            x2,
86            y2,
87            x3,
88            y3,
89        });
90        self
91    }
92
93    pub fn rectangle(mut self, x: f64, y: f64, width: f64, height: f64) -> Self {
94        self.commands.push(PathCommand::Rectangle {
95            x,
96            y,
97            width,
98            height,
99        });
100        self
101    }
102
103    pub fn close(mut self) -> Self {
104        self.commands.push(PathCommand::ClosePath);
105        self
106    }
107
108    pub(crate) fn build(self) -> Vec<PathCommand> {
109        self.commands
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_line_cap_values() {
119        assert_eq!(LineCap::Butt as u8, 0);
120        assert_eq!(LineCap::Round as u8, 1);
121        assert_eq!(LineCap::Square as u8, 2);
122    }
123
124    #[test]
125    fn test_line_cap_equality() {
126        assert_eq!(LineCap::Butt, LineCap::Butt);
127        assert_ne!(LineCap::Butt, LineCap::Round);
128        assert_ne!(LineCap::Round, LineCap::Square);
129    }
130
131    #[test]
132    fn test_line_cap_debug() {
133        let butt = LineCap::Butt;
134        let debug_str = format!("{:?}", butt);
135        assert_eq!(debug_str, "Butt");
136
137        let round = LineCap::Round;
138        assert_eq!(format!("{:?}", round), "Round");
139
140        let square = LineCap::Square;
141        assert_eq!(format!("{:?}", square), "Square");
142    }
143
144    #[test]
145    fn test_line_cap_clone() {
146        let cap = LineCap::Round;
147        let cap_clone = cap;
148        assert_eq!(cap, cap_clone);
149    }
150
151    #[test]
152    fn test_line_cap_copy() {
153        let cap = LineCap::Square;
154        let cap_copy = cap; // Copy semantics
155        assert_eq!(cap, cap_copy);
156
157        // Both should still be usable
158        assert_eq!(cap, LineCap::Square);
159        assert_eq!(cap_copy, LineCap::Square);
160    }
161
162    #[test]
163    fn test_line_join_values() {
164        assert_eq!(LineJoin::Miter as u8, 0);
165        assert_eq!(LineJoin::Round as u8, 1);
166        assert_eq!(LineJoin::Bevel as u8, 2);
167    }
168
169    #[test]
170    fn test_line_join_equality() {
171        assert_eq!(LineJoin::Miter, LineJoin::Miter);
172        assert_ne!(LineJoin::Miter, LineJoin::Round);
173        assert_ne!(LineJoin::Round, LineJoin::Bevel);
174    }
175
176    #[test]
177    fn test_line_join_debug() {
178        let miter = LineJoin::Miter;
179        let debug_str = format!("{:?}", miter);
180        assert_eq!(debug_str, "Miter");
181
182        let round = LineJoin::Round;
183        assert_eq!(format!("{:?}", round), "Round");
184
185        let bevel = LineJoin::Bevel;
186        assert_eq!(format!("{:?}", bevel), "Bevel");
187    }
188
189    #[test]
190    fn test_line_join_clone() {
191        let join = LineJoin::Bevel;
192        let join_clone = join;
193        assert_eq!(join, join_clone);
194    }
195
196    #[test]
197    fn test_line_join_copy() {
198        let join = LineJoin::Miter;
199        let join_copy = join; // Copy semantics
200        assert_eq!(join, join_copy);
201
202        // Both should still be usable
203        assert_eq!(join, LineJoin::Miter);
204        assert_eq!(join_copy, LineJoin::Miter);
205    }
206
207    #[test]
208    fn test_path_builder_new() {
209        let builder = PathBuilder::new();
210        let commands = builder.build();
211        assert!(commands.is_empty());
212    }
213
214    #[test]
215    fn test_path_builder_default() {
216        let builder = PathBuilder::default();
217        let commands = builder.build();
218        assert!(commands.is_empty());
219    }
220
221    #[test]
222    fn test_path_builder_move_to() {
223        let builder = PathBuilder::new().move_to(10.0, 20.0);
224        let commands = builder.build();
225
226        assert_eq!(commands.len(), 1);
227        match &commands[0] {
228            PathCommand::MoveTo { x, y } => {
229                assert_eq!(*x, 10.0);
230                assert_eq!(*y, 20.0);
231            }
232            _ => panic!("Expected MoveTo command"),
233        }
234    }
235
236    #[test]
237    fn test_path_builder_line_to() {
238        let builder = PathBuilder::new().line_to(30.0, 40.0);
239        let commands = builder.build();
240
241        assert_eq!(commands.len(), 1);
242        match &commands[0] {
243            PathCommand::LineTo { x, y } => {
244                assert_eq!(*x, 30.0);
245                assert_eq!(*y, 40.0);
246            }
247            _ => panic!("Expected LineTo command"),
248        }
249    }
250
251    #[test]
252    fn test_path_builder_curve_to() {
253        let builder = PathBuilder::new().curve_to(10.0, 20.0, 30.0, 40.0, 50.0, 60.0);
254        let commands = builder.build();
255
256        assert_eq!(commands.len(), 1);
257        match &commands[0] {
258            PathCommand::CurveTo {
259                x1,
260                y1,
261                x2,
262                y2,
263                x3,
264                y3,
265            } => {
266                assert_eq!(*x1, 10.0);
267                assert_eq!(*y1, 20.0);
268                assert_eq!(*x2, 30.0);
269                assert_eq!(*y2, 40.0);
270                assert_eq!(*x3, 50.0);
271                assert_eq!(*y3, 60.0);
272            }
273            _ => panic!("Expected CurveTo command"),
274        }
275    }
276
277    #[test]
278    fn test_path_builder_close() {
279        let builder = PathBuilder::new().close();
280        let commands = builder.build();
281
282        assert_eq!(commands.len(), 1);
283        match &commands[0] {
284            PathCommand::ClosePath => {}
285            _ => panic!("Expected ClosePath command"),
286        }
287    }
288
289    #[test]
290    fn test_path_builder_complex_path() {
291        let builder = PathBuilder::new()
292            .move_to(0.0, 0.0)
293            .line_to(100.0, 0.0)
294            .line_to(100.0, 100.0)
295            .line_to(0.0, 100.0)
296            .close();
297
298        let commands = builder.build();
299        assert_eq!(commands.len(), 5);
300
301        match &commands[0] {
302            PathCommand::MoveTo { x, y } => {
303                assert_eq!(*x, 0.0);
304                assert_eq!(*y, 0.0);
305            }
306            _ => panic!("Expected MoveTo at index 0"),
307        }
308
309        match &commands[1] {
310            PathCommand::LineTo { x, y } => {
311                assert_eq!(*x, 100.0);
312                assert_eq!(*y, 0.0);
313            }
314            _ => panic!("Expected LineTo at index 1"),
315        }
316
317        match &commands[4] {
318            PathCommand::ClosePath => {}
319            _ => panic!("Expected ClosePath at index 4"),
320        }
321    }
322
323    #[test]
324    fn test_path_builder_bezier_curve() {
325        let builder = PathBuilder::new()
326            .move_to(0.0, 0.0)
327            .curve_to(50.0, 0.0, 100.0, 50.0, 100.0, 100.0)
328            .curve_to(100.0, 150.0, 50.0, 200.0, 0.0, 200.0);
329
330        let commands = builder.build();
331        assert_eq!(commands.len(), 3);
332
333        match &commands[1] {
334            PathCommand::CurveTo {
335                x1,
336                y1,
337                x2,
338                y2,
339                x3,
340                y3,
341            } => {
342                assert_eq!(*x1, 50.0);
343                assert_eq!(*y1, 0.0);
344                assert_eq!(*x2, 100.0);
345                assert_eq!(*y2, 50.0);
346                assert_eq!(*x3, 100.0);
347                assert_eq!(*y3, 100.0);
348            }
349            _ => panic!("Expected CurveTo at index 1"),
350        }
351    }
352
353    #[test]
354    fn test_path_command_debug() {
355        let move_cmd = PathCommand::MoveTo { x: 10.0, y: 20.0 };
356        let debug_str = format!("{:?}", move_cmd);
357        assert!(debug_str.contains("MoveTo"));
358        assert!(debug_str.contains("10.0"));
359        assert!(debug_str.contains("20.0"));
360
361        let line_cmd = PathCommand::LineTo { x: 30.0, y: 40.0 };
362        let line_debug = format!("{:?}", line_cmd);
363        assert!(line_debug.contains("LineTo"));
364
365        let curve_cmd = PathCommand::CurveTo {
366            x1: 1.0,
367            y1: 2.0,
368            x2: 3.0,
369            y2: 4.0,
370            x3: 5.0,
371            y3: 6.0,
372        };
373        let curve_debug = format!("{:?}", curve_cmd);
374        assert!(curve_debug.contains("CurveTo"));
375
376        let close_cmd = PathCommand::ClosePath;
377        let close_debug = format!("{:?}", close_cmd);
378        assert!(close_debug.contains("ClosePath"));
379    }
380
381    #[test]
382    fn test_path_command_clone() {
383        let move_cmd = PathCommand::MoveTo { x: 10.0, y: 20.0 };
384        let move_clone = move_cmd.clone();
385        match (move_cmd, move_clone) {
386            (PathCommand::MoveTo { x: x1, y: y1 }, PathCommand::MoveTo { x: x2, y: y2 }) => {
387                assert_eq!(x1, x2);
388                assert_eq!(y1, y2);
389            }
390            _ => panic!("Clone failed"),
391        }
392
393        let close_cmd = PathCommand::ClosePath;
394        let close_clone = close_cmd;
395        match close_clone {
396            PathCommand::ClosePath => {}
397            _ => panic!("Clone failed for ClosePath"),
398        }
399    }
400
401    #[test]
402    fn test_path_builder_empty_path() {
403        let builder = PathBuilder::new();
404        let commands = builder.build();
405        assert_eq!(commands.len(), 0);
406    }
407
408    #[test]
409    fn test_path_builder_single_command() {
410        // Test each command type in isolation
411        let move_builder = PathBuilder::new().move_to(5.0, 10.0);
412        assert_eq!(move_builder.build().len(), 1);
413
414        let line_builder = PathBuilder::new().line_to(15.0, 20.0);
415        assert_eq!(line_builder.build().len(), 1);
416
417        let curve_builder = PathBuilder::new().curve_to(1.0, 2.0, 3.0, 4.0, 5.0, 6.0);
418        assert_eq!(curve_builder.build().len(), 1);
419
420        let close_builder = PathBuilder::new().close();
421        assert_eq!(close_builder.build().len(), 1);
422    }
423
424    #[test]
425    fn test_path_builder_negative_coordinates() {
426        let builder = PathBuilder::new()
427            .move_to(-10.0, -20.0)
428            .line_to(-30.0, -40.0)
429            .curve_to(-1.0, -2.0, -3.0, -4.0, -5.0, -6.0);
430
431        let commands = builder.build();
432        assert_eq!(commands.len(), 3);
433
434        match &commands[0] {
435            PathCommand::MoveTo { x, y } => {
436                assert_eq!(*x, -10.0);
437                assert_eq!(*y, -20.0);
438            }
439            _ => panic!("Expected MoveTo"),
440        }
441    }
442
443    #[test]
444    fn test_path_builder_zero_values() {
445        let builder = PathBuilder::new()
446            .move_to(0.0, 0.0)
447            .line_to(0.0, 0.0)
448            .curve_to(0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
449
450        let commands = builder.build();
451        assert_eq!(commands.len(), 3);
452
453        // All commands should have zero values
454        match &commands[0] {
455            PathCommand::MoveTo { x, y } => {
456                assert_eq!(*x, 0.0);
457                assert_eq!(*y, 0.0);
458            }
459            _ => panic!("Expected MoveTo"),
460        }
461    }
462
463    #[test]
464    fn test_path_builder_large_values() {
465        let large_val = 1e6;
466        let builder = PathBuilder::new()
467            .move_to(large_val, large_val)
468            .line_to(large_val * 2.0, large_val * 2.0);
469
470        let commands = builder.build();
471        match &commands[0] {
472            PathCommand::MoveTo { x, y } => {
473                assert_eq!(*x, large_val);
474                assert_eq!(*y, large_val);
475            }
476            _ => panic!("Expected MoveTo"),
477        }
478    }
479}