use glam::{DQuat, DVec3};
use indicatrix_cut_core::rough_plan::locate::Rigid;
use super::rig_form::parse_number;
pub const AXIS_NAMES: [&str; 6] = ["+X", "-X", "+Y", "-Y", "+Z", "-Z"];
pub const NUDGE_LABELS: [&str; 6] = [
"Turn about rig X (deg)",
"Turn about rig Y (deg)",
"Turn about rig Z (deg)",
"Shift along rig X (mm)",
"Shift along rig Y (mm)",
"Shift along rig Z (mm)",
];
pub const DEFAULT_AXES: (usize, usize) = (4, 0);
#[must_use]
pub const fn axis_vector(index: usize) -> Option<DVec3> {
match index {
0 => Some(DVec3::X),
1 => Some(DVec3::NEG_X),
2 => Some(DVec3::Y),
3 => Some(DVec3::NEG_Y),
4 => Some(DVec3::Z),
5 => Some(DVec3::NEG_Z),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Nudge {
pub turn_deg: [f64; 3],
pub shift_mm: [f64; 3],
}
impl Nudge {
pub fn from_texts(texts: &[String]) -> Result<Self, String> {
let mut values = [0.0; 6];
for (index, label) in NUDGE_LABELS.iter().enumerate() {
let text = texts.get(index).map_or("", |t| t.trim());
if !text.is_empty() {
values[index] = parse_number(text, label)?;
}
}
Ok(Self {
turn_deg: [values[0], values[1], values[2]],
shift_mm: [values[3], values[4], values[5]],
})
}
}
pub fn start_transform(
z_axis: usize,
x_axis: usize,
mesh_centre: DVec3,
nudge: &Nudge,
) -> Result<Rigid, String> {
let (Some(z), Some(x)) = (axis_vector(z_axis), axis_vector(x_axis)) else {
return Err("Pick one of the six axes for each direction.".to_owned());
};
let base = Rigid::from_axes(z, x, DVec3::ZERO)
.ok_or_else(|| "The two axes must lie along different mesh axes.".to_owned())?;
let [turn_x, turn_y, turn_z] = nudge.turn_deg.map(f64::to_radians);
let turn = DQuat::from_rotation_z(turn_z)
* DQuat::from_rotation_y(turn_y)
* DQuat::from_rotation_x(turn_x);
let rotation = turn * base.quat();
let translation = DVec3::from_array(nudge.shift_mm) - rotation * mesh_centre;
Ok(Rigid::from_parts(rotation, translation))
}
#[cfg(test)]
mod tests {
use super::*;
const CENTRE: DVec3 = DVec3::new(3.0, -2.0, 5.0);
fn close(left: DVec3, right: DVec3) -> bool {
(left - right).length() < 1e-9
}
#[test]
fn the_default_axes_keep_the_mesh_upright_and_put_its_centre_at_the_origin() {
let rigid = start_transform(4, 0, CENTRE, &Nudge::default()).expect("valid");
assert!(close(rigid.to_rig(CENTRE), DVec3::ZERO));
assert!(close(rigid.dir_to_rig(DVec3::Z), DVec3::Z));
assert!(close(rigid.dir_to_rig(DVec3::X), DVec3::X));
}
#[test]
fn a_mesh_that_lies_on_its_side_is_stood_up_by_the_axis_choice() {
let rigid = start_transform(0, 2, CENTRE, &Nudge::default()).expect("valid");
assert!(close(rigid.dir_to_rig(DVec3::X), DVec3::Z));
assert!(close(rigid.dir_to_rig(DVec3::Y), DVec3::X));
assert!(close(rigid.dir_to_rig(DVec3::Z), DVec3::Y));
assert!(close(rigid.to_rig(CENTRE), DVec3::ZERO));
}
#[test]
fn the_same_axis_twice_or_opposite_axes_are_refused() {
assert!(start_transform(4, 4, CENTRE, &Nudge::default()).is_err());
assert!(start_transform(4, 5, CENTRE, &Nudge::default()).is_err());
assert!(start_transform(9, 0, CENTRE, &Nudge::default()).is_err());
}
#[test]
fn the_nudge_shifts_and_turns_about_the_rig_axes() {
let nudge = Nudge {
turn_deg: [0.0, 0.0, 90.0],
shift_mm: [1.0, 2.0, 3.0],
};
let rigid = start_transform(4, 0, CENTRE, &nudge).expect("valid");
assert!(close(rigid.to_rig(CENTRE), DVec3::new(1.0, 2.0, 3.0)));
assert!(close(rigid.dir_to_rig(DVec3::X), DVec3::Y));
}
#[test]
fn nudge_fields_may_be_empty_and_must_be_numbers() {
let texts: Vec<String> = ["", "5", " 1,5 ", "", "", "-2"]
.iter()
.map(ToString::to_string)
.collect();
let nudge = Nudge::from_texts(&texts).expect("valid");
assert_eq!(nudge.turn_deg, [0.0, 5.0, 1.5]);
assert_eq!(nudge.shift_mm, [0.0, 0.0, -2.0]);
let mut bad = texts;
bad[3] = "x".to_owned();
assert!(
Nudge::from_texts(&bad)
.unwrap_err()
.contains("Shift along rig X")
);
assert_eq!(Nudge::from_texts(&[]).unwrap(), Nudge::default());
}
}