use super::{
planes::{ReconstructedPlanesInput, apply_reconstructed_planes},
shared::{
catalogue_material_guess, format_optional_proportion, sides_from_rows, standard_codes,
},
};
use crate::{
AngleItem, DiagramDetailData, FileItem, LibraryModel, MainWindow, TiltModel, ViewportModel,
bridge::render_thread::RenderContext,
};
use indicatrix::geometry::plane::GpuFacetPlane;
use indicatrix_vault::{db::sqlite::Database, model::entry::FullDiagramRecord};
use slint::{ComponentHandle, ModelRc, VecModel};
use std::sync::{
Arc, Mutex,
atomic::{AtomicU64, Ordering},
};
use tracing::error;
struct LoadSequence(AtomicU64);
impl LoadSequence {
const fn new() -> Self {
Self(AtomicU64::new(0))
}
fn begin(&self) -> u64 {
self.0.fetch_add(1, Ordering::AcqRel).wrapping_add(1)
}
fn is_current(&self, sequence: u64) -> bool {
self.0.load(Ordering::Acquire) == sequence
}
}
static DETAIL_LOADS: LoadSequence = LoadSequence::new();
fn planes_still_wanted(is_newest_load: bool, selected_entry_id: i32, entry_id: i64) -> bool {
is_newest_load && i64::from(selected_entry_id) == entry_id
}
fn note_derived_from_if_any(ui: &MainWindow, db: &Database, entry_id: i64) {
if let Some((_, title)) = db.get_derived_from_title(entry_id).ok().flatten() {
ui.global::<LibraryModel>()
.set_status_message(format!("Derived from: {title}").into());
}
}
fn push_shape_picker_and_cached_material(
ui: &MainWindow,
db: &Database,
entry_id: i64,
shape: Option<&str>,
) -> Option<String> {
let (shape_options, shape_index) =
crate::gui::library::local::build_shape_picker_options(db, shape);
ui.global::<LibraryModel>()
.set_shape_picker_options(ModelRc::new(VecModel::from(shape_options)));
ui.global::<LibraryModel>()
.set_shape_picker_current_index(shape_index);
let cached_curve_material = db.get_preview_material(entry_id).ok().flatten();
ui.global::<TiltModel>()
.set_cached_curve_material(cached_curve_material.clone().unwrap_or_default().into());
note_derived_from_if_any(ui, db, entry_id);
cached_curve_material
}
fn resolve_catalogue_planes_for_entry(full: &FullDiagramRecord) -> (Vec<GpuFacetPlane>, u32, f32) {
let resolved = crate::gui::editor::resolve_catalogue_planes(full);
(
resolved.planes,
resolved.gear_teeth,
resolved.gear_reference_angle,
)
}
fn detail_data(full: &FullDiagramRecord) -> DiagramDetailData {
DiagramDetailData {
id: full.entry_id as i32,
title: full.title.clone().into(),
url: full.url.clone().into(),
designer: full.designer_info.clone().unwrap_or_default().into(),
shape: full.shape.clone().unwrap_or_default().into(),
gear: full.index_gear.clone().unwrap_or_default().into(),
facets: full.facets_count.clone().unwrap_or_default().into(),
lw_ratio: format_optional_proportion(full.lw_ratio.as_deref()).into(),
material_guess: catalogue_material_guess(full.refractive_index.as_deref()).into(),
ri: full.refractive_index.clone().unwrap_or_default().into(),
volume: format_optional_proportion(full.volume.as_deref()).into(),
competition: full.competition_diagram.clone().unwrap_or_default().into(),
image_name: full.diagram_image_name.clone().unwrap_or_default().into(),
has_image: full.diagram_image_data.is_some(),
is_local: full.url.starts_with("local://"),
hw_ratio: format_optional_proportion(full.hw_ratio.as_deref()).into(),
cw_ratio: format_optional_proportion(full.cw_ratio.as_deref()).into(),
pw_ratio: format_optional_proportion(full.pw_ratio.as_deref()).into(),
symmetry_order: full.symmetry_order.clone().unwrap_or_default().into(),
mirror_symmetry: full.mirror_symmetry.unwrap_or(false),
}
}
fn angle_items(full: &FullDiagramRecord) -> Vec<AngleItem> {
let sides = sides_from_rows(
full.angle_settings
.iter()
.map(|a| (a.facet.as_str(), a.angle.as_str(), a.index.as_str())),
);
let labels: Vec<(&str, &str)> = full
.angle_settings
.iter()
.map(|a| (a.facet.as_str(), a.index.as_str()))
.collect();
let codes = standard_codes(&labels, &sides);
full.angle_settings
.iter()
.zip(sides)
.zip(codes)
.map(|((a, side), code)| AngleItem {
order_idx: a.order_index as i32,
side,
code: code.into(),
facet: a.facet.clone().into(),
angle: a.angle.trim_start_matches('-').to_string().into(),
index_val: a.index.clone().into(),
notes: a.notes.clone().into(),
second_line: a.tool_line.clone().unwrap_or_default().into(),
})
.collect()
}
fn file_items(full: &FullDiagramRecord) -> Vec<FileItem> {
full.attached_files
.iter()
.map(|f| {
let size_kb = f.content.len() as f64 / 1024.0;
FileItem {
name: f.name.clone().into(),
url: f.url.clone().into(),
size_str: format!("{size_kb:.1} KB").into(),
}
})
.collect()
}
pub(super) fn load_diagram_detail(
ui: &MainWindow,
db_mutex: &Arc<Mutex<Database>>,
render_ctx: &Arc<Mutex<RenderContext>>,
entry_id: i64,
) {
let db = match db_mutex.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
match db.get_diagram_full(entry_id) {
Ok(Some(full)) => {
ui.global::<LibraryModel>()
.set_current_detail(detail_data(&full));
ui.global::<LibraryModel>()
.set_current_angles(ModelRc::new(VecModel::from(angle_items(&full))));
ui.global::<LibraryModel>()
.set_current_files(ModelRc::new(VecModel::from(file_items(&full))));
ui.global::<LibraryModel>()
.set_selected_entry_id(entry_id as i32);
let cached_curve_material =
push_shape_picker_and_cached_material(ui, &db, entry_id, full.shape.as_deref());
drop(db);
spawn_planes_resolution(ui, render_ctx, full, cached_curve_material);
}
Ok(None) => {
ui.global::<LibraryModel>()
.set_status_message("Diagram detail not found.".into());
}
Err(e) => {
error!("Failed to fetch diagram full detail: {:?}", e);
ui.global::<LibraryModel>()
.set_status_message(format!("Error loading detail: {e}").into());
}
}
}
struct ResolvedPlanes {
entry_id: i64,
shape: Option<String>,
index_gear: Option<String>,
refractive_index: Option<String>,
preview_material: Option<String>,
planes: Result<(Vec<GpuFacetPlane>, u32, f32), String>,
}
fn spawn_planes_resolution(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
full: FullDiagramRecord,
preview_material: Option<String>,
) {
let sequence = DETAIL_LOADS.begin();
let ui_weak = ui.as_weak();
let render_ctx = Arc::clone(render_ctx);
let spawned = std::thread::Builder::new()
.name("catalogue-planes".to_string())
.spawn(move || {
if !DETAIL_LOADS.is_current(sequence) {
return;
}
let planes =
crate::gui::library::local::catch_file_panic(std::panic::AssertUnwindSafe(|| {
resolve_catalogue_planes_for_entry(&full)
}));
let resolved = ResolvedPlanes {
entry_id: full.entry_id,
shape: full.shape,
index_gear: full.index_gear,
refractive_index: full.refractive_index,
preview_material,
planes,
};
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
apply_resolved_planes(&ui, &render_ctx, sequence, resolved);
});
});
if let Err(e) = spawned {
error!("Could not start the catalogue planes worker: {e}");
}
}
fn apply_resolved_planes(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
sequence: u64,
resolved: ResolvedPlanes,
) {
let selected = ui.global::<LibraryModel>().get_selected_entry_id();
if !planes_still_wanted(
DETAIL_LOADS.is_current(sequence),
selected,
resolved.entry_id,
) {
return;
}
let real_design = match resolved.planes {
Ok(planes) => planes,
Err(message) => {
error!(
"Resolving the planes of diagram #{} failed: {message}",
resolved.entry_id
);
ui.global::<LibraryModel>().set_status_message(
format!("Could not build the 3D view of this design: {message}").into(),
);
return;
}
};
apply_reconstructed_planes(
ui,
render_ctx,
resolved.entry_id,
ReconstructedPlanesInput {
shape: resolved.shape.as_deref(),
index_gear: resolved.index_gear.as_deref(),
angle_items: &[],
refractive_index: resolved.refractive_index.as_deref(),
preview_material: resolved.preview_material.as_deref(),
real_design: Some(real_design),
},
);
request_live_solid_redraw(ui);
}
fn request_live_solid_redraw(ui: &MainWindow) {
let viewport = ui.global::<ViewportModel>();
let mode = viewport.get_live_view_mode();
if ui.get_render_view_tab() == 0 && mode == 0 {
viewport.invoke_live_view_mode_changed(mode);
}
}
#[cfg(test)]
mod tests {
use super::{LoadSequence, planes_still_wanted};
#[test]
fn only_the_newest_load_is_current() {
let loads = LoadSequence::new();
let first = loads.begin();
assert!(loads.is_current(first));
let second = loads.begin();
assert_ne!(first, second);
assert!(
!loads.is_current(first),
"a later click supersedes the first"
);
assert!(loads.is_current(second));
}
#[test]
fn a_load_that_never_began_is_not_current() {
let loads = LoadSequence::new();
let _ = loads.begin();
assert!(!loads.is_current(0));
}
#[test]
fn planes_are_drawn_only_for_the_newest_load_of_the_selected_entry() {
assert!(planes_still_wanted(true, 7, 7));
assert!(
!planes_still_wanted(false, 7, 7),
"a superseded load never draws"
);
assert!(
!planes_still_wanted(true, 8, 7),
"the selection moved to another row"
);
}
}