mod keep;
mod provisional;
use super::{
CTX, PROVISIONAL_GENERATION, SESSION, Shared, Target,
drag::{AppliedEdit, GestureInputs, Step},
frame_updates_mast_cache, handles, set_hint, tier_label,
};
use crate::{
MainWindow, ManipulateModel, SolidPreviewModel,
gui::{
editor::{
callbacks::resubmit_facet_overlay,
view::{ReplanSource, submit_preview_replan, submit_preview_replan_chained},
},
show_toast,
solid_preview::{cut_slider::current_steps, preview_state::FrameGeometry},
tutorial_events::raise,
},
};
use glam::Vec3;
use indicatrix::geometry::meet_solver::SolvedTier;
use indicatrix_editor::{
guide::viewing_events as events,
manipulate::{SliceSide, text},
};
use provisional::{ProvisionalSlice, rebuild_indices, set_angle, set_mast, turn};
use slint::ComponentHandle as _;
use std::{
collections::BTreeSet,
sync::{Arc, PoisonError},
};
pub(super) use keep::keep;
pub(super) use provisional::{
DiscardReason, LandedAction, SliceLine, SliceState, Snapshot, cut_hides_tier, cut_moved,
expected_plane_count, landed_action, plan_slice, replan_chain, session_outlives,
surviving_facets, tier_step, with_provisional_tier,
};
#[cfg(test)]
pub(super) use provisional::{keep_allowed, wheel_turn};
const TOO_SHORT_HINT: &str =
"That line is too short to cut a facet. Drag a longer line across the stone.";
const NEEDS_PICTURE_HINT: &str =
"Nothing to slice yet: solve the design first so the stone is on screen.";
const CHANGED_TOAST: &str = "Slice discarded -- the design changed";
const SELECTION_TOAST: &str = "Slice discarded -- another tier was selected";
const NO_DEPTH_HINT: &str =
"Drag the depth handle inward first -- the facet does not touch the stone yet";
const CUT_SLIDER_HINT: &str =
"The Cut slider is hiding the new facet: move it to the end to see and adjust it.";
fn with_provisional<R>(f: impl FnOnce(&mut ProvisionalSlice) -> R) -> Option<R> {
SESSION.with(|cell| cell.borrow_mut().slice.provisional.as_mut().map(f))
}
fn with_provisional_ref<R>(f: impl FnOnce(&ProvisionalSlice) -> R) -> Option<R> {
SESSION.with(|cell| cell.borrow().slice.provisional.as_ref().map(f))
}
pub(super) fn is_active() -> bool {
SESSION.with(|cell| cell.borrow().slice.provisional.is_some())
}
fn provisional_hint(p: &ProvisionalSlice) -> String {
let Some(tier) = p.tier() else {
return String::new();
};
if p.surviving == Some(0) {
return format!(
"New tier {}: {NO_DEPTH_HINT}. Esc discards it.",
tier_label(tier, p.tier_index)
);
}
text::slice_provisional_hint(
&tier_label(tier, p.tier_index),
p.facet_count(),
tier.angle_deg,
p.snapped.index,
)
}
pub(super) fn provisional_hint_text() -> Option<String> {
with_provisional_ref(provisional_hint)
}
pub(super) fn resting_hint(ui: &MainWindow) {
let model = ui.global::<ManipulateModel>();
let cutoff = ui.global::<SolidPreviewModel>().get_tier_cutoff();
let hidden_by_cut = with_provisional_ref(|p| {
tier_step(&p.design, p.tier_index).is_some_and(|step| cut_hides_tier(cutoff, step))
});
let hint = hidden_by_cut
.filter(|&hidden| hidden)
.map(|_| CUT_SLIDER_HINT.to_string())
.or_else(provisional_hint_text)
.or_else(|| {
model
.get_slice_mode()
.then(|| text::slice_mode_hint(model.get_slice_symmetric()))
})
.unwrap_or_default();
set_hint(ui, &hint);
}
pub(super) fn place_provisional(geometry: &FrameGeometry) -> Option<Target> {
with_provisional(|p| {
let map = p.facet_map()?;
handles::target_for(geometry, &map, &p.design, p.tier_index, None, true)
})
.flatten()
}
pub(super) fn gesture_inputs() -> Option<GestureInputs> {
with_provisional_ref(|p| {
let tier = p.tier()?;
Some(GestureInputs {
start_angle_deg: tier.angle_deg,
masts: p.masts.clone(),
restore: Some(Snapshot {
tier: tier.clone(),
snapped: p.snapped,
}),
})
})
.flatten()
}
pub(super) fn note_masts(masts: Vec<SolvedTier>) {
with_provisional(|p| {
if masts.len() == p.design.tiers.len() {
p.masts = Some(masts);
p.facet_map = None;
p.masts_new = true;
}
});
}
pub(super) fn sync_outline(ui: &MainWindow, ctx: &Shared) {
let geometry = ctx
.geometry
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
let outcome = with_provisional(|p| {
if !std::mem::take(&mut p.masts_new) {
return None;
}
let touched_before = provisional::keep_allowed(p.surviving);
let map = p.facet_map()?;
let aligned = geometry
.as_ref()
.is_none_or(|g| handles::ids_aligned(map.facet_count(), g.facet_centroids.len()));
let ids = if aligned {
map.facets_of_tier(p.tier_index).to_vec()
} else {
Vec::new()
};
if let Some(geometry) = &geometry {
p.surviving = surviving_facets(&ids, &geometry.facet_centroids, map.facet_count());
}
let changed = ids != p.outline;
if changed {
p.outline.clone_from(&ids);
}
let starts_cutting = !touched_before && provisional::keep_allowed(p.surviving);
Some((changed, ids, starts_cutting))
})
.flatten();
let Some((changed, ids, starts_cutting)) = outcome else {
return;
};
if changed {
resubmit_facet_overlay(&ctx.preview_state, move |overlay| overlay.provisional = ids);
}
if starts_cutting {
raise(ui, events::SLICE_CUTS_STONE);
}
let hover_active = SESSION.with(|cell| cell.borrow().hover_hint_active);
if !ui.global::<ManipulateModel>().get_dragging() && !hover_active {
resting_hint(ui);
}
}
pub(super) fn apply_step(step: Step) -> Option<AppliedEdit> {
with_provisional(|p| {
let changed = match step {
Step::Angle(deg) => set_angle(p, deg),
Step::Mast(mast) => set_mast(p, mast),
Step::Teeth(more) => turn(p, more),
};
if changed {
p.facet_map = None;
}
changed.then_some(AppliedEdit {
generation: 0,
replaced_meet: None,
})
})
.flatten()
}
pub(super) fn restore_tier(ui: &MainWindow, ctx: &Shared, snapshot: Snapshot) {
let restored = with_provisional(|p| {
let index = p.tier_index;
let Some(slot) = Arc::make_mut(&mut p.design).tiers.get_mut(index) else {
return false;
};
*slot = snapshot.tier;
p.snapped = snapshot.snapped;
p.facet_map = None;
true
})
.unwrap_or(false);
if restored {
resubmit(ui, ctx);
}
}
pub(super) fn resubmit(ui: &MainWindow, ctx: &Shared) {
let Some((design, last_solved, dirty)) = with_provisional(|p| {
p.awaiting = true;
let (last_solved, dirty) =
replan_chain(p.masts.as_deref(), p.design.tiers.len(), p.tier_index);
(Arc::clone(&p.design), last_solved, dirty)
}) else {
return;
};
let cut_steps = current_steps(ui, &design);
let moved =
with_provisional(|p| cut_moved(std::mem::replace(&mut p.cut_steps, cut_steps), cut_steps));
if moved == Some(true) {
ctx.preview_state.set_planes_override(None);
}
submit_preview_replan_chained(
ui,
&ctx.render_ctx,
&ctx.preview_state,
ReplanSource {
design: &design,
generation: PROVISIONAL_GENERATION,
multi_selected: &BTreeSet::new(),
},
dirty,
last_solved,
);
}
fn replan_committed(ui: &MainWindow, ctx: &Shared) {
if let Ok(st) = ctx.state.try_borrow() {
submit_preview_replan(
ui,
&ctx.render_ctx,
&ctx.preview_state,
&ctx.solid_last_solved,
&st,
BTreeSet::new(),
false,
);
}
}
fn tear_down(ui: &MainWindow, ctx: &Shared) -> Option<ProvisionalSlice> {
let (session, dragging_it) = SESSION.with(|cell| {
let mut session = cell.borrow_mut();
let taken = session.slice.provisional.take();
let dragging_it = taken.is_some() && session.drag.as_ref().is_some_and(|d| d.provisional);
if dragging_it {
session.drag = None;
}
(taken, dragging_it)
});
let session = session?;
let model = ui.global::<ManipulateModel>();
model.set_provisional_active(false);
model.set_provisional_label("".into());
if dragging_it {
model.set_dragging(false);
model.set_hovered_handle(-1);
}
resubmit_facet_overlay(&ctx.preview_state, |overlay| overlay.provisional.clear());
ctx.preview_state.set_planes_override(None);
Some(session)
}
pub(super) fn discard(ui: &MainWindow, ctx: &Shared, reason: DiscardReason) {
let Some(session) = tear_down(ui, ctx) else {
return;
};
let label = session
.tier()
.map_or_else(String::new, |tier| tier_label(tier, session.tier_index));
if reason.replans() {
replan_committed(ui, ctx);
}
handles::refresh_handles(ui, ctx);
resting_hint(ui);
let toast = match reason {
DiscardReason::User => text::slice_discarded_toast(&label),
DiscardReason::DesignChanged => CHANGED_TOAST.to_string(),
DiscardReason::SelectionChanged => SELECTION_TOAST.to_string(),
};
show_toast(ui, &toast, "info");
}
pub(super) fn drop_for_selection(ui: &MainWindow) {
let Some(ctx) = CTX.with(|cell| cell.borrow().clone()) else {
return;
};
discard(ui, &ctx, DiscardReason::SelectionChanged);
}
pub(super) fn expire_if_stale(ui: &MainWindow, ctx: &Shared) -> bool {
let Some(base) = with_provisional_ref(|p| p.base_generation) else {
return false;
};
let stale = ctx
.state
.try_borrow()
.is_ok_and(|st| !session_outlives(base, st.current_generation()));
if stale {
discard(ui, ctx, DiscardReason::DesignChanged);
}
stale
}
pub(super) fn on_landed(ui: &MainWindow, ctx: &Shared, frame_generation: u64) -> bool {
let Some((base, awaiting)) = with_provisional_ref(|p| (p.base_generation, p.awaiting)) else {
return false;
};
if !frame_updates_mast_cache(frame_generation) {
with_provisional(|p| p.awaiting = false);
}
let current = ctx
.state
.try_borrow()
.map_or(base, |st| st.current_generation());
match landed_action(Some(base), awaiting, current, frame_generation) {
LandedAction::Ignore => false,
LandedAction::Resubmit => {
resubmit(ui, ctx);
false
}
LandedAction::Discard => {
discard(ui, ctx, DiscardReason::DesignChanged);
true
}
}
}
pub(super) fn clear_awaiting() {
with_provisional(|p| p.awaiting = false);
}
pub(super) fn note_planes(planes: &[(Vec3, f32)]) {
let accepted = with_provisional(|p| {
let masts = p
.masts
.as_deref()
.filter(|masts| masts.len() == p.design.tiers.len())?;
(expected_plane_count(&p.design, masts, p.cut_steps) == planes.len())
.then(|| planes.to_vec())
})
.flatten();
let Some(planes) = accepted else {
return;
};
if let Some(ctx) = CTX.with(|cell| cell.borrow().clone()) {
ctx.preview_state.set_planes_override(Some(planes));
}
}
pub(super) fn toggled(ui: &MainWindow, ctx: &Shared) {
if ui.global::<ManipulateModel>().get_slice_mode() {
SESSION.with(|cell| cell.borrow_mut().hover_hint_active = false);
raise(ui, events::SLICE_STARTED);
} else {
cancel_gesture(ui);
discard(ui, ctx, DiscardReason::User);
}
handles::refresh_handles(ui, ctx);
resting_hint(ui);
}
fn set_line(ui: &MainWindow, start: (f32, f32), end: (f32, f32)) {
let model = ui.global::<ManipulateModel>();
model.set_line_x0(start.0);
model.set_line_y0(start.1);
model.set_line_x1(end.0);
model.set_line_y1(end.1);
model.set_slice_line_visible(true);
}
pub(super) fn begin(ui: &MainWindow, x: f32, y: f32) {
if !ui.global::<ManipulateModel>().get_slice_mode() {
return;
}
SESSION.with(|cell| cell.borrow_mut().slice.gesture = Some((x, y)));
set_line(ui, (x, y), (x, y));
}
pub(super) fn move_to(ui: &MainWindow, x: f32, y: f32) {
if let Some(start) = SESSION.with(|cell| cell.borrow().slice.gesture) {
set_line(ui, start, (x, y));
}
}
pub(super) fn cancel_gesture(ui: &MainWindow) {
SESSION.with(|cell| cell.borrow_mut().slice.gesture = None);
ui.global::<ManipulateModel>().set_slice_line_visible(false);
}
pub(super) fn end(ui: &MainWindow, ctx: &Shared, x: f32, y: f32) {
let Some(start) = SESSION.with(|cell| cell.borrow_mut().slice.gesture.take()) else {
return;
};
ui.global::<ManipulateModel>().set_slice_line_visible(false);
let geometry = ctx
.geometry
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
let Some(geometry) = geometry else {
set_hint(ui, NEEDS_PICTURE_HINT);
return;
};
let line = SliceLine::new(
handles::pointer_to_pick(ui, start.0, start.1),
handles::pointer_to_pick(ui, x, y),
&geometry,
);
let corners = with_provisional_ref(|p| Arc::clone(&p.base_corners))
.unwrap_or_else(|| Arc::clone(&geometry.corner_points));
if install(ui, ctx, line, SliceSide::Right, corners) {
raise(ui, events::SLICE_DRAWN);
}
}
fn install(
ui: &MainWindow,
ctx: &Shared,
line: SliceLine,
side: SliceSide,
corners: Arc<Vec<Vec3>>,
) -> bool {
let model = ui.global::<ManipulateModel>();
let symmetric = model.get_slice_symmetric();
let built = ctx.state.try_borrow().ok().and_then(|st| {
let plan = plan_slice(&line, side, &corners, &st.design, symmetric)?;
let (design, tier_index) = with_provisional_tier(&st.design, plan.tier)?;
let label = tier_label(design.tiers.get(tier_index)?, tier_index);
Some((
design,
tier_index,
plan.snapped,
st.current_generation(),
label,
))
});
let Some((design, tier_index, snapped, base_generation, label)) = built else {
set_hint(ui, TOO_SHORT_HINT);
return false;
};
SESSION.with(|cell| {
let mut session = cell.borrow_mut();
session.hover_hint_active = false;
session.slice.provisional = Some(ProvisionalSlice::new(
base_generation,
design,
tier_index,
side,
snapped,
line,
corners,
));
});
ctx.preview_state.set_planes_override(None);
model.set_provisional_label(label.into());
model.set_provisional_active(true);
resubmit(ui, ctx);
resting_hint(ui);
true
}
pub(super) fn flip(ui: &MainWindow, ctx: &Shared) {
if expire_if_stale(ui, ctx) {
return;
}
let Some((line, side, corners)) =
with_provisional_ref(|p| (p.line, p.side.flipped(), Arc::clone(&p.base_corners)))
else {
return;
};
if install(ui, ctx, line, side, corners) {
raise(ui, events::SLICE_FLIPPED);
}
}
pub(super) fn symmetric_toggled(ui: &MainWindow, ctx: &Shared) {
if expire_if_stale(ui, ctx) {
return;
}
let symmetric = ui.global::<ManipulateModel>().get_slice_symmetric();
if with_provisional(|p| rebuild_indices(p, symmetric)).unwrap_or(false) {
resubmit(ui, ctx);
}
resting_hint(ui);
}