Skip to main content

ad_plugins/
overlay.rs

1use std::sync::Arc;
2
3use ad_core::ndarray::{NDArray, NDDataBuffer};
4use ad_core::ndarray_pool::NDArrayPool;
5use ad_core::plugin::runtime::{NDPluginProcess, ProcessResult};
6
7/// Shape to draw.
8#[derive(Debug, Clone)]
9pub enum OverlayShape {
10    Cross { center_x: usize, center_y: usize, size: usize },
11    Rectangle { x: usize, y: usize, width: usize, height: usize },
12    Ellipse { center_x: usize, center_y: usize, rx: usize, ry: usize },
13    Text { x: usize, y: usize, text: String, font_size: usize },
14}
15
16/// Draw mode.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DrawMode {
19    Set,
20    XOR,
21}
22
23/// A single overlay definition.
24#[derive(Debug, Clone)]
25pub struct OverlayDef {
26    pub shape: OverlayShape,
27    pub draw_mode: DrawMode,
28    pub color: [u8; 3], // RGB color; for Mono, only color[0] is used
29}
30
31// ---------------------------------------------------------------------------
32// Minimal 5x7 bitmap font
33// ---------------------------------------------------------------------------
34
35const FONT_WIDTH: usize = 5;
36const FONT_HEIGHT: usize = 7;
37
38/// Return a 5x7 bitmap for printable ASCII characters.
39/// Each row is encoded as a u8 with the 5 MSBs representing pixels.
40fn get_char_bitmap(ch: char) -> [[bool; FONT_WIDTH]; FONT_HEIGHT] {
41    let pattern: [u8; 7] = match ch {
42        ' ' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
43        '!' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04],
44        '"' => [0x0A, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00],
45        '#' => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A],
46        '$' => [0x04, 0x1E, 0x05, 0x0E, 0x14, 0x0F, 0x04],
47        '%' => [0x03, 0x13, 0x08, 0x04, 0x02, 0x19, 0x18],
48        '&' => [0x06, 0x09, 0x05, 0x02, 0x15, 0x09, 0x16],
49        '\'' => [0x04, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00],
50        '(' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
51        ')' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
52        '*' => [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00],
53        '+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
54        ',' => [0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x02],
55        '-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
56        '.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04],
57        '/' => [0x10, 0x08, 0x08, 0x04, 0x02, 0x02, 0x01],
58        '0' => [0x0E, 0x11, 0x19, 0x15, 0x13, 0x11, 0x0E],
59        '1' => [0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x0E],
60        '2' => [0x0E, 0x11, 0x10, 0x08, 0x04, 0x02, 0x1F],
61        '3' => [0x0E, 0x11, 0x10, 0x0C, 0x10, 0x11, 0x0E],
62        '4' => [0x08, 0x0C, 0x0A, 0x09, 0x1F, 0x08, 0x08],
63        '5' => [0x1F, 0x01, 0x0F, 0x10, 0x10, 0x11, 0x0E],
64        '6' => [0x0C, 0x02, 0x01, 0x0F, 0x11, 0x11, 0x0E],
65        '7' => [0x1F, 0x10, 0x08, 0x04, 0x02, 0x02, 0x02],
66        '8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
67        '9' => [0x0E, 0x11, 0x11, 0x1E, 0x10, 0x08, 0x06],
68        ':' => [0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00],
69        ';' => [0x00, 0x00, 0x04, 0x00, 0x04, 0x04, 0x02],
70        '<' => [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08],
71        '=' => [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00],
72        '>' => [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02],
73        '?' => [0x0E, 0x11, 0x10, 0x08, 0x04, 0x00, 0x04],
74        '@' => [0x0E, 0x11, 0x15, 0x1D, 0x05, 0x01, 0x0E],
75        'A' | 'a' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
76        'B' | 'b' => [0x0F, 0x11, 0x11, 0x0F, 0x11, 0x11, 0x0F],
77        'C' | 'c' => [0x0E, 0x11, 0x01, 0x01, 0x01, 0x11, 0x0E],
78        'D' | 'd' => [0x07, 0x09, 0x11, 0x11, 0x11, 0x09, 0x07],
79        'E' | 'e' => [0x1F, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x1F],
80        'F' | 'f' => [0x1F, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x01],
81        'G' | 'g' => [0x0E, 0x11, 0x01, 0x1D, 0x11, 0x11, 0x0E],
82        'H' | 'h' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
83        'I' | 'i' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
84        'J' | 'j' => [0x1C, 0x08, 0x08, 0x08, 0x08, 0x09, 0x06],
85        'K' | 'k' => [0x11, 0x09, 0x05, 0x03, 0x05, 0x09, 0x11],
86        'L' | 'l' => [0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x1F],
87        'M' | 'm' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
88        'N' | 'n' => [0x11, 0x13, 0x15, 0x15, 0x19, 0x11, 0x11],
89        'O' | 'o' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
90        'P' | 'p' => [0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x01],
91        'Q' | 'q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x09, 0x16],
92        'R' | 'r' => [0x0F, 0x11, 0x11, 0x0F, 0x05, 0x09, 0x11],
93        'S' | 's' => [0x0E, 0x11, 0x01, 0x0E, 0x10, 0x11, 0x0E],
94        'T' | 't' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
95        'U' | 'u' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
96        'V' | 'v' => [0x11, 0x11, 0x11, 0x11, 0x0A, 0x0A, 0x04],
97        'W' | 'w' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11],
98        'X' | 'x' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
99        'Y' | 'y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04],
100        'Z' | 'z' => [0x1F, 0x10, 0x08, 0x04, 0x02, 0x01, 0x1F],
101        '[' => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E],
102        '\\' => [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10],
103        ']' => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E],
104        '^' => [0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00],
105        '_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
106        '`' => [0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00],
107        '{' => [0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08],
108        '|' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
109        '}' => [0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02],
110        '~' => [0x00, 0x00, 0x02, 0x15, 0x08, 0x00, 0x00],
111        _ => [0x00; 7], // blank for unknown chars
112    };
113
114    let mut bitmap = [[false; FONT_WIDTH]; FONT_HEIGHT];
115    for row in 0..FONT_HEIGHT {
116        let byte = pattern[row];
117        for col in 0..FONT_WIDTH {
118            bitmap[row][col] = (byte >> col) & 1 != 0;
119        }
120    }
121    bitmap
122}
123
124// ---------------------------------------------------------------------------
125// Per-type drawing via macro
126// ---------------------------------------------------------------------------
127
128macro_rules! draw_on_typed_buffer {
129    ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, xor) => {{
130        draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, |data: &mut [$T], idx: usize, mode: DrawMode, value: $T| {
131            match mode {
132                DrawMode::Set => data[idx] = value,
133                DrawMode::XOR => data[idx] ^= value,
134            }
135        });
136    }};
137    ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, set_only) => {{
138        draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, |data: &mut [$T], idx: usize, _mode: DrawMode, value: $T| {
139            data[idx] = value;
140        });
141    }};
142    (@inner $data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $set_fn:expr) => {{
143        let data: &mut [$T] = $data;
144        let w: usize = $w;
145        let h: usize = $h;
146        let set_fn = $set_fn;
147
148        for overlay in $overlays.iter() {
149            let value: $T = overlay.color[0] as $T;
150
151            // Closure to set a single pixel
152            let mut set_pixel = |x: usize, y: usize| {
153                if x < w && y < h {
154                    let idx = y * w + x;
155                    set_fn(data, idx, overlay.draw_mode, value);
156                }
157            };
158
159            match &overlay.shape {
160                OverlayShape::Cross { center_x, center_y, size } => {
161                    let cx = *center_x;
162                    let cy = *center_y;
163                    let half = *size / 2;
164                    for dx in 0..=half.min(w) {
165                        if cx + dx < w { set_pixel(cx + dx, cy); }
166                        if dx <= cx { set_pixel(cx - dx, cy); }
167                    }
168                    for dy in 0..=half.min(h) {
169                        if cy + dy < h { set_pixel(cx, cy + dy); }
170                        if dy <= cy { set_pixel(cx, cy - dy); }
171                    }
172                }
173                OverlayShape::Rectangle { x, y, width, height } => {
174                    for dx in 0..*width {
175                        set_pixel(x + dx, *y);
176                        if *y + height > 0 {
177                            set_pixel(x + dx, y + height - 1);
178                        }
179                    }
180                    for dy in 0..*height {
181                        set_pixel(*x, y + dy);
182                        if *x + width > 0 {
183                            set_pixel(x + width - 1, y + dy);
184                        }
185                    }
186                }
187                OverlayShape::Ellipse { center_x, center_y, rx, ry } => {
188                    let cx = *center_x as f64;
189                    let cy = *center_y as f64;
190                    let rxf = *rx as f64;
191                    let ryf = *ry as f64;
192                    let steps = ((rxf + ryf) * 4.0) as usize;
193                    for i in 0..steps {
194                        let angle = 2.0 * std::f64::consts::PI * i as f64 / steps as f64;
195                        let px = (cx + rxf * angle.cos()).round() as usize;
196                        let py = (cy + ryf * angle.sin()).round() as usize;
197                        set_pixel(px, py);
198                    }
199                }
200                OverlayShape::Text { x, y, text, font_size } => {
201                    let scale = (*font_size).max(1) / FONT_HEIGHT.max(1);
202                    let scale = scale.max(1);
203                    let mut cursor_x = *x;
204                    for ch in text.chars() {
205                        let bitmap = get_char_bitmap(ch);
206                        for row in 0..FONT_HEIGHT {
207                            for col in 0..FONT_WIDTH {
208                                if bitmap[row][col] {
209                                    for sy in 0..scale {
210                                        for sx in 0..scale {
211                                            set_pixel(
212                                                cursor_x + col * scale + sx,
213                                                *y + row * scale + sy,
214                                            );
215                                        }
216                                    }
217                                }
218                            }
219                        }
220                        cursor_x += (FONT_WIDTH + 1) * scale;
221                    }
222                }
223            }
224        }
225    }};
226}
227
228/// Draw overlays on a 2D array. Supports U8, U16, I16, I32, U32, F32, F64.
229pub fn draw_overlays(src: &NDArray, overlays: &[OverlayDef]) -> NDArray {
230    let mut arr = src.clone();
231    if arr.dims.len() < 2 {
232        return arr;
233    }
234    let w = arr.dims[0].size;
235    let h = arr.dims[1].size;
236
237    match &mut arr.data {
238        NDDataBuffer::U8(ref mut data) => {
239            draw_on_typed_buffer!(data.as_mut_slice(), u8, overlays, w, h, xor);
240        }
241        NDDataBuffer::U16(ref mut data) => {
242            draw_on_typed_buffer!(data.as_mut_slice(), u16, overlays, w, h, xor);
243        }
244        NDDataBuffer::I16(ref mut data) => {
245            draw_on_typed_buffer!(data.as_mut_slice(), i16, overlays, w, h, xor);
246        }
247        NDDataBuffer::I32(ref mut data) => {
248            draw_on_typed_buffer!(data.as_mut_slice(), i32, overlays, w, h, xor);
249        }
250        NDDataBuffer::U32(ref mut data) => {
251            draw_on_typed_buffer!(data.as_mut_slice(), u32, overlays, w, h, xor);
252        }
253        NDDataBuffer::F32(ref mut data) => {
254            draw_on_typed_buffer!(data.as_mut_slice(), f32, overlays, w, h, set_only);
255        }
256        NDDataBuffer::F64(ref mut data) => {
257            draw_on_typed_buffer!(data.as_mut_slice(), f64, overlays, w, h, set_only);
258        }
259        _ => {} // I8, I64, U64 - less common, skip
260    }
261
262    arr
263}
264
265/// Pure overlay processing logic.
266pub struct OverlayProcessor {
267    overlays: Vec<OverlayDef>,
268}
269
270impl OverlayProcessor {
271    pub fn new(overlays: Vec<OverlayDef>) -> Self {
272        Self { overlays }
273    }
274}
275
276impl NDPluginProcess for OverlayProcessor {
277    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
278        let out = draw_overlays(array, &self.overlays);
279        ProcessResult::arrays(vec![Arc::new(out)])
280    }
281
282    fn plugin_type(&self) -> &str {
283        "NDPluginOverlay"
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use ad_core::ndarray::{NDDataType, NDDimension};
291
292    fn make_8x8() -> NDArray {
293        NDArray::new(
294            vec![NDDimension::new(8), NDDimension::new(8)],
295            NDDataType::UInt8,
296        )
297    }
298
299    #[test]
300    fn test_rectangle() {
301        let arr = make_8x8();
302        let overlays = vec![OverlayDef {
303            shape: OverlayShape::Rectangle { x: 1, y: 1, width: 4, height: 3 },
304            draw_mode: DrawMode::Set,
305            color: [255, 0, 0],
306        }];
307
308        let out = draw_overlays(&arr, &overlays);
309        if let NDDataBuffer::U8(ref v) = out.data {
310            // Top edge of rectangle at y=1, x=1..4
311            assert_eq!(v[1 * 8 + 1], 255);
312            assert_eq!(v[1 * 8 + 2], 255);
313            assert_eq!(v[1 * 8 + 3], 255);
314            assert_eq!(v[1 * 8 + 4], 255);
315            // Inside should still be 0
316            assert_eq!(v[2 * 8 + 2], 0);
317        }
318    }
319
320    #[test]
321    fn test_xor_mode() {
322        let mut arr = make_8x8();
323        if let NDDataBuffer::U8(ref mut v) = arr.data {
324            v[0] = 0xFF;
325        }
326
327        let overlays = vec![OverlayDef {
328            shape: OverlayShape::Cross { center_x: 0, center_y: 0, size: 2 },
329            draw_mode: DrawMode::XOR,
330            color: [0xFF, 0, 0],
331        }];
332
333        let out = draw_overlays(&arr, &overlays);
334        if let NDDataBuffer::U8(ref v) = out.data {
335            // Center pixel (0,0) is drawn twice (horiz + vert arms):
336            // 0xFF ^ 0xFF ^ 0xFF = 0xFF
337            assert_eq!(v[0], 0xFF);
338            // Neighbor (1,0) drawn once: 0x00 ^ 0xFF = 0xFF
339            assert_eq!(v[1], 0xFF);
340            // Pixel (0,1) drawn once: 0x00 ^ 0xFF = 0xFF
341            assert_eq!(v[1 * 8], 0xFF);
342        }
343    }
344
345    #[test]
346    fn test_cross() {
347        let arr = make_8x8();
348        let overlays = vec![OverlayDef {
349            shape: OverlayShape::Cross { center_x: 4, center_y: 4, size: 4 },
350            draw_mode: DrawMode::Set,
351            color: [200, 0, 0],
352        }];
353
354        let out = draw_overlays(&arr, &overlays);
355        if let NDDataBuffer::U8(ref v) = out.data {
356            assert_eq!(v[4 * 8 + 4], 200); // center
357            assert_eq!(v[4 * 8 + 6], 200); // right arm
358            assert_eq!(v[6 * 8 + 4], 200); // bottom arm
359        }
360    }
361
362    #[test]
363    fn test_text_rendering() {
364        // Render "Hi" at position (0,0), font_size=7 (1x scale)
365        let arr = NDArray::new(
366            vec![NDDimension::new(20), NDDimension::new(10)],
367            NDDataType::UInt8,
368        );
369        let overlays = vec![OverlayDef {
370            shape: OverlayShape::Text {
371                x: 0,
372                y: 0,
373                text: "Hi".to_string(),
374                font_size: 7,
375            },
376            draw_mode: DrawMode::Set,
377            color: [255, 0, 0],
378        }];
379
380        let out = draw_overlays(&arr, &overlays);
381        if let NDDataBuffer::U8(ref v) = out.data {
382            let w = 20;
383            // 'H' bitmap first row is 0x11 = bits 0 and 4 set
384            // pixel (0,0) should be set (bit 0)
385            assert_eq!(v[0 * w + 0], 255);
386            // pixel (4,0) should be set (bit 4)
387            assert_eq!(v[0 * w + 4], 255);
388            // pixel (2,0) should NOT be set for 'H' row 0
389            assert_eq!(v[0 * w + 2], 0);
390
391            // 'I' starts at cursor_x = 6 (FONT_WIDTH=5 + 1 gap)
392            // 'I' first row is 0x0E = bits 1,2,3 set
393            assert_eq!(v[0 * w + 6 + 1], 255);
394            assert_eq!(v[0 * w + 6 + 2], 255);
395            assert_eq!(v[0 * w + 6 + 3], 255);
396        }
397    }
398
399    #[test]
400    fn test_u16_overlay() {
401        let arr = NDArray::new(
402            vec![NDDimension::new(8), NDDimension::new(8)],
403            NDDataType::UInt16,
404        );
405        // Fill with zeros (already done by NDArray::new)
406        let overlays = vec![OverlayDef {
407            shape: OverlayShape::Rectangle { x: 1, y: 1, width: 4, height: 3 },
408            draw_mode: DrawMode::Set,
409            color: [200, 0, 0],
410        }];
411
412        let out = draw_overlays(&arr, &overlays);
413        if let NDDataBuffer::U16(ref v) = out.data {
414            // Top edge at y=1, x=1
415            assert_eq!(v[1 * 8 + 1], 200);
416            assert_eq!(v[1 * 8 + 4], 200);
417            // Inside should still be 0
418            assert_eq!(v[2 * 8 + 2], 0);
419        }
420    }
421
422    #[test]
423    fn test_f32_overlay_ignores_xor() {
424        let arr = NDArray::new(
425            vec![NDDimension::new(8), NDDimension::new(8)],
426            NDDataType::Float32,
427        );
428        let overlays = vec![OverlayDef {
429            shape: OverlayShape::Cross { center_x: 4, center_y: 4, size: 2 },
430            draw_mode: DrawMode::XOR, // should be treated as Set for floats
431            color: [100, 0, 0],
432        }];
433
434        let out = draw_overlays(&arr, &overlays);
435        if let NDDataBuffer::F32(ref v) = out.data {
436            // Center pixel should be set (XOR falls back to Set for floats)
437            assert_eq!(v[4 * 8 + 4], 100.0);
438        }
439    }
440}