use std::sync::Arc;
use ad_core_rs::ndarray::{NDArray, NDDataBuffer};
use ad_core_rs::ndarray_pool::NDArrayPool;
use ad_core_rs::plugin::runtime::{NDPluginProcess, ProcessResult};
#[derive(Debug, Clone)]
pub enum OverlayShape {
Cross {
center_x: usize,
center_y: usize,
size: usize,
},
Rectangle {
x: usize,
y: usize,
width: usize,
height: usize,
},
Ellipse {
center_x: usize,
center_y: usize,
rx: usize,
ry: usize,
},
Text {
x: usize,
y: usize,
size_x: usize,
size_y: usize,
text: String,
font: usize,
timestamp_format: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrawMode {
Set,
XOR,
}
#[derive(Debug, Clone)]
pub struct OverlayDef {
pub shape: OverlayShape,
pub draw_mode: DrawMode,
pub color: [u8; 3], pub width_x: usize, pub width_y: usize, }
use crate::overlay_font::{BitmapFont, FONTS};
pub const NUM_FONTS: usize = 4;
fn font_for(index: usize) -> &'static BitmapFont {
&FONTS[index.min(NUM_FONTS - 1)]
}
fn font_pixel(font: &BitmapFont, ch: char, row: usize, col: usize) -> bool {
let code = ch as u32;
if code < font.first_char as u32 {
return false;
}
let ci = (code - font.first_char as u32) as usize;
if ci >= font.num_chars || row >= font.height || col >= font.width {
return false;
}
let byte_in_row = col / 8;
let bit = 7 - (col % 8);
let offset = (font.height * ci + row) * font.bytes_per_char + byte_in_row;
(font.bitmap[offset] >> bit) & 1 != 0
}
fn format_epics_time(ts: ad_core_rs::timestamp::EpicsTimestamp, fmt: &str) -> String {
let secs = ts.sec as u64 + 631_152_000; let days = secs / 86_400;
let tod = secs % 86_400;
let (hour, minute, second) = (tod / 3600, (tod % 3600) / 60, tod % 60);
let z = days as i64 + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if month <= 2 { y + 1 } else { y };
let mut out = String::with_capacity(fmt.len() + 16);
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
out.push(c);
continue;
}
match chars.next() {
Some('Y') => out.push_str(&format!("{year:04}")),
Some('m') => out.push_str(&format!("{month:02}")),
Some('d') => out.push_str(&format!("{day:02}")),
Some('H') => out.push_str(&format!("{hour:02}")),
Some('M') => out.push_str(&format!("{minute:02}")),
Some('S') => out.push_str(&format!("{second:02}")),
Some('f') => out.push_str(&format!("{:06}", ts.nsec / 1000)),
Some('%') => out.push('%'),
Some(other) => {
out.push('%');
out.push(other);
}
None => out.push('%'),
}
}
out
}
macro_rules! draw_on_typed_buffer {
($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, xor) => {{
draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, $ts, |data: &mut [$T], idx: usize, mode: DrawMode, value: $T| {
match mode {
DrawMode::Set => data[idx] = value,
DrawMode::XOR => data[idx] ^= value,
}
});
}};
($data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, set_only) => {{
draw_on_typed_buffer!(@inner $data, $T, $overlays, $w, $h, $ts, |data: &mut [$T], idx: usize, _mode: DrawMode, value: $T| {
data[idx] = value;
});
}};
(@inner $data:expr, $T:ty, $overlays:expr, $w:expr, $h:expr, $ts:expr, $set_fn:expr) => {{
let data: &mut [$T] = $data;
let w: usize = $w;
let h: usize = $h;
let array_ts: ad_core_rs::timestamp::EpicsTimestamp = $ts;
let set_fn = $set_fn;
for overlay in $overlays.iter() {
let value: $T = overlay.color[1] as $T;
let wx = overlay.width_x.max(1);
let wy = overlay.width_y.max(1);
let mut set_pixel = |x: usize, y: usize| {
if x < w && y < h {
let idx = y * w + x;
set_fn(data, idx, overlay.draw_mode, value);
}
};
match &overlay.shape {
OverlayShape::Cross { center_x, center_y, size } => {
let cx = *center_x as i64;
let cy = *center_y as i64;
let half = (*size / 2) as i64;
let xwide = (wx / 2) as i64;
let ywide = (wy / 2) as i64;
let mut put = |x: i64, y: i64| {
if x >= 0 && y >= 0 {
set_pixel(x as usize, y as usize);
}
};
for iy in (cy - half)..=(cy + half) {
if iy >= cy - ywide && iy <= cy + ywide {
for ix in (cx - half)..=(cx + half) {
put(ix, iy);
}
} else {
for ix in (cx - xwide)..=(cx + xwide) {
put(ix, iy);
}
}
}
}
OverlayShape::Rectangle { x, y, width, height } => {
let bx = wx.min(*width);
let by = wy.min(*height);
for dy in 0..by {
for dx in 0..*width {
set_pixel(x + dx, y + dy);
}
}
for dy in 0..by {
if *height > dy {
for dx in 0..*width {
set_pixel(x + dx, y + height - 1 - dy);
}
}
}
let inner_start = by;
let inner_end = height.saturating_sub(by);
for dy in inner_start..inner_end {
for t in 0..bx {
set_pixel(x + t, y + dy);
}
}
for dy in inner_start..inner_end {
for t in 0..bx {
if *width > t {
set_pixel(x + width - 1 - t, y + dy);
}
}
}
}
OverlayShape::Ellipse { center_x, center_y, rx, ry } => {
let cx = *center_x as i64;
let cy = *center_y as i64;
let xsize = *rx as i64;
let ysize = *ry as i64;
let xwide = (wx as i64).min((2 * xsize - 1).max(0));
let n_steps = (2 * (xsize + ysize)).max(1);
let theta_step = std::f64::consts::FRAC_PI_2 / n_steps as f64;
let mut pixels: Vec<(i64, i64)> = Vec::new();
for ii in 0..=n_steps {
let theta = ii as f64 * theta_step;
for jj in 0..xwide.max(1) {
let ix = (((xsize - jj) as f64) * theta.cos() + 0.5) as i64;
let iy = (((ysize - jj) as f64) * theta.sin() + 0.5) as i64;
pixels.push((cx + ix, cy + iy));
pixels.push((cx + ix, cy - iy));
pixels.push((cx - ix, cy + iy));
pixels.push((cx - ix, cy - iy));
}
}
pixels.sort_unstable();
pixels.dedup();
for (px, py) in pixels {
if px >= 0 && py >= 0 {
set_pixel(px as usize, py as usize);
}
}
}
OverlayShape::Text { x, y, size_x, size_y, text, font, timestamp_format } => {
let bmp = font_for(*font);
let rendered = if timestamp_format.is_empty() {
text.clone()
} else {
format!("{}{}", text, format_epics_time(array_ts, timestamp_format))
};
let xmin = *x;
let xmax = x.saturating_add(*size_x);
let ymax = y
.saturating_add(*size_y)
.min(y.saturating_add(bmp.height));
for (ci, ch) in rendered.chars().enumerate() {
let char_x0 = xmin + ci * bmp.width;
if char_x0 >= xmax {
break; }
for row in 0..bmp.height {
let iy = *y + row;
if iy >= ymax {
break;
}
for col in 0..bmp.width {
let ix = char_x0 + col;
if ix >= xmax {
break;
}
if font_pixel(bmp, ch, row, col) {
set_pixel(ix, iy);
}
}
}
}
}
}
}
}};
}
pub fn draw_overlays(src: &NDArray, overlays: &[OverlayDef]) -> NDArray {
let mut arr = src.clone();
if arr.dims.len() < 2 {
return arr;
}
let w = arr.dims[0].size;
let h = arr.dims[1].size;
match &mut arr.data {
NDDataBuffer::U8(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), u8, overlays, w, h, arr.timestamp, xor);
}
NDDataBuffer::U16(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), u16, overlays, w, h, arr.timestamp, xor);
}
NDDataBuffer::I16(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), i16, overlays, w, h, arr.timestamp, xor);
}
NDDataBuffer::I32(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), i32, overlays, w, h, arr.timestamp, xor);
}
NDDataBuffer::U32(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), u32, overlays, w, h, arr.timestamp, xor);
}
NDDataBuffer::F32(data) => {
draw_on_typed_buffer!(
data.as_mut_slice(),
f32,
overlays,
w,
h,
arr.timestamp,
set_only
);
}
NDDataBuffer::F64(data) => {
draw_on_typed_buffer!(
data.as_mut_slice(),
f64,
overlays,
w,
h,
arr.timestamp,
set_only
);
}
NDDataBuffer::I8(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), i8, overlays, w, h, arr.timestamp, xor);
}
NDDataBuffer::I64(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), i64, overlays, w, h, arr.timestamp, xor);
}
NDDataBuffer::U64(data) => {
draw_on_typed_buffer!(data.as_mut_slice(), u64, overlays, w, h, arr.timestamp, xor);
}
}
arr
}
const MAX_OVERLAYS: usize = 8;
#[derive(Debug, Clone)]
struct OverlaySlot {
use_overlay: bool,
shape: i32, draw_mode: i32, position_x: usize,
position_y: usize,
center_x: i32,
center_y: i32,
size_x: usize,
size_y: usize,
width_x: usize,
width_y: usize,
red: u8,
green: u8,
blue: u8,
display_text: String,
timestamp_format: String,
font: usize,
freeze_position_x: bool,
freeze_position_y: bool,
}
impl Default for OverlaySlot {
fn default() -> Self {
Self {
use_overlay: false,
shape: 1, draw_mode: 0,
position_x: 0,
position_y: 0,
center_x: 0,
center_y: 0,
size_x: 0,
size_y: 0,
width_x: 1,
width_y: 1,
red: 255,
green: 0,
blue: 0,
display_text: String::new(),
timestamp_format: String::new(),
font: 0,
freeze_position_x: true,
freeze_position_y: true,
}
}
}
impl OverlaySlot {
fn to_overlay_def(&self) -> Option<OverlayDef> {
if !self.use_overlay {
return None;
}
let draw_mode = if self.draw_mode == 1 {
DrawMode::XOR
} else {
DrawMode::Set
};
let color = [self.red, self.green, self.blue];
let shape = match self.shape {
0 => OverlayShape::Cross {
center_x: self.position_x + self.size_x / 2,
center_y: self.position_y + self.size_y / 2,
size: self.size_x.max(self.size_y),
},
1 => OverlayShape::Rectangle {
x: self.position_x,
y: self.position_y,
width: self.size_x,
height: self.size_y,
},
2 => OverlayShape::Ellipse {
center_x: self.position_x + self.size_x / 2,
center_y: self.position_y + self.size_y / 2,
rx: self.size_x / 2,
ry: self.size_y / 2,
},
3 => OverlayShape::Text {
x: self.position_x,
y: self.position_y,
size_x: self.size_x,
size_y: self.size_y,
text: self.display_text.clone(),
font: self.font,
timestamp_format: self.timestamp_format.clone(),
},
_ => OverlayShape::Rectangle {
x: self.position_x,
y: self.position_y,
width: self.size_x,
height: self.size_y,
},
};
Some(OverlayDef {
shape,
draw_mode,
color,
width_x: self.width_x,
width_y: self.width_y,
})
}
}
#[derive(Default)]
struct OverlayParamIndices {
use_overlay: Option<usize>,
position_x: Option<usize>,
position_y: Option<usize>,
center_x: Option<usize>,
center_y: Option<usize>,
size_x: Option<usize>,
size_y: Option<usize>,
width_x: Option<usize>,
width_y: Option<usize>,
shape: Option<usize>,
draw_mode: Option<usize>,
red: Option<usize>,
green: Option<usize>,
blue: Option<usize>,
display_text: Option<usize>,
timestamp_format: Option<usize>,
font: Option<usize>,
}
pub struct OverlayProcessor {
slots: [OverlaySlot; MAX_OVERLAYS],
params: OverlayParamIndices,
}
impl OverlayProcessor {
pub fn new(overlays: Vec<OverlayDef>) -> Self {
let mut slots: [OverlaySlot; MAX_OVERLAYS] = Default::default();
for (i, o) in overlays.into_iter().enumerate().take(MAX_OVERLAYS) {
let slot = &mut slots[i];
slot.use_overlay = true;
slot.draw_mode = if o.draw_mode == DrawMode::XOR { 1 } else { 0 };
slot.red = o.color[0];
slot.green = o.color[1];
slot.blue = o.color[2];
slot.width_x = o.width_x;
slot.width_y = o.width_y;
match o.shape {
OverlayShape::Cross {
center_x,
center_y,
size,
} => {
slot.shape = 0;
slot.position_x = center_x.saturating_sub(size / 2);
slot.position_y = center_y.saturating_sub(size / 2);
slot.size_x = size;
slot.size_y = size;
}
OverlayShape::Rectangle {
x,
y,
width,
height,
} => {
slot.shape = 1;
slot.position_x = x;
slot.position_y = y;
slot.size_x = width;
slot.size_y = height;
}
OverlayShape::Ellipse {
center_x,
center_y,
rx,
ry,
} => {
slot.shape = 2;
slot.position_x = center_x.saturating_sub(rx);
slot.position_y = center_y.saturating_sub(ry);
slot.size_x = rx * 2;
slot.size_y = ry * 2;
}
OverlayShape::Text {
x,
y,
size_x,
size_y,
text,
font,
timestamp_format,
} => {
slot.shape = 3;
slot.position_x = x;
slot.position_y = y;
slot.size_x = size_x;
slot.size_y = size_y;
slot.display_text = text;
slot.timestamp_format = timestamp_format;
slot.font = font.min(NUM_FONTS - 1);
}
}
}
Self {
slots,
params: OverlayParamIndices::default(),
}
}
fn build_active_overlays(&self) -> Vec<OverlayDef> {
self.slots
.iter()
.filter_map(|s| s.to_overlay_def())
.collect()
}
}
impl NDPluginProcess for OverlayProcessor {
fn process_array(&mut self, array: &NDArray, _pool: &NDArrayPool) -> ProcessResult {
let active = self.build_active_overlays();
let out = draw_overlays(array, &active);
ProcessResult::arrays(vec![Arc::new(out)])
}
fn plugin_type(&self) -> &str {
"NDPluginOverlay"
}
fn register_params(
&mut self,
base: &mut asyn_rs::port::PortDriverBase,
) -> asyn_rs::error::AsynResult<()> {
use asyn_rs::param::ParamType;
base.create_param("MAX_SIZE_X", ParamType::Int32)?;
base.create_param("MAX_SIZE_Y", ParamType::Int32)?;
base.create_param("NAME", ParamType::Octet)?;
base.create_param("USE", ParamType::Int32)?;
base.create_param("OVERLAY_POSITION_X", ParamType::Int32)?;
base.create_param("OVERLAY_POSITION_Y", ParamType::Int32)?;
base.create_param("OVERLAY_CENTER_X", ParamType::Int32)?;
base.create_param("OVERLAY_CENTER_Y", ParamType::Int32)?;
base.create_param("OVERLAY_SIZE_X", ParamType::Int32)?;
base.create_param("OVERLAY_SIZE_Y", ParamType::Int32)?;
base.create_param("OVERLAY_WIDTH_X", ParamType::Int32)?;
base.create_param("OVERLAY_WIDTH_Y", ParamType::Int32)?;
base.create_param("OVERLAY_SHAPE", ParamType::Int32)?;
base.create_param("OVERLAY_DRAW_MODE", ParamType::Int32)?;
base.create_param("OVERLAY_RED", ParamType::Int32)?;
base.create_param("OVERLAY_GREEN", ParamType::Int32)?;
base.create_param("OVERLAY_BLUE", ParamType::Int32)?;
base.create_param("OVERLAY_DISPLAY_TEXT", ParamType::Octet)?;
base.create_param("OVERLAY_TIMESTAMP_FORMAT", ParamType::Octet)?;
base.create_param("OVERLAY_FONT", ParamType::Int32)?;
self.params.use_overlay = base.find_param("USE");
self.params.position_x = base.find_param("OVERLAY_POSITION_X");
self.params.position_y = base.find_param("OVERLAY_POSITION_Y");
self.params.center_x = base.find_param("OVERLAY_CENTER_X");
self.params.center_y = base.find_param("OVERLAY_CENTER_Y");
self.params.size_x = base.find_param("OVERLAY_SIZE_X");
self.params.size_y = base.find_param("OVERLAY_SIZE_Y");
self.params.width_x = base.find_param("OVERLAY_WIDTH_X");
self.params.width_y = base.find_param("OVERLAY_WIDTH_Y");
self.params.shape = base.find_param("OVERLAY_SHAPE");
self.params.draw_mode = base.find_param("OVERLAY_DRAW_MODE");
self.params.red = base.find_param("OVERLAY_RED");
self.params.green = base.find_param("OVERLAY_GREEN");
self.params.blue = base.find_param("OVERLAY_BLUE");
self.params.display_text = base.find_param("OVERLAY_DISPLAY_TEXT");
self.params.timestamp_format = base.find_param("OVERLAY_TIMESTAMP_FORMAT");
self.params.font = base.find_param("OVERLAY_FONT");
Ok(())
}
fn on_param_change(
&mut self,
reason: usize,
params: &ad_core_rs::plugin::runtime::PluginParamSnapshot,
) -> ad_core_rs::plugin::runtime::ParamChangeResult {
use ad_core_rs::plugin::runtime::{ParamChangeResult, ParamChangeValue, ParamUpdate};
let idx = params.addr as usize;
if idx >= MAX_OVERLAYS {
return ParamChangeResult::updates(vec![]);
}
let slot = &mut self.slots[idx];
let mut updates = Vec::new();
if Some(reason) == self.params.use_overlay {
slot.use_overlay = params.value.as_i32() != 0;
} else if Some(reason) == self.params.shape {
slot.shape = params.value.as_i32();
} else if Some(reason) == self.params.draw_mode {
slot.draw_mode = params.value.as_i32();
} else if Some(reason) == self.params.position_x {
let pos = params.value.as_i32().max(0);
slot.position_x = pos as usize;
slot.freeze_position_x = true;
slot.center_x = pos + (slot.size_x / 2) as i32;
if let Some(ci) = self.params.center_x {
updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
}
} else if Some(reason) == self.params.position_y {
let pos = params.value.as_i32().max(0);
slot.position_y = pos as usize;
slot.freeze_position_y = true;
slot.center_y = pos + (slot.size_y / 2) as i32;
if let Some(ci) = self.params.center_y {
updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
}
} else if Some(reason) == self.params.center_x {
slot.center_x = params.value.as_i32();
let pos = slot.center_x - (slot.size_x / 2) as i32;
slot.position_x = pos.max(0) as usize;
slot.freeze_position_x = false;
if let Some(pi) = self.params.position_x {
updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
}
} else if Some(reason) == self.params.center_y {
slot.center_y = params.value.as_i32();
let pos = slot.center_y - (slot.size_y / 2) as i32;
slot.position_y = pos.max(0) as usize;
slot.freeze_position_y = false;
if let Some(pi) = self.params.position_y {
updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
}
} else if Some(reason) == self.params.size_x {
slot.size_x = params.value.as_i32().max(0) as usize;
if slot.freeze_position_x {
slot.center_x = slot.position_x as i32 + (slot.size_x / 2) as i32;
if let Some(ci) = self.params.center_x {
updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_x));
}
} else {
let pos = slot.center_x - (slot.size_x / 2) as i32;
slot.position_x = pos.max(0) as usize;
if let Some(pi) = self.params.position_x {
updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
}
}
} else if Some(reason) == self.params.size_y {
slot.size_y = params.value.as_i32().max(0) as usize;
if slot.freeze_position_y {
slot.center_y = slot.position_y as i32 + (slot.size_y / 2) as i32;
if let Some(ci) = self.params.center_y {
updates.push(ParamUpdate::int32_addr(ci, idx as i32, slot.center_y));
}
} else {
let pos = slot.center_y - (slot.size_y / 2) as i32;
slot.position_y = pos.max(0) as usize;
if let Some(pi) = self.params.position_y {
updates.push(ParamUpdate::int32_addr(pi, idx as i32, pos));
}
}
} else if Some(reason) == self.params.width_x {
slot.width_x = params.value.as_i32().max(0) as usize;
} else if Some(reason) == self.params.width_y {
slot.width_y = params.value.as_i32().max(0) as usize;
} else if Some(reason) == self.params.red {
slot.red = params.value.as_i32().clamp(0, 255) as u8;
} else if Some(reason) == self.params.green {
slot.green = params.value.as_i32().clamp(0, 255) as u8;
} else if Some(reason) == self.params.blue {
slot.blue = params.value.as_i32().clamp(0, 255) as u8;
} else if Some(reason) == self.params.display_text {
if let ParamChangeValue::Octet(s) = ¶ms.value {
slot.display_text = s.clone();
}
} else if Some(reason) == self.params.timestamp_format {
if let ParamChangeValue::Octet(s) = ¶ms.value {
slot.timestamp_format = s.clone();
}
} else if Some(reason) == self.params.font {
slot.font = (params.value.as_i32().max(0) as usize).min(NUM_FONTS - 1);
}
ParamChangeResult::updates(updates)
}
}
#[cfg(test)]
mod tests {
use super::*;
use ad_core_rs::ndarray::{NDDataType, NDDimension};
fn make_8x8() -> NDArray {
NDArray::new(
vec![NDDimension::new(8), NDDimension::new(8)],
NDDataType::UInt8,
)
}
#[test]
fn test_rectangle() {
let arr = make_8x8();
let overlays = vec![OverlayDef {
shape: OverlayShape::Rectangle {
x: 1,
y: 1,
width: 4,
height: 3,
},
draw_mode: DrawMode::Set,
color: [0, 255, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::U8(ref v) = out.data {
assert_eq!(v[1 * 8 + 1], 255);
assert_eq!(v[1 * 8 + 2], 255);
assert_eq!(v[1 * 8 + 3], 255);
assert_eq!(v[1 * 8 + 4], 255);
assert_eq!(v[2 * 8 + 2], 0);
}
}
#[test]
fn test_xor_mode() {
let mut arr = make_8x8();
if let NDDataBuffer::U8(ref mut v) = arr.data {
v[0] = 0xFF;
}
let overlays = vec![OverlayDef {
shape: OverlayShape::Cross {
center_x: 0,
center_y: 0,
size: 2,
},
draw_mode: DrawMode::XOR,
color: [0, 0xFF, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::U8(ref v) = out.data {
assert_eq!(v[0], 0x00);
assert_eq!(v[1], 0xFF);
assert_eq!(v[1 * 8], 0xFF);
}
}
#[test]
fn test_cross() {
let arr = make_8x8();
let overlays = vec![OverlayDef {
shape: OverlayShape::Cross {
center_x: 4,
center_y: 4,
size: 4,
},
draw_mode: DrawMode::Set,
color: [0, 200, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::U8(ref v) = out.data {
assert_eq!(v[4 * 8 + 4], 200); assert_eq!(v[4 * 8 + 6], 200); assert_eq!(v[6 * 8 + 4], 200); }
}
#[test]
fn test_text_rendering() {
let arr = NDArray::new(
vec![NDDimension::new(40), NDDimension::new(20)],
NDDataType::UInt8,
);
let overlays = vec![OverlayDef {
shape: OverlayShape::Text {
x: 0,
y: 0,
size_x: 40,
size_y: 20,
text: "Hi".to_string(),
font: 0,
timestamp_format: String::new(),
},
draw_mode: DrawMode::Set,
color: [0, 255, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::U8(ref v) = out.data {
let w = 40;
let bmp = font_for(0);
for (ci, ch) in "Hi".chars().enumerate() {
for row in 0..bmp.height {
for col in 0..bmp.width {
let expect = font_pixel(bmp, ch, row, col);
let px = v[row * w + ci * bmp.width + col];
assert_eq!(px != 0, expect, "glyph {ch} pixel ({col},{row}) mismatch");
}
}
}
assert!(v.iter().any(|&p| p != 0), "text rendered nothing");
}
}
#[test]
fn test_text_font_selection_differs() {
let render = |font: usize| -> usize {
let arr = NDArray::new(
vec![NDDimension::new(80), NDDimension::new(20)],
NDDataType::UInt8,
);
let ov = vec![OverlayDef {
shape: OverlayShape::Text {
x: 0,
y: 0,
size_x: 80,
size_y: 20,
text: "W".to_string(),
font,
timestamp_format: String::new(),
},
draw_mode: DrawMode::Set,
color: [0, 255, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &ov);
if let NDDataBuffer::U8(v) = &out.data {
v.iter().filter(|&&p| p != 0).count()
} else {
0
}
};
assert_ne!(render(0), render(2), "font selection had no effect");
}
#[test]
fn test_text_size_x_clips_characters() {
let arr = NDArray::new(
vec![NDDimension::new(40), NDDimension::new(20)],
NDDataType::UInt8,
);
let ov = vec![OverlayDef {
shape: OverlayShape::Text {
x: 0,
y: 0,
size_x: 6,
size_y: 20,
text: "WW".to_string(),
font: 0,
timestamp_format: String::new(),
},
draw_mode: DrawMode::Set,
color: [0, 255, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &ov);
if let NDDataBuffer::U8(v) = &out.data {
let w = 40;
for row in 0..font_for(0).height {
for col in 6..40 {
assert_eq!(v[row * w + col], 0, "pixel ({col},{row}) past xmax");
}
}
}
}
#[test]
fn test_u16_overlay() {
let arr = NDArray::new(
vec![NDDimension::new(8), NDDimension::new(8)],
NDDataType::UInt16,
);
let overlays = vec![OverlayDef {
shape: OverlayShape::Rectangle {
x: 1,
y: 1,
width: 4,
height: 3,
},
draw_mode: DrawMode::Set,
color: [0, 200, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::U16(ref v) = out.data {
assert_eq!(v[1 * 8 + 1], 200);
assert_eq!(v[1 * 8 + 4], 200);
assert_eq!(v[2 * 8 + 2], 0);
}
}
#[test]
fn test_f32_overlay_ignores_xor() {
let arr = NDArray::new(
vec![NDDimension::new(8), NDDimension::new(8)],
NDDataType::Float32,
);
let overlays = vec![OverlayDef {
shape: OverlayShape::Cross {
center_x: 4,
center_y: 4,
size: 2,
},
draw_mode: DrawMode::XOR, color: [0, 100, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::F32(ref v) = out.data {
assert_eq!(v[4 * 8 + 4], 100.0);
}
}
#[test]
fn test_cross_thickness_half_width() {
let arr = NDArray::new(
vec![NDDimension::new(20), NDDimension::new(20)],
NDDataType::UInt8,
);
let overlays = vec![OverlayDef {
shape: OverlayShape::Cross {
center_x: 10,
center_y: 10,
size: 8,
},
draw_mode: DrawMode::Set,
color: [0, 255, 0],
width_x: 1,
width_y: 4,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::U8(ref v) = out.data {
let w = 20;
for y in 8..=12 {
assert_eq!(v[y * w + 7], 255, "row {y} should be in the band");
}
assert_eq!(v[7 * w + 7], 0, "row 7 is outside the band");
assert_eq!(v[13 * w + 7], 0, "row 13 is outside the band");
}
}
#[test]
fn test_xor_ellipse_no_double_toggle() {
let arr = NDArray::new(
vec![NDDimension::new(40), NDDimension::new(40)],
NDDataType::UInt8,
);
let overlays = vec![OverlayDef {
shape: OverlayShape::Ellipse {
center_x: 20,
center_y: 20,
rx: 12,
ry: 8,
},
draw_mode: DrawMode::XOR,
color: [0, 0xFF, 0],
width_x: 3,
width_y: 3,
}];
let out = draw_overlays(&arr, &overlays);
if let NDDataBuffer::U8(ref v) = out.data {
let mut drawn = 0;
for &px in v.iter() {
assert!(px == 0 || px == 0xFF, "double-toggled pixel: {px}");
if px == 0xFF {
drawn += 1;
}
}
assert!(drawn > 0, "ellipse drew no pixels");
}
}
#[test]
fn test_text_timestamp_format_appends() {
let mut arr = NDArray::new(
vec![NDDimension::new(120), NDDimension::new(12)],
NDDataType::UInt8,
);
arr.timestamp = ad_core_rs::timestamp::EpicsTimestamp {
sec: 0, nsec: 0,
};
let count_set = |arr: &NDArray, fmt: &str| -> usize {
let ov = vec![OverlayDef {
shape: OverlayShape::Text {
x: 0,
y: 0,
size_x: 120,
size_y: 12,
text: "T".to_string(),
font: 0,
timestamp_format: fmt.to_string(),
},
draw_mode: DrawMode::Set,
color: [0, 255, 0],
width_x: 1,
width_y: 1,
}];
let out = draw_overlays(arr, &ov);
if let NDDataBuffer::U8(v) = &out.data {
v.iter().filter(|&&p| p != 0).count()
} else {
0
}
};
let bare = count_set(&arr, "");
let with_ts = count_set(&arr, "%Y-%m-%d");
assert!(with_ts > bare, "timestamp text should add pixels");
}
use ad_core_rs::plugin::runtime::{ParamChangeValue, ParamUpdate, PluginParamSnapshot};
fn drive(p: &mut OverlayProcessor, reason: usize, value: i32) -> Vec<ParamUpdate> {
let snap = PluginParamSnapshot {
enable_callbacks: true,
reason,
addr: 0,
value: ParamChangeValue::Int32(value),
};
p.on_param_change(reason, &snap).param_updates
}
fn find_int_update(updates: &[ParamUpdate], reason: usize) -> Option<i32> {
updates.iter().find_map(|u| match u {
ParamUpdate::Int32 {
reason: r, value, ..
} if *r == reason => Some(*value),
_ => None,
})
}
fn setup_processor() -> (OverlayProcessor, OverlayParamIndices) {
let mut p = OverlayProcessor::new(vec![]);
let mut base =
asyn_rs::port::PortDriverBase::new("OV_TEST", 8, asyn_rs::port::PortFlags::default());
p.register_params(&mut base).unwrap();
let params = OverlayParamIndices {
position_x: base.find_param("OVERLAY_POSITION_X"),
position_y: base.find_param("OVERLAY_POSITION_Y"),
center_x: base.find_param("OVERLAY_CENTER_X"),
center_y: base.find_param("OVERLAY_CENTER_Y"),
size_x: base.find_param("OVERLAY_SIZE_X"),
size_y: base.find_param("OVERLAY_SIZE_Y"),
..Default::default()
};
(p, params)
}
#[test]
fn test_freeze_position_then_resize_moves_center() {
let (mut p, idx) = setup_processor();
drive(&mut p, idx.size_x.unwrap(), 20);
drive(&mut p, idx.position_x.unwrap(), 100);
assert_eq!(p.slots[0].position_x, 100);
assert_eq!(p.slots[0].center_x, 110);
let updates = drive(&mut p, idx.size_x.unwrap(), 40);
assert_eq!(p.slots[0].position_x, 100);
assert_eq!(p.slots[0].center_x, 120);
assert_eq!(find_int_update(&updates, idx.center_x.unwrap()), Some(120));
}
#[test]
fn test_freeze_center_then_resize_moves_position() {
let (mut p, idx) = setup_processor();
drive(&mut p, idx.size_x.unwrap(), 20);
drive(&mut p, idx.center_x.unwrap(), 200);
assert_eq!(p.slots[0].center_x, 200);
assert_eq!(p.slots[0].position_x, 190);
let updates = drive(&mut p, idx.size_x.unwrap(), 60);
assert_eq!(p.slots[0].center_x, 200);
assert_eq!(p.slots[0].position_x, 170);
assert_eq!(
find_int_update(&updates, idx.position_x.unwrap()),
Some(170)
);
}
#[test]
fn test_freeze_y_axis_independent() {
let (mut p, idx) = setup_processor();
drive(&mut p, idx.size_y.unwrap(), 10);
drive(&mut p, idx.center_y.unwrap(), 50); drive(&mut p, idx.size_x.unwrap(), 10);
drive(&mut p, idx.position_x.unwrap(), 5); assert!(p.slots[0].freeze_position_x);
assert!(!p.slots[0].freeze_position_y);
}
#[test]
fn test_format_epics_time_known_date() {
let ts = ad_core_rs::timestamp::EpicsTimestamp {
sec: 0,
nsec: 123_456_000,
};
assert_eq!(
format_epics_time(ts, "%Y-%m-%d %H:%M:%S.%f"),
"1990-01-01 00:00:00.123456"
);
assert_eq!(format_epics_time(ts, "100%%"), "100%");
}
}