use crate::{
EditorModel, MainWindow, RetargetModel, RetargetRowItem,
gui::{
editor::{
native_io,
state::{EditorState, design_label_text},
view,
},
show_toast,
},
};
use indicatrix::geometry::meet_solver::SolvedTier;
use indicatrix_cut_core::{Design, TierDelta, diff_tiers};
use slint::{Color, ComponentHandle, ModelRc, VecModel};
use std::{cell::RefCell, rc::Rc, sync::Arc};
thread_local! {
static DESIGN_SNAPSHOT: RefCell<Option<DesignSnapshot>> = const { RefCell::new(None) };
}
#[derive(Clone)]
struct DesignSnapshot {
design: Design,
solved: Option<Vec<SolvedTier>>,
label: String,
}
pub(super) struct DiffRowView {
pub(super) tier_index: usize,
pub(super) name: String,
pub(super) old_angle: String,
pub(super) new_angle: String,
pub(super) mast_delta: String,
pub(super) status_label: &'static str,
pub(super) status_rgb: (u8, u8, u8),
}
const MAST_DIFF_TOLERANCE: f64 = 1e-4;
fn diff_status_label_and_rgb(delta: &TierDelta) -> (&'static str, (u8, u8, u8)) {
if delta.added() {
("Added", (0x38, 0xbd, 0xf8))
} else if delta.removed() {
("Removed", (0xf4, 0x3f, 0x5e))
} else if delta.angle_changed()
|| delta.indices_changed()
|| delta.mast_changed(MAST_DIFF_TOLERANCE)
{
("Changed", (0xf5, 0x9e, 0x0b))
} else {
("Same", (0x10, 0xb9, 0x81))
}
}
pub(super) fn diff_row_view(delta: &TierDelta) -> DiffRowView {
let angle_text =
|a: Option<f64>| a.map_or_else(|| "-".to_string(), |v| format!("{v:.2}\u{b0}"));
let mast_delta = match (delta.mast_before, delta.mast_after) {
(Some(before), Some(after)) => format!("{:+.4}", after - before),
_ => "-".to_string(),
};
let (status_label, status_rgb) = diff_status_label_and_rgb(delta);
DiffRowView {
tier_index: delta.index,
name: delta.name.clone(),
old_angle: angle_text(delta.angle_before),
new_angle: angle_text(delta.angle_after),
mast_delta,
status_label,
status_rgb,
}
}
#[must_use]
pub(super) fn diff_rows_from_deltas(deltas: &[TierDelta]) -> Vec<RetargetRowItem> {
deltas
.iter()
.map(diff_row_view)
.map(|v| RetargetRowItem {
tier_index: i32::try_from(v.tier_index).unwrap_or(i32::MAX),
block: "".into(),
name: v.name.into(),
old_angle: v.old_angle.into(),
new_angle: v.new_angle.into(),
margin: v.mast_delta.into(),
risk_label: v.status_label.into(),
risk_color: Color::from_rgb_u8(v.status_rgb.0, v.status_rgb.1, v.status_rgb.2),
})
.collect()
}
fn store_design_snapshot(
ui: &MainWindow,
design: &Design,
solved: Option<Vec<SolvedTier>>,
label: &str,
) {
DESIGN_SNAPSHOT.with(|cell| {
*cell.borrow_mut() = Some(DesignSnapshot {
design: design.clone(),
solved,
label: label.to_string(),
});
});
ui.global::<EditorModel>().set_has_snapshot(true);
show_toast(ui, &format!("Snapshot taken: \"{label}\"."), "success");
}
fn show_compare_to_snapshot(
ui: &MainWindow,
snapshot: &DesignSnapshot,
design: &Design,
current_label: &str,
current_solved: Option<&[SolvedTier]>,
) {
let deltas = diff_tiers(
&snapshot.design.tiers,
snapshot.solved.as_deref(),
&design.tiers,
current_solved,
);
let rows = diff_rows_from_deltas(&deltas);
ui.global::<RetargetModel>()
.set_compare_rows(ModelRc::new(VecModel::from(rows)));
ui.global::<RetargetModel>().set_compare_label(
format!("\"{}\" vs. current (\"{current_label}\")", snapshot.label).into(),
);
ui.global::<RetargetModel>().set_compare_open(true);
}
pub(in crate::gui::editor) fn setup_snapshot_callbacks(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
solid_last_solved: &view::SolidLastSolved,
) {
{
let state = Rc::clone(state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_snapshot_design(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let (design, label, cached) = {
let st = state.borrow();
let cached = solid_last_solved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.filter(|s| s.len() == st.design.tiers.len());
(
Arc::new(st.design.clone()),
design_label_text(st.asc_filename.as_deref()),
cached,
)
};
if let Some(solved) = cached {
store_design_snapshot(&ui, &design, Some(solved), &label);
return;
}
native_io::resolve_solved_then(&ui, design, move |ui, design, solved| {
store_design_snapshot(ui, &design, solved.ok(), &label);
});
});
}
{
let state = Rc::clone(state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_compare_to_snapshot(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let snapshot = DESIGN_SNAPSHOT.with(|cell| cell.borrow().clone());
let Some(snapshot) = snapshot else {
show_toast(
&ui,
"No snapshot taken yet -- use Snapshot Design first.",
"error",
);
return;
};
let (design, current_label, cached) = {
let st = state.borrow();
let cached = solid_last_solved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.filter(|s| s.len() == st.design.tiers.len());
(
Arc::new(st.design.clone()),
design_label_text(st.asc_filename.as_deref()),
cached,
)
};
if let Some(current_solved) = &cached {
show_compare_to_snapshot(
&ui,
&snapshot,
&design,
¤t_label,
Some(current_solved),
);
return;
}
native_io::resolve_solved_then(&ui, design, move |ui, design, solved| {
show_compare_to_snapshot(
ui,
&snapshot,
&design,
¤t_label,
solved.ok().as_deref(),
);
});
});
}
let ui_weak = ui.as_weak();
ui.global::<RetargetModel>().on_compare_close(move || {
if let Some(ui) = ui_weak.upgrade() {
ui.global::<RetargetModel>().set_compare_open(false);
}
});
}