Skip to main content

ad_plugins_rs/
overlay.rs

1use std::sync::Arc;
2
3use ad_core_rs::ndarray::{NDArray, NDDataBuffer};
4use ad_core_rs::ndarray_pool::NDArrayPool;
5use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
6
7/// Shape to draw.
8#[derive(Debug, Clone)]
9pub enum OverlayShape {
10    Cross {
11        center_x: usize,
12        center_y: usize,
13        size: usize,
14    },
15    Rectangle {
16        x: usize,
17        y: usize,
18        width: usize,
19        height: usize,
20    },
21    Ellipse {
22        center_x: usize,
23        center_y: usize,
24        rx: usize,
25        ry: usize,
26    },
27    Text {
28        x: usize,
29        y: usize,
30        text: String,
31        font_size: usize,
32    },
33}
34
35/// Draw mode.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DrawMode {
38    Set,
39    XOR,
40}
41
42/// A single overlay definition.
43#[derive(Debug, Clone)]
44pub struct OverlayDef {
45    pub shape: OverlayShape,
46    pub draw_mode: DrawMode,
47    pub color: [u8; 3], // RGB color; for Mono, color[1] (green) is used
48    pub width_x: usize, // line thickness in X direction (0 or 1 = 1px)
49    pub width_y: usize, // line thickness in Y direction (0 or 1 = 1px)
50}
51
52// ---------------------------------------------------------------------------
53// Minimal 5x7 bitmap font
54// ---------------------------------------------------------------------------
55
56const FONT_WIDTH: usize = 5;
57const FONT_HEIGHT: usize = 7;
58
59/// Return a 5x7 bitmap for printable ASCII characters.
60/// Each row is encoded as a u8 with the 5 MSBs representing pixels.
61fn get_char_bitmap(ch: char) -> [[bool; FONT_WIDTH]; FONT_HEIGHT] {
62    let pattern: [u8; 7] = match ch {
63        ' ' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
64        '!' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04],
65        '"' => [0x0A, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00],
66        '#' => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A],
67        '$' => [0x04, 0x1E, 0x05, 0x0E, 0x14, 0x0F, 0x04],
68        '%' => [0x03, 0x13, 0x08, 0x04, 0x02, 0x19, 0x18],
69        '&' => [0x06, 0x09, 0x05, 0x02, 0x15, 0x09, 0x16],
70        '\'' => [0x04, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00],
71        '(' => [0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08],
72        ')' => [0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02],
73        '*' => [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00],
74        '+' => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00],
75        ',' => [0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x02],
76        '-' => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00],
77        '.' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04],
78        '/' => [0x10, 0x08, 0x08, 0x04, 0x02, 0x02, 0x01],
79        '0' => [0x0E, 0x11, 0x19, 0x15, 0x13, 0x11, 0x0E],
80        '1' => [0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x0E],
81        '2' => [0x0E, 0x11, 0x10, 0x08, 0x04, 0x02, 0x1F],
82        '3' => [0x0E, 0x11, 0x10, 0x0C, 0x10, 0x11, 0x0E],
83        '4' => [0x08, 0x0C, 0x0A, 0x09, 0x1F, 0x08, 0x08],
84        '5' => [0x1F, 0x01, 0x0F, 0x10, 0x10, 0x11, 0x0E],
85        '6' => [0x0C, 0x02, 0x01, 0x0F, 0x11, 0x11, 0x0E],
86        '7' => [0x1F, 0x10, 0x08, 0x04, 0x02, 0x02, 0x02],
87        '8' => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E],
88        '9' => [0x0E, 0x11, 0x11, 0x1E, 0x10, 0x08, 0x06],
89        ':' => [0x00, 0x00, 0x04, 0x00, 0x04, 0x00, 0x00],
90        ';' => [0x00, 0x00, 0x04, 0x00, 0x04, 0x04, 0x02],
91        '<' => [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08],
92        '=' => [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00],
93        '>' => [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02],
94        '?' => [0x0E, 0x11, 0x10, 0x08, 0x04, 0x00, 0x04],
95        '@' => [0x0E, 0x11, 0x15, 0x1D, 0x05, 0x01, 0x0E],
96        'A' | 'a' => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
97        'B' | 'b' => [0x0F, 0x11, 0x11, 0x0F, 0x11, 0x11, 0x0F],
98        'C' | 'c' => [0x0E, 0x11, 0x01, 0x01, 0x01, 0x11, 0x0E],
99        'D' | 'd' => [0x07, 0x09, 0x11, 0x11, 0x11, 0x09, 0x07],
100        'E' | 'e' => [0x1F, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x1F],
101        'F' | 'f' => [0x1F, 0x01, 0x01, 0x0F, 0x01, 0x01, 0x01],
102        'G' | 'g' => [0x0E, 0x11, 0x01, 0x1D, 0x11, 0x11, 0x0E],
103        'H' | 'h' => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11],
104        'I' | 'i' => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E],
105        'J' | 'j' => [0x1C, 0x08, 0x08, 0x08, 0x08, 0x09, 0x06],
106        'K' | 'k' => [0x11, 0x09, 0x05, 0x03, 0x05, 0x09, 0x11],
107        'L' | 'l' => [0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x1F],
108        'M' | 'm' => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11],
109        'N' | 'n' => [0x11, 0x13, 0x15, 0x15, 0x19, 0x11, 0x11],
110        'O' | 'o' => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
111        'P' | 'p' => [0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x01],
112        'Q' | 'q' => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x09, 0x16],
113        'R' | 'r' => [0x0F, 0x11, 0x11, 0x0F, 0x05, 0x09, 0x11],
114        'S' | 's' => [0x0E, 0x11, 0x01, 0x0E, 0x10, 0x11, 0x0E],
115        'T' | 't' => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
116        'U' | 'u' => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E],
117        'V' | 'v' => [0x11, 0x11, 0x11, 0x11, 0x0A, 0x0A, 0x04],
118        'W' | 'w' => [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11],
119        'X' | 'x' => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11],
120        'Y' | 'y' => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04],
121        'Z' | 'z' => [0x1F, 0x10, 0x08, 0x04, 0x02, 0x01, 0x1F],
122        '[' => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E],
123        '\\' => [0x01, 0x02, 0x02, 0x04, 0x08, 0x08, 0x10],
124        ']' => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E],
125        '^' => [0x04, 0x0A, 0x11, 0x00, 0x00, 0x00, 0x00],
126        '_' => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F],
127        '`' => [0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00],
128        '{' => [0x08, 0x04, 0x04, 0x02, 0x04, 0x04, 0x08],
129        '|' => [0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04],
130        '}' => [0x02, 0x04, 0x04, 0x08, 0x04, 0x04, 0x02],
131        '~' => [0x00, 0x00, 0x02, 0x15, 0x08, 0x00, 0x00],
132        _ => [0x00; 7], // blank for unknown chars
133    };
134
135    let mut bitmap = [[false; FONT_WIDTH]; FONT_HEIGHT];
136    for row in 0..FONT_HEIGHT {
137        let byte = pattern[row];
138        for col in 0..FONT_WIDTH {
139            bitmap[row][col] = (byte >> col) & 1 != 0;
140        }
141    }
142    bitmap
143}
144
145// ---------------------------------------------------------------------------
146// Per-type drawing via macro
147// ---------------------------------------------------------------------------
148
149macro_rules! draw_on_typed_buffer {
150    ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, xor) => {{
151        draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, |data: &mut [$T], idx: usize, mode: DrawMode, value: $T| {
152            match mode {
153                DrawMode::Set => data[idx] = value,
154                DrawMode::XOR => data[idx] ^= value,
155            }
156        });
157    }};
158    ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, set_only) => {{
159        draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, |data: &mut [$T], idx: usize, _mode: DrawMode, value: $T| {
160            data[idx] = value;
161        });
162    }};
163    (@inner $data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $set_fn:expr) => {{
164        let data: &mut [$T] = $data;
165        let w: usize = $w;
166        let h: usize = $h;
167        let set_fn = $set_fn;
168
169        for overlay in $overlays.iter() {
170            // C++ uses pOverlay->green for mono overlays
171            let value: $T = overlay.color[1] as $T;
172            let wx = overlay.width_x.max(1);
173            let wy = overlay.width_y.max(1);
174
175            // Closure to set a single pixel
176            let mut set_pixel = |x: usize, y: usize| {
177                if x < w && y < h {
178                    let idx = y * w + x;
179                    set_fn(data, idx, overlay.draw_mode, value);
180                }
181            };
182
183            match &overlay.shape {
184                OverlayShape::Cross { center_x, center_y, size } => {
185                    let cx = *center_x;
186                    let cy = *center_y;
187                    let half = *size / 2;
188                    // Horizontal arm: WidthY thickness (centered vertically)
189                    let wy_half = wy / 2;
190                    for dx in 0..=half.min(w) {
191                        for t in 0..wy {
192                            let ty = (cy + t).wrapping_sub(wy_half);
193                            if cx + dx < w { set_pixel(cx + dx, ty); }
194                            if dx <= cx { set_pixel(cx - dx, ty); }
195                        }
196                    }
197                    // Vertical arm: WidthX thickness (centered horizontally)
198                    let wx_half = wx / 2;
199                    for dy in 0..=half.min(h) {
200                        for t in 0..wx {
201                            let tx = (cx + t).wrapping_sub(wx_half);
202                            if cy + dy < h { set_pixel(tx, cy + dy); }
203                            if dy <= cy { set_pixel(tx, cy - dy); }
204                        }
205                    }
206                }
207                OverlayShape::Rectangle { x, y, width, height } => {
208                    // Border thickness grows inward
209                    let bx = wx.min(*width);
210                    let by = wy.min(*height);
211                    // Top edge
212                    for dy in 0..by {
213                        for dx in 0..*width {
214                            set_pixel(x + dx, y + dy);
215                        }
216                    }
217                    // Bottom edge
218                    for dy in 0..by {
219                        if *height > dy {
220                            for dx in 0..*width {
221                                set_pixel(x + dx, y + height - 1 - dy);
222                            }
223                        }
224                    }
225                    // Left edge (between top and bottom borders)
226                    let inner_start = by;
227                    let inner_end = height.saturating_sub(by);
228                    for dy in inner_start..inner_end {
229                        for t in 0..bx {
230                            set_pixel(x + t, y + dy);
231                        }
232                    }
233                    // Right edge (between top and bottom borders)
234                    for dy in inner_start..inner_end {
235                        for t in 0..bx {
236                            if *width > t {
237                                set_pixel(x + width - 1 - t, y + dy);
238                            }
239                        }
240                    }
241                }
242                OverlayShape::Ellipse { center_x, center_y, rx, ry } => {
243                    let cx = *center_x as f64;
244                    let cy = *center_y as f64;
245                    let rxf = *rx as f64;
246                    let ryf = *ry as f64;
247                    // Draw concentric ellipses from outer radius to inner radius
248                    let wx_thickness = (wx as f64).min(rxf);
249                    let wy_thickness = (wy as f64).min(ryf);
250                    let steps = ((rxf + ryf) * 4.0).max(64.0) as usize;
251                    for layer in 0..wx.max(wy) {
252                        let frac = layer as f64;
253                        let rx_cur = (rxf - frac * wx_thickness / wx.max(1) as f64).max(0.0);
254                        let ry_cur = (ryf - frac * wy_thickness / wy.max(1) as f64).max(0.0);
255                        if rx_cur <= 0.0 && ry_cur <= 0.0 {
256                            break;
257                        }
258                        for i in 0..steps {
259                            let angle = 2.0 * std::f64::consts::PI * i as f64 / steps as f64;
260                            let px = (cx + rx_cur * angle.cos()).round() as usize;
261                            let py = (cy + ry_cur * angle.sin()).round() as usize;
262                            set_pixel(px, py);
263                        }
264                    }
265                }
266                OverlayShape::Text { x, y, text, font_size } => {
267                    let scale = (*font_size).max(1) / FONT_HEIGHT.max(1);
268                    let scale = scale.max(1);
269                    let mut cursor_x = *x;
270                    for ch in text.chars() {
271                        let bitmap = get_char_bitmap(ch);
272                        for row in 0..FONT_HEIGHT {
273                            for col in 0..FONT_WIDTH {
274                                if bitmap[row][col] {
275                                    for sy in 0..scale {
276                                        for sx in 0..scale {
277                                            set_pixel(
278                                                cursor_x + col * scale + sx,
279                                                *y + row * scale + sy,
280                                            );
281                                        }
282                                    }
283                                }
284                            }
285                        }
286                        cursor_x += (FONT_WIDTH + 1) * scale;
287                    }
288                }
289            }
290        }
291    }};
292}
293
294/// Draw overlays on a 2D array. Supports I8, U8, I16, U16, I32, U32, I64, U64, F32, F64.
295pub fn draw_overlays(src: &NDArray, overlays: &[OverlayDef]) -> NDArray {
296    let mut arr = src.clone();
297    if arr.dims.len() < 2 {
298        return arr;
299    }
300    let w = arr.dims[0].size;
301    let h = arr.dims[1].size;
302
303    match &mut arr.data {
304        NDDataBuffer::U8(data) => {
305            draw_on_typed_buffer!(data.as_mut_slice(), u8, overlays, w, h, xor);
306        }
307        NDDataBuffer::U16(data) => {
308            draw_on_typed_buffer!(data.as_mut_slice(), u16, overlays, w, h, xor);
309        }
310        NDDataBuffer::I16(data) => {
311            draw_on_typed_buffer!(data.as_mut_slice(), i16, overlays, w, h, xor);
312        }
313        NDDataBuffer::I32(data) => {
314            draw_on_typed_buffer!(data.as_mut_slice(), i32, overlays, w, h, xor);
315        }
316        NDDataBuffer::U32(data) => {
317            draw_on_typed_buffer!(data.as_mut_slice(), u32, overlays, w, h, xor);
318        }
319        NDDataBuffer::F32(data) => {
320            draw_on_typed_buffer!(data.as_mut_slice(), f32, overlays, w, h, set_only);
321        }
322        NDDataBuffer::F64(data) => {
323            draw_on_typed_buffer!(data.as_mut_slice(), f64, overlays, w, h, set_only);
324        }
325        NDDataBuffer::I8(data) => {
326            draw_on_typed_buffer!(data.as_mut_slice(), i8, overlays, w, h, xor);
327        }
328        NDDataBuffer::I64(data) => {
329            draw_on_typed_buffer!(data.as_mut_slice(), i64, overlays, w, h, xor);
330        }
331        NDDataBuffer::U64(data) => {
332            draw_on_typed_buffer!(data.as_mut_slice(), u64, overlays, w, h, xor);
333        }
334    }
335
336    arr
337}
338
339/// Maximum number of overlays.
340const MAX_OVERLAYS: usize = 8;
341
342/// Runtime overlay state — one per addr (0..7).
343#[derive(Debug, Clone)]
344struct OverlaySlot {
345    use_overlay: bool,
346    shape: i32,     // 0=Cross, 1=Rectangle, 2=Ellipse, 3=Text
347    draw_mode: i32, // 0=Set, 1=XOR
348    position_x: usize,
349    position_y: usize,
350    size_x: usize,
351    size_y: usize,
352    width_x: usize,
353    width_y: usize,
354    red: u8,
355    green: u8,
356    blue: u8,
357    display_text: String,
358    font: usize,
359}
360
361impl Default for OverlaySlot {
362    fn default() -> Self {
363        Self {
364            use_overlay: false,
365            shape: 1, // Rectangle
366            draw_mode: 0,
367            position_x: 0,
368            position_y: 0,
369            size_x: 0,
370            size_y: 0,
371            width_x: 1,
372            width_y: 1,
373            red: 255,
374            green: 0,
375            blue: 0,
376            display_text: String::new(),
377            font: 0,
378        }
379    }
380}
381
382impl OverlaySlot {
383    fn to_overlay_def(&self) -> Option<OverlayDef> {
384        if !self.use_overlay {
385            return None;
386        }
387        let draw_mode = if self.draw_mode == 1 {
388            DrawMode::XOR
389        } else {
390            DrawMode::Set
391        };
392        let color = [self.red, self.green, self.blue];
393        let shape = match self.shape {
394            0 => OverlayShape::Cross {
395                center_x: self.position_x + self.size_x / 2,
396                center_y: self.position_y + self.size_y / 2,
397                size: self.size_x.max(self.size_y),
398            },
399            1 => OverlayShape::Rectangle {
400                x: self.position_x,
401                y: self.position_y,
402                width: self.size_x,
403                height: self.size_y,
404            },
405            2 => OverlayShape::Ellipse {
406                center_x: self.position_x + self.size_x / 2,
407                center_y: self.position_y + self.size_y / 2,
408                rx: self.size_x / 2,
409                ry: self.size_y / 2,
410            },
411            3 => OverlayShape::Text {
412                x: self.position_x,
413                y: self.position_y,
414                text: self.display_text.clone(),
415                font_size: (self.font + 1) * FONT_HEIGHT,
416            },
417            _ => OverlayShape::Rectangle {
418                x: self.position_x,
419                y: self.position_y,
420                width: self.size_x,
421                height: self.size_y,
422            },
423        };
424        Some(OverlayDef {
425            shape,
426            draw_mode,
427            color,
428            width_x: self.width_x,
429            width_y: self.width_y,
430        })
431    }
432}
433
434/// Param indices for per-overlay params.
435#[derive(Default)]
436struct OverlayParamIndices {
437    use_overlay: Option<usize>,
438    position_x: Option<usize>,
439    position_y: Option<usize>,
440    center_x: Option<usize>,
441    center_y: Option<usize>,
442    size_x: Option<usize>,
443    size_y: Option<usize>,
444    width_x: Option<usize>,
445    width_y: Option<usize>,
446    shape: Option<usize>,
447    draw_mode: Option<usize>,
448    red: Option<usize>,
449    green: Option<usize>,
450    blue: Option<usize>,
451    display_text: Option<usize>,
452    font: Option<usize>,
453}
454
455/// Pure overlay processing logic with runtime-configurable overlays.
456pub struct OverlayProcessor {
457    slots: [OverlaySlot; MAX_OVERLAYS],
458    params: OverlayParamIndices,
459}
460
461impl OverlayProcessor {
462    pub fn new(overlays: Vec<OverlayDef>) -> Self {
463        let mut slots: [OverlaySlot; MAX_OVERLAYS] = Default::default();
464        for (i, o) in overlays.into_iter().enumerate().take(MAX_OVERLAYS) {
465            let slot = &mut slots[i];
466            slot.use_overlay = true;
467            slot.draw_mode = if o.draw_mode == DrawMode::XOR { 1 } else { 0 };
468            slot.red = o.color[0];
469            slot.green = o.color[1];
470            slot.blue = o.color[2];
471            slot.width_x = o.width_x;
472            slot.width_y = o.width_y;
473            match o.shape {
474                OverlayShape::Cross {
475                    center_x,
476                    center_y,
477                    size,
478                } => {
479                    slot.shape = 0;
480                    slot.position_x = center_x.saturating_sub(size / 2);
481                    slot.position_y = center_y.saturating_sub(size / 2);
482                    slot.size_x = size;
483                    slot.size_y = size;
484                }
485                OverlayShape::Rectangle {
486                    x,
487                    y,
488                    width,
489                    height,
490                } => {
491                    slot.shape = 1;
492                    slot.position_x = x;
493                    slot.position_y = y;
494                    slot.size_x = width;
495                    slot.size_y = height;
496                }
497                OverlayShape::Ellipse {
498                    center_x,
499                    center_y,
500                    rx,
501                    ry,
502                } => {
503                    slot.shape = 2;
504                    slot.position_x = center_x.saturating_sub(rx);
505                    slot.position_y = center_y.saturating_sub(ry);
506                    slot.size_x = rx * 2;
507                    slot.size_y = ry * 2;
508                }
509                OverlayShape::Text {
510                    x,
511                    y,
512                    text,
513                    font_size,
514                } => {
515                    slot.shape = 3;
516                    slot.position_x = x;
517                    slot.position_y = y;
518                    slot.display_text = text;
519                    slot.font = font_size / FONT_HEIGHT.max(1);
520                }
521            }
522        }
523        Self {
524            slots,
525            params: OverlayParamIndices::default(),
526        }
527    }
528
529    fn build_active_overlays(&self) -> Vec<OverlayDef> {
530        self.slots
531            .iter()
532            .filter_map(|s| s.to_overlay_def())
533            .collect()
534    }
535}
536
537impl NDPluginProcess for OverlayProcessor {
538    fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
539        let active = self.build_active_overlays();
540        let out = draw_overlays(array, &active);
541        ProcessResult::arrays(vec![Arc::new(out)])
542    }
543
544    fn plugin_type(&self) -> &str {
545        "NDPluginOverlay"
546    }
547
548    fn register_params(
549        &mut self,
550        base: &mut asyn_rs::port::PortDriverBase,
551    ) -> asyn_rs::error::AsynResult<()> {
552        use asyn_rs::param::ParamType;
553        base.create_param("MAX_SIZE_X", ParamType::Int32)?;
554        base.create_param("MAX_SIZE_Y", ParamType::Int32)?;
555        base.create_param("NAME", ParamType::Octet)?;
556        base.create_param("USE", ParamType::Int32)?;
557        base.create_param("OVERLAY_POSITION_X", ParamType::Int32)?;
558        base.create_param("OVERLAY_POSITION_Y", ParamType::Int32)?;
559        base.create_param("OVERLAY_CENTER_X", ParamType::Int32)?;
560        base.create_param("OVERLAY_CENTER_Y", ParamType::Int32)?;
561        base.create_param("OVERLAY_SIZE_X", ParamType::Int32)?;
562        base.create_param("OVERLAY_SIZE_Y", ParamType::Int32)?;
563        base.create_param("OVERLAY_WIDTH_X", ParamType::Int32)?;
564        base.create_param("OVERLAY_WIDTH_Y", ParamType::Int32)?;
565        base.create_param("OVERLAY_SHAPE", ParamType::Int32)?;
566        base.create_param("OVERLAY_DRAW_MODE", ParamType::Int32)?;
567        base.create_param("OVERLAY_RED", ParamType::Int32)?;
568        base.create_param("OVERLAY_GREEN", ParamType::Int32)?;
569        base.create_param("OVERLAY_BLUE", ParamType::Int32)?;
570        base.create_param("OVERLAY_DISPLAY_TEXT", ParamType::Octet)?;
571        base.create_param("OVERLAY_TIMESTAMP_FORMAT", ParamType::Octet)?;
572        base.create_param("OVERLAY_FONT", ParamType::Int32)?;
573
574        self.params.use_overlay = base.find_param("USE");
575        self.params.position_x = base.find_param("OVERLAY_POSITION_X");
576        self.params.position_y = base.find_param("OVERLAY_POSITION_Y");
577        self.params.center_x = base.find_param("OVERLAY_CENTER_X");
578        self.params.center_y = base.find_param("OVERLAY_CENTER_Y");
579        self.params.size_x = base.find_param("OVERLAY_SIZE_X");
580        self.params.size_y = base.find_param("OVERLAY_SIZE_Y");
581        self.params.width_x = base.find_param("OVERLAY_WIDTH_X");
582        self.params.width_y = base.find_param("OVERLAY_WIDTH_Y");
583        self.params.shape = base.find_param("OVERLAY_SHAPE");
584        self.params.draw_mode = base.find_param("OVERLAY_DRAW_MODE");
585        self.params.red = base.find_param("OVERLAY_RED");
586        self.params.green = base.find_param("OVERLAY_GREEN");
587        self.params.blue = base.find_param("OVERLAY_BLUE");
588        self.params.display_text = base.find_param("OVERLAY_DISPLAY_TEXT");
589        self.params.font = base.find_param("OVERLAY_FONT");
590        Ok(())
591    }
592
593    fn on_param_change(
594        &mut self,
595        reason: usize,
596        params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
597    ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
598        use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue, ParamUpdate};
599
600        let idx = params.addr as usize;
601        if idx >= MAX_OVERLAYS {
602            return ParamChangeResult::updates(vec![]);
603        }
604        let slot = &mut self.slots[idx];
605        let mut updates = Vec::new();
606
607        if Some(reason) == self.params.use_overlay {
608            slot.use_overlay = params.value.as_i32() != 0;
609        } else if Some(reason) == self.params.shape {
610            slot.shape = params.value.as_i32();
611        } else if Some(reason) == self.params.draw_mode {
612            slot.draw_mode = params.value.as_i32();
613        } else if Some(reason) == self.params.position_x {
614            slot.position_x = params.value.as_i32().max(0) as usize;
615            if let Some(ci) = self.params.center_x {
616                updates.push(ParamUpdate::int32_addr(
617                    ci,
618                    idx as i32,
619                    (slot.position_x + slot.size_x / 2) as i32,
620                ));
621            }
622        } else if Some(reason) == self.params.position_y {
623            slot.position_y = params.value.as_i32().max(0) as usize;
624            if let Some(ci) = self.params.center_y {
625                updates.push(ParamUpdate::int32_addr(
626                    ci,
627                    idx as i32,
628                    (slot.position_y + slot.size_y / 2) as i32,
629                ));
630            }
631        } else if Some(reason) == self.params.center_x {
632            let cx = params.value.as_i32().max(0) as usize;
633            slot.position_x = cx.saturating_sub(slot.size_x / 2);
634            if let Some(pi) = self.params.position_x {
635                updates.push(ParamUpdate::int32_addr(
636                    pi,
637                    idx as i32,
638                    slot.position_x as i32,
639                ));
640            }
641        } else if Some(reason) == self.params.center_y {
642            let cy = params.value.as_i32().max(0) as usize;
643            slot.position_y = cy.saturating_sub(slot.size_y / 2);
644            if let Some(pi) = self.params.position_y {
645                updates.push(ParamUpdate::int32_addr(
646                    pi,
647                    idx as i32,
648                    slot.position_y as i32,
649                ));
650            }
651        } else if Some(reason) == self.params.size_x {
652            slot.size_x = params.value.as_i32().max(0) as usize;
653            if let Some(ci) = self.params.center_x {
654                updates.push(ParamUpdate::int32_addr(
655                    ci,
656                    idx as i32,
657                    (slot.position_x + slot.size_x / 2) as i32,
658                ));
659            }
660        } else if Some(reason) == self.params.size_y {
661            slot.size_y = params.value.as_i32().max(0) as usize;
662            if let Some(ci) = self.params.center_y {
663                updates.push(ParamUpdate::int32_addr(
664                    ci,
665                    idx as i32,
666                    (slot.position_y + slot.size_y / 2) as i32,
667                ));
668            }
669        } else if Some(reason) == self.params.width_x {
670            slot.width_x = params.value.as_i32().max(0) as usize;
671        } else if Some(reason) == self.params.width_y {
672            slot.width_y = params.value.as_i32().max(0) as usize;
673        } else if Some(reason) == self.params.red {
674            slot.red = params.value.as_i32().clamp(0, 255) as u8;
675        } else if Some(reason) == self.params.green {
676            slot.green = params.value.as_i32().clamp(0, 255) as u8;
677        } else if Some(reason) == self.params.blue {
678            slot.blue = params.value.as_i32().clamp(0, 255) as u8;
679        } else if Some(reason) == self.params.display_text {
680            if let ParamChangeValue::Octet(s) = &params.value {
681                slot.display_text = s.clone();
682            }
683        } else if Some(reason) == self.params.font {
684            slot.font = params.value.as_i32().max(0) as usize;
685        }
686
687        ParamChangeResult::updates(updates)
688    }
689}
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694    use ad_core_rs::ndarray::{NDDataType, NDDimension};
695
696    fn make_8x8() -> NDArray {
697        NDArray::new(
698            vec![NDDimension::new(8), NDDimension::new(8)],
699            NDDataType::UInt8,
700        )
701    }
702
703    #[test]
704    fn test_rectangle() {
705        let arr = make_8x8();
706        let overlays = vec![OverlayDef {
707            shape: OverlayShape::Rectangle {
708                x: 1,
709                y: 1,
710                width: 4,
711                height: 3,
712            },
713            draw_mode: DrawMode::Set,
714            color: [0, 255, 0],
715            width_x: 1,
716            width_y: 1,
717        }];
718
719        let out = draw_overlays(&arr, &overlays);
720        if let NDDataBuffer::U8(ref v) = out.data {
721            // Top edge of rectangle at y=1, x=1..4
722            assert_eq!(v[1 * 8 + 1], 255);
723            assert_eq!(v[1 * 8 + 2], 255);
724            assert_eq!(v[1 * 8 + 3], 255);
725            assert_eq!(v[1 * 8 + 4], 255);
726            // Inside should still be 0
727            assert_eq!(v[2 * 8 + 2], 0);
728        }
729    }
730
731    #[test]
732    fn test_xor_mode() {
733        let mut arr = make_8x8();
734        if let NDDataBuffer::U8(ref mut v) = arr.data {
735            v[0] = 0xFF;
736        }
737
738        let overlays = vec![OverlayDef {
739            shape: OverlayShape::Cross {
740                center_x: 0,
741                center_y: 0,
742                size: 2,
743            },
744            draw_mode: DrawMode::XOR,
745            color: [0, 0xFF, 0],
746            width_x: 1,
747            width_y: 1,
748        }];
749
750        let out = draw_overlays(&arr, &overlays);
751        if let NDDataBuffer::U8(ref v) = out.data {
752            // Center pixel (0,0) is drawn twice (horiz + vert arms):
753            // 0xFF ^ 0xFF ^ 0xFF = 0xFF
754            assert_eq!(v[0], 0xFF);
755            // Neighbor (1,0) drawn once: 0x00 ^ 0xFF = 0xFF
756            assert_eq!(v[1], 0xFF);
757            // Pixel (0,1) drawn once: 0x00 ^ 0xFF = 0xFF
758            assert_eq!(v[1 * 8], 0xFF);
759        }
760    }
761
762    #[test]
763    fn test_cross() {
764        let arr = make_8x8();
765        let overlays = vec![OverlayDef {
766            shape: OverlayShape::Cross {
767                center_x: 4,
768                center_y: 4,
769                size: 4,
770            },
771            draw_mode: DrawMode::Set,
772            color: [0, 200, 0],
773            width_x: 1,
774            width_y: 1,
775        }];
776
777        let out = draw_overlays(&arr, &overlays);
778        if let NDDataBuffer::U8(ref v) = out.data {
779            assert_eq!(v[4 * 8 + 4], 200); // center
780            assert_eq!(v[4 * 8 + 6], 200); // right arm
781            assert_eq!(v[6 * 8 + 4], 200); // bottom arm
782        }
783    }
784
785    #[test]
786    fn test_text_rendering() {
787        // Render "Hi" at position (0,0), font_size=7 (1x scale)
788        let arr = NDArray::new(
789            vec![NDDimension::new(20), NDDimension::new(10)],
790            NDDataType::UInt8,
791        );
792        let overlays = vec![OverlayDef {
793            shape: OverlayShape::Text {
794                x: 0,
795                y: 0,
796                text: "Hi".to_string(),
797                font_size: 7,
798            },
799            draw_mode: DrawMode::Set,
800            color: [0, 255, 0],
801            width_x: 1,
802            width_y: 1,
803        }];
804
805        let out = draw_overlays(&arr, &overlays);
806        if let NDDataBuffer::U8(ref v) = out.data {
807            let w = 20;
808            // 'H' bitmap first row is 0x11 = bits 0 and 4 set
809            // pixel (0,0) should be set (bit 0)
810            assert_eq!(v[0 * w + 0], 255);
811            // pixel (4,0) should be set (bit 4)
812            assert_eq!(v[0 * w + 4], 255);
813            // pixel (2,0) should NOT be set for 'H' row 0
814            assert_eq!(v[0 * w + 2], 0);
815
816            // 'I' starts at cursor_x = 6 (FONT_WIDTH=5 + 1 gap)
817            // 'I' first row is 0x0E = bits 1,2,3 set
818            assert_eq!(v[0 * w + 6 + 1], 255);
819            assert_eq!(v[0 * w + 6 + 2], 255);
820            assert_eq!(v[0 * w + 6 + 3], 255);
821        }
822    }
823
824    #[test]
825    fn test_u16_overlay() {
826        let arr = NDArray::new(
827            vec![NDDimension::new(8), NDDimension::new(8)],
828            NDDataType::UInt16,
829        );
830        // Fill with zeros (already done by NDArray::new)
831        let overlays = vec![OverlayDef {
832            shape: OverlayShape::Rectangle {
833                x: 1,
834                y: 1,
835                width: 4,
836                height: 3,
837            },
838            draw_mode: DrawMode::Set,
839            color: [0, 200, 0],
840            width_x: 1,
841            width_y: 1,
842        }];
843
844        let out = draw_overlays(&arr, &overlays);
845        if let NDDataBuffer::U16(ref v) = out.data {
846            // Top edge at y=1, x=1
847            assert_eq!(v[1 * 8 + 1], 200);
848            assert_eq!(v[1 * 8 + 4], 200);
849            // Inside should still be 0
850            assert_eq!(v[2 * 8 + 2], 0);
851        }
852    }
853
854    #[test]
855    fn test_f32_overlay_ignores_xor() {
856        let arr = NDArray::new(
857            vec![NDDimension::new(8), NDDimension::new(8)],
858            NDDataType::Float32,
859        );
860        let overlays = vec![OverlayDef {
861            shape: OverlayShape::Cross {
862                center_x: 4,
863                center_y: 4,
864                size: 2,
865            },
866            draw_mode: DrawMode::XOR, // should be treated as Set for floats
867            color: [0, 100, 0],
868            width_x: 1,
869            width_y: 1,
870        }];
871
872        let out = draw_overlays(&arr, &overlays);
873        if let NDDataBuffer::F32(ref v) = out.data {
874            // Center pixel should be set (XOR falls back to Set for floats)
875            assert_eq!(v[4 * 8 + 4], 100.0);
876        }
877    }
878}