use crate::interaction::gizmo::GizmoAxis;
use crate::interaction::input::{Action, ActionFrame};
use super::types::{ManipulationKind, ManipulationState};
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub(super) struct NumericInputState {
pub(super) active_axes: Vec<usize>,
pub(super) current_axis_idx: usize,
pub(super) axis_inputs: [String; 3],
}
#[allow(dead_code)]
impl NumericInputState {
pub(super) fn new(axis: Option<GizmoAxis>, excluded: bool) -> Self {
let active_axes = match axis {
None => vec![0, 1, 2],
Some(GizmoAxis::X) => {
if excluded {
vec![1, 2]
} else {
vec![0]
}
}
Some(GizmoAxis::Y) => {
if excluded {
vec![0, 2]
} else {
vec![1]
}
}
Some(GizmoAxis::Z) | Some(GizmoAxis::None) => {
if excluded {
vec![0, 1]
} else {
vec![2]
}
}
_ => vec![0, 1, 2],
};
Self {
active_axes,
current_axis_idx: 0,
axis_inputs: [String::new(), String::new(), String::new()],
}
}
pub(super) fn current_axis(&self) -> usize {
self.active_axes[self.current_axis_idx]
}
pub(super) fn parsed_values(&self) -> [Option<f32>; 3] {
core::array::from_fn(|i| self.axis_inputs[i].parse::<f32>().ok())
}
pub(super) fn display_string(&self) -> String {
let labels = ["X", "Y", "Z"];
let mut parts = Vec::new();
for (i, label) in labels.iter().enumerate() {
if self.active_axes.contains(&i) {
let val = if self.axis_inputs[i].is_empty() {
"_".to_string()
} else {
self.axis_inputs[i].clone()
};
parts.push(format!("{label}: {val}"));
}
}
parts.join(" ")
}
}
#[derive(Debug, Clone)]
pub(super) struct ManipulationSession {
pub(super) kind: ManipulationKind,
pub(super) axis: Option<GizmoAxis>,
pub(super) exclude_axis: bool,
pub(super) numeric: Option<NumericInputState>,
pub(super) is_gizmo_drag: bool,
pub(super) gizmo_center: glam::Vec3,
pub(super) cursor_anchor: Option<glam::Vec2>,
pub(super) cursor_last_total: glam::Vec2,
pub(super) last_scale_factor: f32,
}
impl ManipulationSession {
pub(super) fn to_state(&self) -> ManipulationState {
let numeric_display = self
.numeric
.as_ref()
.map(|n| n.display_string())
.filter(|s| !s.is_empty());
ManipulationState {
kind: self.kind,
axis: self.axis,
exclude_axis: self.exclude_axis,
is_gizmo_drag: self.is_gizmo_drag,
center: self.gizmo_center,
numeric_display,
}
}
}
pub(super) fn update_constraint(
session: &mut ManipulationSession,
constrain_x: bool,
constrain_y: bool,
constrain_z: bool,
exclude_x: bool,
exclude_y: bool,
exclude_z: bool,
) {
let mut set_axis = |axis: GizmoAxis, exclude: bool| {
session.axis = Some(axis);
session.exclude_axis = exclude;
session.numeric = None;
};
if constrain_x {
set_axis(GizmoAxis::X, false);
}
if constrain_y {
set_axis(GizmoAxis::Y, false);
}
if constrain_z {
set_axis(GizmoAxis::Z, false);
}
if exclude_x {
set_axis(GizmoAxis::X, true);
}
if exclude_y {
set_axis(GizmoAxis::Y, true);
}
if exclude_z {
set_axis(GizmoAxis::Z, true);
}
}
pub(super) fn update_numeric_state(session: &mut ManipulationSession, frame: &ActionFrame) {
if session.numeric.is_none() && !frame.typed_chars.is_empty() {
session.numeric = Some(NumericInputState::new(session.axis, session.exclude_axis));
}
let Some(ref mut numeric) = session.numeric else {
return;
};
let axis_idx = numeric.current_axis();
for &c in &frame.typed_chars {
let buf = &mut numeric.axis_inputs[axis_idx];
if c == '-' && buf.is_empty() {
buf.push(c);
} else if c.is_ascii_digit() || c == '.' {
if c != '.' || !buf.contains('.') {
buf.push(c);
}
}
}
if frame.is_active(Action::NumericBackspace) {
numeric.axis_inputs[axis_idx].pop();
}
if frame.is_active(Action::NumericNextAxis) {
let len = numeric.active_axes.len();
if len > 1 {
numeric.current_axis_idx = (numeric.current_axis_idx + 1) % len;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numeric_input_no_constraint_all_axes() {
let s = NumericInputState::new(None, false);
assert_eq!(s.active_axes, vec![0, 1, 2]);
}
#[test]
fn numeric_input_constrain_x() {
let s = NumericInputState::new(Some(GizmoAxis::X), false);
assert_eq!(s.active_axes, vec![0]);
}
#[test]
fn numeric_input_exclude_x() {
let s = NumericInputState::new(Some(GizmoAxis::X), true);
assert_eq!(s.active_axes, vec![1, 2]);
}
#[test]
fn numeric_input_constrain_y() {
let s = NumericInputState::new(Some(GizmoAxis::Y), false);
assert_eq!(s.active_axes, vec![1]);
}
#[test]
fn numeric_input_exclude_y() {
let s = NumericInputState::new(Some(GizmoAxis::Y), true);
assert_eq!(s.active_axes, vec![0, 2]);
}
#[test]
fn numeric_input_constrain_z() {
let s = NumericInputState::new(Some(GizmoAxis::Z), false);
assert_eq!(s.active_axes, vec![2]);
}
#[test]
fn numeric_input_exclude_z() {
let s = NumericInputState::new(Some(GizmoAxis::Z), true);
assert_eq!(s.active_axes, vec![0, 1]);
}
#[test]
fn numeric_input_parsed_values_empty() {
let s = NumericInputState::new(None, false);
let vals = s.parsed_values();
assert!(vals[0].is_none());
assert!(vals[1].is_none());
assert!(vals[2].is_none());
}
#[test]
fn numeric_input_parsed_values_with_input() {
let mut s = NumericInputState::new(None, false);
s.axis_inputs[0] = "2.5".to_string();
s.axis_inputs[2] = "-1".to_string();
let vals = s.parsed_values();
assert!((vals[0].unwrap() - 2.5).abs() < 1e-6);
assert!(vals[1].is_none());
assert!((vals[2].unwrap() - (-1.0)).abs() < 1e-6);
}
#[test]
fn numeric_input_display_string() {
let mut s = NumericInputState::new(Some(GizmoAxis::X), true);
s.axis_inputs[1] = "3.0".to_string();
let display = s.display_string();
assert!(display.contains("Y: 3.0"));
assert!(display.contains("Z: _"));
assert!(!display.contains("X:"));
}
#[test]
fn numeric_input_current_axis() {
let s = NumericInputState::new(Some(GizmoAxis::Y), true);
assert_eq!(s.current_axis(), 0); }
#[test]
fn update_constraint_sets_axis() {
let mut session = ManipulationSession {
kind: ManipulationKind::Move,
axis: None,
exclude_axis: false,
numeric: None,
is_gizmo_drag: false,
gizmo_center: glam::Vec3::ZERO,
cursor_anchor: None,
cursor_last_total: glam::Vec2::ZERO,
last_scale_factor: 1.0,
};
update_constraint(&mut session, true, false, false, false, false, false);
assert_eq!(session.axis, Some(GizmoAxis::X));
assert!(!session.exclude_axis);
}
#[test]
fn update_constraint_exclude_mode() {
let mut session = ManipulationSession {
kind: ManipulationKind::Scale,
axis: None,
exclude_axis: false,
numeric: None,
is_gizmo_drag: false,
gizmo_center: glam::Vec3::ZERO,
cursor_anchor: None,
cursor_last_total: glam::Vec2::ZERO,
last_scale_factor: 1.0,
};
update_constraint(&mut session, false, false, false, false, true, false);
assert_eq!(session.axis, Some(GizmoAxis::Y));
assert!(session.exclude_axis);
}
#[test]
fn update_constraint_clears_numeric() {
let mut session = ManipulationSession {
kind: ManipulationKind::Move,
axis: Some(GizmoAxis::X),
exclude_axis: false,
numeric: Some(NumericInputState::new(Some(GizmoAxis::X), false)),
is_gizmo_drag: false,
gizmo_center: glam::Vec3::ZERO,
cursor_anchor: None,
cursor_last_total: glam::Vec2::ZERO,
last_scale_factor: 1.0,
};
update_constraint(&mut session, false, true, false, false, false, false);
assert!(session.numeric.is_none());
}
}