mod compose;
mod design_mesh;
mod interaction;
mod picking;
mod render;
mod scene;
mod state;
mod thumbnails;
mod workers;
pub(super) use self::state::ViewState;
use self::{
design_mesh::MeshLibrary,
render::{Hover, RenderOptions, RenderRequest, SharedPick},
scene::{ModelScene, RoughMesh, Scene, SceneKind, StoneDraw},
state::{Runtime, SelectStep},
thumbnails::{ThumbnailBatch, ThumbnailItem, ThumbnailWorker},
workers::BuildRequest,
};
use super::{
host::{Host, on_host},
run::DesignStatus,
shape_worker,
};
use crate::{RoughPlanModel, gui::show_toast};
use indicatrix_cut_core::rough_plan::{RoughLayout, RoughModel};
use slint::{ComponentHandle, Image, Model};
use std::{
collections::{BTreeMap, BTreeSet},
rc::Rc,
sync::Arc,
time::Duration,
};
const WHEEL_QUIET: Duration = Duration::from_millis(120);
const RESIZE_QUIET: Duration = Duration::from_millis(150);
const MAX_FRAME_EDGE: f32 = 1600.0;
#[must_use]
fn frame_size(view_size: (f32, f32), scale_factor: f32, interactive: bool) -> (u32, u32) {
let (width, height) = view_size;
if !(width > 0.0 && height > 0.0) {
return (0, 0);
}
let mut scale = scale_factor.max(0.5);
let longest = width.max(height) * scale;
if longest > MAX_FRAME_EDGE {
scale *= MAX_FRAME_EDGE / longest;
}
if interactive {
scale *= 0.5;
}
(
(width * scale).round().max(1.0) as u32,
(height * scale).round().max(1.0) as u32,
)
}
pub(super) fn setup_view_callbacks(host: &Rc<Host>) {
let meshes = Arc::new(MeshLibrary::new(Arc::clone(&host.db)));
let picks: SharedPick = Arc::default();
let window = host.window.as_weak();
let runtime = Runtime {
renderer: workers::spawn_render_worker(window.clone(), Arc::clone(&picks)),
builder: workers::spawn_build_worker(window.clone(), Arc::clone(&meshes)),
thumbnails: ThumbnailWorker::spawn(window, meshes),
picks,
};
host.session.borrow_mut().view.runtime = Some(runtime);
interaction::register(host);
let model = host.window.global::<RoughPlanModel>();
model.on_select_result(|index| on_host(|host| select_result(host, index)));
model.on_select_group(|result, group| on_host(|host| select_group(host, result, group)));
}
pub(super) fn sync_view_size(host: &Rc<Host>) {
interaction::sync_size(host);
}
fn request_render(host: &Rc<Host>) {
let model = host.window.global::<RoughPlanModel>();
let armed = model.get_face_from_view_armed();
let options = RenderOptions {
show_cut_faces: model.get_show_cut_faces(),
show_rough: model.get_show_rough(),
show_saw: model.get_show_saw(),
show_stones: model.get_show_stones(),
selected_cut: usize::try_from(model.get_selected_cut()).ok(),
highlight_group: usize::try_from(model.get_selected_group()).ok(),
hover: Hover::None,
};
let scale = host.window.window().scale_factor();
let mut guard = host.session.borrow_mut();
let view = &mut guard.view;
view.scale_factor = scale;
let size = frame_size(view.view_size, view.scale_factor, view.interactive());
let (Some(scene), Some(runtime)) = (view.scene.clone(), view.runtime.as_ref()) else {
return;
};
if size.0 == 0 || size.1 == 0 {
return;
}
if matches!(view.hover, Hover::Facet(_)) && !armed {
view.hover = Hover::None;
}
view.generation += 1;
runtime.renderer.submit(RenderRequest {
generation: view.generation,
scene,
pose: view.pose,
size,
options: RenderOptions {
hover: view.hover,
..options
},
});
}
fn show_scene(host: &Rc<Host>, scene: Option<Arc<Scene>>) {
let empty = scene.is_none();
{
let mut session = host.session.borrow_mut();
let view = &mut session.view;
view.scene = scene;
if empty {
view.generation += 1;
view.applied = view.generation;
}
}
if empty {
host.window
.global::<RoughPlanModel>()
.set_view_image(Image::default());
} else {
request_render(host);
}
}
fn frame_ready(host: &Rc<Host>, generation: u64, pixels: render::Pixels) {
let (newer, rehover) = {
let mut session = host.session.borrow_mut();
let view = &mut session.view;
let newer = generation > view.applied;
if newer {
view.applied = generation;
}
let rehover = newer && view.rehover && !view.dragging;
if rehover {
view.rehover = false;
}
(newer, rehover)
};
if newer {
host.window
.global::<RoughPlanModel>()
.set_view_image(Image::from_rgba8(pixels));
}
if rehover {
interaction::rehover(host);
}
}
fn push_busy(host: &Rc<Host>) {
let building = host.session.borrow().view.building;
let model = host.window.global::<RoughPlanModel>();
if model.get_view_busy() != building {
model.set_view_busy(building);
}
}
fn scene_ready(host: &Rc<Host>, ticket: u64, scene: Arc<Scene>) {
let current = host.session.borrow_mut().view.finish_build(ticket, scene);
if current {
push_busy(host);
host.window.global::<RoughPlanModel>().set_view_mode(1);
request_render(host);
}
}
fn draw_failed(host: &Rc<Host>) {
host.session.borrow_mut().view.fail_build();
push_busy(host);
if let Some(main) = host.main.upgrade() {
show_toast(&main, "Could not draw this result", "error");
}
}
fn thumbnails_failed(host: &Rc<Host>, generation: u64) {
if host.session.borrow().view.thumbnail_generation == generation {
host.window
.global::<RoughPlanModel>()
.set_thumbnails_pending(false);
}
}
#[must_use]
fn result_index(index: i32, count: usize) -> Option<usize> {
usize::try_from(index).ok().filter(|&i| i < count)
}
#[must_use]
const fn group_after_select(previous_result: i32, result: i32, group: i32) -> i32 {
if previous_result == result { group } else { -1 }
}
#[must_use]
const fn toggled_group(current: i32, clicked: i32) -> i32 {
if current == clicked { -1 } else { clicked }
}
#[must_use]
fn edited_since_plan(planned: Option<&RoughModel>, model: &RoughModel) -> bool {
planned.is_some_and(|planned| planned != model)
}
fn thumbnail_ready(
host: &Rc<Host>,
generation: u64,
index: usize,
pixels: render::Pixels,
last: bool,
) {
if host.session.borrow().view.thumbnail_generation != generation {
return;
}
let model = host.window.global::<RoughPlanModel>();
let results = model.get_results();
if let Some(mut row) = results.row_data(index) {
row.thumbnail = Image::from_rgba8(pixels);
row.has_thumbnail = true;
results.set_row_data(index, row);
}
if last {
model.set_thumbnails_pending(false);
}
}
fn mesh_ids_for(
layout: &RoughLayout,
statuses: &BTreeMap<i64, DesignStatus>,
) -> BTreeMap<i64, StoneDraw> {
let ids: BTreeSet<i64> = layout.stones.iter().map(|stone| stone.entry_id).collect();
ids.into_iter()
.map(|id| {
let draw = match statuses.get(&id) {
Some(DesignStatus::Deleted) => StoneDraw::Deleted,
Some(DesignStatus::Changed) => StoneDraw::Changed(id),
Some(DesignStatus::MatchedByTitle { resolved_entry_id }) => {
StoneDraw::Matched(*resolved_entry_id)
}
Some(DesignStatus::Unchanged) | None => StoneDraw::Design(id),
};
(id, draw)
})
.collect()
}
fn show_model(host: &Rc<Host>) {
let scene = host.session.borrow_mut().view.show_model();
host.window.global::<RoughPlanModel>().set_view_mode(0);
push_busy(host);
show_scene(host, scene);
}
fn show_fit(host: &Rc<Host>, index: usize) {
let submitted = {
let mut guard = host.session.borrow_mut();
let session = &mut *guard;
let (Some(layout), Some(rough)) = (
session.run.layouts.get(index),
session.view.rough_mesh.clone(),
) else {
return;
};
let ticket = session.view.begin_build(index);
let request = BuildRequest {
ticket,
epoch: session.view.plan_epoch,
layout: layout.clone(),
rough,
titles: session.run.titles.clone(),
mesh_ids: mesh_ids_for(layout, &session.run.statuses),
};
session
.view
.runtime
.as_ref()
.is_some_and(|runtime| runtime.builder.submit(request))
};
push_busy(host);
if !submitted {
draw_failed(host);
}
}
fn select_result(host: &Rc<Host>, index: i32) {
if index < 0 {
deselect_result(host);
return;
}
let count = host.session.borrow().run.layouts.len();
let Some(result) = result_index(index, count) else {
return;
};
let model = host.window.global::<RoughPlanModel>();
model.set_selected_group(group_after_select(
model.get_selected_result(),
index,
model.get_selected_group(),
));
model.set_selected_result(index);
let step = host.session.borrow_mut().view.select(result);
match step {
SelectStep::Idle => {}
SelectStep::Redraw | SelectStep::ShowKept => {
push_busy(host);
model.set_view_mode(1);
request_render(host);
}
SelectStep::Build => show_fit(host, result),
}
}
fn deselect_result(host: &Rc<Host>) {
let model = host.window.global::<RoughPlanModel>();
model.set_selected_result(-1);
model.set_selected_group(-1);
show_model(host);
}
fn select_group(host: &Rc<Host>, result: i32, group: i32) {
select_result(host, result);
let model = host.window.global::<RoughPlanModel>();
model.set_selected_group(toggled_group(model.get_selected_group(), group));
if !host.session.borrow().view.building {
request_render(host);
}
}
fn leave_result_if_edited(host: &Rc<Host>) {
let edited = {
let session = host.session.borrow();
edited_since_plan(session.run.plan_model.as_ref(), &session.model)
};
if edited {
let model = host.window.global::<RoughPlanModel>();
if model.get_selected_result() >= 0 {
model.set_selected_result(-1);
model.set_selected_group(-1);
}
}
}
fn showing_result(host: &Rc<Host>) -> bool {
let count = host.session.borrow().run.layouts.len();
result_index(
host.window.global::<RoughPlanModel>().get_selected_result(),
count,
)
.is_some()
}
pub(super) fn model_changed(host: &Rc<Host>) {
let (edited, reuse) = {
let mut guard = host.session.borrow_mut();
let session = &mut *guard;
let edited = session.view.last_model.as_ref() != Some(&session.model);
if edited {
session.view.last_model = Some(session.model.clone());
}
let reuse = !edited && session.view.model_scene.is_some() && session.model_mesh.is_some();
(edited, reuse)
};
if edited {
leave_result_if_edited(host);
}
if !reuse {
let scene = {
let session = host.session.borrow();
session.model_mesh.as_ref().map(|mesh| {
Arc::new(Scene::new(SceneKind::Model(ModelScene::new(
mesh,
&session.model,
))))
})
};
host.session.borrow_mut().view.model_scene = scene;
}
if !showing_result(host) {
show_model(host);
}
}
fn start_thumbnails(host: &Rc<Host>) {
let pending = {
let mut guard = host.session.borrow_mut();
let session = &mut *guard;
session.view.thumbnail_generation += 1;
let generation = session.view.thumbnail_generation;
let run = &session.run;
let Some(runtime) = &session.view.runtime else {
return;
};
match &session.view.rough_mesh {
Some(rough) if !run.layouts.is_empty() => {
runtime.thumbnails.start(ThumbnailBatch {
generation,
epoch: session.view.plan_epoch,
rough: Arc::clone(rough),
titles: run.titles.clone(),
items: run
.layouts
.iter()
.map(|layout| ThumbnailItem {
layout: layout.clone(),
mesh_ids: mesh_ids_for(layout, &run.statuses),
})
.collect(),
})
}
_ => {
runtime.thumbnails.cancel(generation);
false
}
}
};
host.window
.global::<RoughPlanModel>()
.set_thumbnails_pending(pending);
}
pub(super) fn results_changed(host: &Rc<Host>) {
{
let mut guard = host.session.borrow_mut();
let session = &mut *guard;
session.view.results_replaced();
session.view.rough_mesh = session
.run
.plan_model
.clone()
.map(|model| Arc::new(RoughMesh::new(model)));
}
let count = host.session.borrow().run.layouts.len();
let model = host.window.global::<RoughPlanModel>();
let selected = result_index(model.get_selected_result(), count);
if selected.is_none() {
model.set_selected_result(-1);
}
model.set_selected_group(-1);
match selected {
Some(index) => show_fit(host, index),
None => show_model(host),
}
start_thumbnails(host);
}
fn cut_selected(host: &Rc<Host>) {
shape_worker::selected_cut_changed(host);
request_render(host);
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_cut_core::rough_plan::{
Axis, CutOrder, CutPlan, PlacedStone, RoughBase, RoughCut, StonePose,
};
fn layout_of(ids: &[i64]) -> RoughLayout {
let stones = ids
.iter()
.map(|&entry_id| PlacedStone {
entry_id,
piece_origin_mm: [0.0; 3],
piece_size_mm: [1.0; 3],
stone_size_mm: [1.0; 3],
table_axis: Axis::Y,
carat: 0.1,
volume_mm3: 1.0,
pose: StonePose {
center_mm: [0.5; 3],
axes: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
mm_per_unit: 1.0,
},
})
.collect();
RoughLayout {
cut_order: CutOrder::Xyz,
stones,
cut_plan: CutPlan { slabs: Vec::new() },
total_carat: 0.1,
total_volume_mm3: 1.0,
yield_fraction: 0.1,
exact_fit: false,
}
}
#[test]
fn frames_follow_the_view_size_the_display_scale_and_the_drag_rule() {
assert_eq!(frame_size((800.0, 560.0), 1.0, false), (800, 560));
assert_eq!(frame_size((800.0, 560.0), 1.0, true), (400, 280));
assert_eq!(frame_size((800.0, 560.0), 1.5, false), (1200, 840));
assert_eq!(frame_size((1000.0, 700.0), 2.0, false), (1600, 1120));
assert_eq!(frame_size((1000.0, 700.0), 2.0, true), (800, 560));
assert_eq!(frame_size((0.0, 0.0), 1.0, false), (0, 0));
assert_eq!(frame_size((100.0, -3.0), 1.0, false), (0, 0));
assert_eq!(frame_size((f32::NAN, 100.0), 1.0, false), (0, 0));
}
#[test]
fn a_stone_is_drawn_by_the_standing_of_its_design() {
let layout = layout_of(&[4, 4, 9, 12, 15]);
let statuses = BTreeMap::from([
(9, DesignStatus::Deleted),
(
12,
DesignStatus::MatchedByTitle {
resolved_entry_id: 77,
},
),
(4, DesignStatus::Changed),
(15, DesignStatus::Unchanged),
]);
let ids = mesh_ids_for(&layout, &statuses);
assert_eq!(
ids,
BTreeMap::from([
(4, StoneDraw::Changed(4)),
(9, StoneDraw::Deleted),
(12, StoneDraw::Matched(77)),
(15, StoneDraw::Design(15)),
])
);
assert_eq!(
mesh_ids_for(&layout, &BTreeMap::new()),
BTreeMap::from([
(4, StoneDraw::Design(4)),
(9, StoneDraw::Design(9)),
(12, StoneDraw::Design(12)),
(15, StoneDraw::Design(15)),
])
);
}
#[test]
fn a_result_index_must_name_one_of_the_results() {
assert_eq!(result_index(0, 3), Some(0));
assert_eq!(result_index(2, 3), Some(2));
assert_eq!(result_index(3, 3), None, "one past the last result");
assert_eq!(result_index(-1, 3), None, "nothing selected");
assert_eq!(result_index(0, 0), None, "no results at all");
assert_eq!(result_index(i32::MAX, 10), None);
}
#[test]
fn a_design_outline_belongs_to_the_result_it_was_picked_in() {
assert_eq!(group_after_select(4, 4, 2), 2);
assert_eq!(group_after_select(4, 5, 2), -1);
assert_eq!(group_after_select(-1, 0, 1), -1, "from no selection");
assert_eq!(group_after_select(4, 4, -1), -1, "no outline stays none");
}
#[test]
fn a_second_click_on_a_group_row_clears_its_outline() {
assert_eq!(toggled_group(-1, 2), 2);
assert_eq!(toggled_group(2, 2), -1);
assert_eq!(toggled_group(1, 2), 2, "another row moves the outline");
}
#[test]
fn only_a_changed_model_leaves_the_planned_results() {
let planned = RoughModel::new(
RoughBase::Block {
x_mm: 10.0,
y_mm: 8.0,
z_mm: 6.0,
},
Vec::new(),
);
let mut edited = planned.clone();
edited.cuts.push(RoughCut::Face {
normal: [0.0, 1.0, 0.0],
depth_mm: 1.0,
});
assert!(!edited_since_plan(Some(&planned), &planned.clone()));
assert!(edited_since_plan(Some(&planned), &edited));
assert!(
!edited_since_plan(None, &edited),
"no plan, nothing to leave"
);
}
#[test]
fn a_new_view_state_starts_at_the_reset_pose_with_nothing_shown() {
let view = ViewState::default();
assert_eq!(view.pose, render::reset_pose(1.5));
assert!(view.scene.is_none() && view.runtime.is_none());
}
}