indicatrix-cut 0.6.2

Desktop faceting-design editor: library browsing, spectral 3D rendering, material retargeting, and a solid inspection view.
Documentation
//! The narrow, read-only surface the visual before/after compare window
//! (`gui::editor::compare`) reaches this dialog's pending proposal through.
//!
//! Nothing here commits an edit. The compare window's "Keep after" invokes
//! `RetargetModel.apply()` -- i.e. [`super::apply::setup_retarget_apply_callback`],
//! the exact handler the dialog's own Apply button runs -- after checking with
//! [`current_retarget_guard`] that the proposal it compared is still the one that
//! handler would commit. "Discard" turns the dialog's viewport ghost off through
//! [`discard_retarget_preview`], sharing the close handler's own revert.

use super::{
    RETARGET_ASYNC,
    apply::revert_ghost_to_live_design,
    build_retarget_candidate,
    material::{target_display_name, target_material_selection},
};
use crate::{
    MainWindow, RetargetModel,
    bridge::render_thread::RenderContext,
    gui::{
        editor::{retarget::RetargetProposal, state::EditorState, view},
        solid_preview::preview_state::SolidPreviewState,
    },
};
use indicatrix_cut_core::{Design, MaterialSelection};
use slint::ComponentHandle;
use std::sync::{Arc, Mutex, PoisonError, atomic::Ordering as AtomicOrdering};

/// Everything that decides what `RetargetModel.apply()` would commit right now: the
/// held proposal, the design generation it was built against, and the target
/// material the dialog's picker names. The compare window keeps the value it opened
/// with and refuses "Keep after" unless [`current_retarget_guard`] still returns an
/// equal one -- so the cutter can never keep something other than what they
/// inspected (the dialog stays interactive while the window is open).
#[derive(Debug, Clone, PartialEq)]
pub(in crate::gui::editor) struct RetargetKeepGuard {
    proposal: RetargetProposal,
    generation: u64,
    material: MaterialSelection,
}

/// What the compare window needs to open on the dialog's pending proposal.
pub(in crate::gui::editor) struct RetargetCompareInputs {
    /// The live design, cloned -- the "before" side.
    pub(in crate::gui::editor) current: Design,
    /// [`build_retarget_candidate`]'s candidate with the dialog's TARGET material
    /// attached -- the "after" side, exactly what Apply would leave behind (Apply
    /// commits the angles and the material as one `Edit::Batch`).
    pub(in crate::gui::editor) candidate: Design,
    /// The after side's header label, e.g. "Retarget to Ruby (Shift)".
    pub(in crate::gui::editor) after_label: String,
    /// The proposal this comparison describes -- see [`RetargetKeepGuard`].
    pub(in crate::gui::editor) guard: RetargetKeepGuard,
}

/// The held proposal, looked up in the same order
/// [`super::apply::setup_retarget_apply_callback`] consumes it: a Shift-mode
/// proposal on `EditorState`, else a finished Optimize-mode one in
/// [`RETARGET_ASYNC`]. Cloned, never taken.
fn pending_proposal(st: &EditorState) -> Option<(RetargetProposal, u64)> {
    st.pending_retarget
        .clone()
        .or_else(|| RETARGET_ASYNC.with(|cell| cell.borrow().pending.clone()))
}

/// The dialog's target material, resolved exactly as the Apply handler resolves it.
fn target_selection(
    ui: &MainWindow,
    render_ctx: &Arc<Mutex<RenderContext>>,
    design: &Design,
) -> MaterialSelection {
    let custom = render_ctx
        .lock()
        .unwrap_or_else(PoisonError::into_inner)
        .custom_materials
        .as_ref()
        .clone();
    let model = ui.global::<RetargetModel>();
    target_material_selection(
        design,
        &custom,
        model.get_target_material_index(),
        &model.get_target_ri_override_text(),
    )
}

/// What `RetargetModel.apply()` would commit if pressed right now, or `None` when
/// no proposal is held at all.
#[must_use]
pub(in crate::gui::editor) fn current_retarget_guard(
    ui: &MainWindow,
    st: &EditorState,
    render_ctx: &Arc<Mutex<RenderContext>>,
) -> Option<RetargetKeepGuard> {
    let (proposal, generation) = pending_proposal(st)?;
    let material = target_selection(ui, render_ctx, &st.design);
    Some(RetargetKeepGuard {
        proposal,
        generation,
        material,
    })
}

/// The compare window's inputs for the held proposal.
///
/// # Errors
///
/// A cutter-facing sentence when there is nothing honest to compare: no proposal is
/// held, the design moved on since it was built (the same staleness Apply refuses),
/// or its retarget edit no longer applies to the live design.
pub(in crate::gui::editor) fn retarget_compare_inputs(
    ui: &MainWindow,
    st: &EditorState,
    render_ctx: &Arc<Mutex<RenderContext>>,
) -> Result<RetargetCompareInputs, String> {
    let guard = current_retarget_guard(ui, st, render_ctx)
        .ok_or_else(|| "No retarget proposal to compare yet.".to_string())?;
    if guard.generation != st.generation.load(AtomicOrdering::Relaxed) {
        return Err(
            "The design changed since this proposal was built -- click Recompute first."
                .to_string(),
        );
    }
    let mut candidate = build_retarget_candidate(&st.design, &guard.proposal).ok_or_else(|| {
        "This proposal no longer applies to the current design -- click Recompute first."
            .to_string()
    })?;
    candidate.material = guard.material.clone();
    let mode = if ui.global::<RetargetModel>().get_mode_index() == 1 {
        "Optimize"
    } else {
        "Shift"
    };
    let after_label = format!(
        "Retarget to {} ({mode})",
        target_display_name(&guard.material)
    );
    Ok(RetargetCompareInputs {
        current: st.design.clone(),
        candidate,
        after_label,
        guard,
    })
}

/// The compare window's "Discard" for a Retarget comparison: switches the dialog's
/// "Preview in viewport" off and, when it was on, puts the live design back in the
/// shared viewport through the close handler's own revert
/// ([`revert_ghost_to_live_design`]). The proposal itself stays pending and the
/// dialog stays open, so the cutter can adjust it and compare again.
pub(in crate::gui::editor) fn discard_retarget_preview(
    ui: &MainWindow,
    render_ctx: &Arc<Mutex<RenderContext>>,
    preview_state: &Arc<SolidPreviewState>,
    solid_last_solved: &view::SolidLastSolved,
    st: &EditorState,
) {
    let model = ui.global::<RetargetModel>();
    if !model.get_preview_enabled() {
        return;
    }
    model.set_preview_enabled(false);
    revert_ghost_to_live_design(ui, render_ctx, preview_state, solid_last_solved, st);
}