use indicatrix::geometry::{
meet_solver::{MeetTierInput, SolvedTier, VerifiedSolveReport, solve_meet_points_verified},
stone_metrics::ExternalProportions,
};
use slint::{ComponentHandle, Weak};
use std::{
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
thread,
time::{Duration, Instant},
};
const TICK_INTERVAL: Duration = Duration::from_millis(250);
pub struct DeepSolveHandle {
cancel: Arc<AtomicBool>,
}
impl DeepSolveHandle {
pub fn cancel(&self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy)]
pub struct DeepSolveProgress {
pub elapsed: Duration,
}
pub enum DeepSolveOutcome {
Completed {
solved: Vec<SolvedTier>,
report: VerifiedSolveReport,
},
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TierMastDelta {
pub tier_index: usize,
pub before_mast: f64,
pub after_mast: f64,
}
impl TierMastDelta {
#[must_use]
pub fn delta(&self) -> f64 {
self.after_mast - self.before_mast
}
}
const MAST_DELTA_EPSILON: f64 = 1e-6;
#[must_use]
pub fn tier_mast_deltas(current: &[SolvedTier], verified: &[SolvedTier]) -> Vec<TierMastDelta> {
current
.iter()
.zip(verified.iter())
.enumerate()
.filter_map(|(tier_index, (before, after))| {
let delta = after.mast - before.mast;
(delta.abs() > MAST_DELTA_EPSILON).then_some(TierMastDelta {
tier_index,
before_mast: before.mast,
after_mast: after.mast,
})
})
.collect()
}
#[must_use]
pub fn format_tier_mast_deltas(deltas: &[TierMastDelta]) -> String {
if deltas.is_empty() {
return String::new();
}
let tiers: Vec<String> = deltas
.iter()
.map(|d| format!("tier {} ({:+.4})", d.tier_index, d.delta()))
.collect();
format!(" Disagrees with the last solve on: {}.", tiers.join(", "))
}
#[must_use]
pub fn format_verification_targets(targets: &ExternalProportions) -> String {
let mut parts = Vec::new();
if let Some(v) = targets.vol_w3 {
parts.push(format!("Vol/W3 {v:.4}"));
}
if let Some(v) = targets.lw {
parts.push(format!("L/W {v:.4}"));
}
if let Some(v) = targets.cw {
parts.push(format!("C/W {v:.4}"));
}
if let Some(v) = targets.pw {
parts.push(format!("P/W {v:.4}"));
}
if let Some(v) = targets.hw {
parts.push(format!("H/W {v:.4}"));
}
if parts.is_empty() {
return String::new();
}
format!(
" Verified against the printed proportions: {}.",
parts.join(", ")
)
}
#[must_use]
pub const fn edited_since_load_caveat(edited_since_load: bool) -> &'static str {
if edited_since_load {
" Note: this design has been edited since it was loaded -- these targets \
may no longer describe the design you are holding."
} else {
""
}
}
pub fn spawn_deep_solve<T, P, D>(
ui_weak: Weak<T>,
gear_teeth_abs: u32,
tiers: Vec<MeetTierInput>,
targets: ExternalProportions,
on_progress: P,
on_done: D,
) -> DeepSolveHandle
where
T: ComponentHandle + 'static,
P: Fn(&T, DeepSolveProgress) + Send + 'static + Clone,
D: FnOnce(&T, DeepSolveOutcome) + Send + 'static,
{
let cancel = Arc::new(AtomicBool::new(false));
let cancel_ticker = Arc::clone(&cancel);
let cancel_worker = Arc::clone(&cancel);
let done_flag = Arc::new(AtomicBool::new(false));
let done_ticker = Arc::clone(&done_flag);
let ticker_ui = ui_weak.clone();
thread::spawn(move || {
let start = Instant::now();
loop {
thread::sleep(TICK_INTERVAL);
if done_ticker.load(Ordering::Relaxed) || cancel_ticker.load(Ordering::Relaxed) {
break;
}
let elapsed = start.elapsed();
let on_progress = on_progress.clone();
let _ = ticker_ui.upgrade_in_event_loop(move |ui| {
on_progress(&ui, DeepSolveProgress { elapsed });
});
}
});
thread::spawn(move || {
let (solved, report) = solve_meet_points_verified(gear_teeth_abs, &tiers, &targets, &[]);
done_flag.store(true, Ordering::Relaxed);
let cancelled = cancel_worker.load(Ordering::Relaxed);
let outcome = outcome_for(cancelled, solved, report);
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
on_done(&ui, outcome);
});
});
DeepSolveHandle { cancel }
}
fn outcome_for(
cancelled: bool,
solved: Vec<SolvedTier>,
report: VerifiedSolveReport,
) -> DeepSolveOutcome {
if cancelled {
DeepSolveOutcome::Cancelled
} else {
DeepSolveOutcome::Completed { solved, report }
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::meet_solver::SolveStrategy;
fn dummy_report(accepted: bool) -> VerifiedSolveReport {
VerifiedSolveReport {
initial_score: 0.5,
score_after_calibration: 0.3,
final_score: 0.1,
accepted,
overrides_applied: 2,
anchor_moves_applied: 1,
pipeline_runs: 12,
}
}
fn dummy_solved_tier(mast: f64) -> SolvedTier {
SolvedTier {
mast,
strategy: SolveStrategy::DependencyOrder,
detail: String::new(),
}
}
#[test]
fn cancel_sets_the_flag_the_worker_and_ticker_threads_poll() {
let flag = Arc::new(AtomicBool::new(false));
let handle = DeepSolveHandle {
cancel: Arc::clone(&flag),
};
assert!(!flag.load(Ordering::Relaxed));
handle.cancel();
assert!(flag.load(Ordering::Relaxed));
}
#[test]
fn outcome_for_reports_cancelled_and_discards_the_report_when_cancelled_is_true() {
let outcome = outcome_for(true, Vec::new(), dummy_report(true));
assert!(matches!(outcome, DeepSolveOutcome::Cancelled));
}
#[test]
fn outcome_for_reports_completed_with_the_report_when_not_cancelled() {
let report = dummy_report(false);
let solved = vec![dummy_solved_tier(1.0)];
let outcome = outcome_for(false, solved, report);
match outcome {
DeepSolveOutcome::Completed {
solved: s,
report: r,
} => {
assert_eq!(r.accepted, report.accepted);
assert_eq!(r.pipeline_runs, report.pipeline_runs);
assert_eq!(r.final_score, report.final_score);
assert_eq!(s.len(), 1);
}
DeepSolveOutcome::Cancelled => panic!("expected Completed, got Cancelled"),
}
}
#[test]
fn outcome_for_ignores_the_reports_own_accepted_flag_when_cancelled() {
let outcome = outcome_for(true, Vec::new(), dummy_report(true));
assert!(matches!(outcome, DeepSolveOutcome::Cancelled));
}
#[test]
fn tier_mast_deltas_keeps_only_tiers_that_actually_moved() {
let current = vec![
dummy_solved_tier(1.0),
dummy_solved_tier(2.0),
dummy_solved_tier(3.0),
];
let verified = vec![
dummy_solved_tier(1.0),
dummy_solved_tier(2.5),
dummy_solved_tier(3.0),
];
let deltas = tier_mast_deltas(¤t, &verified);
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].tier_index, 1);
assert_eq!(deltas[0].before_mast, 2.0);
assert_eq!(deltas[0].after_mast, 2.5);
assert!((deltas[0].delta() - 0.5).abs() < 1e-9);
}
#[test]
fn tier_mast_deltas_is_empty_when_every_tier_matches() {
let masts = vec![dummy_solved_tier(1.0), dummy_solved_tier(2.0)];
let deltas = tier_mast_deltas(&masts, &masts.clone());
assert_eq!(
deltas.len(),
0,
"identical solves must report no disagreement"
);
}
#[test]
fn tier_mast_deltas_stops_at_the_shorter_slice_instead_of_panicking() {
let current = vec![dummy_solved_tier(1.0), dummy_solved_tier(2.0)];
let verified = vec![dummy_solved_tier(9.0)];
let deltas = tier_mast_deltas(¤t, &verified);
assert_eq!(deltas.len(), 1);
assert_eq!(deltas[0].tier_index, 0);
}
#[test]
fn format_tier_mast_deltas_is_empty_string_when_no_tiers_disagree() {
assert_eq!(format_tier_mast_deltas(&[]), "");
}
#[test]
fn format_tier_mast_deltas_names_the_tier_and_signed_movement() {
let deltas = vec![TierMastDelta {
tier_index: 3,
before_mast: 1.0,
after_mast: 1.25,
}];
let text = format_tier_mast_deltas(&deltas);
assert!(text.contains("tier 3"));
assert!(text.contains("+0.2500"));
}
#[test]
fn format_verification_targets_names_only_the_fields_that_are_set() {
let targets = ExternalProportions {
vol_w3: Some(0.6013),
lw: None,
cw: Some(0.1489),
pw: None,
hw: None,
};
let text = format_verification_targets(&targets);
assert!(text.contains("Vol/W3 0.6013"));
assert!(text.contains("C/W 0.1489"));
assert!(!text.contains("L/W"));
assert!(!text.contains("P/W"));
assert!(!text.contains("H/W"));
}
#[test]
fn format_verification_targets_is_empty_when_nothing_is_set() {
assert_eq!(
format_verification_targets(&ExternalProportions::default()),
""
);
}
#[test]
fn edited_since_load_caveat_is_empty_when_not_edited() {
assert_eq!(edited_since_load_caveat(false), "");
}
#[test]
fn edited_since_load_caveat_warns_when_edited() {
let text = edited_since_load_caveat(true);
assert!(text.contains("edited since it was loaded"));
}
#[test]
fn progress_carries_the_elapsed_duration_it_was_built_with() {
let progress = DeepSolveProgress {
elapsed: Duration::from_millis(1500),
};
assert_eq!(progress.elapsed, Duration::from_millis(1500));
}
#[test]
fn progress_is_copy_not_just_clone() {
let progress = DeepSolveProgress {
elapsed: Duration::from_secs(3),
};
let copied = progress;
assert_eq!(copied.elapsed, progress.elapsed);
}
}