laser_pdf/elements/
circle.rs1use kurbo::Shape;
2
3use crate::{utils::*, *};
4
5pub struct Circle {
10 pub radius: f32,
12 pub fill: Option<u32>,
14 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}