use std::collections::VecDeque;
use std::time::Duration;
use visual_cortex::{
patterns, DetectorError, DetectorOutput, EventKind, FakeSource, Frame, FrameView, OcrEngine,
PxRect, Rate, Region, Session, TextSpan, ThresholdDirection,
};
struct ScriptedOcr {
script: VecDeque<Vec<TextSpan>>,
last: Vec<TextSpan>,
}
impl ScriptedOcr {
fn new(script: impl IntoIterator<Item = Vec<TextSpan>>) -> Self {
Self {
script: script.into_iter().collect(),
last: Vec::new(),
}
}
}
impl OcrEngine for ScriptedOcr {
fn recognize(&mut self, _view: &FrameView<'_>) -> Result<Vec<TextSpan>, DetectorError> {
if let Some(spans) = self.script.pop_front() {
self.last = spans;
}
Ok(self.last.clone())
}
}
fn span(text: &str, y: u32) -> TextSpan {
TextSpan {
text: text.to_string(),
confidence: 0.95,
bbox: PxRect {
x: 2,
y,
w: 60,
h: 10,
},
}
}
async fn next_event(stream: &mut visual_cortex::EventStream) -> visual_cortex::Event {
tokio::time::timeout(Duration::from_secs(30), stream.next())
.await
.expect("timed out waiting for event")
.expect("stream closed unexpectedly")
}
#[tokio::test(start_paused = true)]
async fn item_observed_via_text_transitions() {
let script = vec![
vec![],
vec![],
vec![span("Ancestral Unique Sword", 20), span("Doombringer", 4)],
vec![span("Ancestral Unique Sword", 20), span("Doombringer", 4)],
vec![span("Ancestral Unique Sword", 20), span("Doombringer", 4)],
vec![],
];
let session = Session::builder()
.source(FakeSource::new([Frame::solid(64, 64, [20, 20, 20, 255])]))
.build()
.await
.unwrap();
let mut stream = session
.watch("tooltip")
.region(Region::Full)
.rate(Rate::hz(10.0))
.frame_diff(false) .ocr_text(ScriptedOcr::new(script))
.subscribe()
.unwrap();
let first = next_event(&mut stream).await;
assert!(
matches!(
&first.kind,
EventKind::Changed {
old: None,
new: DetectorOutput::None
}
),
"expected initial None observation, got {:?}",
first.kind
);
let observed = next_event(&mut stream).await;
let EventKind::Changed {
old: Some(DetectorOutput::None),
new: DetectorOutput::Text(text),
} = &observed.kind
else {
panic!("expected None->Text transition, got {:?}", observed.kind);
};
assert_eq!(text, "Doombringer\nAncestral Unique Sword");
let gone = next_event(&mut stream).await;
assert!(
matches!(
&gone.kind,
EventKind::Changed {
old: Some(DetectorOutput::Text(_)),
new: DetectorOutput::None
}
),
"expected Text->None transition, got {:?}",
gone.kind
);
}
#[tokio::test(start_paused = true)]
async fn hp_threshold_via_ocr_number() {
let script = vec![
vec![span("HP 500", 2)],
vec![span("HP 500", 2)],
vec![span("HP 250", 2)],
];
let session = Session::builder()
.source(FakeSource::new([Frame::solid(64, 16, [0, 0, 0, 255])]))
.build()
.await
.unwrap();
let mut stream = session
.watch("hp")
.region(Region::Full)
.rate(Rate::hz(10.0))
.frame_diff(false)
.ocr(ScriptedOcr::new(script), patterns::number())
.on_threshold(|hp| hp < 300.0)
.subscribe()
.unwrap();
let ev = next_event(&mut stream).await;
assert!(
matches!(
ev.kind,
EventKind::ThresholdCrossed { value, direction: ThresholdDirection::Entered }
if value == 250.0
),
"expected Entered at 250, got {:?}",
ev.kind
);
}