use indicatrix_cut_core::rough_plan::locate::{DiagonalLine, EdgeLine, ViewObservations};
pub const DIAGONAL_EDGES: u8 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CalibMode {
Edge,
Mark,
Diagonal(u8),
}
impl CalibMode {
#[must_use]
pub fn from_index(index: i32) -> Self {
match index {
1 => Self::Mark,
2..=5 => Self::Diagonal(u8::try_from(index - 2).unwrap_or(0)),
_ => Self::Edge,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CalibViewMarks {
pub lines: Vec<EdgeLine>,
pub pending: Option<[f64; 2]>,
pub mark: Option<[f64; 2]>,
pub diagonal: [Vec<[f64; 2]>; DIAGONAL_EDGES as usize],
}
#[derive(Debug, Clone, PartialEq)]
pub struct CalibMarks {
pub views: Vec<CalibViewMarks>,
}
impl CalibMarks {
#[must_use]
pub fn new(views: usize) -> Self {
Self {
views: vec![CalibViewMarks::default(); views],
}
}
pub fn resize(&mut self, views: usize) {
self.views.resize(views, CalibViewMarks::default());
}
pub fn click(&mut self, view: usize, mode: CalibMode, pixel: [f64; 2]) {
let Some(marks) = self.views.get_mut(view) else {
return;
};
match mode {
CalibMode::Edge => {
if let Some(first) = marks.pending.take() {
marks.lines.push(EdgeLine { a: first, b: pixel });
return;
}
marks.pending = Some(pixel);
}
CalibMode::Mark => marks.mark = Some(pixel),
CalibMode::Diagonal(edge) => {
if let Some(clicks) = marks.diagonal.get_mut(usize::from(edge)) {
clicks.push(pixel);
}
}
}
}
pub fn undo(&mut self, view: usize, mode: CalibMode) {
let Some(marks) = self.views.get_mut(view) else {
return;
};
match mode {
CalibMode::Edge => {
if marks.pending.take().is_none() {
marks.lines.pop();
}
}
CalibMode::Mark => marks.mark = None,
CalibMode::Diagonal(edge) => {
if let Some(clicks) = marks.diagonal.get_mut(usize::from(edge)) {
clicks.pop();
}
}
}
}
pub fn clear(&mut self, view: usize) {
if let Some(marks) = self.views.get_mut(view) {
*marks = CalibViewMarks::default();
}
}
#[must_use]
pub fn observations(&self) -> Vec<ViewObservations> {
self.views
.iter()
.enumerate()
.filter(|(_, marks)| !marks.lines.is_empty() || marks.mark.is_some())
.map(|(view, marks)| ViewObservations {
view,
lines: marks.lines.clone(),
mark: marks.mark,
})
.collect()
}
#[must_use]
pub fn diagonal_lines(&self) -> Vec<DiagonalLine> {
let mut lines = Vec::new();
for (view, marks) in self.views.iter().enumerate() {
for (edge, clicks) in marks.diagonal.iter().enumerate() {
if clicks.len() >= 2 {
lines.push(DiagonalLine {
edge: u8::try_from(edge).unwrap_or(0),
view,
pixels: clicks.clone(),
});
}
}
}
lines
}
#[must_use]
pub fn summary(&self, view: usize) -> String {
let Some(marks) = self.views.get(view) else {
return String::new();
};
let diagonal: usize = marks.diagonal.iter().map(Vec::len).sum();
let mut parts = vec![format!("edges {}", marks.lines.len())];
if marks.pending.is_some() {
parts.push("one end waiting".to_owned());
}
if marks.mark.is_some() {
parts.push("dot".to_owned());
}
if diagonal > 0 {
parts.push(format!("diagonal clicks {diagonal}"));
}
parts.join(", ")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_pills_map_to_the_modes() {
assert_eq!(CalibMode::from_index(0), CalibMode::Edge);
assert_eq!(CalibMode::from_index(1), CalibMode::Mark);
assert_eq!(CalibMode::from_index(2), CalibMode::Diagonal(0));
assert_eq!(CalibMode::from_index(5), CalibMode::Diagonal(3));
assert_eq!(CalibMode::from_index(99), CalibMode::Edge);
}
#[test]
fn two_clicks_make_an_edge_and_an_undo_takes_back_the_waiting_end_first() {
let mut marks = CalibMarks::new(2);
marks.click(0, CalibMode::Edge, [1.0, 2.0]);
assert!(
marks.observations().is_empty(),
"one end is not an edge yet"
);
marks.click(0, CalibMode::Edge, [3.0, 4.0]);
marks.click(0, CalibMode::Edge, [5.0, 6.0]);
let observed = marks.observations();
assert_eq!(observed.len(), 1);
assert_eq!(
observed[0].lines,
vec![EdgeLine {
a: [1.0, 2.0],
b: [3.0, 4.0]
}]
);
marks.undo(0, CalibMode::Edge);
assert_eq!(marks.views[0].lines.len(), 1, "the waiting end went first");
marks.undo(0, CalibMode::Edge);
assert_eq!(marks.views[0].lines.len(), 0);
}
#[test]
fn the_corner_dot_replaces_itself_and_counts_as_an_observation() {
let mut marks = CalibMarks::new(2);
marks.click(1, CalibMode::Mark, [10.0, 10.0]);
marks.click(1, CalibMode::Mark, [11.0, 12.0]);
let observed = marks.observations();
assert_eq!(observed[0].view, 1);
assert_eq!(observed[0].mark, Some([11.0, 12.0]));
marks.undo(1, CalibMode::Mark);
assert_eq!(marks.observations().len(), 0);
}
#[test]
fn diagonal_clicks_make_a_line_from_two_clicks_on() {
let mut marks = CalibMarks::new(3);
marks.click(2, CalibMode::Diagonal(1), [1.0, 1.0]);
assert_eq!(marks.diagonal_lines().len(), 0);
marks.click(2, CalibMode::Diagonal(1), [2.0, 2.0]);
marks.click(2, CalibMode::Diagonal(3), [9.0, 9.0]);
marks.click(2, CalibMode::Diagonal(7), [9.0, 9.0]);
let lines = marks.diagonal_lines();
assert_eq!(lines.len(), 1);
assert_eq!((lines[0].edge, lines[0].view), (1, 2));
assert_eq!(lines[0].pixels.len(), 2);
}
#[test]
fn clearing_a_view_leaves_the_others_and_the_summary_says_what_is_marked() {
let mut marks = CalibMarks::new(2);
marks.click(0, CalibMode::Edge, [0.0, 0.0]);
marks.click(0, CalibMode::Edge, [1.0, 1.0]);
marks.click(0, CalibMode::Mark, [2.0, 2.0]);
marks.click(0, CalibMode::Diagonal(0), [3.0, 3.0]);
marks.click(1, CalibMode::Mark, [4.0, 4.0]);
assert_eq!(marks.summary(0), "edges 1, dot, diagonal clicks 1");
marks.clear(0);
assert_eq!(marks.summary(0), "edges 0");
assert!(marks.views[1].mark.is_some());
assert_eq!(marks.summary(9), "");
}
}