use crate::gui::editor::{auto_solve, state::EditorState};
use indicatrix::geometry::meet_solver::SolvedTier;
use indicatrix_cut_core::Design;
use indicatrix_editor::guide::{GoalContext, Guide, GuideCatalog, Perform, goal_met};
use std::{cell::RefCell, rc::Rc};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) enum LaunchWant {
NewDesign,
Library { entry_id: i64 },
}
#[derive(Clone, Debug)]
pub(super) struct Launch {
pub(super) guide_id: String,
pub(super) want: LaunchWant,
pub(super) ticks: u32,
pub(super) design_arrived: bool,
pub(super) uuid_before: String,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct UiFacts {
pub(super) solved_closed: bool,
pub(super) view_mode: i32,
pub(super) inspector_tab: i32,
}
#[derive(Default)]
struct Runtime {
catalog: GuideCatalog,
events: Vec<String>,
launch: Option<Launch>,
state: Option<Rc<RefCell<EditorState>>>,
}
thread_local! {
static RUNTIME: RefCell<Runtime> = RefCell::new(Runtime::default());
}
fn with<R>(f: impl FnOnce(&mut Runtime) -> R) -> R {
RUNTIME.with_borrow_mut(f)
}
pub(super) fn install_state(state: &Rc<RefCell<EditorState>>) {
with(|rt| rt.state = Some(Rc::clone(state)));
}
pub(super) fn state() -> Option<Rc<RefCell<EditorState>>> {
with(|rt| rt.state.clone())
}
pub(super) fn guide(id: &str) -> Option<Guide> {
with(|rt| rt.catalog.get(id).cloned())
}
pub(super) fn step_perform(guide_id: &str, index: usize) -> Option<Perform> {
with(|rt| {
rt.catalog
.get(guide_id)
.and_then(|guide| guide.steps.get(index))
.and_then(|step| step.perform.clone())
})
}
pub(super) fn with_catalog<R>(f: impl FnOnce(&GuideCatalog) -> R) -> R {
with(|rt| f(&rt.catalog))
}
pub(super) fn register_generated(guide: Guide) -> Result<(), String> {
with(|rt| rt.catalog.add_generated(guide))
}
pub(super) fn record_event(name: &str) {
with(|rt| {
if !rt.events.iter().any(|event| event == name) {
rt.events.push(name.to_owned());
}
});
}
pub(super) fn clear_events() {
with(|rt| rt.events.clear());
}
fn masts_if_aligned(cache: Option<&(u64, Vec<SolvedTier>)>, tiers: usize) -> Option<Vec<f64>> {
let (_, solved) = cache?;
(solved.len() == tiers).then(|| solved.iter().map(|tier| tier.mast).collect())
}
fn cached_masts(design: &Design) -> Option<Vec<f64>> {
let cache = auto_solve::solid_last_solved()?;
let guard = cache
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
masts_if_aligned(guard.as_ref(), design.tiers.len())
}
pub(super) fn goal_is_met(guide_id: &str, step: usize, design: &Design, facts: UiFacts) -> bool {
with(|rt| {
let Some(step) = rt
.catalog
.get(guide_id)
.and_then(|guide| guide.steps.get(step))
else {
return false;
};
let masts = if facts.solved_closed && step.goal.wants_solved_masts() {
cached_masts(design)
} else {
None
};
let mut ctx = GoalContext::new(design)
.solved_closed(facts.solved_closed)
.events(&rt.events)
.view_mode(facts.view_mode)
.inspector_tab(facts.inspector_tab);
if let Some(masts) = masts.as_deref() {
ctx = ctx.solved_masts(masts);
}
goal_met(&step.goal, &ctx)
})
}
pub(super) fn set_launch(launch: Option<Launch>) {
with(|rt| rt.launch = launch);
}
pub(super) fn launch() -> Option<Launch> {
with(|rt| rt.launch.clone())
}
pub(super) fn count_launch_tick() {
with(|rt| {
if let Some(launch) = rt.launch.as_mut() {
launch.ticks = launch.ticks.saturating_add(1);
}
});
}
pub(super) fn mark_new_design_arrived() {
with(|rt| {
if let Some(Launch {
want: LaunchWant::NewDesign,
design_arrived,
..
}) = rt.launch.as_mut()
{
*design_arrived = true;
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix::geometry::meet_solver::SolveStrategy;
use indicatrix_cut_core::{ConstraintTier, PreformSpec, ScheduleMeta};
use indicatrix_editor::guide::{COMPARE_STEP_TITLE, build_this_design_guide};
fn solved(masts: &[f64]) -> (u64, Vec<SolvedTier>) {
let tiers = masts
.iter()
.map(|&mast| SolvedTier {
mast,
strategy: SolveStrategy::ScaleReference,
detail: String::new(),
})
.collect();
(3, tiers)
}
#[test]
fn cached_depths_are_used_only_when_they_cover_the_design() {
let cache = solved(&[1.0, 0.5]);
assert_eq!(masts_if_aligned(Some(&cache), 2), Some(vec![1.0, 0.5]));
assert_eq!(masts_if_aligned(Some(&cache), 3), None);
assert_eq!(masts_if_aligned(Some(&cache), 0), None);
assert_eq!(masts_if_aligned(None, 2), None);
}
#[test]
fn a_depth_goal_does_not_complete_without_a_closed_solve_and_cached_depths() {
let design = Design::new(
PreformSpec::cylinder(96, 1.5, 1.0, 1.5),
ScheduleMeta::standard_round_brilliant(),
ConstraintTier::standard_round_brilliant(),
);
let guide = build_this_design_guide(&design, None, "Round brilliant", "test-key")
.expect("a lesson for the round brilliant");
let compare = guide
.steps
.iter()
.position(|step| step.title == COMPARE_STEP_TITLE)
.expect("a compare step");
let id = guide.id.clone();
register_generated(guide).expect("a fit lesson");
let facts = |solved_closed| UiFacts {
solved_closed,
view_mode: 0,
inspector_tab: 0,
};
assert!(!goal_is_met(&id, compare, &design, facts(false)));
assert!(!goal_is_met(&id, compare, &design, facts(true)));
}
#[test]
fn a_goal_of_an_unknown_guide_is_never_met() {
let design = Design::new(
PreformSpec::cylinder(96, 1.5, 1.0, 1.5),
ScheduleMeta::standard_round_brilliant(),
Vec::new(),
);
let facts = UiFacts {
solved_closed: true,
view_mode: 0,
inspector_tab: 0,
};
assert!(!goal_is_met("no-such-guide", 0, &design, facts));
}
}