use crate::gui::{
editor::material_lookup::{EditorMaterialLookup, traced_gem_material, traced_material_for},
render::camera_lighting::{fit_distance_for_radius, orbit_distance_bounds},
solid_preview::{
mesh_cache::{CachedMesh, MeshCache},
preview_state::{CameraPose, DEFAULT_MESH_BOUNDING_RADIUS},
},
};
use glam::Vec3;
use indicatrix::{geometry::meet_solver::SolvedTier, optics::materials::GemMaterial};
use indicatrix_cut_core::Design;
use indicatrix_editor::solve_policy::design_to_gpu_planes_from_solved;
use std::f32::consts::FRAC_PI_2;
pub(super) const ORBIT_RADIANS_PER_PX: f32 = 0.008;
pub(super) const ZOOM_PER_WHEEL_PX: f32 = 0.002;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CompareOrigin {
Retarget,
Optimize,
Snapshot,
}
impl CompareOrigin {
#[must_use]
pub(super) const fn offers_keep(self) -> bool {
!matches!(self, Self::Snapshot)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Renderer {
Solid,
Traced,
}
impl Renderer {
#[must_use]
pub(super) const fn from_index(index: i32) -> Self {
if index == 1 {
Self::Traced
} else {
Self::Solid
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum TracedProgress {
Waiting,
Rendering {
side: u8,
},
Done,
}
pub(super) struct SideInput {
pub(super) design: Design,
pub(super) label: String,
pub(super) material: Result<GemMaterial, String>,
}
pub(super) fn resolve_side_material(
design: &Design,
custom: &[GemMaterial],
) -> Result<GemMaterial, String> {
let (name, unresolved) = traced_material_for(design, custom);
if let Some(reason) = unresolved {
return Err(reason);
}
traced_gem_material(&name, &design.material, &EditorMaterialLookup::new(custom))
.ok_or_else(|| format!("'{name}' is not a built-in preset or a saved custom material."))
}
pub(super) struct CompareSide {
pub(super) label: String,
pub(super) solved: Option<Vec<SolvedTier>>,
pub(super) solve_error: Option<String>,
pub(super) planes: Vec<(Vec3, f32)>,
pub(super) preform_planes: usize,
pub(super) material: Result<GemMaterial, String>,
pub(super) bounding_radius: Option<f64>,
}
impl CompareSide {
#[must_use]
pub(super) fn build(input: SideInput) -> Self {
let preform_planes = input.design.preform.planes().len();
match input.design.solve() {
Ok(solved) => {
let planes: Vec<(Vec3, f32)> =
design_to_gpu_planes_from_solved(&input.design, &solved)
.iter()
.map(|p| (Vec3::from(p.normal), -p.d))
.collect();
let bounding_radius = MeshCache::default()
.get_or_build(&planes)
.map(CachedMesh::bounding_radius);
Self {
label: input.label,
solved: Some(solved),
solve_error: None,
planes,
preform_planes,
material: input.material,
bounding_radius,
}
}
Err(error) => Self {
label: input.label,
solved: None,
solve_error: Some(error.to_string()),
planes: Vec::new(),
preform_planes,
material: input.material,
bounding_radius: None,
},
}
}
#[must_use]
pub(super) const fn is_solved(&self) -> bool {
self.solved.is_some()
}
}
pub(super) struct CompareSession {
pub(super) before: CompareSide,
pub(super) after: CompareSide,
pub(super) origin: CompareOrigin,
pub(super) pose: CameraPose,
pub(super) radius: f64,
}
impl CompareSession {
#[must_use]
pub(super) fn build(
before: SideInput,
after: SideInput,
origin: CompareOrigin,
pose: CameraPose,
) -> Self {
let before = CompareSide::build(before);
let after = CompareSide::build(after);
let radius = before
.bounding_radius
.into_iter()
.chain(after.bounding_radius)
.reduce(f64::max)
.unwrap_or(DEFAULT_MESH_BOUNDING_RADIUS);
let pose = fitted_pose(pose, radius);
Self {
before,
after,
origin,
pose,
radius,
}
}
#[must_use]
pub(super) const fn can_keep(&self) -> bool {
self.origin.offers_keep() && self.before.is_solved() && self.after.is_solved()
}
}
#[must_use]
pub(super) fn fitted_pose(pose: CameraPose, radius: f64) -> CameraPose {
let (min, max) = orbit_distance_bounds(radius);
CameraPose {
yaw: pose.yaw,
pitch: clamp_pitch(pose.pitch),
distance: pose
.distance
.max(fit_distance_for_radius(radius))
.clamp(min, max),
}
}
#[must_use]
pub(super) const fn traced_may_start(drag_held: bool) -> bool {
!drag_held
}
#[must_use]
pub(super) const fn clamp_pitch(pitch: f32) -> f32 {
pitch.clamp(-FRAC_PI_2, FRAC_PI_2)
}
#[must_use]
pub(super) fn orbit(pose: CameraPose, dx: f32, dy: f32) -> CameraPose {
CameraPose {
yaw: dx.mul_add(-ORBIT_RADIANS_PER_PX, pose.yaw),
pitch: clamp_pitch(dy.mul_add(ORBIT_RADIANS_PER_PX, pose.pitch)),
distance: pose.distance,
}
}
#[must_use]
pub(super) fn zoom(pose: CameraPose, delta: f32, radius: f64) -> CameraPose {
let (min, max) = orbit_distance_bounds(radius);
CameraPose {
distance: delta
.mul_add(-ZOOM_PER_WHEEL_PX, pose.distance)
.clamp(min, max),
..pose
}
}
#[must_use]
pub(super) fn default_pose(radius: f64) -> CameraPose {
let (min, max) = orbit_distance_bounds(radius);
CameraPose {
yaw: 0.0,
pitch: 0.0,
distance: fit_distance_for_radius(radius).clamp(min, max),
}
}
#[must_use]
pub(super) const fn clamp_split_fraction(fraction: f32) -> f32 {
if fraction.is_nan() {
0.5
} else {
fraction.clamp(0.0, 1.0)
}
}
#[must_use]
pub(super) fn status_text(
session: Option<&CompareSession>,
renderer: Renderer,
traced: TracedProgress,
traced_spp: u32,
) -> String {
let Some(session) = session else {
return "Solving both sides…".to_string();
};
let mut parts: Vec<String> = Vec::new();
for (name, side) in [("Before", &session.before), ("After", &session.after)] {
if let Some(error) = &side.solve_error {
parts.push(format!("{name} does not solve: {error}"));
} else if renderer == Renderer::Traced && side.material.is_err() {
parts.push(format!("{name}: material not resolved, nothing to trace"));
}
}
let mode = match (renderer, traced) {
(Renderer::Solid, _) => "Solid".to_string(),
(Renderer::Traced, TracedProgress::Waiting) => "Tracing…".to_string(),
(Renderer::Traced, TracedProgress::Rendering { side }) => {
format!("Tracing… {side} of 2")
}
(Renderer::Traced, TracedProgress::Done) => format!("Traced ({traced_spp} spp)"),
};
parts.insert(0, mode);
parts.join(" · ")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum FrameKind {
Solid,
Traced,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct FrameBook {
generation: u64,
traced_shown: bool,
}
impl FrameBook {
pub(super) const fn bump(&mut self) -> u64 {
self.generation = self.generation.wrapping_add(1);
self.traced_shown = false;
self.generation
}
#[must_use]
pub(super) const fn generation(&self) -> u64 {
self.generation
}
pub(super) const fn accept(
&mut self,
kind: FrameKind,
generation: u64,
renderer: Renderer,
) -> bool {
if generation != self.generation {
return false;
}
match kind {
FrameKind::Solid => !self.traced_shown,
FrameKind::Traced => {
if !matches!(renderer, Renderer::Traced) {
return false;
}
self.traced_shown = true;
true
}
}
}
}