use crate::edge::{Direction, EdgeCandidate, EdgeQuad};
use crate::geometry::Px;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SnapPoint {
pub pixel: Px,
pub cursor: Px,
pub edge: Option<EdgeCandidate>,
}
impl SnapPoint {
pub fn loose(cursor: Px) -> Self {
Self {
pixel: cursor,
cursor,
edge: None,
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Measurement {
pub start: SnapPoint,
pub end: SnapPoint,
}
impl Measurement {
pub fn new(start: SnapPoint, end: SnapPoint) -> Self {
Self { start, end }
}
pub fn dx(&self) -> i32 {
self.end.pixel.x - self.start.pixel.x
}
pub fn dy(&self) -> i32 {
self.end.pixel.y - self.start.pixel.y
}
pub fn width(&self) -> u32 {
self.dx().unsigned_abs()
}
pub fn height(&self) -> u32 {
self.dy().unsigned_abs()
}
pub fn euclid(&self) -> f64 {
let x = self.dx() as f64;
let y = self.dy() as f64;
(x * x + y * y).sqrt()
}
pub fn is_horizontal(&self) -> bool {
self.dy() == 0
}
pub fn is_vertical(&self) -> bool {
self.dx() == 0
}
}
pub fn best_snap(cursor: Px, edges: &EdgeQuad) -> SnapPoint {
let nearest = edges
.iter()
.filter_map(|c| c.as_ref())
.min_by_key(|c| c.distance);
match nearest {
Some(e) => SnapPoint {
pixel: e.position,
cursor,
edge: Some(*e),
},
None => SnapPoint::loose(cursor),
}
}
pub fn axis_biased_snap(cursor: Px, edges: &EdgeQuad, axis: Axis) -> SnapPoint {
let want_horizontal = matches!(axis, Axis::Horizontal);
let mut candidates: Vec<&EdgeCandidate> = edges.iter().filter_map(|c| c.as_ref()).collect();
candidates.sort_by_key(|c| {
let off_axis_penalty = match (c.direction, want_horizontal) {
(Direction::Left | Direction::Right, true) => 0,
(Direction::Up | Direction::Down, false) => 0,
_ => u32::MAX / 2, };
c.distance.saturating_add(off_axis_penalty)
});
match candidates.first().copied().copied() {
Some(e) => SnapPoint {
pixel: e.position,
cursor,
edge: Some(e),
},
None => SnapPoint::loose(cursor),
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Axis {
Horizontal,
Vertical,
}
#[derive(Clone, Debug, Default)]
pub enum Mode {
#[default]
Idle,
Hover { cursor: Px },
Drawing { start: SnapPoint, cursor: Px },
Held {
measurement: Measurement,
cursor: Px,
},
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Rgba;
use crate::edge::Direction;
fn fake_edge(dir: Direction, distance: u32, pos: Px) -> EdgeCandidate {
EdgeCandidate {
direction: dir,
distance,
position: pos,
anchor_color: Rgba::WHITE,
edge_color: Rgba::BLACK,
strength: 100,
edge_phys: 0.0,
}
}
#[test]
fn measurement_dimensions() {
let m = Measurement::new(
SnapPoint::loose(Px::new(10, 20)),
SnapPoint::loose(Px::new(40, 80)),
);
assert_eq!(m.dx(), 30);
assert_eq!(m.dy(), 60);
assert_eq!(m.width(), 30);
assert_eq!(m.height(), 60);
assert!((m.euclid() - 67.0820).abs() < 0.01);
}
#[test]
fn measurement_is_horizontal_when_dy_zero() {
let m = Measurement::new(
SnapPoint::loose(Px::new(10, 20)),
SnapPoint::loose(Px::new(40, 20)),
);
assert!(m.is_horizontal());
assert!(!m.is_vertical());
}
#[test]
fn best_snap_picks_nearest_edge() {
let cursor = Px::new(10, 10);
let edges: EdgeQuad = [
Some(fake_edge(Direction::Left, 5, Px::new(5, 10))),
Some(fake_edge(Direction::Right, 3, Px::new(13, 10))),
None,
Some(fake_edge(Direction::Down, 7, Px::new(10, 17))),
];
let snap = best_snap(cursor, &edges);
assert_eq!(snap.pixel, Px::new(13, 10));
assert_eq!(snap.cursor, cursor);
assert!(matches!(
snap.edge.map(|e| e.direction),
Some(Direction::Right)
));
}
#[test]
fn best_snap_returns_loose_when_no_edges() {
let cursor = Px::new(10, 10);
let edges: EdgeQuad = [None; 4];
let snap = best_snap(cursor, &edges);
assert_eq!(snap.pixel, cursor);
assert!(snap.edge.is_none());
}
#[test]
fn axis_biased_snap_prefers_axis_direction() {
let cursor = Px::new(10, 10);
let edges: EdgeQuad = [
Some(fake_edge(Direction::Left, 9, Px::new(1, 10))),
Some(fake_edge(Direction::Right, 5, Px::new(15, 10))),
Some(fake_edge(Direction::Up, 2, Px::new(10, 8))),
Some(fake_edge(Direction::Down, 99, Px::new(10, 109))),
];
let snap = axis_biased_snap(cursor, &edges, Axis::Horizontal);
assert!(matches!(
snap.edge.map(|e| e.direction),
Some(Direction::Right)
));
}
}