use super::super::{
frame_updates_mast_cache,
handles::{CAMERA_FOV_DEG, ids_aligned},
};
use crate::gui::solid_preview::{
cut_slider::MODEL_ROUGH,
facet_map::FacetMap,
live_update::{CutLimit, limit_visible_tiers},
preview_state::FrameGeometry,
};
use glam::Vec3;
use indicatrix::{
geometry::meet_solver::{MeetConstraint, SolvedTier},
optics::raytracer::Camera,
};
use indicatrix_cut_core::{
ConstraintTier, Design, Edit, design::TierRef, expected_orbit, rotate_indices,
};
use indicatrix_editor::{
manipulate::{
ScreenPoint, ScreenSize, SliceSide, SnappedFacet, slice_normal, slice_tier, snap_to_gear,
tangency_mast,
},
session::clamp_nudge_to_side,
};
use std::{collections::BTreeSet, rc::Rc, sync::Arc};
const SLICE_ANGLE_STEP_DEG: f64 = 0.1;
pub(in super::super) const fn session_outlives(
base_generation: u64,
current_generation: u64,
) -> bool {
base_generation == current_generation
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in super::super) enum LandedAction {
Ignore,
Resubmit,
Discard,
}
pub(in super::super) const fn landed_action(
base_generation: Option<u64>,
awaiting: bool,
current_generation: u64,
frame_generation: u64,
) -> LandedAction {
match base_generation {
None => LandedAction::Ignore,
Some(base) if !session_outlives(base, current_generation) => LandedAction::Discard,
Some(_) if awaiting || !frame_updates_mast_cache(frame_generation) => LandedAction::Ignore,
Some(_) => LandedAction::Resubmit,
}
}
pub(in super::super) fn replan_chain(
masts: Option<&[SolvedTier]>,
tier_count: usize,
tier_index: usize,
) -> (Option<Vec<SolvedTier>>, BTreeSet<usize>) {
match masts {
Some(masts) if masts.len() == tier_count => {
(Some(masts.to_vec()), BTreeSet::from([tier_index]))
}
_ => (None, BTreeSet::new()),
}
}
pub(in super::super) fn surviving_facets(
tier_facets: &[u32],
centroids: &[Option<Vec3>],
map_facets: usize,
) -> Option<usize> {
ids_aligned(map_facets, centroids.len()).then(|| {
tier_facets
.iter()
.filter(|&&id| centroids.get(id as usize).is_some_and(Option::is_some))
.count()
})
}
pub(in super::super) fn expected_plane_count(
design: &Design,
masts: &[SolvedTier],
cut_steps: Option<usize>,
) -> usize {
let limit = cut_steps.map_or(CutLimit::Finished, CutLimit::Steps);
let visible = limit_visible_tiers(design, limit);
FacetMap::from_design_cut(design, masts, &[], visible.as_deref()).facet_count()
}
pub(in super::super) fn cut_moved(previous: Option<usize>, now: Option<usize>) -> bool {
previous != now
}
pub(in super::super) fn cut_hides_tier(cutoff: i32, step: usize) -> bool {
cutoff == MODEL_ROUGH || usize::try_from(cutoff).is_ok_and(|through| through < step)
}
pub(in super::super) fn tier_step(design: &Design, tier_index: usize) -> Option<usize> {
design
.preview_steps()
.iter()
.position(|step| *step == TierRef::Flat(tier_index))
}
pub(in super::super) fn keep_allowed(surviving: Option<usize>) -> bool {
surviving.is_some_and(|count| count > 0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in super::super) enum DiscardReason {
User,
DesignChanged,
SelectionChanged,
}
impl DiscardReason {
pub(super) const fn replans(self) -> bool {
!matches!(self, Self::SelectionChanged)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(in super::super) struct SliceLine {
pub(in super::super) a: ScreenPoint,
pub(in super::super) b: ScreenPoint,
pub(in super::super) size: ScreenSize,
pub(in super::super) pose: (f32, f32, f32),
}
impl SliceLine {
pub(super) const fn new(a: ScreenPoint, b: ScreenPoint, geometry: &FrameGeometry) -> Self {
Self {
a,
b,
size: ScreenSize::new(geometry.size.0 as f32, geometry.size.1 as f32),
pose: (
geometry.camera.yaw,
geometry.camera.pitch,
geometry.camera.distance,
),
}
}
pub(in super::super) fn normal(&self, side: SliceSide) -> Option<Vec3> {
let (yaw, pitch, distance) = self.pose;
let camera = Camera::new(yaw, pitch, distance, CAMERA_FOV_DEG);
slice_normal(&camera, self.a, self.b, self.size, side)
}
}
pub(in super::super) struct SlicePlan {
pub(in super::super) snapped: SnappedFacet,
pub(in super::super) tier: ConstraintTier,
}
pub(in super::super) fn plan_slice(
line: &SliceLine,
side: SliceSide,
corners: &[Vec3],
design: &Design,
symmetric: bool,
) -> Option<SlicePlan> {
let normal = line.normal(side)?;
let snapped = snap_to_gear(normal, design.meta.gear_teeth_abs(), SLICE_ANGLE_STEP_DEG);
let mast = tangency_mast(snapped.normal, corners);
let names: Vec<String> = design
.tiers
.iter()
.flat_map(|tier| tier.names().into_iter().map(str::to_string))
.collect();
let tier = slice_tier(&snapped, mast, &design.meta, symmetric, &names);
Some(SlicePlan { snapped, tier })
}
pub(in super::super) fn with_provisional_tier(
design: &Design,
tier: ConstraintTier,
) -> Option<(Design, usize)> {
let mut provisional = design.clone();
let index = provisional.tiers.len();
provisional.apply_edit(Edit::AddTier { index, tier }).ok()?;
Some((provisional, index))
}
#[derive(Debug, Clone)]
pub(in super::super) struct Snapshot {
pub(super) tier: ConstraintTier,
pub(super) snapped: SnappedFacet,
}
pub(super) struct ProvisionalSlice {
pub(super) base_generation: u64,
pub(super) design: Arc<Design>,
pub(super) tier_index: usize,
pub(super) side: SliceSide,
pub(super) snapped: SnappedFacet,
pub(super) line: SliceLine,
pub(super) base_corners: Arc<Vec<Vec3>>,
pub(super) masts: Option<Vec<SolvedTier>>,
pub(super) facet_map: Option<Rc<FacetMap>>,
pub(super) outline: Vec<u32>,
pub(super) masts_new: bool,
pub(super) awaiting: bool,
pub(super) surviving: Option<usize>,
pub(super) cut_steps: Option<usize>,
}
impl ProvisionalSlice {
pub(super) fn new(
base_generation: u64,
design: Design,
tier_index: usize,
side: SliceSide,
snapped: SnappedFacet,
line: SliceLine,
base_corners: Arc<Vec<Vec3>>,
) -> Self {
Self {
base_generation,
design: Arc::new(design),
tier_index,
side,
snapped,
line,
base_corners,
masts: None,
facet_map: None,
outline: Vec::new(),
masts_new: false,
awaiting: false,
surviving: None,
cut_steps: None,
}
}
pub(super) fn tier(&self) -> Option<&ConstraintTier> {
self.design.tiers.get(self.tier_index)
}
pub(super) fn facet_map(&mut self) -> Option<Rc<FacetMap>> {
if self.facet_map.is_none() {
let masts = self
.masts
.as_ref()
.filter(|masts| masts.len() == self.design.tiers.len())?;
self.facet_map = Some(Rc::new(FacetMap::from_design(&self.design, masts)));
}
self.facet_map.clone()
}
pub(super) fn facet_count(&self) -> usize {
self.surviving
.unwrap_or_else(|| self.tier().map_or(0, |tier| tier.indices.len()))
}
}
pub(in super::super) struct SliceState {
pub(super) gesture: Option<(f32, f32)>,
pub(super) provisional: Option<ProvisionalSlice>,
}
impl SliceState {
pub(in super::super) const fn new() -> Self {
Self {
gesture: None,
provisional: None,
}
}
}
pub(super) fn set_angle(p: &mut ProvisionalSlice, deg: f64) -> bool {
let index = p.tier_index;
let Some(tier) = Arc::make_mut(&mut p.design).tiers.get_mut(index) else {
return false;
};
let new = clamp_nudge_to_side(tier.angle_deg, deg);
if !new.is_finite() || new.to_bits() == tier.angle_deg.to_bits() {
return false;
}
tier.angle_deg = new;
p.snapped.angle_deg = new;
true
}
pub(super) fn set_mast(p: &mut ProvisionalSlice, mast: f64) -> bool {
let index = p.tier_index;
let Some(tier) = Arc::make_mut(&mut p.design).tiers.get_mut(index) else {
return false;
};
let unchanged = matches!(&tier.constraint, MeetConstraint::ScaleReference(m) if m.to_bits() == mast.to_bits());
if !mast.is_finite() || unchanged {
return false;
}
tier.constraint = MeetConstraint::ScaleReference(mast);
true
}
pub(in super::super) fn wheel_turn(more: i64, gear: u32) -> Option<u32> {
if gear == 0 {
return None;
}
u32::try_from(more.rem_euclid(i64::from(gear)))
.ok()
.filter(|&teeth| teeth != 0)
}
pub(super) fn turn(p: &mut ProvisionalSlice, more: i64) -> bool {
let gear = p.design.meta.gear_teeth_abs();
let Some(teeth) = wheel_turn(more, gear).map(f64::from) else {
return false;
};
let index = p.tier_index;
let Some(tier) = Arc::make_mut(&mut p.design).tiers.get_mut(index) else {
return false;
};
let mut indices = rotate_indices(&tier.indices, teeth, gear);
indices.sort_by(f64::total_cmp);
tier.indices = indices;
let anchor = rotate_indices(&[p.snapped.index], teeth, gear);
p.snapped.index = anchor.first().copied().unwrap_or(p.snapped.index);
true
}
pub(super) fn rebuild_indices(p: &mut ProvisionalSlice, symmetric: bool) -> bool {
let indices = {
let meta = &p.design.meta;
if symmetric {
expected_orbit(
p.snapped.index,
meta.symmetry_order,
meta.mirror,
meta.gear_teeth_abs(),
)
} else {
vec![p.snapped.index]
}
};
let index = p.tier_index;
let Some(tier) = Arc::make_mut(&mut p.design).tiers.get_mut(index) else {
return false;
};
tier.indices = indices;
tier.detached.clear();
p.facet_map = None;
true
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_cut_core::{PreformSpec, ScheduleMeta};
fn provisional_tier() -> ConstraintTier {
ConstraintTier {
angle_deg: 30.0,
name: "Slice".to_owned(),
indices: vec![0.0],
constraint: MeetConstraint::ScaleReference(0.4),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
}
}
#[test]
fn a_concave_step_between_flat_steps_moves_the_provisional_tiers_step() {
let mut design = Design::concave_fixture();
let index = design.tiers.len();
design
.apply_edit(Edit::AddTier {
index,
tier: provisional_tier(),
})
.expect("the provisional tier appends");
assert_eq!(
index, 5,
"five stored flat tiers before the provisional one"
);
assert_eq!(tier_step(&design, 0), Some(0));
assert_eq!(tier_step(&design, 2), Some(2));
assert_eq!(tier_step(&design, 3), Some(4), "the groove is step 3");
assert_eq!(tier_step(&design, 5), Some(6));
assert_eq!(tier_step(&design, 9), None, "no such tier");
let step = tier_step(&design, index).expect("the tier is a step");
assert!(cut_hides_tier(5, step), "through step 5 ends before step 6");
assert!(!cut_hides_tier(6, step), "through step 6 includes it");
assert!(!cut_hides_tier(-1, step), "-1 shows the whole design");
assert!(cut_hides_tier(MODEL_ROUGH, step), "the rough has no tier");
}
#[test]
fn a_planar_design_stored_out_of_cutting_order_has_its_cutting_step() {
let design = Design::new(
PreformSpec::block(2.0, 1.0, 2.0),
ScheduleMeta::standard_round_brilliant(),
vec![
provisional_tier(),
ConstraintTier {
angle_deg: -40.0,
name: "Main".to_owned(),
..provisional_tier()
},
ConstraintTier {
angle_deg: 0.0,
name: "Table".to_owned(),
..provisional_tier()
},
],
);
assert_eq!(tier_step(&design, 0), Some(1));
assert_eq!(tier_step(&design, 1), Some(0));
assert_eq!(tier_step(&design, 2), Some(2));
assert!(
cut_hides_tier(0, 1),
"through step 0 ends before the crown tier"
);
assert!(!cut_hides_tier(1, 1));
}
#[test]
fn without_concave_tiers_the_step_is_the_stored_index() {
let mut design = Design::concave_fixture();
design.concave_tiers.clear();
design.concave_tier_ids.clear();
let index = design.tiers.len();
design
.apply_edit(Edit::AddTier {
index,
tier: provisional_tier(),
})
.expect("the provisional tier appends");
for stored in 0..=index {
assert_eq!(tier_step(&design, stored), Some(stored));
}
assert!(cut_hides_tier(4, 5));
assert!(!cut_hides_tier(5, 5));
}
}