use super::{CANCELLING_STAGE, IdSource, PlanJob, spawn_worker, state::Screen};
use crate::{
RoughPlanModel, RoughPlanResult,
gui::rough_plan::{
base::is_blank,
counts::{is_remote, refresh_counts},
editing,
format::to_i32,
host::Host,
inputs::{
BAD_CUT_FIELD_MESSAGE, FilterSnapshot, NO_SIZE_MESSAGE, check_skin, current_settings,
},
mesh_task::{self, MESH_BUSY_MESSAGE},
saved::convert::CandidateSource,
session::REMOTE_MESSAGE,
view,
},
plan_limit::{parse_limit, store_limit_secs, stored_limit_secs},
};
use indicatrix_cut_core::rough_plan::{PlanSettings, RoughModel};
use slint::{ComponentHandle, Model, ModelRc, VecModel};
use std::{
rc::Rc,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use tracing::warn;
const NO_MAIN_WINDOW: &str = "The library window is closed, so its filter cannot be read.";
fn id_source(host: &Host, use_filter: bool) -> Result<IdSource, String> {
if !use_filter {
return Ok(IdSource::Library);
}
let main = host.main.upgrade().ok_or(NO_MAIN_WINDOW)?;
Ok(IdSource::Filter(Box::new(FilterSnapshot::read(&main))))
}
struct Request {
model: RoughModel,
settings: PlanSettings,
material_name: String,
weighed_ct: Option<f64>,
time_limit_secs: u32,
}
fn read_time_limit(model: &RoughPlanModel<'_>) -> Result<u32, String> {
if model.get_locate_possible() {
parse_limit(model.get_time_limit_secs().as_str())
} else {
Ok(stored_limit_secs())
}
}
fn check_kerf_and_allowance(settings: &PlanSettings, extents: [f64; 3]) -> Result<(), String> {
let needed = 2.0f64.mul_add(settings.allowance_mm, settings.kerf_mm);
let smallest = extents.into_iter().fold(f64::INFINITY, f64::min);
if needed <= smallest {
Ok(())
} else {
Err(format!(
"The kerf ({:.2} mm) and the allowance on both sides of a stone ({:.2} mm) need \
{needed:.2} mm, more than the rough's smallest side ({smallest:.2} mm).",
settings.kerf_mm,
2.0 * settings.allowance_mm,
))
}
}
fn read_request(host: &Rc<Host>) -> Result<Request, String> {
let (model, bad_field) = {
let session = host.session.borrow();
(session.model.clone(), !session.bad_fields.is_empty())
};
if is_blank(&model) {
return Err(NO_SIZE_MESSAGE.to_string());
}
if bad_field {
return Err(BAD_CUT_FIELD_MESSAGE.to_string());
}
let (settings, material_name, weighed_ct) = current_settings(host)?;
let extents = model.base.bounding_box_extents();
check_skin(&settings, extents)?;
check_kerf_and_allowance(&settings, extents)?;
let time_limit_secs = read_time_limit(&host.window.global::<RoughPlanModel>())?;
Ok(Request {
model,
settings,
material_name,
weighed_ct,
time_limit_secs,
})
}
fn clear_outputs(host: &Rc<Host>) {
let model = host.window.global::<RoughPlanModel>();
model.set_results(ModelRc::new(VecModel::<RoughPlanResult>::default()));
model.set_keep_count(0);
model.set_summary("".into());
model.set_skipped_count(0);
model.set_expanded_index(-1);
model.set_selected_result(-1);
model.set_selected_group(-1);
model.set_thumbnails_pending(false);
model.set_loaded_banner("".into());
model.set_save_name_open(false);
model.set_confirm_replace_open(false);
model.set_results_unsaved(false);
model.set_face_from_view_armed(false);
model.set_progress_fraction(0.0);
model.set_stage("Preparing...".into());
model.set_running(true);
}
fn first_message<'a>(candidates: impl IntoIterator<Item = &'a str>) -> String {
candidates
.into_iter()
.find(|text| !text.trim().is_empty())
.unwrap_or_default()
.to_string()
}
fn blocking_message(model: &RoughPlanModel<'_>, message: &str) -> String {
first_message([
message,
model.get_model_error().as_str(),
model.get_weight_check_text().as_str(),
])
}
struct Prepared {
request: Request,
ids: IdSource,
use_filter: bool,
}
fn prepare(host: &Rc<Host>) -> Result<Prepared, String> {
let request = read_request(host)?;
if is_remote(&host.source) {
return Err(REMOTE_MESSAGE.to_string());
}
let use_filter = host.window.global::<RoughPlanModel>().get_use_filter();
Ok(Prepared {
request,
ids: id_source(host, use_filter)?,
use_filter,
})
}
pub(in crate::gui::rough_plan) fn start_plan(host: &Rc<Host>) {
let model = host.window.global::<RoughPlanModel>();
if model.get_running() {
return;
}
if mesh_task::is_busy(host) {
model.set_error_text(MESH_BUSY_MESSAGE.into());
return;
}
model.set_error_text("".into());
if let Err(message) = editing::commit_pending_base(host) {
model.set_error_text(blocking_message(&model, &message).into());
return;
}
match prepare(host) {
Ok(prepared) => guard_unsaved_results(host, move |host| launch(host, prepared)),
Err(message) => model.set_error_text(message.into()),
}
}
pub(in crate::gui::rough_plan) fn guard_unsaved_results(
host: &Rc<Host>,
proceed: impl FnOnce(&Rc<Host>) + 'static,
) {
let unsaved = {
let session = host.session.borrow();
if session.run.results_unsaved {
session.run.layouts.len()
} else {
0
}
};
if unsaved == 0 {
proceed(host);
return;
}
host.session.borrow_mut().run.pending = Some(Box::new(proceed));
let model = host.window.global::<RoughPlanModel>();
model.set_confirm_replace_count(to_i32(unsaved));
model.set_confirm_replace_open(true);
}
pub(super) fn answer_replace(host: &Rc<Host>, replace: bool) {
let pending = host.session.borrow_mut().run.pending.take();
host.window
.global::<RoughPlanModel>()
.set_confirm_replace_open(false);
if let (true, Some(proceed)) = (replace, pending) {
proceed(host);
}
}
fn launch(host: &Rc<Host>, prepared: Prepared) {
let model = host.window.global::<RoughPlanModel>();
if model.get_running() {
return;
}
let Prepared {
request,
ids,
use_filter,
} = prepared;
let cancel = Arc::new(AtomicBool::new(false));
let screen = Screen::capture(&model);
{
let mut session = host.session.borrow_mut();
session.cancel = Some(Arc::clone(&cancel));
session.run.begin_run(screen);
}
clear_outputs(host);
view::results_changed(host);
if let Some(main) = host.main.upgrade() {
refresh_counts(&main, &host.window, &host.db, &host.source, &host.session);
}
let job = PlanJob {
ids,
model: request.model,
settings: request.settings,
material_name: request.material_name,
weighed_ct: request.weighed_ct,
candidate_source: CandidateSource::from_use_filter(use_filter),
time_limit_secs: request.time_limit_secs,
};
if stored_limit_secs() != request.time_limit_secs {
store_limit_secs(request.time_limit_secs);
}
if let Err(e) = spawn_worker(host.window.as_weak(), Arc::clone(&host.db), job, cancel) {
warn!("Rough planner: could not start the worker thread: {e}");
model.set_running(false);
model.set_stage("".into());
restore_previous(host);
model.set_error_text(format!("Could not start the planner: {e}").into());
}
}
pub(super) fn restore_previous(host: &Rc<Host>) -> bool {
let Some(screen) = host.session.borrow_mut().run.restore_replaced() else {
return false;
};
let model = host.window.global::<RoughPlanModel>();
screen.show(&model);
model.set_results_unsaved(host.session.borrow().run.results_unsaved);
view::results_changed(host);
true
}
pub(super) fn cancel_plan(host: &Rc<Host>) {
if let Some(flag) = &host.session.borrow().cancel {
flag.store(true, Ordering::Relaxed);
}
let model = host.window.global::<RoughPlanModel>();
if model.get_running() {
model.set_stage(CANCELLING_STAGE.into());
}
}
fn push_keep_count(host: &Rc<Host>) {
let kept = host.session.borrow().run.kept();
host.window
.global::<RoughPlanModel>()
.set_keep_count(to_i32(kept));
}
pub(super) fn toggle_keep(host: &Rc<Host>, index: i32) {
let Ok(row_index) = usize::try_from(index) else {
return;
};
let now_kept = {
let mut session = host.session.borrow_mut();
let Some(flag) = session.run.keep.get_mut(row_index) else {
return;
};
*flag = !*flag;
*flag
};
let results = host.window.global::<RoughPlanModel>().get_results();
if let Some(mut row) = results.row_data(row_index) {
row.keep = now_kept;
results.set_row_data(row_index, row);
}
push_keep_count(host);
}
#[cfg(test)]
mod tests {
use super::*;
fn settings(kerf_mm: f64, allowance_mm: f64) -> PlanSettings {
PlanSettings {
kerf_mm,
allowance_mm,
..PlanSettings::default()
}
}
#[test]
fn a_rough_thinner_than_one_kerf_and_both_allowances_is_refused_with_the_figures() {
let ok = check_kerf_and_allowance(&settings(0.25, 0.375), [10.0, 1.0, 8.0]);
assert_eq!(ok, Ok(()), "exactly as thick as needed is enough");
let message = check_kerf_and_allowance(&settings(0.25, 0.375), [10.0, 0.5, 8.0])
.expect_err("0.5 mm is too thin");
assert!(message.contains("(0.25 mm)"), "kerf: {message}");
assert!(message.contains("(0.75 mm)"), "both allowances: {message}");
assert!(message.contains("1.00 mm"), "needed: {message}");
assert!(message.contains("(0.50 mm)"), "smallest side: {message}");
}
#[test]
fn a_block_is_judged_by_its_smallest_side_whichever_axis_that_is() {
let settings = settings(1.0, 1.0);
for extents in [[2.9, 9.0, 9.0], [9.0, 2.9, 9.0], [9.0, 9.0, 2.9]] {
assert!(check_kerf_and_allowance(&settings, extents).is_err());
}
assert_eq!(check_kerf_and_allowance(&settings, [3.0; 3]), Ok(()));
}
#[test]
fn the_first_non_blank_message_wins_and_nothing_gives_an_empty_one() {
assert_eq!(
first_message(["Bad weight", "model", "check"]),
"Bad weight"
);
assert_eq!(first_message(["", " ", "check"]), "check");
assert_eq!(first_message(["", "\t", " "]), "");
}
}