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