use super::{cut_rows::fmt_num, inputs::parse_mm};
use crate::{RoughPlanModel, RoughPlannerWindow};
use indicatrix_cut_core::rough_plan::{Axis, RoughBase, RoughModel};
use slint::{ComponentHandle, SharedString};
const KIND_BLOCK: i32 = 0;
const KIND_CYLINDER: i32 = 1;
const KIND_PEBBLE: i32 = 2;
const KIND_HULL: i32 = 3;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct BaseFields {
pub(super) kind: i32,
pub(super) x: String,
pub(super) y: String,
pub(super) z: String,
pub(super) diameter: String,
pub(super) length: String,
pub(super) axis: i32,
}
impl BaseFields {
#[must_use]
pub(super) fn read(window: &RoughPlannerWindow) -> Self {
let model = window.global::<RoughPlanModel>();
Self {
kind: model.get_base_kind(),
x: model.get_rough_x().to_string(),
y: model.get_rough_y().to_string(),
z: model.get_rough_z().to_string(),
diameter: model.get_cyl_diameter().to_string(),
length: model.get_cyl_length().to_string(),
axis: model.get_cyl_axis(),
}
}
}
fn size(text: &str, label: &str) -> Result<f64, String> {
if text.trim().is_empty() {
Ok(0.0)
} else {
parse_mm(text, label)
}
}
#[must_use]
pub(super) const fn axis_from_number(number: i32) -> Axis {
match number {
0 => Axis::X,
2 => Axis::Z,
_ => Axis::Y,
}
}
#[must_use]
const fn axis_number(axis: Axis) -> i32 {
match axis {
Axis::X => 0,
Axis::Y => 1,
Axis::Z => 2,
}
}
pub(super) fn parse_base(fields: &BaseFields) -> Result<RoughBase, String> {
if fields.kind == KIND_CYLINDER {
return Ok(RoughBase::Cylinder {
diameter_mm: size(&fields.diameter, "Diameter")?,
length_mm: size(&fields.length, "Length")?,
axis: axis_from_number(fields.axis),
});
}
let (x_mm, y_mm, z_mm) = (
size(&fields.x, "Rough X")?,
size(&fields.y, "Rough Y")?,
size(&fields.z, "Rough Z")?,
);
if fields.kind == KIND_PEBBLE {
Ok(RoughBase::Pebble { x_mm, y_mm, z_mm })
} else {
Ok(RoughBase::Block { x_mm, y_mm, z_mm })
}
}
#[must_use]
pub(super) const fn kind_of(base: &RoughBase) -> i32 {
match base {
RoughBase::Block { .. } => KIND_BLOCK,
RoughBase::Cylinder { .. } => KIND_CYLINDER,
RoughBase::Pebble { .. } => KIND_PEBBLE,
RoughBase::Hull { .. } => KIND_HULL,
}
}
#[must_use]
pub(super) const fn switch_base_kind(current: &RoughBase, kind: i32) -> RoughBase {
let [x_mm, y_mm, z_mm] = current.bounding_box_extents();
if kind == KIND_CYLINDER {
if matches!(current, RoughBase::Cylinder { .. }) {
*current
} else {
RoughBase::Cylinder {
diameter_mm: x_mm.min(z_mm),
length_mm: y_mm,
axis: Axis::Y,
}
}
} else if kind == KIND_PEBBLE {
RoughBase::Pebble { x_mm, y_mm, z_mm }
} else {
RoughBase::Block { x_mm, y_mm, z_mm }
}
}
#[must_use]
pub(super) fn is_blank(model: &RoughModel) -> bool {
model
.base
.bounding_box_extents()
.iter()
.any(|extent| extent.abs() < f64::EPSILON)
}
#[must_use]
pub(super) const fn blank_model() -> RoughModel {
RoughModel::new(
RoughBase::Block {
x_mm: 0.0,
y_mm: 0.0,
z_mm: 0.0,
},
Vec::new(),
)
}
fn size_string(value: f64) -> String {
if value.abs() < f64::EPSILON {
return String::new();
}
let short = fmt_num(value, 2);
if short
.parse::<f64>()
.is_ok_and(|parsed| parsed.to_bits() == value.to_bits())
{
short
} else {
format!("{value}")
}
}
fn size_text(value: f64) -> SharedString {
size_string(value).into()
}
pub(super) fn pending_base(
fields: &BaseFields,
current: &RoughBase,
) -> Result<Option<RoughBase>, String> {
if fields.kind == KIND_HULL && matches!(current, RoughBase::Hull { .. }) {
return Ok(None);
}
let typed = parse_base(fields)?;
if typed == *current {
Ok(None)
} else {
Ok(Some(typed))
}
}
pub(super) fn push_base(window: &RoughPlannerWindow, base: &RoughBase) {
let model = window.global::<RoughPlanModel>();
model.set_base_kind(kind_of(base));
match *base {
RoughBase::Block { x_mm, y_mm, z_mm }
| RoughBase::Pebble { x_mm, y_mm, z_mm }
| RoughBase::Hull {
x_mm, y_mm, z_mm, ..
} => {
model.set_rough_x(size_text(x_mm));
model.set_rough_y(size_text(y_mm));
model.set_rough_z(size_text(z_mm));
}
RoughBase::Cylinder {
diameter_mm,
length_mm,
axis,
} => {
model.set_cyl_diameter(size_text(diameter_mm));
model.set_cyl_length(size_text(length_mm));
model.set_cyl_axis(axis_number(axis));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fields_of(base: &RoughBase) -> BaseFields {
let mut fields = BaseFields {
kind: kind_of(base),
..BaseFields::default()
};
match *base {
RoughBase::Block { x_mm, y_mm, z_mm }
| RoughBase::Pebble { x_mm, y_mm, z_mm }
| RoughBase::Hull {
x_mm, y_mm, z_mm, ..
} => {
fields.x = size_string(x_mm);
fields.y = size_string(y_mm);
fields.z = size_string(z_mm);
}
RoughBase::Cylinder {
diameter_mm,
length_mm,
axis,
} => {
fields.diameter = size_string(diameter_mm);
fields.length = size_string(length_mm);
fields.axis = axis_number(axis);
}
}
fields
}
fn block_fields(x: &str, y: &str, z: &str) -> BaseFields {
BaseFields {
kind: KIND_BLOCK,
x: x.into(),
y: y.into(),
z: z.into(),
..BaseFields::default()
}
}
#[test]
fn typed_sizes_become_the_base_and_empty_ones_count_as_blank() {
let base = parse_base(&block_fields("12,5", " 9 ", "8.25")).expect("valid sizes");
assert_eq!(
base,
RoughBase::Block {
x_mm: 12.5,
y_mm: 9.0,
z_mm: 8.25,
}
);
let partial = parse_base(&block_fields("12", "", "")).expect("empty fields are allowed");
assert!(is_blank(&RoughModel::new(partial, Vec::new())));
assert!(!is_blank(&RoughModel::new(base, Vec::new())));
}
#[test]
fn a_field_that_is_not_a_number_names_itself() {
let error = parse_base(&block_fields("12", "abc", "8")).unwrap_err();
assert!(error.starts_with("Rough Y"), "{error}");
let cylinder = BaseFields {
kind: KIND_CYLINDER,
diameter: "x".into(),
..BaseFields::default()
};
assert!(parse_base(&cylinder).unwrap_err().starts_with("Diameter"));
}
#[test]
fn the_cylinder_fields_and_axis_are_read() {
let fields = BaseFields {
kind: KIND_CYLINDER,
diameter: "10".into(),
length: "24".into(),
axis: 2,
..BaseFields::default()
};
assert_eq!(
parse_base(&fields),
Ok(RoughBase::Cylinder {
diameter_mm: 10.0,
length_mm: 24.0,
axis: Axis::Z,
})
);
}
fn block(x: f64, y: f64, z: f64) -> RoughBase {
RoughBase::Block {
x_mm: x,
y_mm: y,
z_mm: z,
}
}
#[test]
fn the_fields_of_a_base_are_what_the_window_shows_for_it() {
let fields = fields_of(&block(12.5, 9.0, 0.0));
assert_eq!(
(fields.x.as_str(), fields.y.as_str(), fields.z.as_str()),
("12.5", "9", ""),
"no trailing zeros, empty for 0"
);
assert_eq!(fields.kind, KIND_BLOCK);
let cylinder = fields_of(&RoughBase::Cylinder {
diameter_mm: 10.0,
length_mm: 24.25,
axis: Axis::Z,
});
assert_eq!(cylinder.kind, KIND_CYLINDER);
assert_eq!(
(cylinder.diameter.as_str(), cylinder.length.as_str()),
("10", "24.25")
);
assert_eq!(cylinder.axis, 2);
}
#[test]
fn typed_sizes_that_change_the_base_are_pending() {
let current = block(10.0, 8.0, 6.0);
assert_eq!(pending_base(&fields_of(¤t), ¤t), Ok(None));
assert_eq!(
pending_base(&block_fields("10.00", " 8,0", "6"), ¤t),
Ok(None)
);
assert_eq!(
pending_base(&block_fields("12", "8", "6"), ¤t),
Ok(Some(block(12.0, 8.0, 6.0)))
);
assert_eq!(
pending_base(&block_fields("12", "9", "8"), &block(0.0, 0.0, 0.0)),
Ok(Some(block(12.0, 9.0, 8.0)))
);
assert!(
pending_base(&block_fields("12", "x", "6"), ¤t)
.unwrap_err()
.starts_with("Rough Y")
);
}
#[test]
fn a_size_is_shown_in_full_so_committing_it_untouched_is_no_edit() {
let current = block(12.345, 8.0, 6.0);
let shown = fields_of(¤t);
assert_eq!(shown.x, "12.345");
assert_eq!(pending_base(&shown, ¤t), Ok(None));
assert_eq!(fields_of(&block(12.5, 8.0, 6.0)).x, "12.5");
}
#[test]
fn a_value_typed_over_a_shown_one_is_an_edit_even_beyond_the_second_decimal() {
let current = block(12.345, 8.0, 6.0);
assert_eq!(
pending_base(&block_fields("12.35", "8", "6"), ¤t),
Ok(Some(block(12.35, 8.0, 6.0)))
);
assert_eq!(
pending_base(&block_fields("12.4", "8", "6"), ¤t),
Ok(Some(block(12.4, 8.0, 6.0)))
);
assert_eq!(
pending_base(&block_fields("12,3450", "8.0", "6"), ¤t),
Ok(None)
);
}
#[test]
fn a_cylinder_axis_change_is_pending() {
let current = RoughBase::Cylinder {
diameter_mm: 10.0,
length_mm: 24.0,
axis: Axis::Y,
};
let mut fields = fields_of(¤t);
assert_eq!(pending_base(&fields, ¤t), Ok(None));
fields.axis = 0;
assert_eq!(
pending_base(&fields, ¤t),
Ok(Some(RoughBase::Cylinder {
diameter_mm: 10.0,
length_mm: 24.0,
axis: Axis::X,
}))
);
}
#[test]
fn switching_the_kind_keeps_the_size() {
let block = RoughBase::Block {
x_mm: 12.0,
y_mm: 9.0,
z_mm: 8.0,
};
assert_eq!(
switch_base_kind(&block, KIND_PEBBLE),
RoughBase::Pebble {
x_mm: 12.0,
y_mm: 9.0,
z_mm: 8.0,
}
);
let cylinder = switch_base_kind(&block, KIND_CYLINDER);
assert_eq!(
cylinder,
RoughBase::Cylinder {
diameter_mm: 8.0,
length_mm: 9.0,
axis: Axis::Y,
}
);
assert_eq!(
switch_base_kind(&cylinder, KIND_BLOCK),
RoughBase::Block {
x_mm: 8.0,
y_mm: 9.0,
z_mm: 8.0,
}
);
assert_eq!(switch_base_kind(&cylinder, KIND_CYLINDER), cylinder);
}
}