#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use num_traits::Float;
fn apply_to_pair(
x_dist: f64,
y_dist: f64,
factor: f64,
force1: &mut [f64; 2],
force2: &mut [f64; 2],
) {
force1[0] += x_dist * factor;
force1[1] += y_dist * factor;
force2[0] -= x_dist * factor;
force2[1] -= y_dist * factor;
}
pub(super) fn apply_lin_repulsion(
pos1: [f64; 2],
pos2: [f64; 2],
mass1: f64,
mass2: f64,
coefficient: f64,
force1: &mut [f64; 2],
force2: &mut [f64; 2],
) {
let x_dist = pos1[0] - pos2[0];
let y_dist = pos1[1] - pos2[1];
let squared_distance = x_dist * x_dist + y_dist * y_dist;
if squared_distance > 0.0 {
let factor = coefficient * mass1 * mass2 / squared_distance;
apply_to_pair(x_dist, y_dist, factor, force1, force2);
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_attraction(
lin_log: bool,
dissuade_hubs: bool,
pos1: [f64; 2],
pos2: [f64; 2],
e: f64,
coefficient: f64,
source_mass: f64,
force1: &mut [f64; 2],
force2: &mut [f64; 2],
) {
let x_dist = pos1[0] - pos2[0];
let y_dist = pos1[1] - pos2[1];
let mut factor = if lin_log {
let distance = (x_dist * x_dist + y_dist * y_dist).sqrt();
if distance <= 0.0 {
return;
}
-coefficient * e * distance.ln_1p() / distance
} else {
-coefficient * e
};
if dissuade_hubs {
factor /= source_mass;
}
apply_to_pair(x_dist, y_dist, factor, force1, force2);
}
fn anti_collision_repulsion_factor(
x_dist: f64,
y_dist: f64,
mass1: f64,
mass2: f64,
size1: f64,
size2: f64,
coefficient: f64,
) -> Option<f64> {
let border_distance = (x_dist * x_dist + y_dist * y_dist).sqrt() - size1 - size2;
if border_distance > 0.0 {
Some(coefficient * mass1 * mass2 / border_distance / border_distance)
} else if border_distance < 0.0 {
Some(100.0 * coefficient * mass1 * mass2)
} else {
None
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_lin_repulsion_anti_collision(
pos1: [f64; 2],
pos2: [f64; 2],
mass1: f64,
mass2: f64,
size1: f64,
size2: f64,
coefficient: f64,
force1: &mut [f64; 2],
force2: &mut [f64; 2],
) {
let x_dist = pos1[0] - pos2[0];
let y_dist = pos1[1] - pos2[1];
if let Some(factor) =
anti_collision_repulsion_factor(x_dist, y_dist, mass1, mass2, size1, size2, coefficient)
{
apply_to_pair(x_dist, y_dist, factor, force1, force2);
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_lin_repulsion_anti_collision_one_sided(
pos: [f64; 2],
other_pos: [f64; 2],
node_mass: f64,
other_mass: f64,
node_size: f64,
other_size: f64,
coefficient: f64,
force: &mut [f64; 2],
) {
let x_dist = pos[0] - other_pos[0];
let y_dist = pos[1] - other_pos[1];
if let Some(factor) = anti_collision_repulsion_factor(
x_dist,
y_dist,
node_mass,
other_mass,
node_size,
other_size,
coefficient,
) {
force[0] += x_dist * factor;
force[1] += y_dist * factor;
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn apply_attraction_anti_collision(
lin_log: bool,
dissuade_hubs: bool,
pos1: [f64; 2],
pos2: [f64; 2],
e: f64,
coefficient: f64,
source_mass: f64,
size1: f64,
size2: f64,
force1: &mut [f64; 2],
force2: &mut [f64; 2],
) {
let x_dist = pos1[0] - pos2[0];
let y_dist = pos1[1] - pos2[1];
let border_distance = (x_dist * x_dist + y_dist * y_dist).sqrt() - size1 - size2;
if border_distance <= 0.0 {
return;
}
let mut factor = if lin_log {
-coefficient * e * border_distance.ln_1p() / border_distance
} else {
-coefficient * e
};
if dissuade_hubs {
factor /= source_mass;
}
apply_to_pair(x_dist, y_dist, factor, force1, force2);
}
pub(super) fn apply_region_repulsion(
pos: [f64; 2],
region_center: [f64; 2],
node_mass: f64,
region_mass: f64,
coefficient: f64,
force: &mut [f64; 2],
) {
let x_dist = pos[0] - region_center[0];
let y_dist = pos[1] - region_center[1];
let squared_distance = x_dist * x_dist + y_dist * y_dist;
if squared_distance > 0.0 {
let factor = coefficient * node_mass * region_mass / squared_distance;
force[0] += x_dist * factor;
force[1] += y_dist * factor;
}
}
pub(super) fn apply_gravity(pos: [f64; 2], mass: f64, kg: f64, strong: bool, force: &mut [f64; 2]) {
let distance = (pos[0] * pos[0] + pos[1] * pos[1]).sqrt();
if distance > 0.0 {
let factor = if strong { kg * mass } else { kg * mass / distance };
force[0] -= pos[0] * factor;
force[1] -= pos[1] * factor;
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::{
apply_attraction, apply_attraction_anti_collision, apply_gravity, apply_lin_repulsion,
apply_lin_repulsion_anti_collision, apply_lin_repulsion_anti_collision_one_sided,
};
#[test]
fn log_attraction_closed_form() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction(true, false, [0.0, 0.0], [3.0, 4.0], 2.0, 1.0, 1.0, &mut f1, &mut f2);
let ln6 = 6.0_f64.ln();
assert!((f1[0] - 6.0 * ln6 / 5.0).abs() < 1e-12);
assert!((f1[1] - 8.0 * ln6 / 5.0).abs() < 1e-12);
assert!((f2[0] + 6.0 * ln6 / 5.0).abs() < 1e-12);
assert!((f2[1] + 8.0 * ln6 / 5.0).abs() < 1e-12);
}
#[test]
fn log_attraction_coincident_is_noop() {
let mut f1 = [1.0, 2.0];
let mut f2 = [3.0, 4.0];
apply_attraction(true, false, [7.0, -7.0], [7.0, -7.0], 2.0, 1.0, 1.0, &mut f1, &mut f2);
assert_eq!(f1, [1.0, 2.0]);
assert_eq!(f2, [3.0, 4.0]);
}
#[test]
fn log_attraction_antisymmetric() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction(true, false, [1.0, -2.0], [-0.5, 3.0], 4.0, 2.0, 1.0, &mut f1, &mut f2);
assert_eq!(f1[0], -f2[0]);
assert_eq!(f1[1], -f2[1]);
}
#[test]
fn lin_attraction_dissuade_closed_form() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction(false, true, [1.0, 1.0], [4.0, 5.0], 3.0, 2.0, 4.0, &mut f1, &mut f2);
assert!((f1[0] - 4.5).abs() < 1e-12);
assert!((f1[1] - 6.0).abs() < 1e-12);
assert!((f2[0] + 4.5).abs() < 1e-12);
assert!((f2[1] + 6.0).abs() < 1e-12);
}
#[test]
fn lin_attraction_dissuade_mass_scaling() {
let mut heavy = [0.0; 2];
let mut light = [0.0; 2];
let mut sink = [0.0; 2];
apply_attraction(false, true, [0.0, 0.0], [3.0, 4.0], 1.0, 1.0, 4.0, &mut heavy, &mut sink);
apply_attraction(false, true, [0.0, 0.0], [3.0, 4.0], 1.0, 1.0, 2.0, &mut light, &mut sink);
assert!((light[0] - 2.0 * heavy[0]).abs() < 1e-12);
assert!((light[1] - 2.0 * heavy[1]).abs() < 1e-12);
}
#[test]
fn log_attraction_dissuade_closed_form() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction(true, true, [0.0, 0.0], [3.0, 4.0], 2.0, 1.0, 4.0, &mut f1, &mut f2);
let ln6 = 6.0_f64.ln();
assert!((f1[0] - 6.0 * ln6 / 20.0).abs() < 1e-12);
assert!((f1[1] - 8.0 * ln6 / 20.0).abs() < 1e-12);
}
#[test]
fn log_attraction_dissuade_coincident_is_noop() {
let mut f1 = [1.0, 2.0];
let mut f2 = [3.0, 4.0];
apply_attraction(true, true, [7.0, -7.0], [7.0, -7.0], 2.0, 1.0, 4.0, &mut f1, &mut f2);
assert_eq!(f1, [1.0, 2.0]);
assert_eq!(f2, [3.0, 4.0]);
}
#[test]
fn anti_collision_repulsion_separated() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_lin_repulsion_anti_collision(
[0.0, 0.0],
[3.0, 4.0],
2.0,
3.0,
1.0,
1.0,
2.0,
&mut f1,
&mut f2,
);
let factor = 2.0 * 2.0 * 3.0 / 9.0;
assert!((f1[0] - (-3.0 * factor)).abs() < 1e-12);
assert!((f1[1] - (-4.0 * factor)).abs() < 1e-12);
assert!((f2[0] - 3.0 * factor).abs() < 1e-12);
assert!((f2[1] - 4.0 * factor).abs() < 1e-12);
}
#[test]
fn anti_collision_repulsion_overlap_kick() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_lin_repulsion_anti_collision(
[0.0, 0.0],
[3.0, 4.0],
2.0,
3.0,
3.0,
3.0,
2.0,
&mut f1,
&mut f2,
);
assert!((f1[0] - (-3.0 * 1200.0)).abs() < 1e-9);
assert!((f1[1] - (-4.0 * 1200.0)).abs() < 1e-9);
}
#[test]
fn anti_collision_repulsion_touching_is_noop() {
let mut f1 = [1.0, 2.0];
let mut f2 = [3.0, 4.0];
apply_lin_repulsion_anti_collision(
[0.0, 0.0],
[3.0, 4.0],
2.0,
3.0,
2.5,
2.5,
2.0,
&mut f1,
&mut f2,
);
assert_eq!(f1, [1.0, 2.0]);
assert_eq!(f2, [3.0, 4.0]);
}
#[test]
fn anti_collision_one_sided_matches_pairwise() {
let mut pair_f1 = [0.0; 2];
let mut pair_f2 = [0.0; 2];
apply_lin_repulsion_anti_collision(
[0.0, 0.0],
[3.0, 4.0],
2.0,
3.0,
1.0,
1.0,
2.0,
&mut pair_f1,
&mut pair_f2,
);
let mut single = [0.0; 2];
apply_lin_repulsion_anti_collision_one_sided(
[0.0, 0.0],
[3.0, 4.0],
2.0,
3.0,
1.0,
1.0,
2.0,
&mut single,
);
assert_eq!(single, pair_f1);
}
#[test]
fn anti_collision_one_sided_touching_is_noop() {
let mut force = [1.0, 2.0];
apply_lin_repulsion_anti_collision_one_sided(
[0.0, 0.0],
[3.0, 4.0],
2.0,
3.0,
2.5,
2.5,
2.0,
&mut force,
);
assert_eq!(force, [1.0, 2.0]);
}
#[test]
fn anti_collision_attraction_gated_on_overlap() {
for (size1, size2) in [(3.0, 3.0), (2.5, 2.5)] {
let mut f1 = [1.0, 1.0];
let mut f2 = [2.0, 2.0];
apply_attraction_anti_collision(
false,
false,
[0.0, 0.0],
[3.0, 4.0],
2.0,
1.0,
1.0,
size1,
size2,
&mut f1,
&mut f2,
);
assert_eq!(f1, [1.0, 1.0]);
assert_eq!(f2, [2.0, 2.0]);
}
}
#[test]
fn anti_collision_attraction_linear_closed_form() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction_anti_collision(
false,
false,
[0.0, 0.0],
[3.0, 4.0],
2.0,
1.0,
1.0,
1.0,
1.0,
&mut f1,
&mut f2,
);
assert!((f1[0] - 6.0).abs() < 1e-12);
assert!((f1[1] - 8.0).abs() < 1e-12);
}
#[test]
fn anti_collision_attraction_linlog_uses_border_distance() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction_anti_collision(
true,
false,
[0.0, 0.0],
[3.0, 4.0],
2.0,
1.0,
1.0,
1.0,
1.0,
&mut f1,
&mut f2,
);
let factor = 2.0 * 4.0_f64.ln() / 3.0;
assert!((f1[0] - 3.0 * factor).abs() < 1e-12);
assert!((f1[1] - 4.0 * factor).abs() < 1e-12);
}
#[test]
fn anti_collision_attraction_dissuade_divides_by_mass() {
let mut plain = [0.0; 2];
let mut dissuaded = [0.0; 2];
let mut sink = [0.0; 2];
apply_attraction_anti_collision(
false,
false,
[0.0, 0.0],
[3.0, 4.0],
2.0,
1.0,
1.0,
1.0,
1.0,
&mut plain,
&mut sink,
);
apply_attraction_anti_collision(
false,
true,
[0.0, 0.0],
[3.0, 4.0],
2.0,
1.0,
4.0,
1.0,
1.0,
&mut dissuaded,
&mut sink,
);
assert!((dissuaded[0] - plain[0] / 4.0).abs() < 1e-12);
assert!((dissuaded[1] - plain[1] / 4.0).abs() < 1e-12);
}
#[test]
fn lin_repulsion_closed_form() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_lin_repulsion([0.0, 0.0], [3.0, 4.0], 2.0, 3.0, 2.0, &mut f1, &mut f2);
assert!((f1[0] - (-1.44)).abs() < 1e-12);
assert!((f1[1] - (-1.92)).abs() < 1e-12);
assert!((f2[0] - 1.44).abs() < 1e-12);
assert!((f2[1] - 1.92).abs() < 1e-12);
}
#[test]
fn lin_repulsion_accumulates() {
let mut f1 = [10.0, 20.0];
let mut f2 = [0.0; 2];
apply_lin_repulsion([0.0, 0.0], [3.0, 4.0], 2.0, 3.0, 2.0, &mut f1, &mut f2);
assert!((f1[0] - (10.0 - 1.44)).abs() < 1e-12);
assert!((f1[1] - (20.0 - 1.92)).abs() < 1e-12);
}
#[test]
fn lin_repulsion_coincident_is_noop() {
let mut f1 = [1.0, 2.0];
let mut f2 = [3.0, 4.0];
apply_lin_repulsion([7.0, -7.0], [7.0, -7.0], 2.0, 3.0, 2.0, &mut f1, &mut f2);
assert_eq!(f1, [1.0, 2.0]);
assert_eq!(f2, [3.0, 4.0]);
}
#[test]
fn lin_repulsion_antisymmetric() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_lin_repulsion([1.0, -2.0], [-0.5, 3.0], 4.0, 5.0, 2.0, &mut f1, &mut f2);
assert_eq!(f1[0], -f2[0]);
assert_eq!(f1[1], -f2[1]);
}
#[test]
fn lin_attraction_closed_form() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction(false, false, [1.0, 1.0], [4.0, 5.0], 2.0, 1.0, 1.0, &mut f1, &mut f2);
assert!((f1[0] - 6.0).abs() < 1e-12);
assert!((f1[1] - 8.0).abs() < 1e-12);
assert!((f2[0] - (-6.0)).abs() < 1e-12);
assert!((f2[1] - (-8.0)).abs() < 1e-12);
}
#[test]
fn lin_attraction_zero_weight_is_noop() {
let mut f1 = [1.0, 1.0];
let mut f2 = [2.0, 2.0];
apply_attraction(false, false, [1.0, 1.0], [4.0, 5.0], 0.0, 1.0, 1.0, &mut f1, &mut f2);
assert_eq!(f1, [1.0, 1.0]);
assert_eq!(f2, [2.0, 2.0]);
}
#[test]
fn lin_attraction_coincident_is_noop() {
let mut f1 = [0.0; 2];
let mut f2 = [0.0; 2];
apply_attraction(false, false, [4.0, 5.0], [4.0, 5.0], 2.0, 1.0, 1.0, &mut f1, &mut f2);
assert_eq!(f1, [0.0, 0.0]);
assert_eq!(f2, [0.0, 0.0]);
}
#[test]
fn gravity_normal_closed_form() {
let mut f = [0.0; 2];
apply_gravity([3.0, 4.0], 2.0, 1.5, false, &mut f);
assert!((f[0] - (-1.8)).abs() < 1e-12);
assert!((f[1] - (-2.4)).abs() < 1e-12);
}
#[test]
fn gravity_strong_closed_form() {
let mut f = [0.0; 2];
apply_gravity([3.0, 4.0], 2.0, 1.5, true, &mut f);
assert!((f[0] - (-9.0)).abs() < 1e-12);
assert!((f[1] - (-12.0)).abs() < 1e-12);
}
#[test]
fn gravity_at_origin_is_noop() {
let mut f = [5.0, 6.0];
apply_gravity([0.0, 0.0], 2.0, 1.5, false, &mut f);
assert_eq!(f, [5.0, 6.0]);
apply_gravity([0.0, 0.0], 2.0, 1.5, true, &mut f);
assert_eq!(f, [5.0, 6.0]);
}
#[test]
fn gravity_zero_constant_is_noop() {
let mut f = [0.0; 2];
apply_gravity([3.0, 4.0], 2.0, 0.0, false, &mut f);
assert_eq!(f, [0.0, 0.0]);
apply_gravity([3.0, 4.0], 2.0, 0.0, true, &mut f);
assert_eq!(f, [0.0, 0.0]);
}
}