use super::{
render::render_request,
request::RedrawRequest,
state::{WorkerMemory, resolve_request_state},
*,
};
use glam::Vec3;
use indicatrix_solid::preview::StoneGeometryBuf;
use std::{
sync::{Arc, Condvar, Mutex, PoisonError},
time::Duration,
};
mod pins;
mod replan;
mod worker_findings;
struct FakeSink {
calls: Mutex<Vec<(bool, String)>>,
plane_counts: Mutex<Vec<usize>>,
arrived: Condvar,
}
impl FakeSink {
fn new() -> Arc<Self> {
Arc::new(Self {
calls: Mutex::new(Vec::new()),
plane_counts: Mutex::new(Vec::new()),
arrived: Condvar::new(),
})
}
fn plane_counts(&self) -> Vec<usize> {
self.plane_counts
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
fn wait_until(
&self,
what: &str,
timeout: Duration,
ready: impl Fn(&[(bool, String)]) -> bool,
) -> Vec<(bool, String)> {
let (guard, result) = self
.arrived
.wait_timeout_while(
self.calls.lock().unwrap_or_else(PoisonError::into_inner),
timeout,
|calls| !ready(calls),
)
.unwrap_or_else(PoisonError::into_inner);
assert!(
!result.timed_out(),
"timed out after {timeout:?} waiting for {what}; frames received: {:?}",
*guard
);
guard.clone()
}
}
impl PreviewSink for FakeSink {
fn apply(&self, frame: PreviewFrame) {
self.plane_counts
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(frame.planes.len());
let mut calls = self.calls.lock().unwrap_or_else(PoisonError::into_inner);
calls.push((frame.has_solid, frame.status));
drop(calls);
self.arrived.notify_all();
}
}
const DEADLINE: Duration = Duration::from_secs(5);
fn box_planes(y_half: f32) -> Vec<(Vec3, f32)> {
vec![
(Vec3::X, 1.0),
(Vec3::NEG_X, 1.0),
(Vec3::Y, y_half),
(Vec3::NEG_Y, y_half),
(Vec3::Z, 1.0),
(Vec3::NEG_Z, 1.0),
]
}
fn unbounded_planes() -> Vec<(Vec3, f32)> {
vec![(Vec3::X, 1.0), (Vec3::NEG_X, 1.0)]
}
const CAMERA: CameraPose = CameraPose {
yaw: 0.0,
pitch: 0.0,
distance: 5.0,
};
#[test]
fn a_single_request_eventually_reaches_the_sink() {
let sink = FakeSink::new();
let state = SolidPreviewState::new(sink.clone());
state.request_redraw(&box_planes(0.6), CAMERA, (16, 16), 0);
let calls = sink.wait_until("the single frame", DEADLINE, |c| !c.is_empty());
assert_eq!(calls.len(), 1);
assert!(calls[0].0, "a closed box must report has_solid");
}
#[test]
fn coalesces_a_burst_of_requests_to_the_latest() {
let sink = FakeSink::new();
let state = SolidPreviewState::new(sink.clone());
for _ in 0..9 {
state.request_redraw(&unbounded_planes(), CAMERA, (16, 16), 0);
}
state.request_redraw(&box_planes(0.6), CAMERA, (16, 16), 0);
let calls = sink.wait_until("the closed frame", DEADLINE, |c| {
c.last().is_some_and(|(has_solid, _)| *has_solid)
});
assert!(
calls.len() < 10,
"expected coalescing to avoid one render per request, got {}",
calls.len()
);
let (has_solid, _status) = calls.last().expect("at least one call must have landed");
assert!(
*has_solid,
"the last frame the sink receives must reflect the LAST submitted request"
);
}
#[test]
fn a_non_closed_request_reports_has_solid_false_with_a_reason() {
let sink = FakeSink::new();
let state = SolidPreviewState::new(sink.clone());
state.request_redraw(&unbounded_planes(), CAMERA, (16, 16), 0);
let calls = sink.wait_until("the unbounded frame", DEADLINE, |c| !c.is_empty());
let (has_solid, status) = calls.last().unwrap();
assert!(!has_solid);
assert!(status.contains("Unbounded"), "got: {status}");
}
#[test]
fn an_unbounded_request_after_a_closed_one_keeps_showing_the_last_solid() {
let sink = FakeSink::new();
let state = SolidPreviewState::new(sink.clone());
state.request_redraw(&box_planes(0.6), CAMERA, (16, 16), 0);
let first = sink.wait_until("the first frame", DEADLINE, |c| !c.is_empty());
assert!(first.last().unwrap().0, "the first frame must close");
state.request_redraw(&unbounded_planes(), CAMERA, (16, 16), 0);
let calls = sink.wait_until("the second frame", DEADLINE, |c| c.len() >= 2);
let (has_solid, status) = calls.last().unwrap().clone();
assert!(
has_solid,
"a held-over closed solid must still be shown, not blanked"
);
assert!(status.contains("Unbounded"), "got: {status}");
}
#[test]
fn facet_overlay_update_reuses_the_last_context_and_applies_to_both_styles() {
let mut memory = WorkerMemory {
planes: Some(box_planes(0.6)),
camera: Some(CAMERA),
size: Some((16, 16)),
view_mode: Some(0),
..WorkerMemory::default()
};
let overlay = FacetOverlay {
hovered: Some(3),
selected_facet: Some(5),
multi_selected: vec![1, 2],
provisional: vec![4],
moved: vec![6],
};
let mut mesh_cache = MeshCache::default();
let (planes, camera, size, view_mode, style, ..) = resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::UpdateFacetOverlay(overlay.clone()),
)
.expect(
"memory.size is Some, so this is not the no-op case of a facet-overlay update \
arriving before the first Planned/Reproject request",
);
assert_eq!(
planes.halfspaces(),
box_planes(0.6),
"must reuse the last-known planes"
);
assert_eq!(camera, CAMERA, "must reuse the last-known camera pose");
assert_eq!(size, (16, 16), "must reuse the last-known viewport size");
assert_eq!(view_mode, 0, "must reuse the last-known view mode");
assert_eq!(style.hovered, overlay.hovered);
assert_eq!(style.selected_facet, overlay.selected_facet);
assert_eq!(style.multi_selected, overlay.multi_selected);
assert_eq!(style.provisional, overlay.provisional);
assert_eq!(style.moved, overlay.moved);
assert_eq!(
memory.diagram.style.hovered, overlay.hovered,
"the diagram's own style must agree, so switching to Diagram mode \
afterward shows the same highlight"
);
}
#[test]
fn an_ordinary_reproject_does_not_clear_a_facet_overlay() {
let mut memory = WorkerMemory::default();
let mut mesh_cache = MeshCache::default();
let _ = resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::UpdateFacetOverlay(FacetOverlay::default()),
);
let _ = resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::Reproject {
geometry: StoneGeometryBuf::from_halfspaces(&box_planes(0.6)),
camera: CAMERA,
size: (16, 16),
view_mode: 0,
gear: None,
},
);
let _ = resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::UpdateFacetOverlay(FacetOverlay {
hovered: Some(7),
..FacetOverlay::default()
}),
);
let (_planes, _camera, _size, _view_mode, style, ..) = resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::Reproject {
geometry: StoneGeometryBuf::from_halfspaces(&box_planes(0.6)),
camera: CAMERA,
size: (16, 16),
view_mode: 0,
gear: None,
},
)
.expect("a Reproject request always resolves");
assert_eq!(
style.hovered,
Some(7),
"an orbit/zoom reproject at the SAME planes must not drop the hover"
);
}
#[test]
fn update_facet_overlay_before_any_frame_resolves_to_nothing() {
let mut memory = WorkerMemory::default();
let mut mesh_cache = MeshCache::default();
assert!(
resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::UpdateFacetOverlay(FacetOverlay {
hovered: Some(3),
..FacetOverlay::default()
}),
)
.is_none(),
"no Planned/Reproject frame has ever resolved, so there is nothing to redraw"
);
let mut rasterizer = SolidRasterizer::new(1, 1);
let mut edges_rasterizer = SolidRasterizer::new(1, 1);
assert!(
render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
RedrawRequest::UpdateFacetOverlay(FacetOverlay::default()),
)
.is_none(),
"render_request must not synthesize a 1x1 placeholder frame either"
);
}