use super::{
editing::{
commit_pending_base, push_history_flags, read_inputs, readout_inputs_changed, resync,
},
host::Host,
inputs::{FIT_FAILED_PREFIX, MODEL_CHANGED_MESSAGE, parse_weighed},
mesh_task,
saved::show_error,
shape_worker::{ShapeOutput, set_weight_check},
};
use crate::RoughPlanModel;
use indicatrix_cut_core::{rough_plan::RoughModel, yield_metrics::carat_weight};
use slint::ComponentHandle;
use std::rc::Rc;
fn is_typed(host: &Rc<Host>, text: &str) -> bool {
!text.trim().is_empty() && host.session.borrow().carat_shown.as_deref() != Some(text)
}
pub(super) fn typed_carat(host: &Rc<Host>) -> Result<Option<f64>, String> {
let text = host.window.global::<RoughPlanModel>().get_weighed_ct();
if is_typed(host, text.as_str()) {
parse_weighed(&text)
} else {
Ok(None)
}
}
pub(super) fn fit_factor(current: f64, target: f64) -> Option<f64> {
if !(current > 0.0 && target > 0.0) {
return None;
}
let factor = (target / current).cbrt();
(factor.is_finite() && (factor - 1.0).abs() >= 1e-4).then_some(factor)
}
fn model_carat_text(volume_mm3: f64, specific_gravity: f64) -> String {
format!("{:.2}", carat_weight(volume_mm3, specific_gravity))
}
pub(super) fn show(host: &Rc<Host>, model: &RoughPlanModel<'_>, output: &ShapeOutput) {
let text = model.get_weighed_ct();
if is_typed(host, text.as_str()) {
set_weight_check(model, output.check_text.clone(), output.check_level);
return;
}
if output.specific_gravity > 0.0 {
let carat = model_carat_text(output.measure.volume_mm3, output.specific_gravity);
model.set_weighed_ct(carat.clone().into());
host.session.borrow_mut().carat_shown = Some(carat);
}
set_weight_check(model, String::new(), 0);
}
fn scaled_model(model: &RoughModel, factor: f64) -> Result<RoughModel, String> {
let scaled = model.scaled(factor);
scaled.base.validate().map_err(|e| e.to_string())?;
Ok(scaled)
}
fn failure_text(reason: &str) -> String {
format!("{FIT_FAILED_PREFIX} {reason}")
}
fn apply_fitted(host: &Rc<Host>, original: &RoughModel, scaled: RoughModel, text: String) {
if host.session.borrow().model != *original {
show_error(host, MODEL_CHANGED_MESSAGE);
return;
}
let recorded = {
let mut session = host.session.borrow_mut();
session.model = scaled;
session.carat_shown = Some(text);
session.commit()
};
if recorded {
push_history_flags(host);
}
resync(host, true);
}
pub(super) fn fit_to_weight(host: &Rc<Host>) {
if commit_pending_base(host).is_err() {
return;
}
let Ok(Some(target)) = typed_carat(host) else {
return;
};
let Some(measure) = host.session.borrow().model_measure else {
return;
};
let text = host
.window
.global::<RoughPlanModel>()
.get_weighed_ct()
.to_string();
let current = carat_weight(measure.volume_mm3, read_inputs(host).specific_gravity);
let Some(factor) = fit_factor(current, target) else {
host.session.borrow_mut().carat_shown = Some(text);
readout_inputs_changed(host);
return;
};
show_error(host, "");
let original = host.session.borrow().model.clone();
if original.mesh().is_none() {
match scaled_model(&original, factor) {
Ok(scaled) => apply_fitted(host, &original, scaled, text),
Err(message) => show_error(host, &failure_text(&message)),
}
return;
}
let job_model = original.clone();
mesh_task::spawn(
host,
"Scaling the mesh...",
move || scaled_model(&job_model, factor),
move |host, result| match result {
Ok(scaled) => apply_fitted(host, &original, scaled, text),
Err(message) => show_error(host, &failure_text(&message)),
},
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gui::rough_plan::inputs::is_input_message;
use indicatrix_cut_core::rough_plan::{RoughBase, RoughCut};
#[test]
fn the_fit_factor_is_the_cube_root_of_the_carat_ratio() {
let factor = fit_factor(3.40, 3.80).expect("a change");
assert!((factor - (3.80f64 / 3.40).cbrt()).abs() < 1e-12, "{factor}");
assert!(factor.powi(3).mul_add(3.40, -3.80).abs() < 1e-12);
let shrink = fit_factor(10.0, 1.25).expect("a change");
assert!((shrink - 0.5).abs() < 1e-12, "{shrink}");
}
#[test]
fn a_matching_or_impossible_weight_gives_no_factor() {
assert_eq!(fit_factor(3.40, 3.40), None);
assert_eq!(fit_factor(3.40, 3.40 * (1.0 + 2e-4)), None);
assert_eq!(fit_factor(0.0, 3.40), None);
assert_eq!(fit_factor(3.40, 0.0), None);
assert_eq!(fit_factor(3.40, -1.0), None);
assert_eq!(fit_factor(f64::NAN, 3.40), None);
}
#[test]
fn the_written_carat_has_two_decimals() {
assert_eq!(model_carat_text(256.6, 2.65), "3.40");
assert_eq!(model_carat_text(0.0, 2.65), "0.00");
}
fn block_with_a_face_cut(x_mm: f64) -> RoughModel {
RoughModel::new(
RoughBase::Block {
x_mm,
y_mm: 8.0,
z_mm: 6.0,
},
vec![RoughCut::Face {
normal: [0.0, 1.0, 0.0],
depth_mm: 1.0,
}],
)
}
#[test]
fn scaling_a_model_multiplies_the_base_and_every_cut_length() {
let scaled = scaled_model(&block_with_a_face_cut(10.0), 2.0).expect("a valid rough");
assert_eq!(
scaled.base,
RoughBase::Block {
x_mm: 20.0,
y_mm: 16.0,
z_mm: 12.0,
}
);
assert_eq!(
scaled.cuts,
vec![RoughCut::Face {
normal: [0.0, 1.0, 0.0],
depth_mm: 2.0,
}]
);
}
#[test]
fn a_scaled_rough_the_planner_cannot_take_is_refused_with_its_message() {
let message = scaled_model(&block_with_a_face_cut(1500.0), 2.0)
.expect_err("3000 mm is over the limit");
assert_eq!(message, "Dimensions must not exceed 2000 mm.");
}
#[test]
fn a_failed_fit_names_its_reason_on_the_error_line() {
let message = scaled_model(&block_with_a_face_cut(1500.0), 2.0)
.expect_err("3000 mm is over the limit");
let text = failure_text(&message);
assert_eq!(
text,
"Fit to weight failed: Dimensions must not exceed 2000 mm."
);
assert!(is_input_message(&text));
assert!(is_input_message(MODEL_CHANGED_MESSAGE));
assert!(MODEL_CHANGED_MESSAGE.ends_with("Press Fit to weight again."));
}
#[test]
fn a_mesh_rough_is_scaled_with_its_mesh_by_the_job_the_worker_runs() {
use crate::gui::rough_plan::obj_import::{C_SHAPE_OBJ, mesh_base_of};
let model = RoughModel::new(mesh_base_of(C_SHAPE_OBJ), Vec::new());
assert!(model.mesh().is_some(), "the C-shape is a mesh rough");
let scaled = scaled_model(&model, 2.0).expect("a mesh scales");
let mesh = scaled.mesh().expect("the scaled rough still has its mesh");
assert!((mesh.volume() - 48_000.0).abs() < 1e-6, "{}", mesh.volume());
}
}