use super::{
LoadedFingerprints, announce,
convert::CandidateSource,
dto::{DesignShape, SavedDesignDto},
format::{RankedLayout, SerializeInput, design_shape, serialize_checked_plan},
naming::{clean_plan_name, current_unix_time, default_plan_name},
show_error, spawn_task, store,
};
use crate::{
RoughPlanModel,
gui::rough_plan::{
editing,
host::Host,
run::{ResultsSource, RunState},
session::Session,
},
};
use indicatrix_cut_core::rough_plan::{PlanSettings, RoughLayout, RoughModel};
use indicatrix_vault::db::sqlite::Database;
use slint::ComponentHandle;
use std::{
collections::{BTreeMap, BTreeSet},
rc::Rc,
sync::{Mutex, PoisonError},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum LibraryId {
Local,
Kept(Option<u32>),
}
pub(super) struct Snapshot {
model: RoughModel,
settings: PlanSettings,
material_name: String,
weighed_ct: Option<f64>,
candidate_source: CandidateSource,
layouts: Vec<(usize, RoughLayout)>,
titles: BTreeMap<i64, String>,
stored_shapes: BTreeMap<i64, DesignShape>,
library: LibraryId,
}
fn rows_to_save(row_count: usize, keep: &[bool], selected_only: bool) -> Vec<usize> {
let ticked: Vec<usize> = (0..row_count)
.filter(|&row| keep.get(row).copied().unwrap_or(false))
.collect();
if selected_only && !ticked.is_empty() {
ticked
} else {
(0..row_count).collect()
}
}
pub(super) fn stored_shapes_and_library(
run: &RunState,
loaded: Option<&LoadedFingerprints>,
) -> (BTreeMap<i64, DesignShape>, LibraryId) {
match (&run.source, loaded) {
(ResultsSource::Loaded { plan_id, .. }, Some(loaded)) if loaded.plan_id == *plan_id => {
(loaded.shapes.clone(), LibraryId::Kept(loaded.library_id))
}
_ => (run.shapes.clone(), LibraryId::Local),
}
}
fn snapshot(session: &Session, selected_only: bool) -> Result<Snapshot, String> {
snapshot_of(&session.run, session.saved.loaded.as_ref(), selected_only)
}
pub(super) fn snapshot_of(
run: &RunState,
loaded: Option<&LoadedFingerprints>,
selected_only: bool,
) -> Result<Snapshot, String> {
if run.layouts.is_empty() {
return Err("There are no results to save. Plan first.".to_string());
}
let (Some(model), Some(settings)) = (&run.plan_model, &run.plan_settings) else {
return Err("The results on screen carry no model to save.".to_string());
};
let layouts = rows_to_save(run.layouts.len(), &run.keep, selected_only)
.into_iter()
.filter_map(|row| Some((row + 1, run.layouts.get(row)?.clone())))
.collect();
let (stored_shapes, library) = stored_shapes_and_library(run, loaded);
Ok(Snapshot {
model: model.clone(),
settings: *settings,
material_name: run.material_name.clone(),
weighed_ct: run.weighed_ct,
candidate_source: run.candidate_source,
layouts,
titles: run.titles.clone(),
stored_shapes,
library,
})
}
pub(super) fn begin_save(host: &Rc<Host>, selected_only: bool) {
let model = host.window.global::<RoughPlanModel>();
if model.get_running() {
show_error(host, "Wait for the plan to finish before saving.");
return;
}
if let Err(message) = editing::commit_pending_base(host) {
show_error(host, &message);
return;
}
let name = {
let mut session = host.session.borrow_mut();
if let Err(message) = snapshot(&session, selected_only) {
drop(session);
show_error(host, &message);
return;
}
session.saved.selected_only = selected_only;
let planned = session.run.plan_model.as_ref().unwrap_or(&session.model);
default_plan_name(planned, &session.run.material_name, current_unix_time())
};
show_error(host, "");
model.set_save_name(name.into());
model.set_saved_open(false);
model.set_save_name_open(true);
}
fn design_records(snapshot: &Snapshot, database: &Database) -> Result<Vec<SavedDesignDto>, String> {
let ids: Vec<i64> = snapshot
.layouts
.iter()
.flat_map(|(_, layout)| layout.stones.iter().map(|stone| stone.entry_id))
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let extents = database
.solid_extents_for(&ids)
.map_err(|e| format!("Could not read the design measurements: {e}"))?;
Ok(ids
.into_iter()
.map(|entry_id| {
let shape = snapshot
.stored_shapes
.get(&entry_id)
.copied()
.or_else(|| {
extents
.get(&entry_id)
.and_then(|row| row.extents)
.map(|found| design_shape(&found))
})
.unwrap_or_default();
SavedDesignDto {
entry_id,
title: snapshot
.titles
.get(&entry_id)
.cloned()
.unwrap_or_else(|| format!("Design {entry_id}")),
fingerprint: shape.fingerprint,
width_caliper: shape.width_caliper,
extents_version: shape.extents_version,
}
})
.collect())
}
pub(super) fn write_plan(
db: &Mutex<Database>,
name: &str,
snapshot: &Snapshot,
) -> Result<(), String> {
let library_id = match snapshot.library {
LibraryId::Local => store::library_stamp(db),
LibraryId::Kept(id) => id,
};
let designs = {
let database = db.lock().unwrap_or_else(PoisonError::into_inner);
design_records(snapshot, &database)?
};
let ranked: Vec<RankedLayout<'_>> = snapshot
.layouts
.iter()
.map(|(rank, layout)| RankedLayout {
rank: *rank,
layout,
})
.collect();
let text = serialize_checked_plan(&SerializeInput {
name,
created_at: current_unix_time(),
library_id,
model: &snapshot.model,
material_name: &snapshot.material_name,
weighed_ct: snapshot.weighed_ct,
settings: &snapshot.settings,
candidate_source: snapshot.candidate_source,
designs: &designs,
layouts: &ranked,
})?;
store::save_plan(db, name, &text).map(|_id| ())
}
fn saved_message(count: usize, name: &str) -> String {
format!(
"Saved {count} result{} as \"{name}\".",
if count == 1 { "" } else { "s" }
)
}
fn mark_saved(host: &Host, saved: usize, rows: usize) {
let model = host.window.global::<RoughPlanModel>();
let mut session = host.session.borrow_mut();
if saved == rows && !model.get_running() && session.run.layouts.len() == rows {
session.run.results_unsaved = false;
model.set_results_unsaved(false);
}
}
pub(super) fn confirm_save(host: &Rc<Host>) {
let model = host.window.global::<RoughPlanModel>();
let name = clean_plan_name(&model.get_save_name());
if name.is_empty() {
show_error(host, "Enter a name for the saved plan.");
return;
}
let selected_only = host.session.borrow().saved.selected_only;
let taken = {
let session = host.session.borrow();
snapshot(&session, selected_only).map(|taken| (taken, session.run.layouts.len()))
};
let (taken, all_rows) = match taken {
Ok(taken) => taken,
Err(message) => {
show_error(host, &message);
return;
}
};
show_error(host, "");
model.set_save_name_open(false);
let count = taken.layouts.len();
let task_name = name.clone();
spawn_task(
host,
move |db| write_plan(db, &task_name, &taken),
move |host, result| match result {
Ok(()) => {
mark_saved(host, count, all_rows);
announce(host, &saved_message(count, &name));
super::list::refresh_list(host);
}
Err(message) => {
show_error(host, &message);
let model = host.window.global::<RoughPlanModel>();
model.set_save_name(name.into());
model.set_save_name_open(true);
}
},
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn save_selected_covers_the_ticked_rows_or_everything_when_none_is_ticked() {
let keep = [false, true, false, true];
assert_eq!(rows_to_save(4, &keep, true), vec![1, 3]);
assert_eq!(rows_to_save(4, &keep, false), vec![0, 1, 2, 3]);
assert_eq!(rows_to_save(3, &[false; 3], true), vec![0, 1, 2]);
assert_eq!(rows_to_save(3, &[false, true], true), vec![1]);
assert_eq!(rows_to_save(0, &[], true), Vec::<usize>::new());
}
#[test]
fn the_confirmation_counts_the_results() {
assert_eq!(saved_message(1, "A"), "Saved 1 result as \"A\".");
assert_eq!(saved_message(3, "B"), "Saved 3 results as \"B\".");
}
}