Skip to main content

qr_code_styling/figures/dot/
drawer.rs

1//! QR dot drawer implementation.
2
3use std::f64::consts::PI;
4
5use crate::figures::traits::{rotate_transform, svg_circle, svg_path, svg_rect};
6use crate::types::DotType;
7
8/// QR code dot drawer.
9pub struct QRDot {
10    dot_type: DotType,
11}
12
13impl QRDot {
14    /// Create a new dot drawer with the specified type.
15    pub fn new(dot_type: DotType) -> Self {
16        Self { dot_type }
17    }
18
19    /// Draw a basic dot (circle).
20    fn basic_dot(&self, x: f64, y: f64, size: f64, rotation: f64) -> String {
21        let transform = rotate_transform(x, y, size, rotation);
22        svg_circle(
23            x + size / 2.0,
24            y + size / 2.0,
25            size / 2.0,
26            transform.as_deref(),
27        )
28    }
29
30    /// Draw a basic square.
31    fn basic_square(&self, x: f64, y: f64, size: f64, rotation: f64) -> String {
32        let transform = rotate_transform(x, y, size, rotation);
33        svg_rect(x, y, size, size, transform.as_deref())
34    }
35
36    /// Draw a side-rounded shape (if rotation === 0, right side is rounded).
37    fn basic_side_rounded(&self, x: f64, y: f64, size: f64, rotation: f64) -> String {
38        let transform = rotate_transform(x, y, size, rotation);
39        let half = size / 2.0;
40        let d = format!(
41            "M {} {} v {} h {} a {} {} 0 0 0 0 {}",
42            x, y, size, half, half, half, -size
43        );
44        svg_path(&d, None, transform.as_deref())
45    }
46
47    /// Draw a corner-rounded shape (if rotation === 0, top right corner is rounded).
48    fn basic_corner_rounded(&self, x: f64, y: f64, size: f64, rotation: f64) -> String {
49        let transform = rotate_transform(x, y, size, rotation);
50        let half = size / 2.0;
51        let d = format!(
52            "M {} {} v {} h {} v {} a {} {} 0 0 0 {} {}",
53            x, y, size, size, -half, half, half, -half, -half
54        );
55        svg_path(&d, None, transform.as_deref())
56    }
57
58    /// Draw an extra-rounded corner shape.
59    fn basic_corner_extra_rounded(&self, x: f64, y: f64, size: f64, rotation: f64) -> String {
60        let transform = rotate_transform(x, y, size, rotation);
61        let d = format!(
62            "M {} {} v {} h {} a {} {} 0 0 0 {} {}",
63            x, y, size, size, size, size, -size, -size
64        );
65        svg_path(&d, None, transform.as_deref())
66    }
67
68    /// Draw corners-rounded shape (left bottom and right top corners are rounded).
69    fn basic_corners_rounded(&self, x: f64, y: f64, size: f64, rotation: f64) -> String {
70        let transform = rotate_transform(x, y, size, rotation);
71        let half = size / 2.0;
72        let d = format!(
73            "M {} {} v {} a {} {} 0 0 0 {} {} h {} v {} a {} {} 0 0 0 {} {}",
74            x, y, half, half, half, half, half, half, -half, half, half, -half, -half
75        );
76        svg_path(&d, None, transform.as_deref())
77    }
78
79    /// Draw the dot and return SVG element string.
80    pub fn draw<F>(&self, x: f64, y: f64, size: f64, get_neighbor: Option<F>) -> String
81    where
82        F: Fn(i32, i32) -> bool,
83    {
84        match self.dot_type {
85            DotType::Dots => self.basic_dot(x, y, size, 0.0),
86            DotType::Square => self.basic_square(x, y, size, 0.0),
87            DotType::Rounded => self.draw_rounded(x, y, size, get_neighbor),
88            DotType::ExtraRounded => self.draw_extra_rounded(x, y, size, get_neighbor),
89            DotType::Classy => self.draw_classy(x, y, size, get_neighbor),
90            DotType::ClassyRounded => self.draw_classy_rounded(x, y, size, get_neighbor),
91        }
92    }
93
94    /// Draw rounded type based on neighbors.
95    fn draw_rounded<F>(&self, x: f64, y: f64, size: f64, get_neighbor: Option<F>) -> String
96    where
97        F: Fn(i32, i32) -> bool,
98    {
99        let (left, right, top, bottom) = self.get_neighbors(&get_neighbor);
100        let count = left + right + top + bottom;
101
102        if count == 0 {
103            return self.basic_dot(x, y, size, 0.0);
104        }
105
106        if count > 2 || (left == 1 && right == 1) || (top == 1 && bottom == 1) {
107            return self.basic_square(x, y, size, 0.0);
108        }
109
110        if count == 2 {
111            let rotation = if left == 1 && top == 1 {
112                PI / 2.0
113            } else if top == 1 && right == 1 {
114                PI
115            } else if right == 1 && bottom == 1 {
116                -PI / 2.0
117            } else {
118                0.0
119            };
120            return self.basic_corner_rounded(x, y, size, rotation);
121        }
122
123        // count == 1
124        let rotation = if top == 1 {
125            PI / 2.0
126        } else if right == 1 {
127            PI
128        } else if bottom == 1 {
129            -PI / 2.0
130        } else {
131            0.0
132        };
133        self.basic_side_rounded(x, y, size, rotation)
134    }
135
136    /// Draw extra-rounded type based on neighbors.
137    fn draw_extra_rounded<F>(&self, x: f64, y: f64, size: f64, get_neighbor: Option<F>) -> String
138    where
139        F: Fn(i32, i32) -> bool,
140    {
141        let (left, right, top, bottom) = self.get_neighbors(&get_neighbor);
142        let count = left + right + top + bottom;
143
144        if count == 0 {
145            return self.basic_dot(x, y, size, 0.0);
146        }
147
148        if count > 2 || (left == 1 && right == 1) || (top == 1 && bottom == 1) {
149            return self.basic_square(x, y, size, 0.0);
150        }
151
152        if count == 2 {
153            let rotation = if left == 1 && top == 1 {
154                PI / 2.0
155            } else if top == 1 && right == 1 {
156                PI
157            } else if right == 1 && bottom == 1 {
158                -PI / 2.0
159            } else {
160                0.0
161            };
162            return self.basic_corner_extra_rounded(x, y, size, rotation);
163        }
164
165        // count == 1
166        let rotation = if top == 1 {
167            PI / 2.0
168        } else if right == 1 {
169            PI
170        } else if bottom == 1 {
171            -PI / 2.0
172        } else {
173            0.0
174        };
175        self.basic_side_rounded(x, y, size, rotation)
176    }
177
178    /// Draw classy type based on neighbors.
179    fn draw_classy<F>(&self, x: f64, y: f64, size: f64, get_neighbor: Option<F>) -> String
180    where
181        F: Fn(i32, i32) -> bool,
182    {
183        let (left, right, top, bottom) = self.get_neighbors(&get_neighbor);
184        let count = left + right + top + bottom;
185
186        if count == 0 {
187            return self.basic_corners_rounded(x, y, size, PI / 2.0);
188        }
189
190        if left == 0 && top == 0 {
191            return self.basic_corner_rounded(x, y, size, -PI / 2.0);
192        }
193
194        if right == 0 && bottom == 0 {
195            return self.basic_corner_rounded(x, y, size, PI / 2.0);
196        }
197
198        self.basic_square(x, y, size, 0.0)
199    }
200
201    /// Draw classy-rounded type based on neighbors.
202    fn draw_classy_rounded<F>(&self, x: f64, y: f64, size: f64, get_neighbor: Option<F>) -> String
203    where
204        F: Fn(i32, i32) -> bool,
205    {
206        let (left, right, top, bottom) = self.get_neighbors(&get_neighbor);
207        let count = left + right + top + bottom;
208
209        if count == 0 {
210            return self.basic_corners_rounded(x, y, size, PI / 2.0);
211        }
212
213        if left == 0 && top == 0 {
214            return self.basic_corner_extra_rounded(x, y, size, -PI / 2.0);
215        }
216
217        if right == 0 && bottom == 0 {
218            return self.basic_corner_extra_rounded(x, y, size, PI / 2.0);
219        }
220
221        self.basic_square(x, y, size, 0.0)
222    }
223
224    /// Get neighbor states.
225    fn get_neighbors<F>(&self, get_neighbor: &Option<F>) -> (u8, u8, u8, u8)
226    where
227        F: Fn(i32, i32) -> bool,
228    {
229        match get_neighbor {
230            Some(f) => (
231                if f(-1, 0) { 1 } else { 0 },
232                if f(1, 0) { 1 } else { 0 },
233                if f(0, -1) { 1 } else { 0 },
234                if f(0, 1) { 1 } else { 0 },
235            ),
236            None => (0, 0, 0, 0),
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_draw_square() {
247        let drawer = QRDot::new(DotType::Square);
248        let svg = drawer.draw(0.0, 0.0, 10.0, None::<fn(i32, i32) -> bool>);
249        assert!(svg.contains("rect"));
250        assert!(svg.contains("width=\"10\""));
251    }
252
253    #[test]
254    fn test_draw_dot() {
255        let drawer = QRDot::new(DotType::Dots);
256        let svg = drawer.draw(0.0, 0.0, 10.0, None::<fn(i32, i32) -> bool>);
257        assert!(svg.contains("circle"));
258        assert!(svg.contains("r=\"5\""));
259    }
260
261    #[test]
262    fn test_draw_rounded_no_neighbors() {
263        let drawer = QRDot::new(DotType::Rounded);
264        let svg = drawer.draw(0.0, 0.0, 10.0, None::<fn(i32, i32) -> bool>);
265        // With no neighbors, should draw a circle
266        assert!(svg.contains("circle"));
267    }
268
269    #[test]
270    fn test_draw_rounded_with_neighbors() {
271        let drawer = QRDot::new(DotType::Rounded);
272        let neighbor_fn = |x: i32, _y: i32| x == 1; // right neighbor
273        let svg = drawer.draw(0.0, 0.0, 10.0, Some(neighbor_fn));
274        // With one neighbor, should draw a side-rounded path
275        assert!(svg.contains("path"));
276    }
277}