use std::{
cell::RefCell,
collections::BTreeSet,
rc::Rc,
sync::{Arc, Mutex},
};
use indicatrix_cut_core::Edit;
use slint::{ComponentHandle, SharedString};
use crate::{
EditorModel, MainWindow,
bridge::render_thread::RenderContext,
gui::{
editor::{
edit_intent, loading,
stall_guard::stall_guard,
state::{EditorState, angle_nudge_coalesce_key},
view::{SolidLastSolved, refresh_editor_panel_stale, submit_preview_replan},
},
show_toast,
solid_preview::preview_state::SolidPreviewState,
},
};
pub(in crate::gui::editor) fn setup_inline_set_angle_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>()
.on_inline_set_angle(move |index: i32, text: SharedString| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Ok(index) = usize::try_from(index) else {
return;
};
let mut st = state.borrow_mut();
let Some(current) = st.design.tiers.get(index) else {
return;
};
match loading::parse_angle_only(&text) {
Ok(angle_deg) => {
if angle_deg.to_bits() == current.angle_deg.to_bits() {
st.history.end_coalesce_run();
show_toast(&ui, "No change.", "info");
return;
}
let mut tier = current.clone();
tier.angle_deg = angle_deg;
match st.apply(Edit::ModifyTier { index, tier }) {
Ok(()) => {
refresh_editor_panel_stale(
&ui,
&render_ctx,
&st,
&BTreeSet::from([index]),
);
submit_preview_replan(
&ui,
&render_ctx,
&preview_state,
&solid_last_solved,
&st,
BTreeSet::from([index]),
false,
);
}
Err(e) => show_toast(&ui, &e.to_string(), "error"),
}
}
Err(e) => show_toast(&ui, &e, "error"),
}
});
}
const fn clamp_nudge_to_side(current: f64, nudged: f64) -> f64 {
if current.is_sign_negative() == nudged.is_sign_negative() {
return nudged;
}
if current.is_sign_negative() {
-0.0
} else {
0.0
}
}
pub(super) fn tier_nudge_label(tier: &indicatrix_cut_core::ConstraintTier, index: usize) -> String {
if tier.name.is_empty() {
format!("tier {}", index + 1)
} else {
format!("tier {} ({})", index + 1, tier.name)
}
}
pub(in crate::gui::editor) fn setup_nudge_angle_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
) {
let intent_queue = {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
edit_intent::EditIntentQueue::new(move |intent| {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let edit_intent::EditIntent::NudgeAngle { targets, delta_deg } = intent else {
return;
};
apply_nudge_intent(
&ui,
&state,
&render_ctx,
&preview_state,
&solid_last_solved,
&targets,
delta_deg,
);
})
};
let state = Rc::clone(state);
ui.global::<EditorModel>()
.on_nudge_angle(move |anchor_index: i32, delta_deg: f32| {
stall_guard("on_nudge_angle", || {
let Ok(anchor_index) = usize::try_from(anchor_index) else {
return;
};
let st = state.borrow();
let is_multi_target =
st.multi_selected.len() > 1 && st.multi_selected.contains(&anchor_index);
let targets: Vec<usize> = if is_multi_target {
st.multi_selected.iter().copied().collect()
} else {
vec![anchor_index]
};
drop(st);
intent_queue.post(edit_intent::EditIntent::NudgeAngle {
targets,
delta_deg: f64::from(delta_deg),
});
});
});
}
fn apply_nudge_intent(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &SolidLastSolved,
targets: &[usize],
delta_deg: f64,
) {
let mut st = state.borrow_mut();
let mut clamped_labels: Vec<String> = Vec::new();
let changes: Option<Vec<(usize, f64, f64)>> = targets
.iter()
.map(|&index| {
st.design.tiers.get(index).map(|tier| {
let wanted = tier.angle_deg + delta_deg;
let nudged = clamp_nudge_to_side(tier.angle_deg, wanted);
if nudged != wanted {
clamped_labels.push(tier_nudge_label(tier, index));
}
(index, tier.angle_deg, nudged)
})
})
.collect();
let Some(changes) = changes else {
return;
};
if changes.is_empty() {
return;
}
let key = angle_nudge_coalesce_key(targets);
match st.apply_coalescing(Edit::RetargetAngles { changes }, key) {
Ok(()) => {
let dirty: BTreeSet<usize> = targets.iter().copied().collect();
refresh_editor_panel_stale(ui, render_ctx, &st, &dirty);
submit_preview_replan(
ui,
render_ctx,
preview_state,
solid_last_solved,
&st,
dirty,
false,
);
if !clamped_labels.is_empty() {
show_toast(
ui,
&format!(
"{} stopped at 0° -- nudging further would move it into the \
other block. Type the angle directly (e.g. \"-0\") to cross \
blocks on purpose.",
clamped_labels.join(", ")
),
"info",
);
}
}
Err(e) => show_toast(ui, &e.to_string(), "error"),
}
}