use super::viewport::scaled_viewport_size;
use crate::{
MainWindow, OptimizeResultRow,
bridge::render_thread::RenderContext,
gui::{
editor::{
optimize_panel,
solve_service::{
SolveHandle, SolveKind, SolveOutcome, SolveRequest, SolveResult, SolveService,
},
},
solid_preview::{
cut_slider,
preview_state::{CameraPose, SolidPreviewState},
},
},
};
use indicatrix::geometry::meet_solver::SolvedTier;
use indicatrix_cut_core::{Design, OptimizeOutcome};
use indicatrix_editor::optimize_view::build_candidate_preview_design;
use slint::ComponentHandle;
use std::{
cell::RefCell,
sync::{Arc, Mutex},
};
#[cfg(test)]
pub(in crate::gui::editor) use indicatrix_editor::optimize_view::{
optimize_status_text, parse_optimize_weights,
};
pub(in crate::gui::editor) fn build_optimize_preview_design(
design: &Design,
outcome: &OptimizeOutcome,
) -> Design {
optimize_panel::candidate_for_outcome(outcome).map_or_else(
|| indicatrix_editor::optimize_view::build_optimize_preview_design(design, outcome),
|candidate| build_candidate_preview_design(design, &candidate),
)
}
pub(in crate::gui::editor) fn optimize_result_rows(
outcome: &OptimizeOutcome,
) -> Vec<OptimizeResultRow> {
indicatrix_editor::optimize_view::optimize_result_rows(outcome)
.into_iter()
.map(result_row)
.collect()
}
pub(in crate::gui::editor) fn result_row(
line: indicatrix_editor::optimize_view::OptimizeResultLine,
) -> OptimizeResultRow {
OptimizeResultRow {
label: line.label.into(),
before: line.before.into(),
after: line.after.into(),
direction: line.direction,
}
}
struct PendingGhost {
design: Arc<Design>,
render_ctx: Arc<Mutex<RenderContext>>,
preview_state: Arc<SolidPreviewState>,
}
struct GhostTracker<P> {
latest: u64,
pending: Option<P>,
}
impl<P> GhostTracker<P> {
const fn new() -> Self {
Self {
latest: 0,
pending: None,
}
}
fn begin(&mut self, pending: P) -> u64 {
self.latest = self.latest.wrapping_add(1);
self.pending = Some(pending);
self.latest
}
fn invalidate(&mut self) {
self.latest = self.latest.wrapping_add(1);
self.pending = None;
}
const fn take_if_current(&mut self, generation: u64, superseded: bool) -> Option<P> {
if superseded || generation != self.latest {
return None;
}
self.pending.take()
}
}
struct GhostState {
service: Option<SolveService>,
handle: Option<SolveHandle>,
tracker: GhostTracker<PendingGhost>,
}
impl GhostState {
const fn new() -> Self {
Self {
service: None,
handle: None,
tracker: GhostTracker::new(),
}
}
fn cancel(&mut self) {
if let Some(handle) = self.handle.take() {
handle.cancel();
}
self.tracker.invalidate();
}
}
thread_local! {
static GHOST: RefCell<GhostState> = const { RefCell::new(GhostState::new()) };
}
pub(super) fn cancel_ghost_preview() {
GHOST.with(|cell| cell.borrow_mut().cancel());
}
#[must_use]
pub(in crate::gui::editor) fn submit_design_ghost_preview(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
design: &Design,
) -> bool {
GHOST.with(|cell| {
let mut ghost = cell.borrow_mut();
let state = &mut *ghost;
let service = state.service.get_or_insert_with(|| {
SolveService::new(ui.as_weak(), |_ui, _progress| {}, apply_ghost_result)
});
if let Some(previous) = state.handle.take() {
previous.cancel();
}
let design = Arc::new(design.clone());
let generation = state.tracker.begin(PendingGhost {
design: Arc::clone(&design),
render_ctx: Arc::clone(render_ctx),
preview_state: Arc::clone(preview_state),
});
state.handle = Some(service.submit(SolveRequest {
design,
generation,
kind: SolveKind::GhostPreview,
}));
true
})
}
fn apply_ghost_result(ui: &MainWindow, result: SolveResult) {
let pending = GHOST.with(|cell| {
cell.borrow_mut()
.tracker
.take_if_current(result.generation, result.superseded)
});
let Some(pending) = pending else {
return;
};
if let SolveOutcome::Solved(Ok(solved)) = result.outcome {
draw_ghost(ui, &pending, &solved);
}
}
fn draw_ghost(ui: &MainWindow, pending: &PendingGhost, solved: &[SolvedTier]) {
let steps = cut_slider::current_steps(ui, &pending.design);
let stone = cut_slider::cut_geometry(&pending.design, Some(solved), steps);
let ctx = pending
.render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let camera = CameraPose {
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
};
let design_gear = ctx.design_gear;
let view_mode = ui.global::<crate::SolidPreviewModel>().get_view_mode() as u8;
let size = crate::gui::render::camera_lighting::contained_request_size(
view_mode,
scaled_viewport_size(ui),
(ctx.width, ctx.height),
);
drop(ctx);
pending
.preview_state
.request_redraw_geometry(stone, camera, size, view_mode, design_gear);
}
#[cfg(test)]
mod tests {
use super::GhostTracker;
#[test]
fn the_newest_request_is_applied_exactly_once() {
let mut tracker = GhostTracker::new();
let generation = tracker.begin("candidate");
assert_eq!(
tracker.take_if_current(generation, false),
Some("candidate")
);
assert_eq!(
tracker.take_if_current(generation, false),
None,
"a result is applied once"
);
}
#[test]
fn a_result_for_an_older_request_is_dropped_and_keeps_the_newer_one_wanted() {
let mut tracker = GhostTracker::new();
let first = tracker.begin("first");
let second = tracker.begin("second");
assert_ne!(first, second);
assert_eq!(tracker.take_if_current(first, false), None);
assert_eq!(
tracker.take_if_current(second, false),
Some("second"),
"the stale result must not consume the newer request"
);
}
#[test]
fn a_superseded_result_is_dropped_even_for_the_newest_generation() {
let mut tracker = GhostTracker::new();
let generation = tracker.begin("candidate");
assert_eq!(tracker.take_if_current(generation, true), None);
}
#[test]
fn an_invalidation_drops_the_result_still_in_flight() {
let mut tracker = GhostTracker::new();
let generation = tracker.begin("candidate");
tracker.invalidate();
assert_eq!(tracker.take_if_current(generation, false), None);
}
#[test]
fn a_request_after_an_invalidation_is_wanted_again() {
let mut tracker = GhostTracker::new();
let stale = tracker.begin("stale");
tracker.invalidate();
let fresh = tracker.begin("fresh");
assert_eq!(tracker.take_if_current(stale, false), None);
assert_eq!(tracker.take_if_current(fresh, false), Some("fresh"));
}
}