use crate::core::scene3d::mat4::Vec3;
use crate::core::ticklayout::{nice_numbers, number_of_digits};
fn format_tick(value: f64, nfrac: usize) -> String {
if nfrac == 0 {
format!("{value}")
} else {
format!("{value:.nfrac$}")
}
}
pub fn ticks(vmin: f64, vmax: f64) -> (Vec<f64>, Vec<String>) {
debug_assert!(vmin <= vmax);
let (mut positions, nfrac);
if vmin == vmax {
positions = vec![vmin];
nfrac = 0;
} else {
let (start, end, step, n) = nice_numbers(vmin, vmax, 5);
let mut t = start;
positions = Vec::new();
while t <= end {
if t >= vmin && t <= vmax {
positions.push(t);
}
t += step;
}
if positions.len() < 2 {
positions = vec![vmin, vmax];
nfrac = number_of_digits(vmax - vmin);
} else {
nfrac = n;
}
}
let labels = positions.iter().map(|&t| format_tick(t, nfrac)).collect();
(positions, labels)
}
#[derive(Clone, Debug, PartialEq)]
pub struct AxisLabel {
pub position: Vec3,
pub text: String,
}
#[derive(Clone, Debug, Default)]
pub struct AxesChrome {
pub axis_labels: Vec<AxisLabel>,
pub tick_segments: Vec<(Vec3, Vec3)>,
pub tick_labels: Vec<AxisLabel>,
}
pub fn labelled_axes_chrome(bounds: (Vec3, Vec3), axis_names: [&str; 3]) -> AxesChrome {
let (lo, hi) = bounds;
let size = hi - lo;
let anchors = [
Vec3::new(lo.x + size.x * 0.5, lo.y, lo.z),
Vec3::new(lo.x, lo.y + size.y * 0.5, lo.z),
Vec3::new(lo.x, lo.y, lo.z + size.z * 0.5),
];
let axis_labels = anchors
.into_iter()
.zip(axis_names)
.filter(|(_, name)| !name.is_empty())
.map(|(position, name)| AxisLabel {
position,
text: name.to_string(),
})
.collect();
let len = Vec3::new(size.x.abs(), size.y.abs(), size.z.abs());
let (xticks, xlabels) = ticks(lo.x as f64, hi.x as f64);
let (yticks, ylabels) = ticks(lo.y as f64, hi.y as f64);
let (zticks, zlabels) = ticks(lo.z as f64, hi.z as f64);
let mut tick_segments = Vec::new();
for &t in &xticks {
let base = Vec3::new(t as f32, lo.y, lo.z);
tick_segments.push((base, Vec3::new(t as f32, lo.y + len.y, lo.z))); tick_segments.push((base, Vec3::new(t as f32, lo.y, lo.z + len.z))); }
for &t in &yticks {
let base = Vec3::new(lo.x, t as f32, lo.z);
tick_segments.push((base, Vec3::new(lo.x + len.x, t as f32, lo.z))); tick_segments.push((base, Vec3::new(lo.x, t as f32, lo.z + len.z))); }
for &t in &zticks {
let base = Vec3::new(lo.x, lo.y, t as f32);
tick_segments.push((base, Vec3::new(lo.x + len.x, lo.y, t as f32))); tick_segments.push((base, Vec3::new(lo.x, lo.y + len.y, t as f32))); }
let off = lo - len * (1.0 / 20.0);
let mut tick_labels = Vec::new();
for (&t, label) in xticks.iter().zip(xlabels) {
tick_labels.push(AxisLabel {
position: Vec3::new(t as f32, off.y, off.z),
text: label,
});
}
for (&t, label) in yticks.iter().zip(ylabels) {
tick_labels.push(AxisLabel {
position: Vec3::new(off.x, t as f32, off.z),
text: label,
});
}
for (&t, label) in zticks.iter().zip(zlabels) {
tick_labels.push(AxisLabel {
position: Vec3::new(off.x, off.y, t as f32),
text: label,
});
}
AxesChrome {
axis_labels,
tick_segments,
tick_labels,
}
}
pub fn dash_segments(a: Vec3, b: Vec3, on: f32, off: f32) -> Vec<(Vec3, Vec3)> {
let dir = b - a;
let length = dir.length();
if length == 0.0 || on <= 0.0 {
return Vec::new();
}
if off <= 0.0 {
return vec![(a, b)];
}
let period = on + off;
let mut out = Vec::new();
let mut s = 0.0f32;
while s < length {
let e = (s + on).min(length);
out.push((a + dir * (s / length), a + dir * (e / length)));
s += period;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ticks_match_silx_nice_numbers() {
let (pos, labels) = ticks(0.0, 10.0);
assert_eq!(pos, vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0]);
assert_eq!(labels, vec!["0", "2", "4", "6", "8", "10"]);
let (pos, labels) = ticks(0.5, 10.5);
assert_eq!(pos, vec![2.0, 4.0, 6.0, 8.0, 10.0]);
assert_eq!(labels, vec!["2", "4", "6", "8", "10"]);
let (pos, labels) = ticks(0.0, 1.0);
assert_eq!(labels, vec!["0.0", "0.2", "0.4", "0.6", "0.8", "1.0"]);
assert_eq!(pos[3], 0.6000000000000001);
let (pos, labels) = ticks(2.5, 2.5);
assert_eq!(pos, vec![2.5]);
assert_eq!(labels, vec!["2.5"]);
}
#[test]
fn chrome_places_names_ticks_and_value_labels_like_silx() {
let bounds = (Vec3::ZERO, Vec3::new(2.0, 4.0, 6.0));
let chrome = labelled_axes_chrome(bounds, ["X", "", "Depth"]);
assert_eq!(chrome.axis_labels.len(), 2);
assert_eq!(chrome.axis_labels[0].position, Vec3::new(1.0, 0.0, 0.0));
assert_eq!(chrome.axis_labels[0].text, "X");
assert_eq!(chrome.axis_labels[1].position, Vec3::new(0.0, 0.0, 3.0));
assert_eq!(chrome.axis_labels[1].text, "Depth");
let n_ticks = ticks(0.0, 2.0).0.len() + ticks(0.0, 4.0).0.len() + ticks(0.0, 6.0).0.len();
assert_eq!(chrome.tick_segments.len(), 2 * n_ticks);
assert_eq!(chrome.tick_labels.len(), n_ticks);
assert_eq!(
chrome.tick_segments[0],
(Vec3::ZERO, Vec3::new(0.0, 4.0, 0.0))
);
assert_eq!(
chrome.tick_segments[1],
(Vec3::ZERO, Vec3::new(0.0, 0.0, 6.0))
);
assert_eq!(chrome.tick_labels[0].position, Vec3::new(0.0, -0.2, -0.3));
assert_eq!(chrome.tick_labels[0].text, "0.0");
assert!(
chrome
.tick_segments
.iter()
.any(|&(a, b)| a == Vec3::new(0.0, 4.0, 0.0) && b == Vec3::new(2.0, 4.0, 0.0)),
"y tick at 4 must span the XY plane"
);
}
#[test]
fn dash_segments_cover_on_lengths_only() {
let out = dash_segments(Vec3::ZERO, Vec3::new(3.0, 0.0, 0.0), 1.0, 0.5);
assert_eq!(out.len(), 2);
assert!((out[0].0.x - 0.0).abs() < 1e-6 && (out[0].1.x - 1.0).abs() < 1e-6);
assert!((out[1].0.x - 1.5).abs() < 1e-6 && (out[1].1.x - 2.5).abs() < 1e-6);
assert!(dash_segments(Vec3::ZERO, Vec3::ZERO, 1.0, 1.0).is_empty());
assert!(dash_segments(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), 0.0, 1.0).is_empty());
let solid = dash_segments(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), 5.0, 0.0);
assert_eq!(solid, vec![(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0))]);
}
}