Skip to main content

laser_pdf/elements/
circle.rs

1use kurbo::Shape;
2
3use crate::{utils::*, *};
4
5/// A circular shape element with optional fill and outline.
6/// 
7/// The circle is rendered with the specified radius and can have both
8/// a fill color and an outline with configurable thickness and color.
9pub struct Circle {
10    /// Radius of the circle in millimeters
11    pub radius: f32,
12    /// Optional fill color as RGBA (None for transparent)
13    pub fill: Option<u32>,
14    /// Optional outline as (thickness_mm, color_rgba)
15    pub outline: Option<(f32, u32)>,
16}
17
18impl Element for Circle {
19    fn measure(&self, mut ctx: MeasureCtx) -> ElementSize {
20        let outline_thickness = outline_thickness(self);
21        ctx.break_if_appropriate_for_min_height(self.radius * 2. + outline_thickness);
22
23        size(self)
24    }
25
26    fn draw(&self, mut ctx: DrawCtx) -> ElementSize {
27        let outline_thickness = outline_thickness(self);
28        ctx.break_if_appropriate_for_min_height(self.radius * 2. + outline_thickness);
29
30        let extra_outline_offset = outline_thickness / 2.0;
31
32        let resource_id;
33
34        let fill_alpha = self
35            .fill
36            .map(|c| u32_to_color_and_alpha(c).1)
37            .filter(|&a| a != 1.);
38
39        let outline_alpha = self
40            .outline
41            .map(|(_, c)| u32_to_color_and_alpha(c).1)
42            .filter(|&a| a != 1.);
43
44        if fill_alpha.is_some() || outline_alpha.is_some() {
45            let ext_graphics_ref = ctx.pdf.alloc();
46
47            let mut ext_graphics = ctx.pdf.pdf.ext_graphics(ext_graphics_ref);
48            fill_alpha.inspect(|&a| {
49                ext_graphics.non_stroking_alpha(a);
50            });
51            outline_alpha.inspect(|&a| {
52                ext_graphics.stroking_alpha(a);
53            });
54
55            resource_id =
56                Some(ctx.pdf.pages[ctx.location.page_idx].add_ext_g_state(ext_graphics_ref));
57        } else {
58            resource_id = None;
59        }
60
61        let layer = ctx.location.layer(ctx.pdf);
62
63        layer.save_state();
64
65        if let Some(color) = self.fill {
66            set_fill_color(layer, color);
67        }
68
69        if let Some((thickness, color)) = self.outline {
70            layer.set_line_width(mm_to_pt(thickness) as f32);
71
72            set_stroke_color(layer, color);
73        }
74
75        if let Some(ext_graphics) = resource_id {
76            layer.set_parameters(Name(format!("{}", ext_graphics).as_bytes()));
77        }
78
79        let shape = kurbo::Circle::new(
80            (
81                mm_to_pt(ctx.location.pos.0 + self.radius + extra_outline_offset) as f64,
82                mm_to_pt(ctx.location.pos.1 - self.radius - extra_outline_offset) as f64,
83            ),
84            mm_to_pt(self.radius) as f64,
85        );
86
87        let els = shape.path_elements(0.1);
88
89        let mut closed = false;
90
91        for el in els {
92            use kurbo::PathEl::*;
93
94            match el {
95                MoveTo(point) => {
96                    layer.move_to(point.x as f32, point.y as f32);
97                }
98                LineTo(point) => {
99                    layer.line_to(point.x as f32, point.y as f32);
100                }
101                QuadTo(a, b) => {
102                    layer.cubic_to_initial(a.x as f32, a.y as f32, b.x as f32, b.y as f32);
103                }
104                CurveTo(a, b, c) => {
105                    layer.cubic_to(
106                        a.x as f32, a.y as f32, b.x as f32, b.y as f32, c.x as f32, c.y as f32,
107                    );
108                }
109                ClosePath => closed = true,
110            };
111        }
112
113        assert!(closed);
114
115        match (self.fill.is_some(), self.outline.is_some()) {
116            (true, true) => layer.fill_nonzero_and_stroke(),
117            (true, false) => layer.fill_nonzero(),
118            (false, true) => layer.stroke(),
119            (false, false) => layer,
120        };
121
122        layer.restore_state();
123
124        size(self)
125    }
126}
127
128fn outline_thickness(circle: &Circle) -> f32 {
129    circle.outline.map(|o| o.0).unwrap_or(0.0)
130}
131
132fn size(circle: &Circle) -> ElementSize {
133    let outline_thickness = outline_thickness(circle);
134
135    let size = circle.radius * 2. + outline_thickness;
136
137    ElementSize {
138        width: Some(size),
139        height: Some(size),
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use insta::*;
147    use test_utils::binary_snapshots::*;
148
149    #[test]
150    fn test_circle() {
151        use crate::test_utils::*;
152
153        for output in (ElementTestParams {
154            first_height: 11.,
155            ..Default::default()
156        })
157        .run(&Circle {
158            radius: 5.5,
159            fill: None,
160            outline: Some((1., 0)),
161        }) {
162            output.assert_size(ElementSize {
163                width: Some(12.),
164                height: Some(12.),
165            });
166
167            if let Some(b) = output.breakable {
168                if output.first_height == 11. {
169                    b.assert_break_count(1);
170                } else {
171                    b.assert_break_count(0);
172                }
173
174                b.assert_extra_location_min_height(None);
175            }
176        }
177    }
178
179    #[test]
180    fn test() {
181        let bytes = test_element_bytes(TestElementParams::breakable(), |callback| {
182            callback.call(
183                &Circle {
184                    radius: 52.5,
185                    fill: Some(0x00_FF_00_77),
186                    outline: Some((12., 0x00_00_FF_44)),
187                }
188                .debug(0)
189                .show_max_width()
190                .show_last_location_max_height(),
191            );
192        });
193        assert_binary_snapshot!(".pdf", bytes);
194    }
195}