mod late_rows;
use crate::{
MainWindow, SolidPreviewModel,
bridge::render_thread::{PlanesOwner, RenderContext},
gui::{
editor,
solid_preview::{
cut_slider,
diagram_wiring::{DiagramFacetOwners, DiagramHoverText, DiagramPick},
facet_selection,
preview_state::{
FacetOwners, FrameGeometry, LateFindings, PickBuffer, PreviewFrame, PreviewSink,
SolidLastSolved,
},
},
},
};
use indicatrix::geometry::{ToolPrimitive, meet_solver::SolvedTier, plane::GpuFacetPlane};
use indicatrix_cut_core::ManufacturabilityWarning;
use late_rows::LateRows;
use slint::{ComponentHandle, Weak};
use std::{
cell::RefCell,
sync::{Arc, Mutex},
time::Duration,
};
thread_local! {
static LATE_ROWS: RefCell<LateRows> = RefCell::default();
}
pub(super) struct SlintSolidSink {
pub(super) ui: Weak<MainWindow>,
pub(super) pick: Arc<Mutex<Option<PickBuffer>>>,
pub(super) last_solved: SolidLastSolved,
pub(super) diagram_pick: DiagramPick,
pub(super) diagram_hover_text: DiagramHoverText,
pub(super) diagram_facet_owners: DiagramFacetOwners,
pub(super) diagram_tooth_pick: DiagramPick,
pub(super) diagram_panel_pick: DiagramPick,
pub(super) hover_text: Arc<Mutex<Vec<String>>>,
pub(super) facet_owners: Arc<Mutex<FacetOwners>>,
pub(super) render_ctx: Arc<Mutex<RenderContext>>,
pub(super) solid_active_planes: Arc<Mutex<Option<Arc<Vec<GpuFacetPlane>>>>>,
pub(super) trace_active_planes: Arc<Mutex<Option<Arc<Vec<GpuFacetPlane>>>>>,
pub(super) mesh_bounding_radius: Arc<Mutex<f64>>,
pub(super) geometry: Arc<Mutex<Option<FrameGeometry>>>,
}
pub(super) fn planes_generations_match(
trace: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
solid: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
) -> bool {
let trace = trace
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let solid = solid
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
match (trace, solid) {
(Some(trace), Some(solid)) => Arc::ptr_eq(&trace, &solid),
_ => true,
}
}
const PLANE_REPUBLISH_EPSILON: f32 = 1e-5;
#[must_use]
fn planes_equal_within(a: &[GpuFacetPlane], b: &[GpuFacetPlane], epsilon: f32) -> bool {
a.len() == b.len()
&& a.iter().zip(b).all(|(left, right)| {
(left.d - right.d).abs() <= epsilon
&& left
.normal
.iter()
.zip(&right.normal)
.all(|(l, r)| (l - r).abs() <= epsilon)
})
}
#[derive(Clone, Copy)]
struct StonePublish<'a> {
planes: &'a [(glam::Vec3, f32)],
tools: &'a [ToolPrimitive],
placements: &'a [(usize, usize)],
}
impl<'a> StonePublish<'a> {
const fn new(
planes: &'a [(glam::Vec3, f32)],
tools: &'a [ToolPrimitive],
placements: &'a [(usize, usize)],
) -> Self {
Self {
planes,
tools,
placements,
}
}
}
fn sync_planes_and_check_trace_match(
stone: &StonePublish<'_>,
render_ctx: &Mutex<RenderContext>,
solid_active_planes: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
trace_active_planes: &Mutex<Option<Arc<Vec<GpuFacetPlane>>>>,
generation: u64,
) -> bool {
let StonePublish {
planes,
tools,
placements,
} = *stone;
if !planes.is_empty() {
let converted: Vec<GpuFacetPlane> = planes
.iter()
.map(|&(normal, offset)| GpuFacetPlane::new(normal, -offset))
.collect();
let mut ctx = render_ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if generation != 0
&& !matches!(ctx.planes_owner, PlanesOwner::Catalogue { .. })
&& (!planes_equal_within(
ctx.active_planes.as_slice(),
&converted,
PLANE_REPUBLISH_EPSILON,
) || ctx.active_tools.as_slice() != tools)
{
let design_gear = ctx.design_gear;
if ctx.claim_active_geometry(
Arc::new(converted),
Arc::new(tools.to_vec()),
placements.to_vec(),
design_gear,
PlanesOwner::Editor { generation },
) {
ctx.dirty = true;
}
}
*solid_active_planes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(Arc::clone(&ctx.active_planes));
}
planes_generations_match(trace_active_planes, solid_active_planes)
}
fn store_solved(
committed: bool,
generation: u64,
masts: Vec<SolvedTier>,
last_solved: &SolidLastSolved,
) {
if committed {
*last_solved
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some((generation, masts));
} else {
editor::note_provisional_frame_masts(masts);
}
}
fn store_value<T>(state: &Mutex<T>, value: T) {
*state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
}
fn store_if_some<T>(state: &Mutex<Option<T>>, value: Option<T>) {
if let Some(value) = value {
*state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
}
}
fn push_committed_rows(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
generation: u64,
masts: &[SolvedTier],
warnings: Option<&Arc<Vec<ManufacturabilityWarning>>>,
) {
let due = LATE_ROWS.with(|rows| {
late_rows::land_committed_frame(rows, generation, warnings, |shown| {
editor::apply_matching_preview_frame(ui, render_ctx, generation, masts, shown)
})
});
push_due_findings(ui, render_ctx, due, FINDINGS_RETRIES);
}
fn note_drawn_cut(committed: bool, geometry: Option<&FrameGeometry>) {
if committed {
facet_selection::note_drawn_tiers(geometry.and_then(|shown| shown.visible_tiers.clone()));
}
}
pub(super) const fn frame_is_superseded(
planned: bool,
generation: u64,
cached_generation: u64,
) -> bool {
planned && generation < cached_generation
}
pub(in crate::gui) const fn frame_files_masts(planned: bool, superseded: bool) -> bool {
planned && !superseded
}
const FINDINGS_RETRY_INTERVAL: Duration = Duration::from_millis(100);
const FINDINGS_RETRIES: u8 = 30;
fn push_due_findings(
ui: &MainWindow,
render_ctx: &Arc<Mutex<RenderContext>>,
due: Option<Arc<LateFindings>>,
retries_left: u8,
) {
let Some(late) = due else {
return;
};
let outcome = editor::apply_late_findings(
ui,
render_ctx,
late.generation,
&late.design,
&late.masts,
&late.warnings,
);
if outcome != editor::LatePush::Held {
return;
}
LATE_ROWS.with(|rows| rows.borrow_mut().put_back(&late));
let Some(next) = cut_slider::retries_after_busy(retries_left) else {
return;
};
let ui_weak = ui.as_weak();
let render_ctx = Arc::clone(render_ctx);
slint::Timer::single_shot(FINDINGS_RETRY_INTERVAL, move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let due = LATE_ROWS.with(|rows| rows.borrow_mut().owed());
push_due_findings(&ui, &render_ctx, due, next);
});
}
impl PreviewSink for SlintSolidSink {
fn apply(&self, frame: PreviewFrame) {
let PreviewFrame {
image,
has_solid,
status,
solved,
stale,
pick,
edges_image,
diagram_image,
has_diagram,
diagram_pick,
diagram_tooth_pick,
diagram_panel_pick,
diagram_hover_text,
diagram_facet_owners,
planes,
tools,
placements,
hover_text,
facet_owners,
generation,
planned,
mesh_bounding_radius,
geometry,
warnings,
} = frame;
let pick_state = Arc::clone(&self.pick);
let last_solved_state = Arc::clone(&self.last_solved);
let diagram_pick_state = Arc::clone(&self.diagram_pick);
let diagram_tooth_pick_state = Arc::clone(&self.diagram_tooth_pick);
let diagram_panel_pick_state = Arc::clone(&self.diagram_panel_pick);
let diagram_hover_text_state = Arc::clone(&self.diagram_hover_text);
let diagram_facet_owners_state = Arc::clone(&self.diagram_facet_owners);
let hover_text_state = Arc::clone(&self.hover_text);
let facet_owners_state = Arc::clone(&self.facet_owners);
let render_ctx_state = Arc::clone(&self.render_ctx);
let solid_active_planes_state = Arc::clone(&self.solid_active_planes);
let trace_active_planes_state = Arc::clone(&self.trace_active_planes);
let mesh_bounding_radius_state = Arc::clone(&self.mesh_bounding_radius);
let geometry_state = Arc::clone(&self.geometry);
let _ = self.ui.upgrade_in_event_loop(move |ui| {
let cached_generation = last_solved_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map_or(0, |(cached_generation, _)| *cached_generation);
let superseded = generation < cached_generation;
if frame_is_superseded(planned, generation, cached_generation) {
return;
}
let committed = editor::frame_updates_mast_cache(generation);
store_value(&pick_state, Some(pick));
note_drawn_cut(committed, geometry.as_ref());
store_value(&geometry_state, geometry);
store_value(&mesh_bounding_radius_state, mesh_bounding_radius);
let files_masts = frame_files_masts(planned, superseded);
if committed
&& files_masts
&& let Some(masts) = solved.as_ref()
{
push_committed_rows(&ui, &render_ctx_state, generation, masts, warnings.as_ref());
}
if files_masts && let Some(masts) = solved {
store_solved(committed, generation, masts, &last_solved_state);
}
if !committed {
editor::note_provisional_frame_planes(&planes);
}
store_if_some(&diagram_pick_state, diagram_pick);
store_if_some(&diagram_tooth_pick_state, diagram_tooth_pick);
store_if_some(&diagram_panel_pick_state, diagram_panel_pick);
store_if_some(&diagram_hover_text_state, diagram_hover_text);
store_if_some(&diagram_facet_owners_state, diagram_facet_owners);
store_value(&hover_text_state, hover_text);
store_value(&facet_owners_state, facet_owners);
let trace_matches = sync_planes_and_check_trace_match(
&StonePublish::new(&planes, &tools, &placements),
&render_ctx_state,
&solid_active_planes_state,
&trace_active_planes_state,
if committed { generation } else { 0 },
);
let model = ui.global::<SolidPreviewModel>();
model.set_trace_matches_solid(trace_matches);
model.set_image(slint::Image::from_rgba8(image));
model.set_has_solid(has_solid);
model.set_status(status.into());
model.set_stale(stale);
if let Some(edges_image) = edges_image {
model.set_edges_image(slint::Image::from_rgba8(edges_image));
model.set_has_solid_edges(true);
} else {
model.set_has_solid_edges(false);
}
if let Some(diagram_image) = diagram_image {
model.set_diagram_image(slint::Image::from_rgba8(diagram_image));
}
model.set_has_diagram(has_diagram);
editor::on_solid_frame_landed(&ui, generation);
});
}
fn apply_findings(&self, findings: LateFindings) {
let render_ctx = Arc::clone(&self.render_ctx);
let _ = self.ui.upgrade_in_event_loop(move |ui| {
let due = LATE_ROWS.with(|rows| rows.borrow_mut().findings_arrived(findings));
push_due_findings(&ui, &render_ctx, due, FINDINGS_RETRIES);
});
}
}
#[cfg(test)]
mod tests {
use super::{
GpuFacetPlane, PLANE_REPUBLISH_EPSILON, frame_files_masts, frame_is_superseded,
planes_equal_within,
};
use glam::Vec3;
fn sample_planes() -> Vec<GpuFacetPlane> {
let mut planes = Vec::new();
for yaw_step in 0..8_u8 {
for pitch_step in 0..6_u8 {
let yaw = f32::from(yaw_step) * 0.7;
let pitch = f32::from(pitch_step).mul_add(0.4, -1.0);
let normal = Vec3::new(
pitch.cos() * yaw.cos(),
pitch.sin(),
pitch.cos() * yaw.sin(),
);
planes.push(GpuFacetPlane::new(normal, -0.5));
}
}
planes
}
#[test]
fn identical_planes_are_equal() {
let planes = sample_planes();
assert!(planes_equal_within(
&planes,
&planes.clone(),
PLANE_REPUBLISH_EPSILON
));
}
#[test]
fn a_one_ulp_normal_difference_does_not_republish() {
let planes = sample_planes();
let mut perturbed = planes.clone();
perturbed[3].normal[1] = f32::from_bits(perturbed[3].normal[1].to_bits() + 1);
assert!(planes_equal_within(
&planes,
&perturbed,
PLANE_REPUBLISH_EPSILON
));
}
#[test]
fn a_real_geometry_change_republishes() {
let planes = sample_planes();
let mut changed = planes.clone();
changed[3].normal[1] += 0.01;
assert!(!planes_equal_within(
&planes,
&changed,
PLANE_REPUBLISH_EPSILON
));
}
#[test]
fn different_plane_counts_never_match() {
let planes = sample_planes();
assert!(!planes_equal_within(
&planes,
&planes[..planes.len() - 1],
PLANE_REPUBLISH_EPSILON
));
}
#[test]
fn gpu_facet_plane_new_round_trip_stays_within_epsilon() {
let planes = sample_planes();
let once: Vec<GpuFacetPlane> = planes
.iter()
.map(|p| GpuFacetPlane::new(Vec3::from(p.normal), p.d))
.collect();
let twice: Vec<GpuFacetPlane> = once
.iter()
.map(|p| GpuFacetPlane::new(Vec3::from(p.normal), p.d))
.collect();
assert!(planes_equal_within(&planes, &once, PLANE_REPUBLISH_EPSILON));
assert!(planes_equal_within(
&planes,
&twice,
PLANE_REPUBLISH_EPSILON
));
}
#[test]
fn a_camera_follow_frame_behind_the_cache_watermark_is_never_superseded() {
assert!(!frame_is_superseded(false, 3, 9));
}
#[test]
fn a_planned_frame_behind_the_cache_watermark_is_superseded() {
assert!(frame_is_superseded(true, 3, 9));
}
#[test]
fn a_planned_frame_at_or_past_the_watermark_is_not_superseded() {
assert!(!frame_is_superseded(true, 9, 9));
assert!(!frame_is_superseded(true, 10, 9));
}
#[test]
fn only_a_current_planned_frame_files_its_masts() {
assert!(frame_files_masts(true, false));
assert!(!frame_files_masts(true, true), "behind the watermark");
assert!(!frame_files_masts(false, false), "a camera-follow frame");
assert!(!frame_files_masts(false, true));
}
}