use super::{
CachedFacetMap, SESSION, Shared, Target, diagram, kind_to_int, set_hint, slice, snap_mode,
tier_label,
};
use crate::{
EditorModel, MainWindow, ManipulateModel, PreferencesModel,
gui::{
editor::callbacks::{
map_to_pick_coordinates, pick_margin, pick_pixels_to_logical, selected_facet_id,
},
solid_preview::{facet_map::FacetMap, preview_state::FrameGeometry},
},
};
use glam::Vec3;
use indicatrix::{geometry::meet_solver::SolvedTier, optics::raytracer::Camera};
use indicatrix_editor::manipulate::{
FacetFrame, HANDLE_HIT_RADIUS_PX, HandleKind, HandleLayout, ScreenPoint, ScreenSize,
handle_layout, hit_test, text, tiers_meeting,
};
use slint::ComponentHandle as _;
use std::{rc::Rc, sync::PoisonError};
pub(super) const CAMERA_FOV_DEG: f32 = indicatrix::optics::raytracer::DEFAULT_FOV_DEG;
const HANDLE_LENGTH_FRACTION: f64 = 0.35;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct LogicalHandles {
pub(super) anchor: (f32, f32),
pub(super) angle_tip: (f32, f32),
pub(super) depth_tip: (f32, f32),
pub(super) index_tip: (f32, f32),
}
pub(super) fn layout_to_logical(
layout: &HandleLayout,
scale: f32,
margin: (f32, f32),
) -> LogicalHandles {
let convert = |p: ScreenPoint| pick_pixels_to_logical((p.x, p.y), scale, margin);
LogicalHandles {
anchor: convert(layout.anchor),
angle_tip: convert(layout.angle_tip),
depth_tip: convert(layout.depth_tip),
index_tip: convert(layout.index_tip),
}
}
pub(super) fn pick_facet(
remembered: Option<u32>,
tier: usize,
facets_of_tier: &[u32],
tier_of: impl Fn(u32) -> Option<usize>,
has_centroid: impl Fn(u32) -> bool,
) -> Option<u32> {
remembered
.filter(|&id| tier_of(id) == Some(tier) && has_centroid(id))
.or_else(|| facets_of_tier.iter().copied().find(|&id| has_centroid(id)))
}
pub(super) const fn masts_aligned(tier_count: usize, masts_len: usize) -> bool {
tier_count == masts_len
}
pub(super) const fn ids_aligned(map_facets: usize, centroid_count: usize) -> bool {
map_facets == centroid_count
}
pub(super) fn hit_kind(
layout: &HandleLayout,
indexless: bool,
p: ScreenPoint,
radius: f32,
) -> Option<HandleKind> {
let mut layout = *layout;
if indexless {
layout.index_tip = ScreenPoint::new(f32::NAN, f32::NAN);
}
hit_test(&layout, p, radius)
}
pub(super) fn hit_kind_without_angle(
layout: &HandleLayout,
indexless: bool,
p: ScreenPoint,
radius: f32,
) -> Option<HandleKind> {
let mut layout = *layout;
layout.angle_tip = ScreenPoint::new(f32::NAN, f32::NAN);
hit_kind(&layout, indexless, p, radius)
}
pub(super) fn driven_relation(ctx: &Shared, tier: usize) -> Option<String> {
ctx.state
.try_borrow()
.ok()
.and_then(|st| st.design.relation_text(tier))
}
pub(super) fn driven_hint_due(previous: Option<SelectionKey>, now: SelectionKey) -> bool {
!now.1 && previous != Some(now)
}
pub(super) fn pointer_to_pick(ui: &MainWindow, x: f32, y: f32) -> ScreenPoint {
let (px, py) = map_to_pick_coordinates(ui, x, y);
ScreenPoint::new(px, py)
}
fn solved_summary(ctx: &Shared) -> Option<(u64, usize)> {
ctx.solid_last_solved
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.map(|(generation, masts)| (*generation, masts.len()))
}
pub(super) fn aligned_masts(ctx: &Shared, tier_count: usize) -> Option<Vec<SolvedTier>> {
ctx.solid_last_solved
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_ref()
.filter(|(_, masts)| masts_aligned(tier_count, masts.len()))
.map(|(_, masts)| masts.clone())
}
pub(super) fn facet_map_for(
ctx: &Shared,
design_generation: u64,
design: &indicatrix_cut_core::Design,
) -> Option<Rc<FacetMap>> {
let (solved_generation, masts_len) = solved_summary(ctx)?;
if !masts_aligned(design.tiers.len(), masts_len) {
return None;
}
let cached = SESSION.with(|cell| {
cell.borrow()
.facet_map
.as_ref()
.filter(|c| {
c.design_generation == design_generation && c.solved_generation == solved_generation
})
.map(|c| Rc::clone(&c.map))
});
if cached.is_some() {
return cached;
}
let masts = aligned_masts(ctx, design.tiers.len())?;
let map = Rc::new(FacetMap::from_design(design, &masts));
SESSION.with(|cell| {
cell.borrow_mut().facet_map = Some(CachedFacetMap {
design_generation,
solved_generation,
map: Rc::clone(&map),
});
});
Some(map)
}
fn centroid_of(centroids: &[Option<Vec3>], facet_id: u32) -> Option<Vec3> {
centroids.get(facet_id as usize).copied().flatten()
}
pub(super) fn target_for(
geometry: &FrameGeometry,
map: &FacetMap,
design: &indicatrix_cut_core::Design,
tier: usize,
remembered: Option<u32>,
provisional: bool,
) -> Option<Target> {
let tier_data = design.tiers.get(tier)?;
let centroids = geometry.facet_centroids.as_slice();
if !ids_aligned(map.facet_count(), centroids.len()) {
return None;
}
let facet_id = pick_facet(
remembered,
tier,
map.facets_of_tier(tier),
|id| map.tier_of(id as usize),
|id| centroid_of(centroids, id).is_some(),
)?;
let centroid = centroid_of(centroids, facet_id)?;
let frame = FacetFrame::from_tier_with_reference(
tier_data,
f64::from(map.index_on_gear(facet_id as usize)),
design.meta.gear_reference_angle as f32,
design.meta.gear_teeth_abs(),
centroid,
);
let pose = geometry.camera;
let camera = Camera::new(pose.yaw, pose.pitch, pose.distance, CAMERA_FOV_DEG);
let size = ScreenSize::new(geometry.size.0 as f32, geometry.size.1 as f32);
let length = (HANDLE_LENGTH_FRACTION * geometry.bounding_radius) as f32;
let layout = handle_layout(&frame, &camera, size, length)?;
Some(Target {
tier,
frame,
layout,
label: tier_label(tier_data, tier),
provisional,
diagram: None,
})
}
fn place(ui: &MainWindow, ctx: &Shared) -> Option<Target> {
let geometry = ctx
.geometry
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()?;
if diagram::is_diagram_view(ui) {
return diagram::place(ui, ctx, &geometry);
}
if geometry.diagram.is_some() {
return None;
}
if ui.global::<ManipulateModel>().get_slice_mode() {
return slice::place_provisional(&geometry);
}
let tier = usize::try_from(ui.global::<EditorModel>().get_selected_tier_index()).ok()?;
let st = ctx.state.try_borrow().ok()?;
let map = facet_map_for(ctx, st.current_generation(), &st.design)?;
target_for(
&geometry,
&map,
&st.design,
tier,
selected_facet_id(),
false,
)
}
pub(super) type SelectionKey = (usize, bool);
const fn selection_key(target: &Target) -> SelectionKey {
(target.tier, target.provisional)
}
pub(super) fn selection_hint_due(
large_handles: bool,
previous: Option<SelectionKey>,
now: SelectionKey,
) -> bool {
large_handles && !now.1 && previous != Some(now)
}
fn hint_line_is_free(ui: &MainWindow) -> bool {
let model = ui.global::<ManipulateModel>();
!model.get_dragging()
&& !model.get_slice_mode()
&& SESSION.with(|cell| cell.borrow().hovered.is_none())
}
fn show_selection_hint(ui: &MainWindow, hint: &str) {
SESSION.with(|cell| cell.borrow_mut().hover_hint_active = true);
set_hint(ui, hint);
}
pub(super) fn selection_hint_text(
label: &str,
driven_by: Option<&str>,
large_handles: bool,
previous: Option<SelectionKey>,
now: SelectionKey,
) -> Option<String> {
if let Some(relation) = driven_by {
return driven_hint_due(previous, now)
.then(|| text::angle_follows_relation_hint(label, relation));
}
selection_hint_due(large_handles, previous, now).then(|| text::handle_select_hint(label))
}
pub(super) fn refresh_handles(ui: &MainWindow, ctx: &Shared) {
let previous = SESSION.with(|cell| cell.borrow().target.as_ref().map(selection_key));
match place(ui, ctx) {
Some(target) => {
let key = selection_key(&target);
let label = target.label.clone();
let driven_by = driven_relation(ctx, target.tier);
show(ui, target, driven_by.is_none());
let large = ui.global::<PreferencesModel>().get_large_handles();
let hint = selection_hint_text(&label, driven_by.as_deref(), large, previous, key);
if let Some(hint) = hint
&& hint_line_is_free(ui)
{
show_selection_hint(ui, &hint);
}
}
None => hide(ui),
}
}
pub(super) const fn hit_radius_px(window_scale: f32, handle_scale: f32) -> f32 {
HANDLE_HIT_RADIUS_PX * window_scale * handle_scale
}
fn show(ui: &MainWindow, target: Target, angle_visible: bool) {
let scale = ui.window().scale_factor();
let logical = layout_to_logical(&target.layout, scale, pick_margin(ui));
let indexless = target.frame.is_indexless();
let offered = target.diagram.map(|placed| placed.available);
let finite_tip = |tip: (f32, f32)| {
if tip.0.is_finite() && tip.1.is_finite() {
tip
} else {
logical.anchor
}
};
let (angle_tip, depth_tip, index_tip) = (
finite_tip(logical.angle_tip),
finite_tip(logical.depth_tip),
finite_tip(logical.index_tip),
);
let model = ui.global::<ManipulateModel>();
model.set_anchor_x(logical.anchor.0);
model.set_anchor_y(logical.anchor.1);
model.set_angle_tip_x(angle_tip.0);
model.set_angle_tip_y(angle_tip.1);
model.set_depth_tip_x(depth_tip.0);
model.set_depth_tip_y(depth_tip.1);
model.set_index_tip_x(index_tip.0);
model.set_index_tip_y(index_tip.1);
model.set_index_visible(!indexless && offered.is_none_or(|a| a.index));
model.set_angle_visible(angle_visible && offered.is_none_or(|a| a.angle));
model.set_depth_visible(offered.is_none_or(|a| a.depth));
model.set_handles_visible(true);
SESSION.with(|cell| cell.borrow_mut().target = Some(target));
}
fn hide(ui: &MainWindow) {
let had_hint = SESSION.with(|cell| {
let mut session = cell.borrow_mut();
session.target = None;
session.hovered = None;
std::mem::take(&mut session.hover_hint_active)
});
let model = ui.global::<ManipulateModel>();
model.set_handles_visible(false);
model.set_hovered_handle(-1);
if had_hint {
slice::resting_hint(ui);
}
}
fn hit_kind_at(ui: &MainWindow, x: f32, y: f32) -> Option<HandleKind> {
let (layout, indexless) = SESSION.with(|cell| {
cell.borrow()
.target
.as_ref()
.map(|t| (t.layout, t.frame.is_indexless()))
})?;
let model = ui.global::<ManipulateModel>();
let radius = diagram::hit_radius(
ui,
hit_radius_px(ui.window().scale_factor(), model.get_handle_scale()),
);
let pointer = pointer_to_pick(ui, x, y);
if model.get_angle_visible() {
hit_kind(&layout, indexless, pointer, radius)
} else {
hit_kind_without_angle(&layout, indexless, pointer, radius)
}
}
pub(super) fn hit_test_int(ui: &MainWindow, x: f32, y: f32) -> i32 {
hit_kind_at(ui, x, y).map_or(-1, kind_to_int)
}
fn hover_hint(ui: &MainWindow, ctx: &Shared, kind: HandleKind) -> Option<String> {
let (tier, label, provisional) = SESSION.with(|cell| {
cell.borrow()
.target
.as_ref()
.map(|t| (t.tier, t.label.clone(), t.provisional))
})?;
let meeting = if provisional {
0
} else {
ctx.state
.try_borrow()
.map_or(0, |st| tiers_meeting(&st.design, tier).len())
};
let snap = snap_mode(false, ui.global::<ManipulateModel>().get_snap_off());
Some(text::handle_hover_hint(kind, &label, meeting, snap))
}
fn apply_hover(ui: &MainWindow, ctx: &Shared, kind: Option<HandleKind>) {
let model = ui.global::<ManipulateModel>();
model.set_hovered_handle(kind.map_or(-1, kind_to_int));
let Some(hint) = kind.and_then(|kind| hover_hint(ui, ctx, kind)) else {
let had_hint =
SESSION.with(|cell| std::mem::take(&mut cell.borrow_mut().hover_hint_active));
if had_hint {
slice::resting_hint(ui);
}
return;
};
SESSION.with(|cell| cell.borrow_mut().hover_hint_active = true);
set_hint(ui, &hint);
}
pub(super) fn hover(ui: &MainWindow, ctx: &Shared, x: f32, y: f32) {
if ui.global::<ManipulateModel>().get_dragging() {
return;
}
diagram::follow_pointer(ui, ctx, x, y);
let kind = hit_kind_at(ui, x, y);
let changed = SESSION.with(|cell| {
let mut session = cell.borrow_mut();
let changed = session.hovered != kind;
session.hovered = kind;
changed
});
if changed {
apply_hover(ui, ctx, kind);
}
}
pub(super) fn refresh_hover_hint(ui: &MainWindow, ctx: &Shared) {
if ui.global::<ManipulateModel>().get_dragging() {
return;
}
let hovered = SESSION.with(|cell| cell.borrow().hovered);
if hovered.is_some() {
apply_hover(ui, ctx, hovered);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn layout() -> HandleLayout {
HandleLayout {
anchor: ScreenPoint::new(100.0, 100.0),
angle_tip: ScreenPoint::new(100.0, 40.0),
depth_tip: ScreenPoint::new(160.0, 100.0),
index_tip: ScreenPoint::new(40.0, 100.0),
angle_dir: (0.0, -1.0),
depth_dir: (1.0, 0.0),
index_dir: (-1.0, 0.0),
pixels_per_degree: 2.0,
pixels_per_mast_unit: 100.0,
pixels_per_tooth: 10.0,
}
}
#[test]
fn a_tier_that_follows_a_relation_has_no_angle_handle_to_grab() {
let layout = layout();
let on_angle_tip = ScreenPoint::new(100.0, 40.0);
assert_eq!(
hit_kind(&layout, false, on_angle_tip, 12.0),
Some(HandleKind::Angle)
);
assert_eq!(
hit_kind_without_angle(&layout, false, on_angle_tip, 12.0),
None
);
assert_eq!(
hit_kind_without_angle(&layout, false, ScreenPoint::new(160.0, 100.0), 12.0),
Some(HandleKind::Depth)
);
assert_eq!(
hit_kind_without_angle(&layout, false, ScreenPoint::new(40.0, 100.0), 12.0),
Some(HandleKind::Index)
);
assert_eq!(
hit_kind_without_angle(&layout, true, ScreenPoint::new(40.0, 100.0), 12.0),
None
);
}
#[test]
fn the_why_hint_is_due_once_per_new_selection_and_never_for_the_slice_tier() {
assert!(driven_hint_due(None, (3, false)));
assert!(driven_hint_due(Some((2, false)), (3, false)));
assert!(!driven_hint_due(Some((3, false)), (3, false)), "same frame");
assert!(!driven_hint_due(None, (3, true)), "the provisional tier");
}
#[test]
fn a_driven_tier_says_why_whatever_the_handle_size_and_others_keep_the_old_rule() {
let driven = |large, previous| {
selection_hint_text("P2", Some("P1 - 2"), large, previous, (1, false))
};
let expected = text::angle_follows_relation_hint("P2", "P1 - 2");
assert_eq!(driven(false, None), Some(expected.clone()));
assert_eq!(driven(true, None), Some(expected));
assert_eq!(driven(true, Some((1, false))), None, "already announced");
let free = |large, previous| selection_hint_text("P1", None, large, previous, (0, false));
assert_eq!(free(true, None), Some(text::handle_select_hint("P1")));
assert_eq!(free(false, None), None, "only with Larger handles");
assert_eq!(free(true, Some((0, false))), None);
}
}