use graph_explorer_style::interner::IdKind;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HitTarget {
pub index: u32,
pub kind: IdKind,
pub pos: [f32; 2],
pub radius: f32,
pub shape: u32,
pub opacity: f32,
}
const MIN_HITTABLE_OPACITY: f32 = 0.05;
pub fn contains(t: &HitTarget, point: [f32; 2], tolerance: f32) -> bool {
let r = t.radius + tolerance;
let dx = point[0] - t.pos[0];
let dy = point[1] - t.pos[1];
match t.shape {
1 => dx.abs() <= r && dy.abs() <= r, 2 => dx.abs() + dy.abs() <= t.radius + tolerance * std::f32::consts::SQRT_2,
_ => (dx * dx + dy * dy).sqrt() <= r, }
}
pub fn pick(targets: &[HitTarget], point: [f32; 2], tolerance: f32) -> Option<usize> {
targets
.iter()
.enumerate()
.rev()
.find(|(_, t)| {
t.kind != IdKind::Loading
&& t.opacity >= MIN_HITTABLE_OPACITY
&& contains(t, point, tolerance)
})
.map(|(i, _)| i)
}
#[cfg(test)]
mod tests {
use super::*;
fn t(index: u32, pos: [f32; 2], radius: f32, shape: u32) -> HitTarget {
HitTarget { index, kind: IdKind::Node, pos, radius, shape, opacity: 1.0 }
}
#[test]
fn circle_containment() {
let c = t(0, [0.0, 0.0], 10.0, 0);
assert!(contains(&c, [0.0, 0.0], 0.0));
assert!(contains(&c, [9.9, 0.0], 0.0));
assert!(!contains(&c, [10.1, 0.0], 0.0));
assert!(!contains(&c, [8.0, 8.0], 0.0), "corner is outside a circle");
}
#[test]
fn square_containment() {
let s = t(0, [0.0, 0.0], 10.0, 1);
assert!(contains(&s, [8.0, 8.0], 0.0), "corner IS inside a square");
assert!(contains(&s, [-9.9, 9.9], 0.0));
assert!(!contains(&s, [10.1, 0.0], 0.0));
}
#[test]
fn diamond_containment() {
let d = t(0, [0.0, 0.0], 10.0, 2);
assert!(contains(&d, [9.9, 0.0], 0.0));
assert!(contains(&d, [4.0, 4.0], 0.0));
assert!(!contains(&d, [8.0, 8.0], 0.0), "|dx|+|dy| > r is outside a diamond");
}
#[test]
fn tolerance_widens_the_target() {
let c = t(0, [0.0, 0.0], 10.0, 0);
assert!(!contains(&c, [12.0, 0.0], 0.0));
assert!(contains(&c, [12.0, 0.0], 4.0), "tolerance makes it hittable");
}
#[test]
fn diamond_tolerance_is_perpendicular_distance() {
let d = t(0, [0.0, 0.0], 10.0, 2);
let n = std::f32::consts::FRAC_1_SQRT_2; assert!(contains(&d, [5.0 + 3.9 * n, 5.0 + 3.9 * n], 4.0));
assert!(!contains(&d, [5.0 + 4.1 * n, 5.0 + 4.1 * n], 4.0), "beyond the slop is outside");
}
#[test]
fn offset_position_is_respected() {
let c = t(0, [100.0, -50.0], 5.0, 0);
assert!(contains(&c, [102.0, -50.0], 0.0));
assert!(!contains(&c, [0.0, 0.0], 0.0));
}
#[test]
fn topmost_wins_on_overlap() {
let targets = vec![t(7, [0.0, 0.0], 10.0, 0), t(8, [0.0, 0.0], 10.0, 0)];
assert_eq!(pick(&targets, [0.0, 0.0], 0.0), Some(1));
}
#[test]
fn misses_return_none() {
let targets = vec![t(0, [0.0, 0.0], 5.0, 0)];
assert_eq!(pick(&targets, [100.0, 100.0], 0.0), None);
}
#[test]
fn loading_placeholders_are_not_hittable() {
let mut loading = t(0, [0.0, 0.0], 10.0, 0);
loading.kind = IdKind::Loading;
assert_eq!(pick(&[loading], [0.0, 0.0], 0.0), None);
}
#[test]
fn other_synthetic_targets_are_hittable() {
let mut synth = t(3, [0.0, 0.0], 10.0, 0);
synth.kind = IdKind::OtherSynthetic;
assert_eq!(pick(&[synth], [0.0, 0.0], 0.0), Some(0));
}
#[test]
fn faded_targets_are_not_hittable() {
let mut ghost = t(0, [0.0, 0.0], 10.0, 0);
ghost.opacity = 0.02;
assert_eq!(pick(&[ghost], [0.0, 0.0], 0.0), None);
}
}