use egui::Pos2;
#[derive(Debug, Clone)]
pub struct SnapOptions {
pub snap_to_price: bool,
pub snap_to_time: bool,
pub snap_distance: f32,
pub magnet_mode: bool,
pub magnet_distance: f32,
}
impl Default for SnapOptions {
fn default() -> Self {
Self {
snap_to_price: true,
snap_to_time: true,
snap_distance: 10.0,
magnet_mode: false,
magnet_distance: 15.0,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SnapTargets {
pub prices: Vec<f32>,
pub times: Vec<f32>,
pub drawing_points: Vec<Pos2>,
}
impl SnapTargets {
pub fn new() -> Self {
Self::default()
}
pub fn with_price_time(prices: Vec<f32>, times: Vec<f32>) -> Self {
Self {
prices,
times,
drawing_points: Vec::new(),
}
}
pub fn with_drawing_points(mut self, points: Vec<Pos2>) -> Self {
self.drawing_points = points;
self
}
pub fn set_prices(&mut self, prices: Vec<f32>) {
self.prices = prices;
}
pub fn set_times(&mut self, times: Vec<f32>) {
self.times = times;
}
pub fn set_drawing_points(&mut self, points: Vec<Pos2>) {
self.drawing_points = points;
}
pub fn clear(&mut self) {
self.prices.clear();
self.times.clear();
self.drawing_points.clear();
}
}
#[derive(Debug, Clone)]
pub struct SnapService {
options: SnapOptions,
}
impl Default for SnapService {
fn default() -> Self {
Self::new()
}
}
impl SnapService {
pub fn new() -> Self {
Self {
options: SnapOptions::default(),
}
}
pub fn with_options(options: SnapOptions) -> Self {
Self { options }
}
pub fn options(&self) -> &SnapOptions {
&self.options
}
pub fn options_mut(&mut self) -> &mut SnapOptions {
&mut self.options
}
pub fn snap_point(&self, point: Pos2, targets: &SnapTargets) -> Pos2 {
self.snap_point_with_drawing_points(
point,
&targets.prices,
&targets.times,
targets.drawing_points.iter().copied(),
)
}
pub fn snap_point_with_drawing_points<I>(
&self,
point: Pos2,
prices: &[f32],
times: &[f32],
drawing_points: I,
) -> Pos2
where
I: IntoIterator<Item = Pos2>,
{
let mut result = point;
if self.options.snap_to_price
&& !prices.is_empty()
&& let Some(snapped_y) = self.find_closest(prices, point.y)
{
result.y = snapped_y;
}
if self.options.snap_to_time
&& !times.is_empty()
&& let Some(snapped_x) = self.find_closest(times, point.x)
{
result.x = snapped_x;
}
if self.options.magnet_mode
&& let Some(closest_point) = self.find_closest_point(drawing_points, point)
{
result = closest_point;
}
result
}
fn find_closest(&self, values: &[f32], target: f32) -> Option<f32> {
values
.iter()
.min_by(|a, b| {
(target - **a)
.abs()
.partial_cmp(&(target - **b).abs())
.unwrap()
})
.filter(|&&closest| (target - closest).abs() < self.options.snap_distance)
.copied()
}
fn find_closest_point<I>(&self, points: I, target: Pos2) -> Option<Pos2>
where
I: IntoIterator<Item = Pos2>,
{
let mut min_dist = self.options.magnet_distance;
let mut closest = None;
for p in points {
let dist = ((target.x - p.x).powi(2) + (target.y - p.y).powi(2)).sqrt();
if dist < min_dist {
min_dist = dist;
closest = Some(p);
}
}
closest
}
pub fn would_snap(&self, point: Pos2, targets: &SnapTargets) -> bool {
let snapped = self.snap_point(point, targets);
(snapped.x - point.x).abs() > 0.001 || (snapped.y - point.y).abs() > 0.001
}
pub fn get_snap_indicator(&self, point: Pos2, targets: &SnapTargets) -> Option<Pos2> {
let snapped = self.snap_point(point, targets);
if (snapped.x - point.x).abs() > 0.001 || (snapped.y - point.y).abs() > 0.001 {
Some(snapped)
} else {
None
}
}
pub fn set_snap_to_price(&mut self, enabled: bool) {
self.options.snap_to_price = enabled;
}
pub fn set_snap_to_time(&mut self, enabled: bool) {
self.options.snap_to_time = enabled;
}
pub fn set_magnet_mode(&mut self, enabled: bool) {
self.options.magnet_mode = enabled;
}
pub fn set_snap_distance(&mut self, distance: f32) {
self.options.snap_distance = distance;
}
pub fn set_magnet_distance(&mut self, distance: f32) {
self.options.magnet_distance = distance;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_snap_to_price() {
let service = SnapService::new();
let targets = SnapTargets::with_price_time(vec![100.0, 150.0, 200.0], vec![]);
let point = Pos2::new(50.0, 152.0);
let snapped = service.snap_point(point, &targets);
assert_eq!(snapped.y, 150.0);
assert_eq!(snapped.x, 50.0); }
#[test]
fn test_snap_to_time() {
let service = SnapService::new();
let targets = SnapTargets::with_price_time(vec![], vec![100.0, 200.0, 300.0]);
let point = Pos2::new(203.0, 50.0);
let snapped = service.snap_point(point, &targets);
assert_eq!(snapped.x, 200.0);
assert_eq!(snapped.y, 50.0); }
#[test]
fn test_no_snap_outside_distance() {
let service = SnapService::new(); let targets = SnapTargets::with_price_time(vec![100.0], vec![200.0]);
let point = Pos2::new(250.0, 150.0);
let snapped = service.snap_point(point, &targets);
assert_eq!(snapped, point); }
#[test]
fn test_magnet_mode() {
let mut service = SnapService::new();
service.set_magnet_mode(true);
let targets = SnapTargets::with_price_time(vec![], vec![])
.with_drawing_points(vec![Pos2::new(100.0, 100.0), Pos2::new(200.0, 200.0)]);
let point = Pos2::new(105.0, 103.0);
let snapped = service.snap_point(point, &targets);
assert_eq!(snapped, Pos2::new(100.0, 100.0));
}
#[test]
fn test_magnet_outside_distance() {
let mut service = SnapService::new();
service.set_magnet_mode(true);
service.set_magnet_distance(10.0);
let targets = SnapTargets::with_price_time(vec![], vec![])
.with_drawing_points(vec![Pos2::new(100.0, 100.0)]);
let point = Pos2::new(150.0, 150.0);
let snapped = service.snap_point(point, &targets);
assert_eq!(snapped, point);
}
#[test]
fn test_disabled_snap() {
let mut service = SnapService::new();
service.set_snap_to_price(false);
service.set_snap_to_time(false);
let targets = SnapTargets::with_price_time(vec![100.0], vec![100.0]);
let point = Pos2::new(101.0, 101.0);
let snapped = service.snap_point(point, &targets);
assert_eq!(snapped, point); }
#[test]
fn test_would_snap() {
let service = SnapService::new();
let targets = SnapTargets::with_price_time(vec![100.0], vec![]);
assert!(service.would_snap(Pos2::new(50.0, 102.0), &targets));
assert!(!service.would_snap(Pos2::new(50.0, 150.0), &targets));
}
#[test]
fn test_snap_indicator() {
let service = SnapService::new();
let targets = SnapTargets::with_price_time(vec![100.0], vec![]);
let indicator = service.get_snap_indicator(Pos2::new(50.0, 102.0), &targets);
assert!(indicator.is_some());
assert_eq!(indicator.unwrap().y, 100.0);
let indicator = service.get_snap_indicator(Pos2::new(50.0, 150.0), &targets);
assert!(indicator.is_none());
}
}