pub const DEFAULT_ANGULAR_TOL: f64 = 0.35;
#[must_use]
pub fn segments_for_chord_deviation(radius: f64, arc_range: f64, deflection: f64) -> usize {
segments_for_chord_deviation_with_angle(
radius,
arc_range,
deflection,
DEFAULT_ANGULAR_TOL,
0.0,
true,
)
}
#[must_use]
pub fn segments_for_chord_deviation_with_angle(
radius: f64,
arc_range: f64,
deflection: f64,
angular_tol: f64,
min_len: f64,
apply_curvature_floor: bool,
) -> usize {
use std::f64::consts::FRAC_PI_2;
if radius <= 0.0 || deflection <= 0.0 || arc_range <= 0.0 {
return 8;
}
let theta_lin = 2.0 * (1.0 - deflection / radius).clamp(0.0, 1.0).acos();
let mut theta_step = if angular_tol > 0.0 {
theta_lin.min(angular_tol)
} else {
theta_lin
};
if min_len > 0.0 {
let theta_minsize = (min_len / radius).min(FRAC_PI_2);
theta_step = theta_step.max(theta_minsize);
}
if theta_step <= 0.0 {
return 8;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let n = (arc_range / theta_step).ceil() as usize;
if !apply_curvature_floor {
return n.max(4);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let n_min = (arc_range * (radius / deflection).sqrt()).ceil() as usize;
n.max(n_min).max(4)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use std::f64::consts::TAU;
#[test]
fn degenerate_inputs() {
assert_eq!(segments_for_chord_deviation(0.0, TAU, 0.01), 8);
assert_eq!(segments_for_chord_deviation(1.0, 0.0, 0.01), 8);
assert_eq!(segments_for_chord_deviation(1.0, TAU, 0.0), 8);
assert_eq!(segments_for_chord_deviation(-1.0, TAU, 0.01), 8);
}
#[test]
fn minimum_four_segments() {
let n = segments_for_chord_deviation(1.0, TAU, 10.0);
assert!(n >= 4, "got {n}");
}
#[test]
fn finer_deflection_more_segments() {
let coarse = segments_for_chord_deviation(1.0, TAU, 0.1);
let fine = segments_for_chord_deviation(1.0, TAU, 0.01);
assert!(fine > coarse, "fine={fine} should be > coarse={coarse}");
}
#[test]
fn larger_radius_more_segments_linear_only() {
let small = segments_for_chord_deviation_with_angle(1.0, TAU, 0.05, 0.0, 0.0, true);
let large = segments_for_chord_deviation_with_angle(10.0, TAU, 0.05, 0.0, 0.0, true);
assert!(large > small, "large={large} should be > small={small}");
}
#[test]
fn angular_cap_floors_small_radius() {
let n = segments_for_chord_deviation_with_angle(0.5, TAU, 0.1, 0.35, 0.0, true);
let floor = (TAU / 0.35).ceil() as usize;
assert!(n >= floor, "got {n}, expected >= {floor}");
}
#[test]
fn large_angular_cap_matches_linear_only() {
for (r, d) in [(1.0, 0.05), (10.0, 0.01), (0.4, 0.02)] {
let capped = segments_for_chord_deviation_with_angle(r, TAU, d, 10.0, 0.0, true);
let linear = segments_for_chord_deviation_with_angle(r, TAU, d, 0.0, 0.0, true);
assert_eq!(capped, linear, "r={r} d={d}");
}
}
#[test]
fn curvature_floor_skipped_drops_large_radius_count() {
for (r, d) in [(3.25, 0.05), (5.0, 0.01), (1.0, 0.02)] {
let floored = segments_for_chord_deviation_with_angle(r, TAU, d, 0.0, 0.0, true);
let exact = segments_for_chord_deviation_with_angle(r, TAU, d, 0.0, 0.0, false);
assert!(
exact < floored,
"r={r} d={d}: exact={exact} should be < floored={floored}"
);
}
}
#[test]
fn angular_degenerate_inputs_return_default() {
assert_eq!(
segments_for_chord_deviation_with_angle(0.0, TAU, 0.1, 0.35, 0.0, true),
8
);
assert_eq!(
segments_for_chord_deviation_with_angle(1.0, 0.0, 0.1, 0.35, 0.0, true),
8
);
assert_eq!(
segments_for_chord_deviation_with_angle(1.0, TAU, 0.0, 0.35, 0.0, true),
8
);
}
#[test]
fn min_len_caps_blow_up_on_tiny_radius() {
let unbounded = segments_for_chord_deviation_with_angle(1e-4, TAU, 1e-6, 0.05, 0.0, true);
let bounded = segments_for_chord_deviation_with_angle(1e-4, TAU, 1e-6, 0.05, 0.1, true);
assert!(
bounded < unbounded,
"bounded={bounded} should be < unbounded={unbounded}"
);
}
#[test]
fn min_size_angle_capped_at_half_pi() {
let n = segments_for_chord_deviation_with_angle(0.1, TAU, 1e-6, 0.01, 100.0, true);
let floor = (TAU / std::f64::consts::FRAC_PI_2).ceil() as usize;
assert!(n >= floor, "got {n}, expected >= {floor}");
}
}