use super::{
SESSION, Shared, Target, handles, kind_from_int, kind_to_int, set_hint, slice, snap_mode,
};
use crate::{
MainWindow, ManipulateModel,
gui::{
editor::{
callbacks::resubmit_facet_overlay,
edit_intent::{EditIntent, EditIntentQueue},
relation_ui::{edit_error_text, with_followers},
state::{EditorState, coalesce_timestamp},
view::{refresh_editor_panel_stale, submit_preview_replan},
},
show_toast,
},
};
use indicatrix::geometry::meet_solver::{MeetConstraint, SolvedTier};
use indicatrix_cut_core::EditError;
use indicatrix_editor::manipulate::{
DragStart, DragValue, HandleKind, IndexRotation, drag_value, moved_tiers, text,
};
use slint::ComponentHandle as _;
use std::{collections::BTreeSet, time::Duration};
const MOVED_TOLERANCE: f64 = 1e-6;
const CANCELLED_TOAST: &str = "Drag cancelled. The design is back the way it was.";
const NEEDS_SOLVE_HINT: &str = "Solve the design first: a depth drag needs the tier's solved mast.";
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum Step {
Angle(f64),
Mast(f64),
Teeth(i64),
}
#[derive(Debug, Clone, PartialEq)]
pub(super) struct AppliedEdit {
pub(super) generation: u64,
pub(super) replaced_meet: Option<MeetConstraint>,
}
#[derive(Debug, Default)]
pub(super) struct DragProgress {
pub(super) last_value: Option<DragValue>,
pub(super) teeth_applied: i64,
pub(super) applied_any: bool,
pub(super) applied_generation: Option<u64>,
pub(super) replaced_meet: Option<MeetConstraint>,
}
impl DragProgress {
pub(super) fn plan(&mut self, value: DragValue) -> Option<Step> {
if self.last_value == Some(value) {
return None;
}
self.last_value = Some(value);
match value {
DragValue::AngleDeg(deg) => Some(Step::Angle(deg)),
DragValue::Mast(mast) => Some(Step::Mast(mast)),
DragValue::IndexTeeth(total) => {
let more = total - self.teeth_applied;
(more != 0).then_some(Step::Teeth(more))
}
}
}
pub(super) fn record(&mut self, step: Step, applied: AppliedEdit) {
self.applied_any = true;
self.applied_generation = Some(applied.generation);
if let Step::Teeth(more) = step {
self.teeth_applied += more;
}
if self.replaced_meet.is_none() {
self.replaced_meet = applied.replaced_meet;
}
}
}
pub(super) fn drain_value(
progress: &mut DragProgress,
value: DragValue,
apply: impl FnOnce(Step) -> Option<AppliedEdit>,
) -> bool {
let Some(step) = progress.plan(value) else {
return false;
};
let Some(applied) = apply(step) else {
return false;
};
progress.record(step, applied);
true
}
pub(super) struct ActiveDrag {
pub(super) start: DragStart,
pub(super) tier: usize,
pub(super) label: String,
pub(super) start_masts: Vec<SolvedTier>,
pub(super) progress: DragProgress,
pub(super) requested: Option<DragValue>,
pub(super) outlined: Vec<usize>,
pub(super) gesture_now: Duration,
pub(super) provisional: bool,
pub(super) restore: Option<slice::Snapshot>,
pub(super) rotation: Option<IndexRotation>,
}
pub(super) struct GestureInputs {
pub(super) start_angle_deg: f64,
pub(super) masts: Option<Vec<SolvedTier>>,
pub(super) restore: Option<slice::Snapshot>,
}
pub(super) const fn start_value(
kind: HandleKind,
start_angle_deg: f64,
start_mast: f64,
) -> DragValue {
match kind {
HandleKind::Angle => DragValue::AngleDeg(start_angle_deg),
HandleKind::Depth => DragValue::Mast(start_mast),
HandleKind::Index => DragValue::IndexTeeth(0),
}
}
fn live_value(drag: &ActiveDrag) -> DragValue {
let untouched = start_value(
drag.start.kind,
drag.start.start_angle_deg,
drag.start.start_mast,
);
drag.requested
.or(drag.progress.last_value)
.unwrap_or(untouched)
}
fn final_value(drag: &ActiveDrag) -> DragValue {
match drag.start.kind {
HandleKind::Index => DragValue::IndexTeeth(drag.progress.teeth_applied),
_ => live_value(drag),
}
}
fn gesture_inputs(ctx: &Shared, target: &Target) -> Option<GestureInputs> {
if target.provisional {
return slice::gesture_inputs();
}
let mut st = ctx.state.try_borrow_mut().ok()?;
let start_angle_deg = st.design.tiers.get(target.tier)?.angle_deg;
let masts = handles::aligned_masts(ctx, st.design.tiers.len());
st.history.end_coalesce_run();
Some(GestureInputs {
start_angle_deg,
masts,
restore: None,
})
}
pub(super) fn begin(ui: &MainWindow, ctx: &Shared, kind: i32, x: f32, y: f32) {
let Some(kind) = kind_from_int(kind) else {
return;
};
let Some(target) = SESSION.with(|cell| cell.borrow().target.clone()) else {
return;
};
if kind == HandleKind::Index && target.frame.is_indexless() {
return;
}
if kind == HandleKind::Angle
&& let Some(relation) = handles::driven_relation(ctx, target.tier)
{
set_hint(
ui,
&text::angle_follows_relation_hint(&target.label, &relation),
);
return;
}
let Some(GestureInputs {
start_angle_deg,
masts,
restore,
}) = gesture_inputs(ctx, &target)
else {
return;
};
if kind == HandleKind::Depth && masts.is_none() {
set_hint(ui, NEEDS_SOLVE_HINT);
return;
}
let start_masts = masts.unwrap_or_default();
let start_mast = start_masts.get(target.tier).map_or(0.0, |s| s.mast);
let pointer = handles::pointer_to_pick(ui, x, y);
let rotation = target
.diagram
.and_then(|placed| placed.rotation)
.filter(|_| kind == HandleKind::Index)
.map(|mut rotation| {
rotation.begin(pointer);
rotation
});
let drag = ActiveDrag {
start: DragStart {
kind,
start_angle_deg,
start_mast,
pointer,
layout: target.layout,
},
tier: target.tier,
label: target.label,
start_masts,
progress: DragProgress::default(),
requested: None,
outlined: Vec::new(),
gesture_now: coalesce_timestamp(),
provisional: target.provisional,
restore,
rotation,
};
let hint = if drag.provisional {
slice::provisional_hint_text().unwrap_or_default()
} else {
text::drag_live_hint(
kind,
&drag.label,
&start_value(kind, start_angle_deg, start_mast),
0,
)
};
SESSION.with(|cell| {
let mut session = cell.borrow_mut();
session.drag = Some(drag);
session.hover_hint_active = false;
});
let model = ui.global::<ManipulateModel>();
model.set_dragging(true);
model.set_hovered_handle(kind_to_int(kind));
set_hint(ui, &hint);
}
pub(super) fn move_to(ui: &MainWindow, queue: &EditIntentQueue, x: f32, y: f32, shift: bool) {
let snap = snap_mode(shift, ui.global::<ManipulateModel>().get_snap_off());
let pointer = handles::pointer_to_pick(ui, x, y);
let posted = SESSION.with(|cell| {
let mut session = cell.borrow_mut();
let drag = session.drag.as_mut()?;
let value = match drag.rotation.as_mut() {
Some(rotation) => DragValue::IndexTeeth(rotation.teeth_at(pointer)),
None => drag_value(&drag.start, pointer, snap),
};
drag.requested = Some(value);
Some((drag.tier, value))
});
if let Some((tier, value)) = posted {
queue.post(EditIntent::DragTier { tier, value });
}
}
pub(super) fn apply_intent(ui: &MainWindow, ctx: &Shared, intent: &EditIntent) {
let EditIntent::DragTier { tier, value } = *intent else {
return;
};
let Some(mut drag) = SESSION.with(|cell| cell.borrow_mut().drag.take()) else {
return;
};
if drag.tier == tier {
apply_value(ui, ctx, &mut drag, value);
refresh_feedback(ui, ctx, &mut drag);
}
SESSION.with(|cell| cell.borrow_mut().drag = Some(drag));
}
fn apply_step(
st: &mut EditorState,
tier: usize,
step: Step,
now: Duration,
) -> Result<Option<AppliedEdit>, EditError> {
Ok(match step {
Step::Angle(deg) => st
.set_tier_angle(tier, deg, now)?
.map(|change| AppliedEdit {
generation: change.generation,
replaced_meet: None,
}),
Step::Mast(mast) => st.pin_tier_mast(tier, mast, now)?.map(|pin| AppliedEdit {
generation: pin.change.generation,
replaced_meet: pin.replaced_meet,
}),
Step::Teeth(more) => st
.rotate_tier_indices(tier, more, now)?
.map(|change| AppliedEdit {
generation: change.generation,
replaced_meet: None,
}),
})
}
fn apply_value(ui: &MainWindow, ctx: &Shared, drag: &mut ActiveDrag, value: DragValue) {
if drag.provisional {
if drain_value(&mut drag.progress, value, slice::apply_step) {
slice::resubmit(ui, ctx);
}
return;
}
let Ok(mut st) = ctx.state.try_borrow_mut() else {
return;
};
let (tier, now) = (drag.tier, drag.gesture_now);
let mut failure: Option<String> = None;
let applied = drain_value(&mut drag.progress, value, |step| {
match apply_step(&mut st, tier, step, now) {
Ok(edit) => edit,
Err(error) => {
failure = Some(edit_error_text(&mut st, &error));
None
}
}
});
if applied {
let dirty = with_followers(&st.design, [tier]);
refresh_editor_panel_stale(ui, &ctx.render_ctx, &st, &dirty);
submit_preview_replan(
ui,
&ctx.render_ctx,
&ctx.preview_state,
&ctx.solid_last_solved,
&st,
dirty,
false,
);
}
drop(st);
if let Some(message) = failure {
show_toast(ui, &message, "error");
}
}
fn facets_of_tiers(ctx: &Shared, tiers: &[usize]) -> Vec<u32> {
let Ok(st) = ctx.state.try_borrow() else {
return Vec::new();
};
let Some(map) = handles::facet_map_for(ctx, st.current_generation(), &st.design) else {
return Vec::new();
};
tiers
.iter()
.flat_map(|&tier| map.facets_of_tier(tier).iter().copied())
.collect()
}
fn refresh_feedback(ui: &MainWindow, ctx: &Shared, drag: &mut ActiveDrag) {
if drag.provisional {
if let Some(hint) = slice::provisional_hint_text() {
set_hint(ui, &hint);
}
return;
}
let moved = handles::aligned_masts(ctx, drag.start_masts.len()).map_or_else(Vec::new, |now| {
moved_tiers(&drag.start_masts, &now, drag.tier, MOVED_TOLERANCE)
});
let hint = text::drag_live_hint(drag.start.kind, &drag.label, &live_value(drag), moved.len());
set_hint(ui, &hint);
if moved != drag.outlined {
let ids = facets_of_tiers(ctx, &moved);
drag.outlined = moved;
resubmit_facet_overlay(&ctx.preview_state, |overlay| overlay.moved = ids);
}
}
pub(super) fn update_feedback(ui: &MainWindow, ctx: &Shared) {
let Some(mut drag) = SESSION.with(|cell| cell.borrow_mut().drag.take()) else {
return;
};
refresh_feedback(ui, ctx, &mut drag);
SESSION.with(|cell| cell.borrow_mut().drag = Some(drag));
}
fn finish_ui(ui: &MainWindow, ctx: &Shared, drag: &ActiveDrag) {
if !drag.outlined.is_empty() {
resubmit_facet_overlay(&ctx.preview_state, |overlay| overlay.moved.clear());
}
let model = ui.global::<ManipulateModel>();
model.set_dragging(false);
model.set_hovered_handle(-1);
SESSION.with(|cell| {
let mut session = cell.borrow_mut();
session.hovered = None;
session.hover_hint_active = false;
});
slice::resting_hint(ui);
}
pub(super) fn end(ui: &MainWindow, ctx: &Shared) {
let Some(mut drag) = SESSION.with(|cell| cell.borrow_mut().drag.take()) else {
return;
};
if let Some(value) = drag.requested {
apply_value(ui, ctx, &mut drag, value);
}
if !drag.provisional
&& let Ok(mut st) = ctx.state.try_borrow_mut()
{
st.history.end_coalesce_run();
}
finish_ui(ui, ctx, &drag);
if drag.progress.applied_any && !drag.provisional {
let toast = text::drag_done_toast(
drag.start.kind,
&drag.label,
&final_value(&drag),
drag.progress.replaced_meet.as_ref(),
);
show_toast(ui, &toast, "info");
}
}
pub(super) fn cancel(ui: &MainWindow, ctx: &Shared) {
let Some(drag) = SESSION.with(|cell| cell.borrow_mut().drag.take()) else {
return;
};
if drag.provisional {
if let Some(tier) = drag.restore.clone() {
slice::restore_tier(ui, ctx, tier);
}
} else if drag.progress.applied_any {
undo_gesture(ui, ctx, &drag);
} else if let Ok(mut st) = ctx.state.try_borrow_mut() {
st.history.end_coalesce_run();
}
finish_ui(ui, ctx, &drag);
}
fn undo_gesture(ui: &MainWindow, ctx: &Shared, drag: &ActiveDrag) {
let Ok(mut st) = ctx.state.try_borrow_mut() else {
return;
};
st.history.end_coalesce_run();
if Some(st.current_generation()) != drag.progress.applied_generation {
drop(st);
show_toast(
ui,
"Something else changed the design during the drag, so Escape left it alone. Use Undo to step back.",
"info",
);
return;
}
match st.undo() {
Ok(true) => {
refresh_editor_panel_stale(
ui,
&ctx.render_ctx,
&st,
&(0..st.design.tiers.len()).collect(),
);
submit_preview_replan(
ui,
&ctx.render_ctx,
&ctx.preview_state,
&ctx.solid_last_solved,
&st,
BTreeSet::new(),
true,
);
drop(st);
show_toast(ui, CANCELLED_TOAST, "info");
}
Ok(false) => {}
Err(error) => {
drop(st);
show_toast(ui, &format!("Could not cancel the drag: {error}"), "error");
}
}
}