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#[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 size_x: usize,
33 size_y: usize,
35 text: String,
36 font: usize,
38 timestamp_format: String,
41 },
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum DrawMode {
47 Set,
48 XOR,
49}
50
51#[derive(Debug, Clone)]
53pub struct OverlayDef {
54 pub shape: OverlayShape,
55 pub draw_mode: DrawMode,
56 pub color: [u8; 3], pub width_x: usize, pub width_y: usize, }
60
61use crate::overlay_font::{BitmapFont, FONTS};
66
67pub const NUM_FONTS: usize = 4;
69
70fn font_for(index: usize) -> &'static BitmapFont {
73 &FONTS[index.min(NUM_FONTS - 1)]
74}
75
76fn font_pixel(font: &BitmapFont, ch: char, row: usize, col: usize) -> bool {
83 let code = ch as u32;
84 if code < font.first_char as u32 {
85 return false;
86 }
87 let ci = (code - font.first_char as u32) as usize;
88 if ci >= font.num_chars || row >= font.height || col >= font.width {
89 return false;
90 }
91 let byte_in_row = col / 8;
92 let bit = 7 - (col % 8);
93 let offset = (font.height * ci + row) * font.bytes_per_char + byte_in_row;
94 (font.bitmap[offset] >> bit) & 1 != 0
95}
96
97fn format_epics_time(ts: ad_core_rs::timestamp::EpicsTimestamp, fmt: &str) -> String {
104 let secs = ts.sec as u64 + 631_152_000; let days = secs / 86_400;
107 let tod = secs % 86_400;
108 let (hour, minute, second) = (tod / 3600, (tod % 3600) / 60, tod % 60);
109
110 let z = days as i64 + 719_468;
112 let era = z.div_euclid(146_097);
113 let doe = z - era * 146_097;
114 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
115 let y = yoe + era * 400;
116 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
117 let mp = (5 * doy + 2) / 153;
118 let day = doy - (153 * mp + 2) / 5 + 1;
119 let month = if mp < 10 { mp + 3 } else { mp - 9 };
120 let year = if month <= 2 { y + 1 } else { y };
121
122 let mut out = String::with_capacity(fmt.len() + 16);
123 let mut chars = fmt.chars().peekable();
124 while let Some(c) = chars.next() {
125 if c != '%' {
126 out.push(c);
127 continue;
128 }
129 match chars.next() {
130 Some('Y') => out.push_str(&format!("{year:04}")),
131 Some('m') => out.push_str(&format!("{month:02}")),
132 Some('d') => out.push_str(&format!("{day:02}")),
133 Some('H') => out.push_str(&format!("{hour:02}")),
134 Some('M') => out.push_str(&format!("{minute:02}")),
135 Some('S') => out.push_str(&format!("{second:02}")),
136 Some('f') => out.push_str(&format!("{:06}", ts.nsec / 1000)),
137 Some('%') => out.push('%'),
138 Some(other) => {
139 out.push('%');
140 out.push(other);
141 }
142 None => out.push('%'),
143 }
144 }
145 out
146}
147
148macro_rules! draw_on_typed_buffer {
153 ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, xor) => {{
154 draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, $ts, |data: &mut [$T], idx: usize, mode: DrawMode, value: $T| {
155 match mode {
156 DrawMode::Set => data[idx] = value,
157 DrawMode::XOR => data[idx] ^= value,
158 }
159 });
160 }};
161 ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, set_only) => {{
162 draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, $ts, |data: &mut [$T], idx: usize, _mode: DrawMode, value: $T| {
163 data[idx] = value;
164 });
165 }};
166 (@inner $data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, $set_fn:expr) => {{
167 let data: &mut [$T] = $data;
168 let w: usize = $w;
169 let h: usize = $h;
170 let array_ts: ad_core_rs::timestamp::EpicsTimestamp = $ts;
171 let set_fn = $set_fn;
172
173 for overlay in $overlays.iter() {
174 let value: $T = overlay.color[1] as $T;
176 let wx = overlay.width_x.max(1);
177 let wy = overlay.width_y.max(1);
178
179 let mut set_pixel = |x: usize, y: usize| {
181 if x < w && y < h {
182 let idx = y * w + x;
183 set_fn(data, idx, overlay.draw_mode, value);
184 }
185 };
186
187 match &overlay.shape {
188 OverlayShape::Cross { center_x, center_y, size } => {
189 let cx = *center_x as i64;
196 let cy = *center_y as i64;
197 let half = (*size / 2) as i64;
198 let xwide = (wx / 2) as i64;
199 let ywide = (wy / 2) as i64;
200 let mut put = |x: i64, y: i64| {
201 if x >= 0 && y >= 0 {
202 set_pixel(x as usize, y as usize);
203 }
204 };
205 for iy in (cy - half)..=(cy + half) {
206 if iy >= cy - ywide && iy <= cy + ywide {
207 for ix in (cx - half)..=(cx + half) {
209 put(ix, iy);
210 }
211 } else {
212 for ix in (cx - xwide)..=(cx + xwide) {
214 put(ix, iy);
215 }
216 }
217 }
218 }
219 OverlayShape::Rectangle { x, y, width, height } => {
220 let bx = wx.min(*width);
222 let by = wy.min(*height);
223 for dy in 0..by {
225 for dx in 0..*width {
226 set_pixel(x + dx, y + dy);
227 }
228 }
229 for dy in 0..by {
231 if *height > dy {
232 for dx in 0..*width {
233 set_pixel(x + dx, y + height - 1 - dy);
234 }
235 }
236 }
237 let inner_start = by;
239 let inner_end = height.saturating_sub(by);
240 for dy in inner_start..inner_end {
241 for t in 0..bx {
242 set_pixel(x + t, y + dy);
243 }
244 }
245 for dy in inner_start..inner_end {
247 for t in 0..bx {
248 if *width > t {
249 set_pixel(x + width - 1 - t, y + dy);
250 }
251 }
252 }
253 }
254 OverlayShape::Ellipse { center_x, center_y, rx, ry } => {
255 let cx = *center_x as i64;
263 let cy = *center_y as i64;
264 let xsize = *rx as i64;
265 let ysize = *ry as i64;
266 let xwide = (wx as i64).min((2 * xsize - 1).max(0));
268 let n_steps = (2 * (xsize + ysize)).max(1);
269 let theta_step = std::f64::consts::FRAC_PI_2 / n_steps as f64;
270 let mut pixels: Vec<(i64, i64)> = Vec::new();
271 for ii in 0..=n_steps {
272 let theta = ii as f64 * theta_step;
273 for jj in 0..xwide.max(1) {
274 let ix = (((xsize - jj) as f64) * theta.cos() + 0.5) as i64;
275 let iy = (((ysize - jj) as f64) * theta.sin() + 0.5) as i64;
276 pixels.push((cx + ix, cy + iy));
277 pixels.push((cx + ix, cy - iy));
278 pixels.push((cx - ix, cy + iy));
279 pixels.push((cx - ix, cy - iy));
280 }
281 }
282 pixels.sort_unstable();
284 pixels.dedup();
285 for (px, py) in pixels {
286 if px >= 0 && py >= 0 {
287 set_pixel(px as usize, py as usize);
288 }
289 }
290 }
291 OverlayShape::Text { x, y, size_x, size_y, text, font, timestamp_format } => {
292 let bmp = font_for(*font);
297 let rendered = if timestamp_format.is_empty() {
300 text.clone()
301 } else {
302 format!("{}{}", text, format_epics_time(array_ts, timestamp_format))
303 };
304 let xmin = *x;
305 let xmax = x.saturating_add(*size_x);
306 let ymax = y
307 .saturating_add(*size_y)
308 .min(y.saturating_add(bmp.height));
309 for (ci, ch) in rendered.chars().enumerate() {
310 let char_x0 = xmin + ci * bmp.width;
311 if char_x0 >= xmax {
312 break; }
314 for row in 0..bmp.height {
315 let iy = *y + row;
316 if iy >= ymax {
317 break;
318 }
319 for col in 0..bmp.width {
320 let ix = char_x0 + col;
321 if ix >= xmax {
322 break;
323 }
324 if font_pixel(bmp, ch, row, col) {
325 set_pixel(ix, iy);
326 }
327 }
328 }
329 }
330 }
331 }
332 }
333 }};
334}
335
336pub fn draw_overlays(src: &NDArray, overlays: &[OverlayDef]) -> NDArray {
338 let mut arr = src.clone();
339 if arr.dims.len() < 2 {
340 return arr;
341 }
342 let w = arr.dims[0].size;
343 let h = arr.dims[1].size;
344
345 match &mut arr.data {
346 NDDataBuffer::U8(data) => {
347 draw_on_typed_buffer!(data.as_mut_slice(), u8, overlays, w, h, arr.timestamp, xor);
348 }
349 NDDataBuffer::U16(data) => {
350 draw_on_typed_buffer!(data.as_mut_slice(), u16, overlays, w, h, arr.timestamp, xor);
351 }
352 NDDataBuffer::I16(data) => {
353 draw_on_typed_buffer!(data.as_mut_slice(), i16, overlays, w, h, arr.timestamp, xor);
354 }
355 NDDataBuffer::I32(data) => {
356 draw_on_typed_buffer!(data.as_mut_slice(), i32, overlays, w, h, arr.timestamp, xor);
357 }
358 NDDataBuffer::U32(data) => {
359 draw_on_typed_buffer!(data.as_mut_slice(), u32, overlays, w, h, arr.timestamp, xor);
360 }
361 NDDataBuffer::F32(data) => {
362 draw_on_typed_buffer!(
363 data.as_mut_slice(),
364 f32,
365 overlays,
366 w,
367 h,
368 arr.timestamp,
369 set_only
370 );
371 }
372 NDDataBuffer::F64(data) => {
373 draw_on_typed_buffer!(
374 data.as_mut_slice(),
375 f64,
376 overlays,
377 w,
378 h,
379 arr.timestamp,
380 set_only
381 );
382 }
383 NDDataBuffer::I8(data) => {
384 draw_on_typed_buffer!(data.as_mut_slice(), i8, overlays, w, h, arr.timestamp, xor);
385 }
386 NDDataBuffer::I64(data) => {
387 draw_on_typed_buffer!(data.as_mut_slice(), i64, overlays, w, h, arr.timestamp, xor);
388 }
389 NDDataBuffer::U64(data) => {
390 draw_on_typed_buffer!(data.as_mut_slice(), u64, overlays, w, h, arr.timestamp, xor);
391 }
392 }
393
394 arr
395}
396
397const MAX_OVERLAYS: usize = 8;
399
400#[derive(Debug, Clone)]
402struct OverlaySlot {
403 use_overlay: bool,
404 shape: i32, draw_mode: i32, position_x: usize,
407 position_y: usize,
408 center_x: i32,
411 center_y: i32,
412 size_x: usize,
413 size_y: usize,
414 width_x: usize,
415 width_y: usize,
416 red: u8,
417 green: u8,
418 blue: u8,
419 display_text: String,
420 timestamp_format: String,
421 font: usize,
422 freeze_position_x: bool,
426 freeze_position_y: bool,
427}
428
429impl Default for OverlaySlot {
430 fn default() -> Self {
431 Self {
432 use_overlay: false,
433 shape: 1, draw_mode: 0,
435 position_x: 0,
436 position_y: 0,
437 center_x: 0,
438 center_y: 0,
439 size_x: 0,
440 size_y: 0,
441 width_x: 1,
442 width_y: 1,
443 red: 255,
444 green: 0,
445 blue: 0,
446 display_text: String::new(),
447 timestamp_format: String::new(),
448 font: 0,
449 freeze_position_x: true,
450 freeze_position_y: true,
451 }
452 }
453}
454
455impl OverlaySlot {
456 fn to_overlay_def(&self) -> Option<OverlayDef> {
457 if !self.use_overlay {
458 return None;
459 }
460 let draw_mode = if self.draw_mode == 1 {
461 DrawMode::XOR
462 } else {
463 DrawMode::Set
464 };
465 let color = [self.red, self.green, self.blue];
466 let shape = match self.shape {
467 0 => OverlayShape::Cross {
468 center_x: self.position_x + self.size_x / 2,
469 center_y: self.position_y + self.size_y / 2,
470 size: self.size_x.max(self.size_y),
471 },
472 1 => OverlayShape::Rectangle {
473 x: self.position_x,
474 y: self.position_y,
475 width: self.size_x,
476 height: self.size_y,
477 },
478 2 => OverlayShape::Ellipse {
479 center_x: self.position_x + self.size_x / 2,
480 center_y: self.position_y + self.size_y / 2,
481 rx: self.size_x / 2,
482 ry: self.size_y / 2,
483 },
484 3 => OverlayShape::Text {
485 x: self.position_x,
486 y: self.position_y,
487 size_x: self.size_x,
488 size_y: self.size_y,
489 text: self.display_text.clone(),
490 font: self.font,
491 timestamp_format: self.timestamp_format.clone(),
492 },
493 _ => OverlayShape::Rectangle {
494 x: self.position_x,
495 y: self.position_y,
496 width: self.size_x,
497 height: self.size_y,
498 },
499 };
500 Some(OverlayDef {
501 shape,
502 draw_mode,
503 color,
504 width_x: self.width_x,
505 width_y: self.width_y,
506 })
507 }
508}
509
510#[derive(Default)]
512struct OverlayParamIndices {
513 use_overlay: Option<usize>,
514 position_x: Option<usize>,
515 position_y: Option<usize>,
516 center_x: Option<usize>,
517 center_y: Option<usize>,
518 size_x: Option<usize>,
519 size_y: Option<usize>,
520 width_x: Option<usize>,
521 width_y: Option<usize>,
522 shape: Option<usize>,
523 draw_mode: Option<usize>,
524 red: Option<usize>,
525 green: Option<usize>,
526 blue: Option<usize>,
527 display_text: Option<usize>,
528 timestamp_format: Option<usize>,
529 font: Option<usize>,
530}
531
532pub struct OverlayProcessor {
534 slots: [OverlaySlot; MAX_OVERLAYS],
535 params: OverlayParamIndices,
536}
537
538impl OverlayProcessor {
539 pub fn new(overlays: Vec<OverlayDef>) -> Self {
540 let mut slots: [OverlaySlot; MAX_OVERLAYS] = Default::default();
541 for (i, o) in overlays.into_iter().enumerate().take(MAX_OVERLAYS) {
542 let slot = &mut slots[i];
543 slot.use_overlay = true;
544 slot.draw_mode = if o.draw_mode == DrawMode::XOR { 1 } else { 0 };
545 slot.red = o.color[0];
546 slot.green = o.color[1];
547 slot.blue = o.color[2];
548 slot.width_x = o.width_x;
549 slot.width_y = o.width_y;
550 match o.shape {
551 OverlayShape::Cross {
552 center_x,
553 center_y,
554 size,
555 } => {
556 slot.shape = 0;
557 slot.position_x = center_x.saturating_sub(size / 2);
558 slot.position_y = center_y.saturating_sub(size / 2);
559 slot.size_x = size;
560 slot.size_y = size;
561 }
562 OverlayShape::Rectangle {
563 x,
564 y,
565 width,
566 height,
567 } => {
568 slot.shape = 1;
569 slot.position_x = x;
570 slot.position_y = y;
571 slot.size_x = width;
572 slot.size_y = height;
573 }
574 OverlayShape::Ellipse {
575 center_x,
576 center_y,
577 rx,
578 ry,
579 } => {
580 slot.shape = 2;
581 slot.position_x = center_x.saturating_sub(rx);
582 slot.position_y = center_y.saturating_sub(ry);
583 slot.size_x = rx * 2;
584 slot.size_y = ry * 2;
585 }
586 OverlayShape::Text {
587 x,
588 y,
589 size_x,
590 size_y,
591 text,
592 font,
593 timestamp_format,
594 } => {
595 slot.shape = 3;
596 slot.position_x = x;
597 slot.position_y = y;
598 slot.size_x = size_x;
599 slot.size_y = size_y;
600 slot.display_text = text;
601 slot.timestamp_format = timestamp_format;
602 slot.font = font.min(NUM_FONTS - 1);
603 }
604 }
605 }
606 Self {
607 slots,
608 params: OverlayParamIndices::default(),
609 }
610 }
611
612 fn build_active_overlays(&self) -> Vec<OverlayDef> {
613 self.slots
614 .iter()
615 .filter_map(|s| s.to_overlay_def())
616 .collect()
617 }
618}
619
620impl NDPluginProcess for OverlayProcessor {
621 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
622 let active = self.build_active_overlays();
623 let out = draw_overlays(array, &active);
624 ProcessResult::arrays(vec![Arc::new(out)])
625 }
626
627 fn plugin_type(&self) -> &str {
628 "NDPluginOverlay"
629 }
630
631 fn register_params(
632 &mut self,
633 base: &mut asyn_rs::port::PortDriverBase,
634 ) -> asyn_rs::error::AsynResult<()> {
635 use asyn_rs::param::ParamType;
636 base.create_param("MAX_SIZE_X", ParamType::Int32)?;
637 base.create_param("MAX_SIZE_Y", ParamType::Int32)?;
638 base.create_param("NAME", ParamType::Octet)?;
639 base.create_param("USE", ParamType::Int32)?;
640 base.create_param("OVERLAY_POSITION_X", ParamType::Int32)?;
641 base.create_param("OVERLAY_POSITION_Y", ParamType::Int32)?;
642 base.create_param("OVERLAY_CENTER_X", ParamType::Int32)?;
643 base.create_param("OVERLAY_CENTER_Y", ParamType::Int32)?;
644 base.create_param("OVERLAY_SIZE_X", ParamType::Int32)?;
645 base.create_param("OVERLAY_SIZE_Y", ParamType::Int32)?;
646 base.create_param("OVERLAY_WIDTH_X", ParamType::Int32)?;
647 base.create_param("OVERLAY_WIDTH_Y", ParamType::Int32)?;
648 base.create_param("OVERLAY_SHAPE", ParamType::Int32)?;
649 base.create_param("OVERLAY_DRAW_MODE", ParamType::Int32)?;
650 base.create_param("OVERLAY_RED", ParamType::Int32)?;
651 base.create_param("OVERLAY_GREEN", ParamType::Int32)?;
652 base.create_param("OVERLAY_BLUE", ParamType::Int32)?;
653 base.create_param("OVERLAY_DISPLAY_TEXT", ParamType::Octet)?;
654 base.create_param("OVERLAY_TIMESTAMP_FORMAT", ParamType::Octet)?;
655 base.create_param("OVERLAY_FONT", ParamType::Int32)?;
656
657 self.params.use_overlay = base.find_param("USE");
658 self.params.position_x = base.find_param("OVERLAY_POSITION_X");
659 self.params.position_y = base.find_param("OVERLAY_POSITION_Y");
660 self.params.center_x = base.find_param("OVERLAY_CENTER_X");
661 self.params.center_y = base.find_param("OVERLAY_CENTER_Y");
662 self.params.size_x = base.find_param("OVERLAY_SIZE_X");
663 self.params.size_y = base.find_param("OVERLAY_SIZE_Y");
664 self.params.width_x = base.find_param("OVERLAY_WIDTH_X");
665 self.params.width_y = base.find_param("OVERLAY_WIDTH_Y");
666 self.params.shape = base.find_param("OVERLAY_SHAPE");
667 self.params.draw_mode = base.find_param("OVERLAY_DRAW_MODE");
668 self.params.red = base.find_param("OVERLAY_RED");
669 self.params.green = base.find_param("OVERLAY_GREEN");
670 self.params.blue = base.find_param("OVERLAY_BLUE");
671 self.params.display_text = base.find_param("OVERLAY_DISPLAY_TEXT");
672 self.params.timestamp_format = base.find_param("OVERLAY_TIMESTAMP_FORMAT");
673 self.params.font = base.find_param("OVERLAY_FONT");
674 Ok(())
675 }
676
677 fn on_param_change(
678 &mut self,
679 reason: usize,
680 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
681 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
682 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue, ParamUpdate};
683
684 let idx = params.addr as usize;
685 if idx >= MAX_OVERLAYS {
686 return ParamChangeResult::updates(vec![]);
687 }
688 let slot = &mut self.slots[idx];
689 let mut updates = Vec::new();
690
691 if Some(reason) == self.params.use_overlay {
695 slot.use_overlay = params.value.as_i32() != 0;
696 } else if Some(reason) == self.params.shape {
697 slot.shape = params.value.as_i32();
698 } else if Some(reason) == self.params.draw_mode {
699 slot.draw_mode = params.value.as_i32();
700 } else if Some(reason) == self.params.position_x {
701 let pos = params.value.as_i32().max(0);
703 slot.position_x = pos as usize;
704 slot.freeze_position_x = true;
705 slot.center_x = pos + (slot.size_x / 2) as i32;
706 if let Some(ci) = self.params.center_x {
707 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
708 }
709 } else if Some(reason) == self.params.position_y {
710 let pos = params.value.as_i32().max(0);
711 slot.position_y = pos as usize;
712 slot.freeze_position_y = true;
713 slot.center_y = pos + (slot.size_y / 2) as i32;
714 if let Some(ci) = self.params.center_y {
715 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
716 }
717 } else if Some(reason) == self.params.center_x {
718 slot.center_x = params.value.as_i32();
720 let pos = slot.center_x - (slot.size_x / 2) as i32;
721 slot.position_x = pos.max(0) as usize;
722 slot.freeze_position_x = false;
723 if let Some(pi) = self.params.position_x {
724 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
725 }
726 } else if Some(reason) == self.params.center_y {
727 slot.center_y = params.value.as_i32();
728 let pos = slot.center_y - (slot.size_y / 2) as i32;
729 slot.position_y = pos.max(0) as usize;
730 slot.freeze_position_y = false;
731 if let Some(pi) = self.params.position_y {
732 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
733 }
734 } else if Some(reason) == self.params.size_x {
735 slot.size_x = params.value.as_i32().max(0) as usize;
738 if slot.freeze_position_x {
739 slot.center_x = slot.position_x as i32 + (slot.size_x / 2) as i32;
740 if let Some(ci) = self.params.center_x {
741 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
742 }
743 } else {
744 let pos = slot.center_x - (slot.size_x / 2) as i32;
745 slot.position_x = pos.max(0) as usize;
746 if let Some(pi) = self.params.position_x {
747 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
748 }
749 }
750 } else if Some(reason) == self.params.size_y {
751 slot.size_y = params.value.as_i32().max(0) as usize;
752 if slot.freeze_position_y {
753 slot.center_y = slot.position_y as i32 + (slot.size_y / 2) as i32;
754 if let Some(ci) = self.params.center_y {
755 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
756 }
757 } else {
758 let pos = slot.center_y - (slot.size_y / 2) as i32;
759 slot.position_y = pos.max(0) as usize;
760 if let Some(pi) = self.params.position_y {
761 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
762 }
763 }
764 } else if Some(reason) == self.params.width_x {
765 slot.width_x = params.value.as_i32().max(0) as usize;
766 } else if Some(reason) == self.params.width_y {
767 slot.width_y = params.value.as_i32().max(0) as usize;
768 } else if Some(reason) == self.params.red {
769 slot.red = params.value.as_i32().clamp(0, 255) as u8;
770 } else if Some(reason) == self.params.green {
771 slot.green = params.value.as_i32().clamp(0, 255) as u8;
772 } else if Some(reason) == self.params.blue {
773 slot.blue = params.value.as_i32().clamp(0, 255) as u8;
774 } else if Some(reason) == self.params.display_text {
775 if let ParamChangeValue::Octet(s) = ¶ms.value {
776 slot.display_text = s.clone();
777 }
778 } else if Some(reason) == self.params.timestamp_format {
779 if let ParamChangeValue::Octet(s) = ¶ms.value {
780 slot.timestamp_format = s.clone();
781 }
782 } else if Some(reason) == self.params.font {
783 slot.font = (params.value.as_i32().max(0) as usize).min(NUM_FONTS - 1);
784 }
785
786 ParamChangeResult::updates(updates)
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793 use ad_core_rs::ndarray::{NDDataType, NDDimension};
794
795 fn make_8x8() -> NDArray {
796 NDArray::new(
797 vec![NDDimension::new(8), NDDimension::new(8)],
798 NDDataType::UInt8,
799 )
800 }
801
802 #[test]
803 fn test_rectangle() {
804 let arr = make_8x8();
805 let overlays = vec![OverlayDef {
806 shape: OverlayShape::Rectangle {
807 x: 1,
808 y: 1,
809 width: 4,
810 height: 3,
811 },
812 draw_mode: DrawMode::Set,
813 color: [0, 255, 0],
814 width_x: 1,
815 width_y: 1,
816 }];
817
818 let out = draw_overlays(&arr, &overlays);
819 if let NDDataBuffer::U8(ref v) = out.data {
820 assert_eq!(v[1 * 8 + 1], 255);
822 assert_eq!(v[1 * 8 + 2], 255);
823 assert_eq!(v[1 * 8 + 3], 255);
824 assert_eq!(v[1 * 8 + 4], 255);
825 assert_eq!(v[2 * 8 + 2], 0);
827 }
828 }
829
830 #[test]
831 fn test_xor_mode() {
832 let mut arr = make_8x8();
833 if let NDDataBuffer::U8(ref mut v) = arr.data {
834 v[0] = 0xFF;
835 }
836
837 let overlays = vec![OverlayDef {
838 shape: OverlayShape::Cross {
839 center_x: 0,
840 center_y: 0,
841 size: 2,
842 },
843 draw_mode: DrawMode::XOR,
844 color: [0, 0xFF, 0],
845 width_x: 1,
846 width_y: 1,
847 }];
848
849 let out = draw_overlays(&arr, &overlays);
850 if let NDDataBuffer::U8(ref v) = out.data {
851 assert_eq!(v[0], 0x00);
854 assert_eq!(v[1], 0xFF);
856 assert_eq!(v[1 * 8], 0xFF);
858 }
859 }
860
861 #[test]
862 fn test_cross() {
863 let arr = make_8x8();
864 let overlays = vec![OverlayDef {
865 shape: OverlayShape::Cross {
866 center_x: 4,
867 center_y: 4,
868 size: 4,
869 },
870 draw_mode: DrawMode::Set,
871 color: [0, 200, 0],
872 width_x: 1,
873 width_y: 1,
874 }];
875
876 let out = draw_overlays(&arr, &overlays);
877 if let NDDataBuffer::U8(ref v) = out.data {
878 assert_eq!(v[4 * 8 + 4], 200); assert_eq!(v[4 * 8 + 6], 200); assert_eq!(v[6 * 8 + 4], 200); }
882 }
883
884 #[test]
885 fn test_text_rendering() {
886 let arr = NDArray::new(
889 vec![NDDimension::new(40), NDDimension::new(20)],
890 NDDataType::UInt8,
891 );
892 let overlays = vec![OverlayDef {
893 shape: OverlayShape::Text {
894 x: 0,
895 y: 0,
896 size_x: 40,
897 size_y: 20,
898 text: "Hi".to_string(),
899 font: 0,
900 timestamp_format: String::new(),
901 },
902 draw_mode: DrawMode::Set,
903 color: [0, 255, 0],
904 width_x: 1,
905 width_y: 1,
906 }];
907
908 let out = draw_overlays(&arr, &overlays);
909 if let NDDataBuffer::U8(ref v) = out.data {
910 let w = 40;
911 let bmp = font_for(0);
912 for (ci, ch) in "Hi".chars().enumerate() {
914 for row in 0..bmp.height {
915 for col in 0..bmp.width {
916 let expect = font_pixel(bmp, ch, row, col);
917 let px = v[row * w + ci * bmp.width + col];
918 assert_eq!(px != 0, expect, "glyph {ch} pixel ({col},{row}) mismatch");
919 }
920 }
921 }
922 assert!(v.iter().any(|&p| p != 0), "text rendered nothing");
924 }
925 }
926
927 #[test]
928 fn test_text_font_selection_differs() {
929 let render = |font: usize| -> usize {
932 let arr = NDArray::new(
933 vec![NDDimension::new(80), NDDimension::new(20)],
934 NDDataType::UInt8,
935 );
936 let ov = vec![OverlayDef {
937 shape: OverlayShape::Text {
938 x: 0,
939 y: 0,
940 size_x: 80,
941 size_y: 20,
942 text: "W".to_string(),
943 font,
944 timestamp_format: String::new(),
945 },
946 draw_mode: DrawMode::Set,
947 color: [0, 255, 0],
948 width_x: 1,
949 width_y: 1,
950 }];
951 let out = draw_overlays(&arr, &ov);
952 if let NDDataBuffer::U8(v) = &out.data {
953 v.iter().filter(|&&p| p != 0).count()
954 } else {
955 0
956 }
957 };
958 assert_ne!(render(0), render(2), "font selection had no effect");
959 }
960
961 #[test]
962 fn test_text_size_x_clips_characters() {
963 let arr = NDArray::new(
966 vec![NDDimension::new(40), NDDimension::new(20)],
967 NDDataType::UInt8,
968 );
969 let ov = vec![OverlayDef {
970 shape: OverlayShape::Text {
971 x: 0,
972 y: 0,
973 size_x: 6,
974 size_y: 20,
975 text: "WW".to_string(),
976 font: 0,
977 timestamp_format: String::new(),
978 },
979 draw_mode: DrawMode::Set,
980 color: [0, 255, 0],
981 width_x: 1,
982 width_y: 1,
983 }];
984 let out = draw_overlays(&arr, &ov);
985 if let NDDataBuffer::U8(v) = &out.data {
986 let w = 40;
987 for row in 0..font_for(0).height {
990 for col in 6..40 {
991 assert_eq!(v[row * w + col], 0, "pixel ({col},{row}) past xmax");
992 }
993 }
994 }
995 }
996
997 #[test]
998 fn test_u16_overlay() {
999 let arr = NDArray::new(
1000 vec![NDDimension::new(8), NDDimension::new(8)],
1001 NDDataType::UInt16,
1002 );
1003 let overlays = vec![OverlayDef {
1005 shape: OverlayShape::Rectangle {
1006 x: 1,
1007 y: 1,
1008 width: 4,
1009 height: 3,
1010 },
1011 draw_mode: DrawMode::Set,
1012 color: [0, 200, 0],
1013 width_x: 1,
1014 width_y: 1,
1015 }];
1016
1017 let out = draw_overlays(&arr, &overlays);
1018 if let NDDataBuffer::U16(ref v) = out.data {
1019 assert_eq!(v[1 * 8 + 1], 200);
1021 assert_eq!(v[1 * 8 + 4], 200);
1022 assert_eq!(v[2 * 8 + 2], 0);
1024 }
1025 }
1026
1027 #[test]
1028 fn test_f32_overlay_ignores_xor() {
1029 let arr = NDArray::new(
1030 vec![NDDimension::new(8), NDDimension::new(8)],
1031 NDDataType::Float32,
1032 );
1033 let overlays = vec![OverlayDef {
1034 shape: OverlayShape::Cross {
1035 center_x: 4,
1036 center_y: 4,
1037 size: 2,
1038 },
1039 draw_mode: DrawMode::XOR, color: [0, 100, 0],
1041 width_x: 1,
1042 width_y: 1,
1043 }];
1044
1045 let out = draw_overlays(&arr, &overlays);
1046 if let NDDataBuffer::F32(ref v) = out.data {
1047 assert_eq!(v[4 * 8 + 4], 100.0);
1049 }
1050 }
1051
1052 #[test]
1053 fn test_cross_thickness_half_width() {
1054 let arr = NDArray::new(
1057 vec![NDDimension::new(20), NDDimension::new(20)],
1058 NDDataType::UInt8,
1059 );
1060 let overlays = vec![OverlayDef {
1061 shape: OverlayShape::Cross {
1062 center_x: 10,
1063 center_y: 10,
1064 size: 8,
1065 },
1066 draw_mode: DrawMode::Set,
1067 color: [0, 255, 0],
1068 width_x: 1,
1069 width_y: 4,
1070 }];
1071 let out = draw_overlays(&arr, &overlays);
1072 if let NDDataBuffer::U8(ref v) = out.data {
1073 let w = 20;
1074 for y in 8..=12 {
1078 assert_eq!(v[y * w + 7], 255, "row {y} should be in the band");
1079 }
1080 assert_eq!(v[7 * w + 7], 0, "row 7 is outside the band");
1081 assert_eq!(v[13 * w + 7], 0, "row 13 is outside the band");
1082 }
1083 }
1084
1085 #[test]
1086 fn test_xor_ellipse_no_double_toggle() {
1087 let arr = NDArray::new(
1090 vec![NDDimension::new(40), NDDimension::new(40)],
1091 NDDataType::UInt8,
1092 );
1093 let overlays = vec![OverlayDef {
1094 shape: OverlayShape::Ellipse {
1095 center_x: 20,
1096 center_y: 20,
1097 rx: 12,
1098 ry: 8,
1099 },
1100 draw_mode: DrawMode::XOR,
1101 color: [0, 0xFF, 0],
1102 width_x: 3,
1103 width_y: 3,
1104 }];
1105 let out = draw_overlays(&arr, &overlays);
1106 if let NDDataBuffer::U8(ref v) = out.data {
1107 let mut drawn = 0;
1111 for &px in v.iter() {
1112 assert!(px == 0 || px == 0xFF, "double-toggled pixel: {px}");
1113 if px == 0xFF {
1114 drawn += 1;
1115 }
1116 }
1117 assert!(drawn > 0, "ellipse drew no pixels");
1118 }
1119 }
1120
1121 #[test]
1122 fn test_text_timestamp_format_appends() {
1123 let mut arr = NDArray::new(
1126 vec![NDDimension::new(120), NDDimension::new(12)],
1127 NDDataType::UInt8,
1128 );
1129 arr.timestamp = ad_core_rs::timestamp::EpicsTimestamp {
1131 sec: 0, nsec: 0,
1133 };
1134 let count_set = |arr: &NDArray, fmt: &str| -> usize {
1135 let ov = vec![OverlayDef {
1136 shape: OverlayShape::Text {
1137 x: 0,
1138 y: 0,
1139 size_x: 120,
1140 size_y: 12,
1141 text: "T".to_string(),
1142 font: 0,
1143 timestamp_format: fmt.to_string(),
1144 },
1145 draw_mode: DrawMode::Set,
1146 color: [0, 255, 0],
1147 width_x: 1,
1148 width_y: 1,
1149 }];
1150 let out = draw_overlays(arr, &ov);
1151 if let NDDataBuffer::U8(v) = &out.data {
1152 v.iter().filter(|&&p| p != 0).count()
1153 } else {
1154 0
1155 }
1156 };
1157 let bare = count_set(&arr, "");
1158 let with_ts = count_set(&arr, "%Y-%m-%d");
1159 assert!(with_ts > bare, "timestamp text should add pixels");
1161 }
1162
1163 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1166
1167 fn drive(p: &mut OverlayProcessor, reason: usize, value: i32) -> Vec<ParamUpdate> {
1169 let snap = PluginParamSnapshot {
1170 enable_callbacks: true,
1171 reason,
1172 addr: 0,
1173 value: ParamChangeValue::Int32(value),
1174 };
1175 p.on_param_change(reason, &snap).param_updates
1176 }
1177
1178 fn find_int_update(updates: &[ParamUpdate], reason: usize) -> Option<i32> {
1179 updates.iter().find_map(|u| match u {
1180 ParamUpdate::Int32 {
1181 reason: r, value, ..
1182 } if *r == reason => Some(*value),
1183 _ => None,
1184 })
1185 }
1186
1187 fn setup_processor() -> (OverlayProcessor, OverlayParamIndices) {
1188 let mut p = OverlayProcessor::new(vec![]);
1189 let mut base =
1190 asyn_rs::port::PortDriverBase::new("OV_TEST", 8, asyn_rs::port::PortFlags::default());
1191 p.register_params(&mut base).unwrap();
1192 let params = OverlayParamIndices {
1193 position_x: base.find_param("OVERLAY_POSITION_X"),
1194 position_y: base.find_param("OVERLAY_POSITION_Y"),
1195 center_x: base.find_param("OVERLAY_CENTER_X"),
1196 center_y: base.find_param("OVERLAY_CENTER_Y"),
1197 size_x: base.find_param("OVERLAY_SIZE_X"),
1198 size_y: base.find_param("OVERLAY_SIZE_Y"),
1199 ..Default::default()
1200 };
1201 (p, params)
1202 }
1203
1204 #[test]
1205 fn test_freeze_position_then_resize_moves_center() {
1206 let (mut p, idx) = setup_processor();
1209 drive(&mut p, idx.size_x.unwrap(), 20);
1210 drive(&mut p, idx.position_x.unwrap(), 100);
1211 assert_eq!(p.slots[0].position_x, 100);
1212 assert_eq!(p.slots[0].center_x, 110); let updates = drive(&mut p, idx.size_x.unwrap(), 40);
1215 assert_eq!(p.slots[0].position_x, 100);
1217 assert_eq!(p.slots[0].center_x, 120);
1218 assert_eq!(find_int_update(&updates, idx.center_x.unwrap()), Some(120));
1219 }
1220
1221 #[test]
1222 fn test_freeze_center_then_resize_moves_position() {
1223 let (mut p, idx) = setup_processor();
1226 drive(&mut p, idx.size_x.unwrap(), 20);
1227 drive(&mut p, idx.center_x.unwrap(), 200);
1228 assert_eq!(p.slots[0].center_x, 200);
1229 assert_eq!(p.slots[0].position_x, 190); let updates = drive(&mut p, idx.size_x.unwrap(), 60);
1232 assert_eq!(p.slots[0].center_x, 200);
1234 assert_eq!(p.slots[0].position_x, 170);
1235 assert_eq!(
1236 find_int_update(&updates, idx.position_x.unwrap()),
1237 Some(170)
1238 );
1239 }
1240
1241 #[test]
1242 fn test_freeze_y_axis_independent() {
1243 let (mut p, idx) = setup_processor();
1245 drive(&mut p, idx.size_y.unwrap(), 10);
1246 drive(&mut p, idx.center_y.unwrap(), 50); drive(&mut p, idx.size_x.unwrap(), 10);
1248 drive(&mut p, idx.position_x.unwrap(), 5); assert!(p.slots[0].freeze_position_x);
1250 assert!(!p.slots[0].freeze_position_y);
1251 }
1252
1253 #[test]
1254 fn test_format_epics_time_known_date() {
1255 let ts = ad_core_rs::timestamp::EpicsTimestamp {
1257 sec: 0,
1258 nsec: 123_456_000,
1259 };
1260 assert_eq!(
1261 format_epics_time(ts, "%Y-%m-%d %H:%M:%S.%f"),
1262 "1990-01-01 00:00:00.123456"
1263 );
1264 assert_eq!(format_epics_time(ts, "100%%"), "100%");
1265 }
1266}