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};
6use parking_lot::Mutex;
7
8#[derive(Debug, Clone)]
10pub enum OverlayShape {
11 Cross {
12 center_x: usize,
13 center_y: usize,
14 size_x: usize,
17 size_y: usize,
19 },
20 Rectangle {
21 x: usize,
22 y: usize,
23 width: usize,
24 height: usize,
25 },
26 Ellipse {
27 center_x: usize,
28 center_y: usize,
29 rx: usize,
30 ry: usize,
31 },
32 Text {
33 x: usize,
34 y: usize,
35 size_x: usize,
38 size_y: usize,
40 text: String,
41 font: usize,
43 timestamp_format: String,
46 },
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum DrawMode {
52 Set,
53 XOR,
54}
55
56#[derive(Debug, Clone)]
58pub struct OverlayDef {
59 pub shape: OverlayShape,
60 pub draw_mode: DrawMode,
61 pub color: [i32; 3],
70 pub width_x: usize, pub width_y: usize, }
73
74use crate::overlay_font::{BitmapFont, FONTS};
79
80pub const NUM_FONTS: usize = 4;
82
83fn font_for(index: usize) -> &'static BitmapFont {
86 &FONTS[index.min(NUM_FONTS - 1)]
87}
88
89fn font_pixel(font: &BitmapFont, ch: char, row: usize, col: usize) -> bool {
96 let code = ch as u32;
97 if code < font.first_char as u32 {
98 return false;
99 }
100 let ci = (code - font.first_char as u32) as usize;
101 if ci >= font.num_chars || row >= font.height || col >= font.width {
102 return false;
103 }
104 let byte_in_row = col / 8;
105 let bit = 7 - (col % 8);
106 let offset = (font.height * ci + row) * font.bytes_per_char + byte_in_row;
107 (font.bitmap[offset] >> bit) & 1 != 0
108}
109
110fn format_epics_time(ts: ad_core_rs::timestamp::EpicsTimestamp, fmt: &str) -> String {
117 let secs = ts.sec as u64 + 631_152_000; let days = secs / 86_400;
120 let tod = secs % 86_400;
121 let (hour, minute, second) = (tod / 3600, (tod % 3600) / 60, tod % 60);
122
123 let z = days as i64 + 719_468;
125 let era = z.div_euclid(146_097);
126 let doe = z - era * 146_097;
127 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
128 let y = yoe + era * 400;
129 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
130 let mp = (5 * doy + 2) / 153;
131 let day = doy - (153 * mp + 2) / 5 + 1;
132 let month = if mp < 10 { mp + 3 } else { mp - 9 };
133 let year = if month <= 2 { y + 1 } else { y };
134
135 let mut out = String::with_capacity(fmt.len() + 16);
136 let mut chars = fmt.chars().peekable();
137 while let Some(c) = chars.next() {
138 if c != '%' {
139 out.push(c);
140 continue;
141 }
142 match chars.next() {
143 Some('Y') => out.push_str(&format!("{year:04}")),
144 Some('m') => out.push_str(&format!("{month:02}")),
145 Some('d') => out.push_str(&format!("{day:02}")),
146 Some('H') => out.push_str(&format!("{hour:02}")),
147 Some('M') => out.push_str(&format!("{minute:02}")),
148 Some('S') => out.push_str(&format!("{second:02}")),
149 Some('f') => out.push_str(&format!("{:06}", ts.nsec / 1000)),
150 Some('%') => out.push('%'),
151 Some(other) => {
152 out.push('%');
153 out.push(other);
154 }
155 None => out.push('%'),
156 }
157 }
158 out
159}
160
161macro_rules! draw_on_typed_buffer {
166 ($data:expr, $T:ty, $overlays:expr, $info:expr, $ts:expr) => {{
167 let data: &mut [$T] = $data;
168 let info: &ad_core_rs::ndarray::NDArrayInfo = $info;
169 let array_ts: ad_core_rs::timestamp::EpicsTimestamp = $ts;
170
171 let is_rgb = matches!(
179 info.color_mode,
180 ad_core_rs::color::NDColorMode::RGB1
181 | ad_core_rs::color::NDColorMode::RGB2
182 | ad_core_rs::color::NDColorMode::RGB3
183 );
184
185 for overlay in $overlays.iter() {
186 let rgb: [i32; 3] = overlay.color;
188 let wx = overlay.width_x.max(1);
189 let wy = overlay.width_y.max(1);
190
191 let put_sample = |data: &mut [$T], idx: usize, color: i32| {
203 if let Some(slot) = data.get_mut(idx) {
206 *slot = match overlay.draw_mode {
207 DrawMode::Set => color as $T,
208 DrawMode::XOR => ((*slot as i32) ^ color) as $T,
209 };
210 }
211 };
212
213 let mut set_pixel = |x: usize, y: usize| {
214 if x < info.x_size && y < info.y_size {
215 let idx = y * info.y_stride + x * info.x_stride;
216 if is_rgb {
217 for (plane, color) in rgb.iter().enumerate() {
218 put_sample(data, idx + plane * info.color_stride, *color);
219 }
220 } else {
221 put_sample(data, idx, rgb[1]);
222 }
223 }
224 };
225
226 match &overlay.shape {
227 OverlayShape::Cross {
228 center_x,
229 center_y,
230 size_x,
231 size_y,
232 } => {
233 let cx = *center_x as i64;
242 let cy = *center_y as i64;
243 let half_x = (*size_x / 2) as i64;
244 let half_y = (*size_y / 2) as i64;
245 let xwide = (wx / 2) as i64;
246 let ywide = (wy / 2) as i64;
247 let mut put = |x: i64, y: i64| {
248 if x >= 0 && y >= 0 {
249 set_pixel(x as usize, y as usize);
250 }
251 };
252 for iy in (cy - half_y)..=(cy + half_y) {
253 if iy >= cy - ywide && iy <= cy + ywide {
254 for ix in (cx - half_x)..=(cx + half_x) {
256 put(ix, iy);
257 }
258 } else {
259 for ix in (cx - xwide)..=(cx + xwide) {
261 put(ix, iy);
262 }
263 }
264 }
265 }
266 OverlayShape::Rectangle {
267 x,
268 y,
269 width,
270 height,
271 } => {
272 let xmin = *x as i64;
278 let xmax = (*x + *width) as i64;
279 let ymin = *y as i64;
280 let ymax = (*y + *height) as i64;
281 let xwide = (overlay.width_x as i64).min(*width as i64 - 1);
282 let ywide = (overlay.width_y as i64).min(*height as i64 - 1);
283 let mut put = |x: i64, y: i64| {
284 if x >= 0 && y >= 0 {
285 set_pixel(x as usize, y as usize);
286 }
287 };
288 for iy in ymin..=ymax {
289 if iy < ymin + ywide || iy > ymax - ywide {
290 for ix in xmin..=xmax {
292 put(ix, iy);
293 }
294 } else {
295 for ix in xmin..(xmin + xwide) {
297 put(ix, iy);
298 }
299 for ix in (xmax - xwide + 1)..=xmax {
300 put(ix, iy);
301 }
302 }
303 }
304 }
305 OverlayShape::Ellipse {
306 center_x,
307 center_y,
308 rx,
309 ry,
310 } => {
311 let cx = *center_x as i64;
319 let cy = *center_y as i64;
320 let xsize = *rx as i64;
321 let ysize = *ry as i64;
322 let xwide = (wx as i64).min((2 * xsize - 1).max(0));
324 let n_steps = (2 * (xsize + ysize)).max(1);
325 let theta_step = std::f64::consts::FRAC_PI_2 / n_steps as f64;
326 let mut pixels: Vec<(i64, i64)> = Vec::new();
327 for ii in 0..=n_steps {
328 let theta = ii as f64 * theta_step;
329 for jj in 0..xwide.max(1) {
330 let ix = (((xsize - jj) as f64) * theta.cos() + 0.5) as i64;
331 let iy = (((ysize - jj) as f64) * theta.sin() + 0.5) as i64;
332 pixels.push((cx + ix, cy + iy));
333 pixels.push((cx + ix, cy - iy));
334 pixels.push((cx - ix, cy + iy));
335 pixels.push((cx - ix, cy - iy));
336 }
337 }
338 pixels.sort_unstable();
340 pixels.dedup();
341 for (px, py) in pixels {
342 if px >= 0 && py >= 0 {
343 set_pixel(px as usize, py as usize);
344 }
345 }
346 }
347 OverlayShape::Text {
348 x,
349 y,
350 size_x,
351 size_y,
352 text,
353 font,
354 timestamp_format,
355 } => {
356 let bmp = font_for(*font);
361 let rendered = if timestamp_format.is_empty() {
364 text.clone()
365 } else {
366 format!("{}{}", text, format_epics_time(array_ts, timestamp_format))
367 };
368 let xmin = *x;
369 let xmax = x.saturating_add(*size_x);
370 let ymax = y.saturating_add(*size_y).min(y.saturating_add(bmp.height));
371 for (ci, ch) in rendered.chars().enumerate() {
372 let code = ch as u32;
377 if !(32..128).contains(&code) {
378 continue;
379 }
380 let char_x0 = xmin + ci * bmp.width;
381 if char_x0 >= xmax {
382 break; }
384 for row in 0..bmp.height {
385 let iy = *y + row;
386 if iy >= ymax {
387 break;
388 }
389 for col in 0..bmp.width {
390 let ix = char_x0 + col;
391 if ix >= xmax {
392 break;
393 }
394 if font_pixel(bmp, ch, row, col) {
395 set_pixel(ix, iy);
396 }
397 }
398 }
399 }
400 }
401 }
402 }
403 }};
404}
405
406pub fn draw_overlays(src: &NDArray, overlays: &[OverlayDef]) -> NDArray {
414 let mut arr = src.clone();
415 let info = arr.info();
416 let ts = arr.timestamp;
417
418 match &mut arr.data {
419 NDDataBuffer::U8(data) => {
420 draw_on_typed_buffer!(data.as_mut_slice(), u8, overlays, &info, ts);
421 }
422 NDDataBuffer::U16(data) => {
423 draw_on_typed_buffer!(data.as_mut_slice(), u16, overlays, &info, ts);
424 }
425 NDDataBuffer::I16(data) => {
426 draw_on_typed_buffer!(data.as_mut_slice(), i16, overlays, &info, ts);
427 }
428 NDDataBuffer::I32(data) => {
429 draw_on_typed_buffer!(data.as_mut_slice(), i32, overlays, &info, ts);
430 }
431 NDDataBuffer::U32(data) => {
432 draw_on_typed_buffer!(data.as_mut_slice(), u32, overlays, &info, ts);
433 }
434 NDDataBuffer::F32(data) => {
435 draw_on_typed_buffer!(data.as_mut_slice(), f32, overlays, &info, ts);
436 }
437 NDDataBuffer::F64(data) => {
438 draw_on_typed_buffer!(data.as_mut_slice(), f64, overlays, &info, ts);
439 }
440 NDDataBuffer::I8(data) => {
441 draw_on_typed_buffer!(data.as_mut_slice(), i8, overlays, &info, ts);
442 }
443 NDDataBuffer::I64(data) => {
444 draw_on_typed_buffer!(data.as_mut_slice(), i64, overlays, &info, ts);
445 }
446 NDDataBuffer::U64(data) => {
447 draw_on_typed_buffer!(data.as_mut_slice(), u64, overlays, &info, ts);
448 }
449 }
450
451 arr
452}
453
454const MAX_OVERLAYS: usize = 8;
456
457#[derive(Debug, Clone)]
459struct OverlaySlot {
460 use_overlay: bool,
461 shape: i32, draw_mode: i32, position_x: usize,
464 position_y: usize,
465 center_x: i32,
468 center_y: i32,
469 size_x: usize,
470 size_y: usize,
471 width_x: usize,
472 width_y: usize,
473 red: i32,
476 green: i32,
477 blue: i32,
478 display_text: String,
479 timestamp_format: String,
480 font: usize,
481 freeze_position_x: bool,
485 freeze_position_y: bool,
486}
487
488impl Default for OverlaySlot {
489 fn default() -> Self {
490 Self {
491 use_overlay: false,
492 shape: 1, draw_mode: 0,
494 position_x: 0,
495 position_y: 0,
496 center_x: 0,
497 center_y: 0,
498 size_x: 0,
499 size_y: 0,
500 width_x: 1,
501 width_y: 1,
502 red: 255,
503 green: 0,
504 blue: 0,
505 display_text: String::new(),
506 timestamp_format: String::new(),
507 font: 0,
508 freeze_position_x: true,
509 freeze_position_y: true,
510 }
511 }
512}
513
514impl OverlaySlot {
515 fn to_overlay_def(&self) -> Option<OverlayDef> {
516 if !self.use_overlay {
517 return None;
518 }
519 let draw_mode = if self.draw_mode == 1 {
520 DrawMode::XOR
521 } else {
522 DrawMode::Set
523 };
524 let color = [self.red, self.green, self.blue];
525 let shape = match self.shape {
526 0 => OverlayShape::Cross {
527 center_x: self.position_x + self.size_x / 2,
528 center_y: self.position_y + self.size_y / 2,
529 size_x: self.size_x,
530 size_y: self.size_y,
531 },
532 1 => OverlayShape::Rectangle {
533 x: self.position_x,
534 y: self.position_y,
535 width: self.size_x,
536 height: self.size_y,
537 },
538 2 => OverlayShape::Text {
541 x: self.position_x,
542 y: self.position_y,
543 size_x: self.size_x,
544 size_y: self.size_y,
545 text: self.display_text.clone(),
546 font: self.font,
547 timestamp_format: self.timestamp_format.clone(),
548 },
549 3 => OverlayShape::Ellipse {
550 center_x: self.position_x + self.size_x / 2,
551 center_y: self.position_y + self.size_y / 2,
552 rx: self.size_x / 2,
553 ry: self.size_y / 2,
554 },
555 _ => OverlayShape::Rectangle {
556 x: self.position_x,
557 y: self.position_y,
558 width: self.size_x,
559 height: self.size_y,
560 },
561 };
562 Some(OverlayDef {
563 shape,
564 draw_mode,
565 color,
566 width_x: self.width_x,
567 width_y: self.width_y,
568 })
569 }
570}
571
572#[derive(Default)]
574struct OverlayParamIndices {
575 use_overlay: Option<usize>,
576 position_x: Option<usize>,
577 position_y: Option<usize>,
578 center_x: Option<usize>,
579 center_y: Option<usize>,
580 size_x: Option<usize>,
581 size_y: Option<usize>,
582 width_x: Option<usize>,
583 width_y: Option<usize>,
584 shape: Option<usize>,
585 draw_mode: Option<usize>,
586 red: Option<usize>,
587 green: Option<usize>,
588 blue: Option<usize>,
589 display_text: Option<usize>,
590 timestamp_format: Option<usize>,
591 font: Option<usize>,
592}
593
594pub struct OverlayProcessor {
596 slots: Mutex<[OverlaySlot; MAX_OVERLAYS]>,
599 params: OverlayParamIndices,
600}
601
602impl OverlayProcessor {
603 pub fn new(overlays: Vec<OverlayDef>) -> Self {
604 let mut slots: [OverlaySlot; MAX_OVERLAYS] = Default::default();
605 for (i, o) in overlays.into_iter().enumerate().take(MAX_OVERLAYS) {
606 let slot = &mut slots[i];
607 slot.use_overlay = true;
608 slot.draw_mode = if o.draw_mode == DrawMode::XOR { 1 } else { 0 };
609 slot.red = o.color[0];
610 slot.green = o.color[1];
611 slot.blue = o.color[2];
612 slot.width_x = o.width_x;
613 slot.width_y = o.width_y;
614 match o.shape {
615 OverlayShape::Cross {
616 center_x,
617 center_y,
618 size_x,
619 size_y,
620 } => {
621 slot.shape = 0;
622 slot.position_x = center_x.saturating_sub(size_x / 2);
623 slot.position_y = center_y.saturating_sub(size_y / 2);
624 slot.size_x = size_x;
625 slot.size_y = size_y;
626 }
627 OverlayShape::Rectangle {
628 x,
629 y,
630 width,
631 height,
632 } => {
633 slot.shape = 1;
634 slot.position_x = x;
635 slot.position_y = y;
636 slot.size_x = width;
637 slot.size_y = height;
638 }
639 OverlayShape::Ellipse {
640 center_x,
641 center_y,
642 rx,
643 ry,
644 } => {
645 slot.shape = 3;
646 slot.position_x = center_x.saturating_sub(rx);
647 slot.position_y = center_y.saturating_sub(ry);
648 slot.size_x = rx * 2;
649 slot.size_y = ry * 2;
650 }
651 OverlayShape::Text {
652 x,
653 y,
654 size_x,
655 size_y,
656 text,
657 font,
658 timestamp_format,
659 } => {
660 slot.shape = 2;
661 slot.position_x = x;
662 slot.position_y = y;
663 slot.size_x = size_x;
664 slot.size_y = size_y;
665 slot.display_text = text;
666 slot.timestamp_format = timestamp_format;
667 slot.font = font.min(NUM_FONTS - 1);
668 }
669 }
670 }
671 Self {
672 slots: Mutex::new(slots),
673 params: OverlayParamIndices::default(),
674 }
675 }
676
677 fn build_active_overlays(&self) -> Vec<OverlayDef> {
678 self.slots
679 .lock()
680 .iter()
681 .filter_map(|s| s.to_overlay_def())
682 .collect()
683 }
684}
685
686impl NDPluginProcess for OverlayProcessor {
687 fn process_array(&self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
688 let active = self.build_active_overlays();
689 let out = draw_overlays(array, &active);
690 ProcessResult::arrays(vec![Arc::new(out)])
691 }
692
693 fn plugin_type(&self) -> &str {
694 "NDPluginOverlay"
695 }
696
697 fn register_params(
698 &mut self,
699 base: &mut asyn_rs::port::PortDriverBase,
700 ) -> asyn_rs::error::AsynResult<()> {
701 use asyn_rs::param::ParamType;
702 base.create_param("MAX_SIZE_X", ParamType::Int32)?;
703 base.create_param("MAX_SIZE_Y", ParamType::Int32)?;
704 base.create_param("NAME", ParamType::Octet)?;
705 base.create_param("USE", ParamType::Int32)?;
706 base.create_param("OVERLAY_POSITION_X", ParamType::Int32)?;
707 base.create_param("OVERLAY_POSITION_Y", ParamType::Int32)?;
708 base.create_param("OVERLAY_CENTER_X", ParamType::Int32)?;
709 base.create_param("OVERLAY_CENTER_Y", ParamType::Int32)?;
710 base.create_param("OVERLAY_SIZE_X", ParamType::Int32)?;
711 base.create_param("OVERLAY_SIZE_Y", ParamType::Int32)?;
712 base.create_param("OVERLAY_WIDTH_X", ParamType::Int32)?;
713 base.create_param("OVERLAY_WIDTH_Y", ParamType::Int32)?;
714 base.create_param("OVERLAY_SHAPE", ParamType::Int32)?;
715 base.create_param("OVERLAY_DRAW_MODE", ParamType::Int32)?;
716 base.create_param("OVERLAY_RED", ParamType::Int32)?;
717 base.create_param("OVERLAY_GREEN", ParamType::Int32)?;
718 base.create_param("OVERLAY_BLUE", ParamType::Int32)?;
719 base.create_param("OVERLAY_DISPLAY_TEXT", ParamType::Octet)?;
720 base.create_param("OVERLAY_TIMESTAMP_FORMAT", ParamType::Octet)?;
721 base.create_param("OVERLAY_FONT", ParamType::Int32)?;
722
723 self.params.use_overlay = base.find_param("USE");
724 self.params.position_x = base.find_param("OVERLAY_POSITION_X");
725 self.params.position_y = base.find_param("OVERLAY_POSITION_Y");
726 self.params.center_x = base.find_param("OVERLAY_CENTER_X");
727 self.params.center_y = base.find_param("OVERLAY_CENTER_Y");
728 self.params.size_x = base.find_param("OVERLAY_SIZE_X");
729 self.params.size_y = base.find_param("OVERLAY_SIZE_Y");
730 self.params.width_x = base.find_param("OVERLAY_WIDTH_X");
731 self.params.width_y = base.find_param("OVERLAY_WIDTH_Y");
732 self.params.shape = base.find_param("OVERLAY_SHAPE");
733 self.params.draw_mode = base.find_param("OVERLAY_DRAW_MODE");
734 self.params.red = base.find_param("OVERLAY_RED");
735 self.params.green = base.find_param("OVERLAY_GREEN");
736 self.params.blue = base.find_param("OVERLAY_BLUE");
737 self.params.display_text = base.find_param("OVERLAY_DISPLAY_TEXT");
738 self.params.timestamp_format = base.find_param("OVERLAY_TIMESTAMP_FORMAT");
739 self.params.font = base.find_param("OVERLAY_FONT");
740 Ok(())
741 }
742
743 fn on_param_change(
744 &self,
745 reason: usize,
746 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
747 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
748 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue, ParamUpdate};
749
750 let idx = params.addr as usize;
751 if idx >= MAX_OVERLAYS {
752 return ParamChangeResult::updates(vec![]);
753 }
754 let mut slots = self.slots.lock();
755 let slot = &mut slots[idx];
756 let mut updates = Vec::new();
757
758 if Some(reason) == self.params.use_overlay {
762 slot.use_overlay = params.value.as_i32() != 0;
763 } else if Some(reason) == self.params.shape {
764 slot.shape = params.value.as_i32();
765 } else if Some(reason) == self.params.draw_mode {
766 slot.draw_mode = params.value.as_i32();
767 } else if Some(reason) == self.params.position_x {
768 let pos = params.value.as_i32().max(0);
770 slot.position_x = pos as usize;
771 slot.freeze_position_x = true;
772 slot.center_x = pos + (slot.size_x / 2) as i32;
773 if let Some(ci) = self.params.center_x {
774 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
775 }
776 } else if Some(reason) == self.params.position_y {
777 let pos = params.value.as_i32().max(0);
778 slot.position_y = pos as usize;
779 slot.freeze_position_y = true;
780 slot.center_y = pos + (slot.size_y / 2) as i32;
781 if let Some(ci) = self.params.center_y {
782 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
783 }
784 } else if Some(reason) == self.params.center_x {
785 slot.center_x = params.value.as_i32();
787 let pos = slot.center_x - (slot.size_x / 2) as i32;
788 slot.position_x = pos.max(0) as usize;
789 slot.freeze_position_x = false;
790 if let Some(pi) = self.params.position_x {
791 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
792 }
793 } else if Some(reason) == self.params.center_y {
794 slot.center_y = params.value.as_i32();
795 let pos = slot.center_y - (slot.size_y / 2) as i32;
796 slot.position_y = pos.max(0) as usize;
797 slot.freeze_position_y = false;
798 if let Some(pi) = self.params.position_y {
799 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
800 }
801 } else if Some(reason) == self.params.size_x {
802 slot.size_x = params.value.as_i32().max(0) as usize;
805 if slot.freeze_position_x {
806 slot.center_x = slot.position_x as i32 + (slot.size_x / 2) as i32;
807 if let Some(ci) = self.params.center_x {
808 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
809 }
810 } else {
811 let pos = slot.center_x - (slot.size_x / 2) as i32;
812 slot.position_x = pos.max(0) as usize;
813 if let Some(pi) = self.params.position_x {
814 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
815 }
816 }
817 } else if Some(reason) == self.params.size_y {
818 slot.size_y = params.value.as_i32().max(0) as usize;
819 if slot.freeze_position_y {
820 slot.center_y = slot.position_y as i32 + (slot.size_y / 2) as i32;
821 if let Some(ci) = self.params.center_y {
822 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
823 }
824 } else {
825 let pos = slot.center_y - (slot.size_y / 2) as i32;
826 slot.position_y = pos.max(0) as usize;
827 if let Some(pi) = self.params.position_y {
828 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
829 }
830 }
831 } else if Some(reason) == self.params.width_x {
832 slot.width_x = params.value.as_i32().max(0) as usize;
833 } else if Some(reason) == self.params.width_y {
834 slot.width_y = params.value.as_i32().max(0) as usize;
835 } else if Some(reason) == self.params.red {
836 slot.red = params.value.as_i32();
842 } else if Some(reason) == self.params.green {
843 slot.green = params.value.as_i32();
844 } else if Some(reason) == self.params.blue {
845 slot.blue = params.value.as_i32();
846 } else if Some(reason) == self.params.display_text {
847 if let ParamChangeValue::Octet(s) = ¶ms.value {
848 slot.display_text = s.clone();
849 }
850 } else if Some(reason) == self.params.timestamp_format {
851 if let ParamChangeValue::Octet(s) = ¶ms.value {
852 slot.timestamp_format = s.clone();
853 }
854 } else if Some(reason) == self.params.font {
855 slot.font = (params.value.as_i32().max(0) as usize).min(NUM_FONTS - 1);
856 }
857
858 ParamChangeResult::updates(updates)
859 }
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865 use ad_core_rs::ndarray::{NDDataType, NDDimension};
866
867 fn make_8x8() -> NDArray {
868 NDArray::new(
869 vec![NDDimension::new(8), NDDimension::new(8)],
870 NDDataType::UInt8,
871 )
872 }
873
874 #[test]
875 fn test_adp10_shape_ordinals_match_c_enum() {
876 let mut slot = OverlaySlot {
879 use_overlay: true,
880 ..Default::default()
881 };
882 slot.shape = 0;
883 assert!(matches!(
884 slot.to_overlay_def().map(|d| d.shape),
885 Some(OverlayShape::Cross { .. })
886 ));
887 slot.shape = 1;
888 assert!(matches!(
889 slot.to_overlay_def().map(|d| d.shape),
890 Some(OverlayShape::Rectangle { .. })
891 ));
892 slot.shape = 2;
893 assert!(
894 matches!(
895 slot.to_overlay_def().map(|d| d.shape),
896 Some(OverlayShape::Text { .. })
897 ),
898 "OVERLAY_SHAPE=2 must draw Text (C NDOverlayText)"
899 );
900 slot.shape = 3;
901 assert!(
902 matches!(
903 slot.to_overlay_def().map(|d| d.shape),
904 Some(OverlayShape::Ellipse { .. })
905 ),
906 "OVERLAY_SHAPE=3 must draw Ellipse (C NDOverlayEllipse)"
907 );
908 }
909
910 #[test]
915 fn test_r6_72_color_above_255_reaches_a_16bit_pixel() {
916 let arr = NDArray::new(
917 vec![NDDimension::new(8), NDDimension::new(8)],
918 NDDataType::UInt16,
919 );
920 let overlays = vec![OverlayDef {
921 shape: OverlayShape::Rectangle {
922 x: 1,
923 y: 1,
924 width: 4,
925 height: 3,
926 },
927 draw_mode: DrawMode::Set,
928 color: [0, 65535, 0],
930 width_x: 1,
931 width_y: 1,
932 }];
933
934 let out = draw_overlays(&arr, &overlays);
935 let NDDataBuffer::U16(ref v) = out.data else {
936 panic!("expected U16 buffer");
937 };
938 assert_eq!(v[8 + 1], 65535, "full-scale 16-bit marker must survive");
939 assert_eq!(v[2 * 8 + 2], 0, "interior untouched");
940 }
941
942 #[test]
944 fn test_r6_72_param_write_above_255_is_not_clamped() {
945 use ad_core_rs::plugin::runtime::{ParamChangeValue, PluginParamSnapshot};
946 use asyn_rs::port::{PortDriverBase, PortFlags};
947
948 let mut proc = OverlayProcessor::new(vec![]);
949 let mut base = PortDriverBase::new("R6_72", MAX_OVERLAYS + 1, PortFlags::default());
950 proc.register_params(&mut base).unwrap();
951
952 let red = proc.params.red.expect("OVERLAY_RED registered");
953 let green = proc.params.green.expect("OVERLAY_GREEN registered");
954 let blue = proc.params.blue.expect("OVERLAY_BLUE registered");
955 for (reason, value) in [(red, 256), (green, 65535), (blue, 4095)] {
956 proc.on_param_change(
957 reason,
958 &PluginParamSnapshot {
959 enable_callbacks: true,
960 reason,
961 addr: 0,
962 value: ParamChangeValue::Int32(value),
963 },
964 );
965 }
966 proc.on_param_change(
967 proc.params.use_overlay.unwrap(),
968 &PluginParamSnapshot {
969 enable_callbacks: true,
970 reason: proc.params.use_overlay.unwrap(),
971 addr: 0,
972 value: ParamChangeValue::Int32(1),
973 },
974 );
975
976 let def = proc.slots.lock()[0]
977 .to_overlay_def()
978 .expect("overlay in use");
979 assert_eq!(
980 def.color,
981 [256, 65535, 4095],
982 "C stores the raw epicsInt32 and narrows only at the pixel write"
983 );
984 }
985
986 #[test]
987 fn test_rectangle() {
988 let arr = make_8x8();
989 let overlays = vec![OverlayDef {
990 shape: OverlayShape::Rectangle {
991 x: 1,
992 y: 1,
993 width: 4,
994 height: 3,
995 },
996 draw_mode: DrawMode::Set,
997 color: [0, 255, 0],
998 width_x: 1,
999 width_y: 1,
1000 }];
1001
1002 let out = draw_overlays(&arr, &overlays);
1003 if let NDDataBuffer::U8(ref v) = out.data {
1004 assert_eq!(v[1 * 8 + 1], 255);
1006 assert_eq!(v[1 * 8 + 2], 255);
1007 assert_eq!(v[1 * 8 + 3], 255);
1008 assert_eq!(v[1 * 8 + 4], 255);
1009 assert_eq!(v[2 * 8 + 2], 0);
1011 }
1012 }
1013
1014 #[test]
1015 fn test_adp21_rectangle_inclusive_bounds() {
1016 let arr = NDArray::new(
1020 vec![NDDimension::new(10), NDDimension::new(10)],
1021 NDDataType::UInt8,
1022 );
1023 let overlays = vec![OverlayDef {
1024 shape: OverlayShape::Rectangle {
1025 x: 1,
1026 y: 1,
1027 width: 4,
1028 height: 3,
1029 },
1030 draw_mode: DrawMode::Set,
1031 color: [0, 255, 0],
1032 width_x: 1,
1033 width_y: 1,
1034 }];
1035 let out = draw_overlays(&arr, &overlays);
1036 let px = |x: usize, y: usize| {
1037 if let NDDataBuffer::U8(ref v) = out.data {
1038 v[y * 10 + x]
1039 } else {
1040 0
1041 }
1042 };
1043 assert_eq!(
1045 px(5, 1),
1046 255,
1047 "top-right corner x=PositionX+SizeX inclusive"
1048 );
1049 assert_eq!(px(5, 4), 255, "bottom-right corner");
1050 assert_eq!(px(1, 4), 255);
1052 assert_eq!(px(3, 4), 255);
1053 assert_eq!(px(6, 1), 0, "no pixel past PositionX+SizeX");
1055 assert_eq!(px(1, 5), 0, "no pixel past PositionY+SizeY");
1056 assert_eq!(px(3, 2), 0);
1058 }
1059
1060 #[test]
1061 fn test_xor_mode() {
1062 let mut arr = make_8x8();
1063 if let NDDataBuffer::U8(ref mut v) = arr.data {
1064 v[0] = 0xFF;
1065 }
1066
1067 let overlays = vec![OverlayDef {
1068 shape: OverlayShape::Cross {
1069 center_x: 0,
1070 center_y: 0,
1071 size_x: 2,
1072 size_y: 2,
1073 },
1074 draw_mode: DrawMode::XOR,
1075 color: [0, 0xFF, 0],
1076 width_x: 1,
1077 width_y: 1,
1078 }];
1079
1080 let out = draw_overlays(&arr, &overlays);
1081 if let NDDataBuffer::U8(ref v) = out.data {
1082 assert_eq!(v[0], 0x00);
1085 assert_eq!(v[1], 0xFF);
1087 assert_eq!(v[1 * 8], 0xFF);
1089 }
1090 }
1091
1092 #[test]
1093 fn test_cross() {
1094 let arr = make_8x8();
1095 let overlays = vec![OverlayDef {
1096 shape: OverlayShape::Cross {
1097 center_x: 4,
1098 center_y: 4,
1099 size_x: 4,
1100 size_y: 4,
1101 },
1102 draw_mode: DrawMode::Set,
1103 color: [0, 200, 0],
1104 width_x: 1,
1105 width_y: 1,
1106 }];
1107
1108 let out = draw_overlays(&arr, &overlays);
1109 if let NDDataBuffer::U8(ref v) = out.data {
1110 assert_eq!(v[4 * 8 + 4], 200); assert_eq!(v[4 * 8 + 6], 200); assert_eq!(v[6 * 8 + 4], 200); }
1114 }
1115
1116 #[test]
1117 fn test_adp20_cross_independent_size_x_size_y() {
1118 let arr = NDArray::new(
1122 vec![NDDimension::new(20), NDDimension::new(20)],
1123 NDDataType::UInt8,
1124 );
1125 let overlays = vec![OverlayDef {
1126 shape: OverlayShape::Cross {
1127 center_x: 10,
1128 center_y: 10,
1129 size_x: 6,
1130 size_y: 2,
1131 },
1132 draw_mode: DrawMode::Set,
1133 color: [0, 200, 0],
1134 width_x: 1,
1135 width_y: 1,
1136 }];
1137 let out = draw_overlays(&arr, &overlays);
1138 let px = |x: usize, y: usize| {
1139 if let NDDataBuffer::U8(ref v) = out.data {
1140 v[y * 20 + x]
1141 } else {
1142 0
1143 }
1144 };
1145 assert_eq!(px(13, 10), 200, "horizontal arm spans SizeX/2 = 3");
1147 assert_eq!(px(7, 10), 200);
1148 assert_eq!(px(10, 9), 200);
1150 assert_eq!(px(10, 11), 200);
1151 assert_eq!(
1153 px(10, 7),
1154 0,
1155 "vertical arm must span SizeY/2, not SizeX/2 (no square collapse)"
1156 );
1157 assert_eq!(px(10, 13), 0);
1158 }
1159
1160 #[test]
1161 fn test_adp22_extended_chars_not_drawn() {
1162 let count_set = |s: &str| {
1166 let arr = NDArray::new(
1167 vec![NDDimension::new(40), NDDimension::new(20)],
1168 NDDataType::UInt8,
1169 );
1170 let overlays = vec![OverlayDef {
1171 shape: OverlayShape::Text {
1172 x: 0,
1173 y: 0,
1174 size_x: 40,
1175 size_y: 20,
1176 text: s.to_string(),
1177 font: 0,
1178 timestamp_format: String::new(),
1179 },
1180 draw_mode: DrawMode::Set,
1181 color: [0, 255, 0],
1182 width_x: 1,
1183 width_y: 1,
1184 }];
1185 let out = draw_overlays(&arr, &overlays);
1186 if let NDDataBuffer::U8(ref v) = out.data {
1187 v.iter().filter(|&&p| p != 0).count()
1188 } else {
1189 0
1190 }
1191 };
1192 assert!(count_set("A") > 0, "printable ASCII 'A' must render");
1193 assert_eq!(
1194 count_set("\u{00C0}\u{00C9}\u{00D1}"),
1195 0,
1196 "codes >= 128 (À É Ñ) must not draw (C signed-char skip)"
1197 );
1198 }
1199
1200 #[test]
1201 fn test_text_rendering() {
1202 let arr = NDArray::new(
1205 vec![NDDimension::new(40), NDDimension::new(20)],
1206 NDDataType::UInt8,
1207 );
1208 let overlays = vec![OverlayDef {
1209 shape: OverlayShape::Text {
1210 x: 0,
1211 y: 0,
1212 size_x: 40,
1213 size_y: 20,
1214 text: "Hi".to_string(),
1215 font: 0,
1216 timestamp_format: String::new(),
1217 },
1218 draw_mode: DrawMode::Set,
1219 color: [0, 255, 0],
1220 width_x: 1,
1221 width_y: 1,
1222 }];
1223
1224 let out = draw_overlays(&arr, &overlays);
1225 if let NDDataBuffer::U8(ref v) = out.data {
1226 let w = 40;
1227 let bmp = font_for(0);
1228 for (ci, ch) in "Hi".chars().enumerate() {
1230 for row in 0..bmp.height {
1231 for col in 0..bmp.width {
1232 let expect = font_pixel(bmp, ch, row, col);
1233 let px = v[row * w + ci * bmp.width + col];
1234 assert_eq!(px != 0, expect, "glyph {ch} pixel ({col},{row}) mismatch");
1235 }
1236 }
1237 }
1238 assert!(v.iter().any(|&p| p != 0), "text rendered nothing");
1240 }
1241 }
1242
1243 #[test]
1244 fn test_text_font_selection_differs() {
1245 let render = |font: usize| -> usize {
1248 let arr = NDArray::new(
1249 vec![NDDimension::new(80), NDDimension::new(20)],
1250 NDDataType::UInt8,
1251 );
1252 let ov = vec![OverlayDef {
1253 shape: OverlayShape::Text {
1254 x: 0,
1255 y: 0,
1256 size_x: 80,
1257 size_y: 20,
1258 text: "W".to_string(),
1259 font,
1260 timestamp_format: String::new(),
1261 },
1262 draw_mode: DrawMode::Set,
1263 color: [0, 255, 0],
1264 width_x: 1,
1265 width_y: 1,
1266 }];
1267 let out = draw_overlays(&arr, &ov);
1268 if let NDDataBuffer::U8(v) = &out.data {
1269 v.iter().filter(|&&p| p != 0).count()
1270 } else {
1271 0
1272 }
1273 };
1274 assert_ne!(render(0), render(2), "font selection had no effect");
1275 }
1276
1277 #[test]
1278 fn test_text_size_x_clips_characters() {
1279 let arr = NDArray::new(
1282 vec![NDDimension::new(40), NDDimension::new(20)],
1283 NDDataType::UInt8,
1284 );
1285 let ov = vec![OverlayDef {
1286 shape: OverlayShape::Text {
1287 x: 0,
1288 y: 0,
1289 size_x: 6,
1290 size_y: 20,
1291 text: "WW".to_string(),
1292 font: 0,
1293 timestamp_format: String::new(),
1294 },
1295 draw_mode: DrawMode::Set,
1296 color: [0, 255, 0],
1297 width_x: 1,
1298 width_y: 1,
1299 }];
1300 let out = draw_overlays(&arr, &ov);
1301 if let NDDataBuffer::U8(v) = &out.data {
1302 let w = 40;
1303 for row in 0..font_for(0).height {
1306 for col in 6..40 {
1307 assert_eq!(v[row * w + col], 0, "pixel ({col},{row}) past xmax");
1308 }
1309 }
1310 }
1311 }
1312
1313 #[test]
1314 fn test_u16_overlay() {
1315 let arr = NDArray::new(
1316 vec![NDDimension::new(8), NDDimension::new(8)],
1317 NDDataType::UInt16,
1318 );
1319 let overlays = vec![OverlayDef {
1321 shape: OverlayShape::Rectangle {
1322 x: 1,
1323 y: 1,
1324 width: 4,
1325 height: 3,
1326 },
1327 draw_mode: DrawMode::Set,
1328 color: [0, 200, 0],
1329 width_x: 1,
1330 width_y: 1,
1331 }];
1332
1333 let out = draw_overlays(&arr, &overlays);
1334 if let NDDataBuffer::U16(ref v) = out.data {
1335 assert_eq!(v[1 * 8 + 1], 200);
1337 assert_eq!(v[1 * 8 + 4], 200);
1338 assert_eq!(v[2 * 8 + 2], 0);
1340 }
1341 }
1342
1343 fn make_rgb1(x: usize, y: usize) -> NDArray {
1345 use ad_core_rs::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
1346 use ad_core_rs::color::NDColorMode;
1347 let mut arr = NDArray::new(
1348 vec![
1349 NDDimension::new(3),
1350 NDDimension::new(x),
1351 NDDimension::new(y),
1352 ],
1353 NDDataType::UInt8,
1354 );
1355 arr.attributes.add(NDAttribute {
1356 name: "ColorMode".into(),
1357 description: "Color Mode".into(),
1358 source: NDAttrSource::Driver,
1359 value: NDAttrValue::Int32(NDColorMode::RGB1 as i32),
1360 source_impl: None,
1361 });
1362 arr
1363 }
1364
1365 #[test]
1366 fn test_r6_61_rgb1_uses_color_strides_and_writes_three_planes() {
1367 let arr = make_rgb1(8, 6);
1372 let overlays = vec![OverlayDef {
1373 shape: OverlayShape::Cross {
1374 center_x: 4,
1375 center_y: 3,
1376 size_x: 0,
1377 size_y: 0,
1378 },
1379 draw_mode: DrawMode::Set,
1380 color: [10, 20, 30],
1381 width_x: 1,
1382 width_y: 1,
1383 }];
1384
1385 let out = draw_overlays(&arr, &overlays);
1386 let NDDataBuffer::U8(ref v) = out.data else {
1387 panic!("expected U8 buffer");
1388 };
1389 let base = 3 * (3 * 8 + 4); assert_eq!(
1391 (v[base], v[base + 1], v[base + 2]),
1392 (10, 20, 30),
1393 "RGB1 pixel must receive red/green/blue on the three color planes"
1394 );
1395 assert_eq!(v.iter().filter(|&&s| s != 0).count(), 3);
1397 }
1398
1399 #[test]
1400 fn test_r6_61_rgb1_out_of_range_pixel_is_clipped_by_x_size() {
1401 let arr = make_rgb1(8, 6);
1404 let overlays = vec![OverlayDef {
1405 shape: OverlayShape::Cross {
1406 center_x: 8,
1407 center_y: 5,
1408 size_x: 0,
1409 size_y: 0,
1410 },
1411 draw_mode: DrawMode::Set,
1412 color: [10, 20, 30],
1413 width_x: 1,
1414 width_y: 1,
1415 }];
1416 let out = draw_overlays(&arr, &overlays);
1417 let NDDataBuffer::U8(ref v) = out.data else {
1418 panic!("expected U8 buffer");
1419 };
1420 assert!(
1421 v.iter().all(|&s| s == 0),
1422 "x == xSize is out of bounds in C addPixel"
1423 );
1424 }
1425
1426 #[test]
1427 fn test_r6_68_f32_xor_narrows_through_int() {
1428 let mut arr = NDArray::new(
1434 vec![NDDimension::new(8), NDDimension::new(8)],
1435 NDDataType::Float32,
1436 );
1437 if let NDDataBuffer::F32(ref mut v) = arr.data {
1439 v[4 * 8 + 4] = 12.75;
1440 }
1441 let overlays = vec![OverlayDef {
1442 shape: OverlayShape::Cross {
1443 center_x: 4,
1444 center_y: 4,
1445 size_x: 2,
1446 size_y: 2,
1447 },
1448 draw_mode: DrawMode::XOR,
1449 color: [0, 100, 0],
1450 width_x: 1,
1451 width_y: 1,
1452 }];
1453
1454 let out = draw_overlays(&arr, &overlays);
1455 let NDDataBuffer::F32(ref v) = out.data else {
1456 panic!("expected F32 buffer");
1457 };
1458 assert_eq!(v[4 * 8 + 4], 104.0, "float XOR must narrow through int");
1460 assert_eq!(v[4 * 8 + 5], 100.0);
1462 }
1463
1464 #[test]
1465 fn test_r6_68_i64_xor_narrows_through_int() {
1466 let mut arr = NDArray::new(
1469 vec![NDDimension::new(4), NDDimension::new(4)],
1470 NDDataType::Int64,
1471 );
1472 if let NDDataBuffer::I64(ref mut v) = arr.data {
1473 v[1 * 4 + 1] = 0x0000_0007_0000_0005; }
1475 let overlays = vec![OverlayDef {
1476 shape: OverlayShape::Cross {
1477 center_x: 1,
1478 center_y: 1,
1479 size_x: 0,
1480 size_y: 0,
1481 },
1482 draw_mode: DrawMode::XOR,
1483 color: [0, 3, 0],
1484 width_x: 1,
1485 width_y: 1,
1486 }];
1487
1488 let out = draw_overlays(&arr, &overlays);
1489 let NDDataBuffer::I64(ref v) = out.data else {
1490 panic!("expected I64 buffer");
1491 };
1492 assert_eq!(v[1 * 4 + 1], 6);
1494 }
1495
1496 #[test]
1497 fn test_cross_thickness_half_width() {
1498 let arr = NDArray::new(
1501 vec![NDDimension::new(20), NDDimension::new(20)],
1502 NDDataType::UInt8,
1503 );
1504 let overlays = vec![OverlayDef {
1505 shape: OverlayShape::Cross {
1506 center_x: 10,
1507 center_y: 10,
1508 size_x: 8,
1509 size_y: 8,
1510 },
1511 draw_mode: DrawMode::Set,
1512 color: [0, 255, 0],
1513 width_x: 1,
1514 width_y: 4,
1515 }];
1516 let out = draw_overlays(&arr, &overlays);
1517 if let NDDataBuffer::U8(ref v) = out.data {
1518 let w = 20;
1519 for y in 8..=12 {
1523 assert_eq!(v[y * w + 7], 255, "row {y} should be in the band");
1524 }
1525 assert_eq!(v[7 * w + 7], 0, "row 7 is outside the band");
1526 assert_eq!(v[13 * w + 7], 0, "row 13 is outside the band");
1527 }
1528 }
1529
1530 #[test]
1531 fn test_xor_ellipse_no_double_toggle() {
1532 let arr = NDArray::new(
1535 vec![NDDimension::new(40), NDDimension::new(40)],
1536 NDDataType::UInt8,
1537 );
1538 let overlays = vec![OverlayDef {
1539 shape: OverlayShape::Ellipse {
1540 center_x: 20,
1541 center_y: 20,
1542 rx: 12,
1543 ry: 8,
1544 },
1545 draw_mode: DrawMode::XOR,
1546 color: [0, 0xFF, 0],
1547 width_x: 3,
1548 width_y: 3,
1549 }];
1550 let out = draw_overlays(&arr, &overlays);
1551 if let NDDataBuffer::U8(ref v) = out.data {
1552 let mut drawn = 0;
1556 for &px in v.iter() {
1557 assert!(px == 0 || px == 0xFF, "double-toggled pixel: {px}");
1558 if px == 0xFF {
1559 drawn += 1;
1560 }
1561 }
1562 assert!(drawn > 0, "ellipse drew no pixels");
1563 }
1564 }
1565
1566 #[test]
1567 fn test_text_timestamp_format_appends() {
1568 let mut arr = NDArray::new(
1571 vec![NDDimension::new(120), NDDimension::new(12)],
1572 NDDataType::UInt8,
1573 );
1574 arr.timestamp = ad_core_rs::timestamp::EpicsTimestamp {
1576 sec: 0, nsec: 0,
1578 };
1579 let count_set = |arr: &NDArray, fmt: &str| -> usize {
1580 let ov = vec![OverlayDef {
1581 shape: OverlayShape::Text {
1582 x: 0,
1583 y: 0,
1584 size_x: 120,
1585 size_y: 12,
1586 text: "T".to_string(),
1587 font: 0,
1588 timestamp_format: fmt.to_string(),
1589 },
1590 draw_mode: DrawMode::Set,
1591 color: [0, 255, 0],
1592 width_x: 1,
1593 width_y: 1,
1594 }];
1595 let out = draw_overlays(arr, &ov);
1596 if let NDDataBuffer::U8(v) = &out.data {
1597 v.iter().filter(|&&p| p != 0).count()
1598 } else {
1599 0
1600 }
1601 };
1602 let bare = count_set(&arr, "");
1603 let with_ts = count_set(&arr, "%Y-%m-%d");
1604 assert!(with_ts > bare, "timestamp text should add pixels");
1606 }
1607
1608 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1611
1612 fn drive(p: &mut OverlayProcessor, reason: usize, value: i32) -> Vec<ParamUpdate> {
1614 let snap = PluginParamSnapshot {
1615 enable_callbacks: true,
1616 reason,
1617 addr: 0,
1618 value: ParamChangeValue::Int32(value),
1619 };
1620 p.on_param_change(reason, &snap).param_updates
1621 }
1622
1623 fn find_int_update(updates: &[ParamUpdate], reason: usize) -> Option<i32> {
1624 updates.iter().find_map(|u| match u {
1625 ParamUpdate::Int32 {
1626 reason: r, value, ..
1627 } if *r == reason => Some(*value),
1628 _ => None,
1629 })
1630 }
1631
1632 fn setup_processor() -> (OverlayProcessor, OverlayParamIndices) {
1633 let mut p = OverlayProcessor::new(vec![]);
1634 let mut base =
1635 asyn_rs::port::PortDriverBase::new("OV_TEST", 8, asyn_rs::port::PortFlags::default());
1636 p.register_params(&mut base).unwrap();
1637 let params = OverlayParamIndices {
1638 position_x: base.find_param("OVERLAY_POSITION_X"),
1639 position_y: base.find_param("OVERLAY_POSITION_Y"),
1640 center_x: base.find_param("OVERLAY_CENTER_X"),
1641 center_y: base.find_param("OVERLAY_CENTER_Y"),
1642 size_x: base.find_param("OVERLAY_SIZE_X"),
1643 size_y: base.find_param("OVERLAY_SIZE_Y"),
1644 ..Default::default()
1645 };
1646 (p, params)
1647 }
1648
1649 #[test]
1650 fn test_freeze_position_then_resize_moves_center() {
1651 let (mut p, idx) = setup_processor();
1654 drive(&mut p, idx.size_x.unwrap(), 20);
1655 drive(&mut p, idx.position_x.unwrap(), 100);
1656 assert_eq!(p.slots.lock()[0].position_x, 100);
1657 assert_eq!(p.slots.lock()[0].center_x, 110); let updates = drive(&mut p, idx.size_x.unwrap(), 40);
1660 assert_eq!(p.slots.lock()[0].position_x, 100);
1662 assert_eq!(p.slots.lock()[0].center_x, 120);
1663 assert_eq!(find_int_update(&updates, idx.center_x.unwrap()), Some(120));
1664 }
1665
1666 #[test]
1667 fn test_freeze_center_then_resize_moves_position() {
1668 let (mut p, idx) = setup_processor();
1671 drive(&mut p, idx.size_x.unwrap(), 20);
1672 drive(&mut p, idx.center_x.unwrap(), 200);
1673 assert_eq!(p.slots.lock()[0].center_x, 200);
1674 assert_eq!(p.slots.lock()[0].position_x, 190); let updates = drive(&mut p, idx.size_x.unwrap(), 60);
1677 assert_eq!(p.slots.lock()[0].center_x, 200);
1679 assert_eq!(p.slots.lock()[0].position_x, 170);
1680 assert_eq!(
1681 find_int_update(&updates, idx.position_x.unwrap()),
1682 Some(170)
1683 );
1684 }
1685
1686 #[test]
1687 fn test_freeze_y_axis_independent() {
1688 let (mut p, idx) = setup_processor();
1690 drive(&mut p, idx.size_y.unwrap(), 10);
1691 drive(&mut p, idx.center_y.unwrap(), 50); drive(&mut p, idx.size_x.unwrap(), 10);
1693 drive(&mut p, idx.position_x.unwrap(), 5); assert!(p.slots.lock()[0].freeze_position_x);
1695 assert!(!p.slots.lock()[0].freeze_position_y);
1696 }
1697
1698 #[test]
1699 fn test_format_epics_time_known_date() {
1700 let ts = ad_core_rs::timestamp::EpicsTimestamp {
1702 sec: 0,
1703 nsec: 123_456_000,
1704 };
1705 assert_eq!(
1706 format_epics_time(ts, "%Y-%m-%d %H:%M:%S.%f"),
1707 "1990-01-01 00:00:00.123456"
1708 );
1709 assert_eq!(format_epics_time(ts, "100%%"), "100%");
1710 }
1711}