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