sara-tasks 2.0.0

Sara — folder-aware task manager
use std::collections::HashMap;

use crate::infrastructure::memory::graph::MemoryGraph;

use super::types::{Bond, Dir, Star};

pub(super) fn bond_exists(bonds: &[Bond], a: usize, b: usize) -> bool {
    bonds
        .iter()
        .any(|bd| (bd.a == a && bd.b == b) || (bd.a == b && bd.b == a))
}

pub(super) fn nearest_in_direction(stars: &[Star], from: usize, dir: Dir) -> usize {
    let (ox, oy) = (stars[from].x, stars[from].y);
    let mut best = from;
    let mut best_score = f64::MAX;
    for (i, s) in stars.iter().enumerate() {
        if i == from {
            continue;
        }
        let (dx, dy) = (s.x - ox, s.y - oy);
        let (along, perp) = match dir {
            Dir::Right => (dx, dy.abs()),
            Dir::Left => (-dx, dy.abs()),
            Dir::Up => (dy, dx.abs()),
            Dir::Down => (-dy, dx.abs()),
        };
        if along <= 0.0 {
            continue;
        }
        let score = along + 2.0 * perp;
        if score < best_score {
            best_score = score;
            best = i;
        }
    }
    best
}

pub(super) const MIN_EDGE: f64 = 0.15;

pub(super) const CONTRAST: f64 = 3.0;

pub(super) const AFFINITY_SCALE: f64 = 0.12;

pub(super) const REPULSION: f64 = 190.0;

pub(super) const GRAVITY: f64 = 0.14;

pub(super) fn graph_edges(
    graph: &MemoryGraph,
    index: &HashMap<String, usize>,
) -> Vec<(usize, usize, f64)> {
    graph
        .edges()
        .into_iter()
        .filter(|(_, _, w)| *w >= MIN_EDGE)
        .filter_map(|(a, b, w)| {
            let ia = *index.get(&a.to_string())?;
            let ib = *index.get(&b.to_string())?;
            Some((ia, ib, w))
        })
        .collect()
}

pub(super) fn spring_stiffness(weight: f64) -> f64 {
    weight.powf(CONTRAST) * AFFINITY_SCALE
}

pub(super) fn force_layout(
    stars: &mut [Star],
    affinity: &[(usize, usize, f64)],
    iterations: usize,
) {
    let n = stars.len();
    if n < 2 {
        return;
    }
    for (i, s) in stars.iter_mut().enumerate() {
        let r = 70.0 * ((i + 1) as f64 / n as f64).sqrt();
        let a = i as f64 * 2.399_963;
        s.x = a.cos() * r * 1.3;
        s.y = a.sin() * r;
    }

    for _ in 0..iterations {
        let mut fx = vec![0.0f64; n];
        let mut fy = vec![0.0f64; n];
        for i in 0..n {
            for j in (i + 1)..n {
                let dx = stars[i].x - stars[j].x;
                let dy = stars[i].y - stars[j].y;
                let d2 = (dx * dx + dy * dy).max(4.0);
                let rep = REPULSION / d2;
                let d = d2.sqrt();
                fx[i] += dx / d * rep;
                fy[i] += dy / d * rep;
                fx[j] -= dx / d * rep;
                fy[j] -= dy / d * rep;
            }
            fx[i] -= stars[i].x * GRAVITY;
            fy[i] -= stars[i].y * GRAVITY;
        }
        for &(i, j, k) in affinity {
            let dx = stars[j].x - stars[i].x;
            let dy = stars[j].y - stars[i].y;
            fx[i] += dx * k;
            fy[i] += dy * k;
            fx[j] -= dx * k;
            fy[j] -= dy * k;
        }
        for i in 0..n {
            stars[i].x = (stars[i].x + fx[i].clamp(-4.0, 4.0)).clamp(-95.0, 95.0);
            stars[i].y = (stars[i].y + fy[i].clamp(-4.0, 4.0)).clamp(-68.0, 68.0);
        }
    }
}