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: [u8; 3], pub width_x: usize, pub width_y: usize, }
64
65use crate::overlay_font::{BitmapFont, FONTS};
70
71pub const NUM_FONTS: usize = 4;
73
74fn font_for(index: usize) -> &'static BitmapFont {
77 &FONTS[index.min(NUM_FONTS - 1)]
78}
79
80fn font_pixel(font: &BitmapFont, ch: char, row: usize, col: usize) -> bool {
87 let code = ch as u32;
88 if code < font.first_char as u32 {
89 return false;
90 }
91 let ci = (code - font.first_char as u32) as usize;
92 if ci >= font.num_chars || row >= font.height || col >= font.width {
93 return false;
94 }
95 let byte_in_row = col / 8;
96 let bit = 7 - (col % 8);
97 let offset = (font.height * ci + row) * font.bytes_per_char + byte_in_row;
98 (font.bitmap[offset] >> bit) & 1 != 0
99}
100
101fn format_epics_time(ts: ad_core_rs::timestamp::EpicsTimestamp, fmt: &str) -> String {
108 let secs = ts.sec as u64 + 631_152_000; let days = secs / 86_400;
111 let tod = secs % 86_400;
112 let (hour, minute, second) = (tod / 3600, (tod % 3600) / 60, tod % 60);
113
114 let z = days as i64 + 719_468;
116 let era = z.div_euclid(146_097);
117 let doe = z - era * 146_097;
118 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
119 let y = yoe + era * 400;
120 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
121 let mp = (5 * doy + 2) / 153;
122 let day = doy - (153 * mp + 2) / 5 + 1;
123 let month = if mp < 10 { mp + 3 } else { mp - 9 };
124 let year = if month <= 2 { y + 1 } else { y };
125
126 let mut out = String::with_capacity(fmt.len() + 16);
127 let mut chars = fmt.chars().peekable();
128 while let Some(c) = chars.next() {
129 if c != '%' {
130 out.push(c);
131 continue;
132 }
133 match chars.next() {
134 Some('Y') => out.push_str(&format!("{year:04}")),
135 Some('m') => out.push_str(&format!("{month:02}")),
136 Some('d') => out.push_str(&format!("{day:02}")),
137 Some('H') => out.push_str(&format!("{hour:02}")),
138 Some('M') => out.push_str(&format!("{minute:02}")),
139 Some('S') => out.push_str(&format!("{second:02}")),
140 Some('f') => out.push_str(&format!("{:06}", ts.nsec / 1000)),
141 Some('%') => out.push('%'),
142 Some(other) => {
143 out.push('%');
144 out.push(other);
145 }
146 None => out.push('%'),
147 }
148 }
149 out
150}
151
152macro_rules! draw_on_typed_buffer {
157 ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, xor) => {{
158 draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, $ts, |data: &mut [$T], idx: usize, mode: DrawMode, value: $T| {
159 match mode {
160 DrawMode::Set => data[idx] = value,
161 DrawMode::XOR => data[idx] ^= value,
162 }
163 });
164 }};
165 ($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, set_only) => {{
166 draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, $ts, |data: &mut [$T], idx: usize, _mode: DrawMode, value: $T| {
167 data[idx] = value;
168 });
169 }};
170 (@inner $data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, $set_fn:expr) => {{
171 let data: &mut [$T] = $data;
172 let w: usize = $w;
173 let h: usize = $h;
174 let array_ts: ad_core_rs::timestamp::EpicsTimestamp = $ts;
175 let set_fn = $set_fn;
176
177 for overlay in $overlays.iter() {
178 let value: $T = overlay.color[1] as $T;
180 let wx = overlay.width_x.max(1);
181 let wy = overlay.width_y.max(1);
182
183 let mut set_pixel = |x: usize, y: usize| {
185 if x < w && y < h {
186 let idx = y * w + x;
187 set_fn(data, idx, overlay.draw_mode, value);
188 }
189 };
190
191 match &overlay.shape {
192 OverlayShape::Cross { center_x, center_y, size_x, size_y } => {
193 let cx = *center_x as i64;
202 let cy = *center_y as i64;
203 let half_x = (*size_x / 2) as i64;
204 let half_y = (*size_y / 2) as i64;
205 let xwide = (wx / 2) as i64;
206 let ywide = (wy / 2) as i64;
207 let mut put = |x: i64, y: i64| {
208 if x >= 0 && y >= 0 {
209 set_pixel(x as usize, y as usize);
210 }
211 };
212 for iy in (cy - half_y)..=(cy + half_y) {
213 if iy >= cy - ywide && iy <= cy + ywide {
214 for ix in (cx - half_x)..=(cx + half_x) {
216 put(ix, iy);
217 }
218 } else {
219 for ix in (cx - xwide)..=(cx + xwide) {
221 put(ix, iy);
222 }
223 }
224 }
225 }
226 OverlayShape::Rectangle { x, y, width, height } => {
227 let xmin = *x as i64;
233 let xmax = (*x + *width) as i64;
234 let ymin = *y as i64;
235 let ymax = (*y + *height) as i64;
236 let xwide = (overlay.width_x as i64).min(*width as i64 - 1);
237 let ywide = (overlay.width_y as i64).min(*height as i64 - 1);
238 let mut put = |x: i64, y: i64| {
239 if x >= 0 && y >= 0 {
240 set_pixel(x as usize, y as usize);
241 }
242 };
243 for iy in ymin..=ymax {
244 if iy < ymin + ywide || iy > ymax - ywide {
245 for ix in xmin..=xmax {
247 put(ix, iy);
248 }
249 } else {
250 for ix in xmin..(xmin + xwide) {
252 put(ix, iy);
253 }
254 for ix in (xmax - xwide + 1)..=xmax {
255 put(ix, iy);
256 }
257 }
258 }
259 }
260 OverlayShape::Ellipse { center_x, center_y, rx, ry } => {
261 let cx = *center_x as i64;
269 let cy = *center_y as i64;
270 let xsize = *rx as i64;
271 let ysize = *ry as i64;
272 let xwide = (wx as i64).min((2 * xsize - 1).max(0));
274 let n_steps = (2 * (xsize + ysize)).max(1);
275 let theta_step = std::f64::consts::FRAC_PI_2 / n_steps as f64;
276 let mut pixels: Vec<(i64, i64)> = Vec::new();
277 for ii in 0..=n_steps {
278 let theta = ii as f64 * theta_step;
279 for jj in 0..xwide.max(1) {
280 let ix = (((xsize - jj) as f64) * theta.cos() + 0.5) as i64;
281 let iy = (((ysize - jj) as f64) * theta.sin() + 0.5) as i64;
282 pixels.push((cx + ix, cy + iy));
283 pixels.push((cx + ix, cy - iy));
284 pixels.push((cx - ix, cy + iy));
285 pixels.push((cx - ix, cy - iy));
286 }
287 }
288 pixels.sort_unstable();
290 pixels.dedup();
291 for (px, py) in pixels {
292 if px >= 0 && py >= 0 {
293 set_pixel(px as usize, py as usize);
294 }
295 }
296 }
297 OverlayShape::Text { x, y, size_x, size_y, text, font, timestamp_format } => {
298 let bmp = font_for(*font);
303 let rendered = if timestamp_format.is_empty() {
306 text.clone()
307 } else {
308 format!("{}{}", text, format_epics_time(array_ts, timestamp_format))
309 };
310 let xmin = *x;
311 let xmax = x.saturating_add(*size_x);
312 let ymax = y
313 .saturating_add(*size_y)
314 .min(y.saturating_add(bmp.height));
315 for (ci, ch) in rendered.chars().enumerate() {
316 let code = ch as u32;
321 if !(32..128).contains(&code) {
322 continue;
323 }
324 let char_x0 = xmin + ci * bmp.width;
325 if char_x0 >= xmax {
326 break; }
328 for row in 0..bmp.height {
329 let iy = *y + row;
330 if iy >= ymax {
331 break;
332 }
333 for col in 0..bmp.width {
334 let ix = char_x0 + col;
335 if ix >= xmax {
336 break;
337 }
338 if font_pixel(bmp, ch, row, col) {
339 set_pixel(ix, iy);
340 }
341 }
342 }
343 }
344 }
345 }
346 }
347 }};
348}
349
350pub fn draw_overlays(src: &NDArray, overlays: &[OverlayDef]) -> NDArray {
352 let mut arr = src.clone();
353 if arr.dims.len() < 2 {
354 return arr;
355 }
356 let w = arr.dims[0].size;
357 let h = arr.dims[1].size;
358
359 match &mut arr.data {
360 NDDataBuffer::U8(data) => {
361 draw_on_typed_buffer!(data.as_mut_slice(), u8, overlays, w, h, arr.timestamp, xor);
362 }
363 NDDataBuffer::U16(data) => {
364 draw_on_typed_buffer!(data.as_mut_slice(), u16, overlays, w, h, arr.timestamp, xor);
365 }
366 NDDataBuffer::I16(data) => {
367 draw_on_typed_buffer!(data.as_mut_slice(), i16, overlays, w, h, arr.timestamp, xor);
368 }
369 NDDataBuffer::I32(data) => {
370 draw_on_typed_buffer!(data.as_mut_slice(), i32, overlays, w, h, arr.timestamp, xor);
371 }
372 NDDataBuffer::U32(data) => {
373 draw_on_typed_buffer!(data.as_mut_slice(), u32, overlays, w, h, arr.timestamp, xor);
374 }
375 NDDataBuffer::F32(data) => {
376 draw_on_typed_buffer!(
377 data.as_mut_slice(),
378 f32,
379 overlays,
380 w,
381 h,
382 arr.timestamp,
383 set_only
384 );
385 }
386 NDDataBuffer::F64(data) => {
387 draw_on_typed_buffer!(
388 data.as_mut_slice(),
389 f64,
390 overlays,
391 w,
392 h,
393 arr.timestamp,
394 set_only
395 );
396 }
397 NDDataBuffer::I8(data) => {
398 draw_on_typed_buffer!(data.as_mut_slice(), i8, overlays, w, h, arr.timestamp, xor);
399 }
400 NDDataBuffer::I64(data) => {
401 draw_on_typed_buffer!(data.as_mut_slice(), i64, overlays, w, h, arr.timestamp, xor);
402 }
403 NDDataBuffer::U64(data) => {
404 draw_on_typed_buffer!(data.as_mut_slice(), u64, overlays, w, h, arr.timestamp, xor);
405 }
406 }
407
408 arr
409}
410
411const MAX_OVERLAYS: usize = 8;
413
414#[derive(Debug, Clone)]
416struct OverlaySlot {
417 use_overlay: bool,
418 shape: i32, draw_mode: i32, position_x: usize,
421 position_y: usize,
422 center_x: i32,
425 center_y: i32,
426 size_x: usize,
427 size_y: usize,
428 width_x: usize,
429 width_y: usize,
430 red: u8,
431 green: u8,
432 blue: u8,
433 display_text: String,
434 timestamp_format: String,
435 font: usize,
436 freeze_position_x: bool,
440 freeze_position_y: bool,
441}
442
443impl Default for OverlaySlot {
444 fn default() -> Self {
445 Self {
446 use_overlay: false,
447 shape: 1, draw_mode: 0,
449 position_x: 0,
450 position_y: 0,
451 center_x: 0,
452 center_y: 0,
453 size_x: 0,
454 size_y: 0,
455 width_x: 1,
456 width_y: 1,
457 red: 255,
458 green: 0,
459 blue: 0,
460 display_text: String::new(),
461 timestamp_format: String::new(),
462 font: 0,
463 freeze_position_x: true,
464 freeze_position_y: true,
465 }
466 }
467}
468
469impl OverlaySlot {
470 fn to_overlay_def(&self) -> Option<OverlayDef> {
471 if !self.use_overlay {
472 return None;
473 }
474 let draw_mode = if self.draw_mode == 1 {
475 DrawMode::XOR
476 } else {
477 DrawMode::Set
478 };
479 let color = [self.red, self.green, self.blue];
480 let shape = match self.shape {
481 0 => OverlayShape::Cross {
482 center_x: self.position_x + self.size_x / 2,
483 center_y: self.position_y + self.size_y / 2,
484 size_x: self.size_x,
485 size_y: self.size_y,
486 },
487 1 => OverlayShape::Rectangle {
488 x: self.position_x,
489 y: self.position_y,
490 width: self.size_x,
491 height: self.size_y,
492 },
493 2 => OverlayShape::Text {
496 x: self.position_x,
497 y: self.position_y,
498 size_x: self.size_x,
499 size_y: self.size_y,
500 text: self.display_text.clone(),
501 font: self.font,
502 timestamp_format: self.timestamp_format.clone(),
503 },
504 3 => OverlayShape::Ellipse {
505 center_x: self.position_x + self.size_x / 2,
506 center_y: self.position_y + self.size_y / 2,
507 rx: self.size_x / 2,
508 ry: self.size_y / 2,
509 },
510 _ => OverlayShape::Rectangle {
511 x: self.position_x,
512 y: self.position_y,
513 width: self.size_x,
514 height: self.size_y,
515 },
516 };
517 Some(OverlayDef {
518 shape,
519 draw_mode,
520 color,
521 width_x: self.width_x,
522 width_y: self.width_y,
523 })
524 }
525}
526
527#[derive(Default)]
529struct OverlayParamIndices {
530 use_overlay: Option<usize>,
531 position_x: Option<usize>,
532 position_y: Option<usize>,
533 center_x: Option<usize>,
534 center_y: Option<usize>,
535 size_x: Option<usize>,
536 size_y: Option<usize>,
537 width_x: Option<usize>,
538 width_y: Option<usize>,
539 shape: Option<usize>,
540 draw_mode: Option<usize>,
541 red: Option<usize>,
542 green: Option<usize>,
543 blue: Option<usize>,
544 display_text: Option<usize>,
545 timestamp_format: Option<usize>,
546 font: Option<usize>,
547}
548
549pub struct OverlayProcessor {
551 slots: [OverlaySlot; MAX_OVERLAYS],
552 params: OverlayParamIndices,
553}
554
555impl OverlayProcessor {
556 pub fn new(overlays: Vec<OverlayDef>) -> Self {
557 let mut slots: [OverlaySlot; MAX_OVERLAYS] = Default::default();
558 for (i, o) in overlays.into_iter().enumerate().take(MAX_OVERLAYS) {
559 let slot = &mut slots[i];
560 slot.use_overlay = true;
561 slot.draw_mode = if o.draw_mode == DrawMode::XOR { 1 } else { 0 };
562 slot.red = o.color[0];
563 slot.green = o.color[1];
564 slot.blue = o.color[2];
565 slot.width_x = o.width_x;
566 slot.width_y = o.width_y;
567 match o.shape {
568 OverlayShape::Cross {
569 center_x,
570 center_y,
571 size_x,
572 size_y,
573 } => {
574 slot.shape = 0;
575 slot.position_x = center_x.saturating_sub(size_x / 2);
576 slot.position_y = center_y.saturating_sub(size_y / 2);
577 slot.size_x = size_x;
578 slot.size_y = size_y;
579 }
580 OverlayShape::Rectangle {
581 x,
582 y,
583 width,
584 height,
585 } => {
586 slot.shape = 1;
587 slot.position_x = x;
588 slot.position_y = y;
589 slot.size_x = width;
590 slot.size_y = height;
591 }
592 OverlayShape::Ellipse {
593 center_x,
594 center_y,
595 rx,
596 ry,
597 } => {
598 slot.shape = 3;
599 slot.position_x = center_x.saturating_sub(rx);
600 slot.position_y = center_y.saturating_sub(ry);
601 slot.size_x = rx * 2;
602 slot.size_y = ry * 2;
603 }
604 OverlayShape::Text {
605 x,
606 y,
607 size_x,
608 size_y,
609 text,
610 font,
611 timestamp_format,
612 } => {
613 slot.shape = 2;
614 slot.position_x = x;
615 slot.position_y = y;
616 slot.size_x = size_x;
617 slot.size_y = size_y;
618 slot.display_text = text;
619 slot.timestamp_format = timestamp_format;
620 slot.font = font.min(NUM_FONTS - 1);
621 }
622 }
623 }
624 Self {
625 slots,
626 params: OverlayParamIndices::default(),
627 }
628 }
629
630 fn build_active_overlays(&self) -> Vec<OverlayDef> {
631 self.slots
632 .iter()
633 .filter_map(|s| s.to_overlay_def())
634 .collect()
635 }
636}
637
638impl NDPluginProcess for OverlayProcessor {
639 fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
640 let active = self.build_active_overlays();
641 let out = draw_overlays(array, &active);
642 ProcessResult::arrays(vec![Arc::new(out)])
643 }
644
645 fn plugin_type(&self) -> &str {
646 "NDPluginOverlay"
647 }
648
649 fn register_params(
650 &mut self,
651 base: &mut asyn_rs::port::PortDriverBase,
652 ) -> asyn_rs::error::AsynResult<()> {
653 use asyn_rs::param::ParamType;
654 base.create_param("MAX_SIZE_X", ParamType::Int32)?;
655 base.create_param("MAX_SIZE_Y", ParamType::Int32)?;
656 base.create_param("NAME", ParamType::Octet)?;
657 base.create_param("USE", ParamType::Int32)?;
658 base.create_param("OVERLAY_POSITION_X", ParamType::Int32)?;
659 base.create_param("OVERLAY_POSITION_Y", ParamType::Int32)?;
660 base.create_param("OVERLAY_CENTER_X", ParamType::Int32)?;
661 base.create_param("OVERLAY_CENTER_Y", ParamType::Int32)?;
662 base.create_param("OVERLAY_SIZE_X", ParamType::Int32)?;
663 base.create_param("OVERLAY_SIZE_Y", ParamType::Int32)?;
664 base.create_param("OVERLAY_WIDTH_X", ParamType::Int32)?;
665 base.create_param("OVERLAY_WIDTH_Y", ParamType::Int32)?;
666 base.create_param("OVERLAY_SHAPE", ParamType::Int32)?;
667 base.create_param("OVERLAY_DRAW_MODE", ParamType::Int32)?;
668 base.create_param("OVERLAY_RED", ParamType::Int32)?;
669 base.create_param("OVERLAY_GREEN", ParamType::Int32)?;
670 base.create_param("OVERLAY_BLUE", ParamType::Int32)?;
671 base.create_param("OVERLAY_DISPLAY_TEXT", ParamType::Octet)?;
672 base.create_param("OVERLAY_TIMESTAMP_FORMAT", ParamType::Octet)?;
673 base.create_param("OVERLAY_FONT", ParamType::Int32)?;
674
675 self.params.use_overlay = base.find_param("USE");
676 self.params.position_x = base.find_param("OVERLAY_POSITION_X");
677 self.params.position_y = base.find_param("OVERLAY_POSITION_Y");
678 self.params.center_x = base.find_param("OVERLAY_CENTER_X");
679 self.params.center_y = base.find_param("OVERLAY_CENTER_Y");
680 self.params.size_x = base.find_param("OVERLAY_SIZE_X");
681 self.params.size_y = base.find_param("OVERLAY_SIZE_Y");
682 self.params.width_x = base.find_param("OVERLAY_WIDTH_X");
683 self.params.width_y = base.find_param("OVERLAY_WIDTH_Y");
684 self.params.shape = base.find_param("OVERLAY_SHAPE");
685 self.params.draw_mode = base.find_param("OVERLAY_DRAW_MODE");
686 self.params.red = base.find_param("OVERLAY_RED");
687 self.params.green = base.find_param("OVERLAY_GREEN");
688 self.params.blue = base.find_param("OVERLAY_BLUE");
689 self.params.display_text = base.find_param("OVERLAY_DISPLAY_TEXT");
690 self.params.timestamp_format = base.find_param("OVERLAY_TIMESTAMP_FORMAT");
691 self.params.font = base.find_param("OVERLAY_FONT");
692 Ok(())
693 }
694
695 fn on_param_change(
696 &mut self,
697 reason: usize,
698 params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
699 ) -> ad_core_rs::plugin::runtime::ParamChangeResult {
700 use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue, ParamUpdate};
701
702 let idx = params.addr as usize;
703 if idx >= MAX_OVERLAYS {
704 return ParamChangeResult::updates(vec![]);
705 }
706 let slot = &mut self.slots[idx];
707 let mut updates = Vec::new();
708
709 if Some(reason) == self.params.use_overlay {
713 slot.use_overlay = params.value.as_i32() != 0;
714 } else if Some(reason) == self.params.shape {
715 slot.shape = params.value.as_i32();
716 } else if Some(reason) == self.params.draw_mode {
717 slot.draw_mode = params.value.as_i32();
718 } else if Some(reason) == self.params.position_x {
719 let pos = params.value.as_i32().max(0);
721 slot.position_x = pos as usize;
722 slot.freeze_position_x = true;
723 slot.center_x = pos + (slot.size_x / 2) as i32;
724 if let Some(ci) = self.params.center_x {
725 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
726 }
727 } else if Some(reason) == self.params.position_y {
728 let pos = params.value.as_i32().max(0);
729 slot.position_y = pos as usize;
730 slot.freeze_position_y = true;
731 slot.center_y = pos + (slot.size_y / 2) as i32;
732 if let Some(ci) = self.params.center_y {
733 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
734 }
735 } else if Some(reason) == self.params.center_x {
736 slot.center_x = params.value.as_i32();
738 let pos = slot.center_x - (slot.size_x / 2) as i32;
739 slot.position_x = pos.max(0) as usize;
740 slot.freeze_position_x = false;
741 if let Some(pi) = self.params.position_x {
742 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
743 }
744 } else if Some(reason) == self.params.center_y {
745 slot.center_y = params.value.as_i32();
746 let pos = slot.center_y - (slot.size_y / 2) as i32;
747 slot.position_y = pos.max(0) as usize;
748 slot.freeze_position_y = false;
749 if let Some(pi) = self.params.position_y {
750 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
751 }
752 } else if Some(reason) == self.params.size_x {
753 slot.size_x = params.value.as_i32().max(0) as usize;
756 if slot.freeze_position_x {
757 slot.center_x = slot.position_x as i32 + (slot.size_x / 2) as i32;
758 if let Some(ci) = self.params.center_x {
759 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
760 }
761 } else {
762 let pos = slot.center_x - (slot.size_x / 2) as i32;
763 slot.position_x = pos.max(0) as usize;
764 if let Some(pi) = self.params.position_x {
765 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
766 }
767 }
768 } else if Some(reason) == self.params.size_y {
769 slot.size_y = params.value.as_i32().max(0) as usize;
770 if slot.freeze_position_y {
771 slot.center_y = slot.position_y as i32 + (slot.size_y / 2) as i32;
772 if let Some(ci) = self.params.center_y {
773 updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
774 }
775 } else {
776 let pos = slot.center_y - (slot.size_y / 2) as i32;
777 slot.position_y = pos.max(0) as usize;
778 if let Some(pi) = self.params.position_y {
779 updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
780 }
781 }
782 } else if Some(reason) == self.params.width_x {
783 slot.width_x = params.value.as_i32().max(0) as usize;
784 } else if Some(reason) == self.params.width_y {
785 slot.width_y = params.value.as_i32().max(0) as usize;
786 } else if Some(reason) == self.params.red {
787 slot.red = params.value.as_i32().clamp(0, 255) as u8;
788 } else if Some(reason) == self.params.green {
789 slot.green = params.value.as_i32().clamp(0, 255) as u8;
790 } else if Some(reason) == self.params.blue {
791 slot.blue = params.value.as_i32().clamp(0, 255) as u8;
792 } else if Some(reason) == self.params.display_text {
793 if let ParamChangeValue::Octet(s) = ¶ms.value {
794 slot.display_text = s.clone();
795 }
796 } else if Some(reason) == self.params.timestamp_format {
797 if let ParamChangeValue::Octet(s) = ¶ms.value {
798 slot.timestamp_format = s.clone();
799 }
800 } else if Some(reason) == self.params.font {
801 slot.font = (params.value.as_i32().max(0) as usize).min(NUM_FONTS - 1);
802 }
803
804 ParamChangeResult::updates(updates)
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811 use ad_core_rs::ndarray::{NDDataType, NDDimension};
812
813 fn make_8x8() -> NDArray {
814 NDArray::new(
815 vec![NDDimension::new(8), NDDimension::new(8)],
816 NDDataType::UInt8,
817 )
818 }
819
820 #[test]
821 fn test_adp10_shape_ordinals_match_c_enum() {
822 let mut slot = OverlaySlot {
825 use_overlay: true,
826 ..Default::default()
827 };
828 slot.shape = 0;
829 assert!(matches!(
830 slot.to_overlay_def().map(|d| d.shape),
831 Some(OverlayShape::Cross { .. })
832 ));
833 slot.shape = 1;
834 assert!(matches!(
835 slot.to_overlay_def().map(|d| d.shape),
836 Some(OverlayShape::Rectangle { .. })
837 ));
838 slot.shape = 2;
839 assert!(
840 matches!(
841 slot.to_overlay_def().map(|d| d.shape),
842 Some(OverlayShape::Text { .. })
843 ),
844 "OVERLAY_SHAPE=2 must draw Text (C NDOverlayText)"
845 );
846 slot.shape = 3;
847 assert!(
848 matches!(
849 slot.to_overlay_def().map(|d| d.shape),
850 Some(OverlayShape::Ellipse { .. })
851 ),
852 "OVERLAY_SHAPE=3 must draw Ellipse (C NDOverlayEllipse)"
853 );
854 }
855
856 #[test]
857 fn test_rectangle() {
858 let arr = make_8x8();
859 let overlays = vec![OverlayDef {
860 shape: OverlayShape::Rectangle {
861 x: 1,
862 y: 1,
863 width: 4,
864 height: 3,
865 },
866 draw_mode: DrawMode::Set,
867 color: [0, 255, 0],
868 width_x: 1,
869 width_y: 1,
870 }];
871
872 let out = draw_overlays(&arr, &overlays);
873 if let NDDataBuffer::U8(ref v) = out.data {
874 assert_eq!(v[1 * 8 + 1], 255);
876 assert_eq!(v[1 * 8 + 2], 255);
877 assert_eq!(v[1 * 8 + 3], 255);
878 assert_eq!(v[1 * 8 + 4], 255);
879 assert_eq!(v[2 * 8 + 2], 0);
881 }
882 }
883
884 #[test]
885 fn test_adp21_rectangle_inclusive_bounds() {
886 let arr = NDArray::new(
890 vec![NDDimension::new(10), NDDimension::new(10)],
891 NDDataType::UInt8,
892 );
893 let overlays = vec![OverlayDef {
894 shape: OverlayShape::Rectangle {
895 x: 1,
896 y: 1,
897 width: 4,
898 height: 3,
899 },
900 draw_mode: DrawMode::Set,
901 color: [0, 255, 0],
902 width_x: 1,
903 width_y: 1,
904 }];
905 let out = draw_overlays(&arr, &overlays);
906 let px = |x: usize, y: usize| {
907 if let NDDataBuffer::U8(ref v) = out.data {
908 v[y * 10 + x]
909 } else {
910 0
911 }
912 };
913 assert_eq!(
915 px(5, 1),
916 255,
917 "top-right corner x=PositionX+SizeX inclusive"
918 );
919 assert_eq!(px(5, 4), 255, "bottom-right corner");
920 assert_eq!(px(1, 4), 255);
922 assert_eq!(px(3, 4), 255);
923 assert_eq!(px(6, 1), 0, "no pixel past PositionX+SizeX");
925 assert_eq!(px(1, 5), 0, "no pixel past PositionY+SizeY");
926 assert_eq!(px(3, 2), 0);
928 }
929
930 #[test]
931 fn test_xor_mode() {
932 let mut arr = make_8x8();
933 if let NDDataBuffer::U8(ref mut v) = arr.data {
934 v[0] = 0xFF;
935 }
936
937 let overlays = vec![OverlayDef {
938 shape: OverlayShape::Cross {
939 center_x: 0,
940 center_y: 0,
941 size_x: 2,
942 size_y: 2,
943 },
944 draw_mode: DrawMode::XOR,
945 color: [0, 0xFF, 0],
946 width_x: 1,
947 width_y: 1,
948 }];
949
950 let out = draw_overlays(&arr, &overlays);
951 if let NDDataBuffer::U8(ref v) = out.data {
952 assert_eq!(v[0], 0x00);
955 assert_eq!(v[1], 0xFF);
957 assert_eq!(v[1 * 8], 0xFF);
959 }
960 }
961
962 #[test]
963 fn test_cross() {
964 let arr = make_8x8();
965 let overlays = vec![OverlayDef {
966 shape: OverlayShape::Cross {
967 center_x: 4,
968 center_y: 4,
969 size_x: 4,
970 size_y: 4,
971 },
972 draw_mode: DrawMode::Set,
973 color: [0, 200, 0],
974 width_x: 1,
975 width_y: 1,
976 }];
977
978 let out = draw_overlays(&arr, &overlays);
979 if let NDDataBuffer::U8(ref v) = out.data {
980 assert_eq!(v[4 * 8 + 4], 200); assert_eq!(v[4 * 8 + 6], 200); assert_eq!(v[6 * 8 + 4], 200); }
984 }
985
986 #[test]
987 fn test_adp20_cross_independent_size_x_size_y() {
988 let arr = NDArray::new(
992 vec![NDDimension::new(20), NDDimension::new(20)],
993 NDDataType::UInt8,
994 );
995 let overlays = vec![OverlayDef {
996 shape: OverlayShape::Cross {
997 center_x: 10,
998 center_y: 10,
999 size_x: 6,
1000 size_y: 2,
1001 },
1002 draw_mode: DrawMode::Set,
1003 color: [0, 200, 0],
1004 width_x: 1,
1005 width_y: 1,
1006 }];
1007 let out = draw_overlays(&arr, &overlays);
1008 let px = |x: usize, y: usize| {
1009 if let NDDataBuffer::U8(ref v) = out.data {
1010 v[y * 20 + x]
1011 } else {
1012 0
1013 }
1014 };
1015 assert_eq!(px(13, 10), 200, "horizontal arm spans SizeX/2 = 3");
1017 assert_eq!(px(7, 10), 200);
1018 assert_eq!(px(10, 9), 200);
1020 assert_eq!(px(10, 11), 200);
1021 assert_eq!(
1023 px(10, 7),
1024 0,
1025 "vertical arm must span SizeY/2, not SizeX/2 (no square collapse)"
1026 );
1027 assert_eq!(px(10, 13), 0);
1028 }
1029
1030 #[test]
1031 fn test_adp22_extended_chars_not_drawn() {
1032 let count_set = |s: &str| {
1036 let arr = NDArray::new(
1037 vec![NDDimension::new(40), NDDimension::new(20)],
1038 NDDataType::UInt8,
1039 );
1040 let overlays = vec![OverlayDef {
1041 shape: OverlayShape::Text {
1042 x: 0,
1043 y: 0,
1044 size_x: 40,
1045 size_y: 20,
1046 text: s.to_string(),
1047 font: 0,
1048 timestamp_format: String::new(),
1049 },
1050 draw_mode: DrawMode::Set,
1051 color: [0, 255, 0],
1052 width_x: 1,
1053 width_y: 1,
1054 }];
1055 let out = draw_overlays(&arr, &overlays);
1056 if let NDDataBuffer::U8(ref v) = out.data {
1057 v.iter().filter(|&&p| p != 0).count()
1058 } else {
1059 0
1060 }
1061 };
1062 assert!(count_set("A") > 0, "printable ASCII 'A' must render");
1063 assert_eq!(
1064 count_set("\u{00C0}\u{00C9}\u{00D1}"),
1065 0,
1066 "codes >= 128 (À É Ñ) must not draw (C signed-char skip)"
1067 );
1068 }
1069
1070 #[test]
1071 fn test_text_rendering() {
1072 let arr = NDArray::new(
1075 vec![NDDimension::new(40), NDDimension::new(20)],
1076 NDDataType::UInt8,
1077 );
1078 let overlays = vec![OverlayDef {
1079 shape: OverlayShape::Text {
1080 x: 0,
1081 y: 0,
1082 size_x: 40,
1083 size_y: 20,
1084 text: "Hi".to_string(),
1085 font: 0,
1086 timestamp_format: String::new(),
1087 },
1088 draw_mode: DrawMode::Set,
1089 color: [0, 255, 0],
1090 width_x: 1,
1091 width_y: 1,
1092 }];
1093
1094 let out = draw_overlays(&arr, &overlays);
1095 if let NDDataBuffer::U8(ref v) = out.data {
1096 let w = 40;
1097 let bmp = font_for(0);
1098 for (ci, ch) in "Hi".chars().enumerate() {
1100 for row in 0..bmp.height {
1101 for col in 0..bmp.width {
1102 let expect = font_pixel(bmp, ch, row, col);
1103 let px = v[row * w + ci * bmp.width + col];
1104 assert_eq!(px != 0, expect, "glyph {ch} pixel ({col},{row}) mismatch");
1105 }
1106 }
1107 }
1108 assert!(v.iter().any(|&p| p != 0), "text rendered nothing");
1110 }
1111 }
1112
1113 #[test]
1114 fn test_text_font_selection_differs() {
1115 let render = |font: usize| -> usize {
1118 let arr = NDArray::new(
1119 vec![NDDimension::new(80), NDDimension::new(20)],
1120 NDDataType::UInt8,
1121 );
1122 let ov = vec![OverlayDef {
1123 shape: OverlayShape::Text {
1124 x: 0,
1125 y: 0,
1126 size_x: 80,
1127 size_y: 20,
1128 text: "W".to_string(),
1129 font,
1130 timestamp_format: String::new(),
1131 },
1132 draw_mode: DrawMode::Set,
1133 color: [0, 255, 0],
1134 width_x: 1,
1135 width_y: 1,
1136 }];
1137 let out = draw_overlays(&arr, &ov);
1138 if let NDDataBuffer::U8(v) = &out.data {
1139 v.iter().filter(|&&p| p != 0).count()
1140 } else {
1141 0
1142 }
1143 };
1144 assert_ne!(render(0), render(2), "font selection had no effect");
1145 }
1146
1147 #[test]
1148 fn test_text_size_x_clips_characters() {
1149 let arr = NDArray::new(
1152 vec![NDDimension::new(40), NDDimension::new(20)],
1153 NDDataType::UInt8,
1154 );
1155 let ov = vec![OverlayDef {
1156 shape: OverlayShape::Text {
1157 x: 0,
1158 y: 0,
1159 size_x: 6,
1160 size_y: 20,
1161 text: "WW".to_string(),
1162 font: 0,
1163 timestamp_format: String::new(),
1164 },
1165 draw_mode: DrawMode::Set,
1166 color: [0, 255, 0],
1167 width_x: 1,
1168 width_y: 1,
1169 }];
1170 let out = draw_overlays(&arr, &ov);
1171 if let NDDataBuffer::U8(v) = &out.data {
1172 let w = 40;
1173 for row in 0..font_for(0).height {
1176 for col in 6..40 {
1177 assert_eq!(v[row * w + col], 0, "pixel ({col},{row}) past xmax");
1178 }
1179 }
1180 }
1181 }
1182
1183 #[test]
1184 fn test_u16_overlay() {
1185 let arr = NDArray::new(
1186 vec![NDDimension::new(8), NDDimension::new(8)],
1187 NDDataType::UInt16,
1188 );
1189 let overlays = vec![OverlayDef {
1191 shape: OverlayShape::Rectangle {
1192 x: 1,
1193 y: 1,
1194 width: 4,
1195 height: 3,
1196 },
1197 draw_mode: DrawMode::Set,
1198 color: [0, 200, 0],
1199 width_x: 1,
1200 width_y: 1,
1201 }];
1202
1203 let out = draw_overlays(&arr, &overlays);
1204 if let NDDataBuffer::U16(ref v) = out.data {
1205 assert_eq!(v[1 * 8 + 1], 200);
1207 assert_eq!(v[1 * 8 + 4], 200);
1208 assert_eq!(v[2 * 8 + 2], 0);
1210 }
1211 }
1212
1213 #[test]
1214 fn test_f32_overlay_ignores_xor() {
1215 let arr = NDArray::new(
1216 vec![NDDimension::new(8), NDDimension::new(8)],
1217 NDDataType::Float32,
1218 );
1219 let overlays = vec![OverlayDef {
1220 shape: OverlayShape::Cross {
1221 center_x: 4,
1222 center_y: 4,
1223 size_x: 2,
1224 size_y: 2,
1225 },
1226 draw_mode: DrawMode::XOR, color: [0, 100, 0],
1228 width_x: 1,
1229 width_y: 1,
1230 }];
1231
1232 let out = draw_overlays(&arr, &overlays);
1233 if let NDDataBuffer::F32(ref v) = out.data {
1234 assert_eq!(v[4 * 8 + 4], 100.0);
1236 }
1237 }
1238
1239 #[test]
1240 fn test_cross_thickness_half_width() {
1241 let arr = NDArray::new(
1244 vec![NDDimension::new(20), NDDimension::new(20)],
1245 NDDataType::UInt8,
1246 );
1247 let overlays = vec![OverlayDef {
1248 shape: OverlayShape::Cross {
1249 center_x: 10,
1250 center_y: 10,
1251 size_x: 8,
1252 size_y: 8,
1253 },
1254 draw_mode: DrawMode::Set,
1255 color: [0, 255, 0],
1256 width_x: 1,
1257 width_y: 4,
1258 }];
1259 let out = draw_overlays(&arr, &overlays);
1260 if let NDDataBuffer::U8(ref v) = out.data {
1261 let w = 20;
1262 for y in 8..=12 {
1266 assert_eq!(v[y * w + 7], 255, "row {y} should be in the band");
1267 }
1268 assert_eq!(v[7 * w + 7], 0, "row 7 is outside the band");
1269 assert_eq!(v[13 * w + 7], 0, "row 13 is outside the band");
1270 }
1271 }
1272
1273 #[test]
1274 fn test_xor_ellipse_no_double_toggle() {
1275 let arr = NDArray::new(
1278 vec![NDDimension::new(40), NDDimension::new(40)],
1279 NDDataType::UInt8,
1280 );
1281 let overlays = vec![OverlayDef {
1282 shape: OverlayShape::Ellipse {
1283 center_x: 20,
1284 center_y: 20,
1285 rx: 12,
1286 ry: 8,
1287 },
1288 draw_mode: DrawMode::XOR,
1289 color: [0, 0xFF, 0],
1290 width_x: 3,
1291 width_y: 3,
1292 }];
1293 let out = draw_overlays(&arr, &overlays);
1294 if let NDDataBuffer::U8(ref v) = out.data {
1295 let mut drawn = 0;
1299 for &px in v.iter() {
1300 assert!(px == 0 || px == 0xFF, "double-toggled pixel: {px}");
1301 if px == 0xFF {
1302 drawn += 1;
1303 }
1304 }
1305 assert!(drawn > 0, "ellipse drew no pixels");
1306 }
1307 }
1308
1309 #[test]
1310 fn test_text_timestamp_format_appends() {
1311 let mut arr = NDArray::new(
1314 vec![NDDimension::new(120), NDDimension::new(12)],
1315 NDDataType::UInt8,
1316 );
1317 arr.timestamp = ad_core_rs::timestamp::EpicsTimestamp {
1319 sec: 0, nsec: 0,
1321 };
1322 let count_set = |arr: &NDArray, fmt: &str| -> usize {
1323 let ov = vec![OverlayDef {
1324 shape: OverlayShape::Text {
1325 x: 0,
1326 y: 0,
1327 size_x: 120,
1328 size_y: 12,
1329 text: "T".to_string(),
1330 font: 0,
1331 timestamp_format: fmt.to_string(),
1332 },
1333 draw_mode: DrawMode::Set,
1334 color: [0, 255, 0],
1335 width_x: 1,
1336 width_y: 1,
1337 }];
1338 let out = draw_overlays(arr, &ov);
1339 if let NDDataBuffer::U8(v) = &out.data {
1340 v.iter().filter(|&&p| p != 0).count()
1341 } else {
1342 0
1343 }
1344 };
1345 let bare = count_set(&arr, "");
1346 let with_ts = count_set(&arr, "%Y-%m-%d");
1347 assert!(with_ts > bare, "timestamp text should add pixels");
1349 }
1350
1351 use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
1354
1355 fn drive(p: &mut OverlayProcessor, reason: usize, value: i32) -> Vec<ParamUpdate> {
1357 let snap = PluginParamSnapshot {
1358 enable_callbacks: true,
1359 reason,
1360 addr: 0,
1361 value: ParamChangeValue::Int32(value),
1362 };
1363 p.on_param_change(reason, &snap).param_updates
1364 }
1365
1366 fn find_int_update(updates: &[ParamUpdate], reason: usize) -> Option<i32> {
1367 updates.iter().find_map(|u| match u {
1368 ParamUpdate::Int32 {
1369 reason: r, value, ..
1370 } if *r == reason => Some(*value),
1371 _ => None,
1372 })
1373 }
1374
1375 fn setup_processor() -> (OverlayProcessor, OverlayParamIndices) {
1376 let mut p = OverlayProcessor::new(vec![]);
1377 let mut base =
1378 asyn_rs::port::PortDriverBase::new("OV_TEST", 8, asyn_rs::port::PortFlags::default());
1379 p.register_params(&mut base).unwrap();
1380 let params = OverlayParamIndices {
1381 position_x: base.find_param("OVERLAY_POSITION_X"),
1382 position_y: base.find_param("OVERLAY_POSITION_Y"),
1383 center_x: base.find_param("OVERLAY_CENTER_X"),
1384 center_y: base.find_param("OVERLAY_CENTER_Y"),
1385 size_x: base.find_param("OVERLAY_SIZE_X"),
1386 size_y: base.find_param("OVERLAY_SIZE_Y"),
1387 ..Default::default()
1388 };
1389 (p, params)
1390 }
1391
1392 #[test]
1393 fn test_freeze_position_then_resize_moves_center() {
1394 let (mut p, idx) = setup_processor();
1397 drive(&mut p, idx.size_x.unwrap(), 20);
1398 drive(&mut p, idx.position_x.unwrap(), 100);
1399 assert_eq!(p.slots[0].position_x, 100);
1400 assert_eq!(p.slots[0].center_x, 110); let updates = drive(&mut p, idx.size_x.unwrap(), 40);
1403 assert_eq!(p.slots[0].position_x, 100);
1405 assert_eq!(p.slots[0].center_x, 120);
1406 assert_eq!(find_int_update(&updates, idx.center_x.unwrap()), Some(120));
1407 }
1408
1409 #[test]
1410 fn test_freeze_center_then_resize_moves_position() {
1411 let (mut p, idx) = setup_processor();
1414 drive(&mut p, idx.size_x.unwrap(), 20);
1415 drive(&mut p, idx.center_x.unwrap(), 200);
1416 assert_eq!(p.slots[0].center_x, 200);
1417 assert_eq!(p.slots[0].position_x, 190); let updates = drive(&mut p, idx.size_x.unwrap(), 60);
1420 assert_eq!(p.slots[0].center_x, 200);
1422 assert_eq!(p.slots[0].position_x, 170);
1423 assert_eq!(
1424 find_int_update(&updates, idx.position_x.unwrap()),
1425 Some(170)
1426 );
1427 }
1428
1429 #[test]
1430 fn test_freeze_y_axis_independent() {
1431 let (mut p, idx) = setup_processor();
1433 drive(&mut p, idx.size_y.unwrap(), 10);
1434 drive(&mut p, idx.center_y.unwrap(), 50); drive(&mut p, idx.size_x.unwrap(), 10);
1436 drive(&mut p, idx.position_x.unwrap(), 5); assert!(p.slots[0].freeze_position_x);
1438 assert!(!p.slots[0].freeze_position_y);
1439 }
1440
1441 #[test]
1442 fn test_format_epics_time_known_date() {
1443 let ts = ad_core_rs::timestamp::EpicsTimestamp {
1445 sec: 0,
1446 nsec: 123_456_000,
1447 };
1448 assert_eq!(
1449 format_epics_time(ts, "%Y-%m-%d %H:%M:%S.%f"),
1450 "1990-01-01 00:00:00.123456"
1451 );
1452 assert_eq!(format_epics_time(ts, "100%%"), "100%");
1453 }
1454}