use super::{
diagram2d::{self, DiagramConfig, DiagramStyle, PanelKind},
edges_layer::render_edges_layer,
mesh_cache::{CachedMesh, MeshCache},
raster::{SolidRasterizer, SolidStyle},
to_diagram_pixel_buffer, to_pixel_buffer,
};
#[cfg(feature = "editor")]
use super::{facet_map::FacetMap, live_update};
use glam::Vec3;
use indicatrix::{
geometry::{
meet_solver::SolvedTier,
stone_metrics::{SolidMesh, SolidStatus},
},
optics::raytracer::Camera,
};
use std::sync::{
Arc, Mutex, PoisonError,
mpsc::{self, Sender},
};
#[cfg(feature = "editor")]
use std::sync::Weak;
use crate::bridge::render_thread::RedrawGate;
pub type SolidLastSolved = Arc<Mutex<Option<Vec<SolvedTier>>>>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CameraPose {
pub yaw: f32,
pub pitch: f32,
pub distance: f32,
}
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct FacetOverlay {
pub hovered: Option<u32>,
pub selected_facet: Option<u32>,
pub multi_selected: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct PickBuffer {
pub width: u32,
pub height: u32,
pick: Vec<u32>,
}
impl PickBuffer {
#[must_use]
pub fn facet_at(&self, x: u32, y: u32) -> Option<u32> {
if x >= self.width || y >= self.height {
return None;
}
let v = self.pick[(y * self.width + x) as usize];
(v != 0).then(|| v - 1)
}
}
pub struct SolidPickState {
pub pick: Arc<Mutex<Option<PickBuffer>>>,
pub hover_text: Arc<Mutex<Vec<String>>>,
pub facet_tier: Arc<Mutex<Vec<Option<usize>>>>,
}
enum RedrawRequest {
Reproject {
planes: Vec<(Vec3, f32)>,
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
gear: Option<(u32, f32)>,
},
UpdateFacetOverlay(FacetOverlay),
#[cfg(feature = "editor")]
Planned(Box<PlannedFrame>),
}
#[cfg(feature = "editor")]
pub struct ReplanRequest {
pub design: indicatrix_cut_core::Design,
pub dirty: std::collections::BTreeSet<usize>,
pub last_solved: Option<Vec<SolvedTier>>,
pub camera: CameraPose,
pub size: (u32, u32),
pub selected_tier: Option<usize>,
pub n_d: f64,
pub view_mode: u8,
pub generation: u64,
pub show_preform: bool,
pub enlarged_panel: i32,
}
#[cfg(feature = "editor")]
const fn panel_kind_from_index(index: i32) -> Option<PanelKind> {
match index {
0 => Some(PanelKind::Crown),
1 => Some(PanelKind::Pavilion),
2 => Some(PanelKind::Profile),
_ => None,
}
}
pub trait PreviewSink: Send + Sync + 'static {
fn apply(&self, frame: PreviewFrame);
}
pub struct PreviewFrame {
pub image: slint::SharedPixelBuffer<slint::Rgba8Pixel>,
pub has_solid: bool,
pub status: String,
pub solved: Option<Vec<SolvedTier>>,
pub stale: bool,
pub pick: PickBuffer,
pub edges_image: Option<slint::SharedPixelBuffer<slint::Rgba8Pixel>>,
pub diagram_image: Option<slint::SharedPixelBuffer<slint::Rgba8Pixel>>,
pub has_diagram: bool,
pub diagram_pick: Option<PickBuffer>,
pub diagram_tooth_pick: Option<PickBuffer>,
pub diagram_hover_text: Option<Vec<String>>,
pub diagram_facet_tier: Option<Vec<Option<usize>>>,
pub planes: Vec<(Vec3, f32)>,
pub hover_text: Vec<String>,
pub facet_tier: Vec<Option<usize>>,
pub generation: u64,
pub mesh_bounding_radius: f64,
}
pub const DEFAULT_MESH_BOUNDING_RADIUS: f64 = 1.5;
struct DiagramMemory {
gear_teeth: u32,
gear_reference_angle: f32,
symmetry_order: u32,
mirror: bool,
enlarged_panel: Option<PanelKind>,
style: DiagramStyle,
hover_text: Vec<String>,
facet_tier: Vec<Option<usize>>,
}
impl Default for DiagramMemory {
fn default() -> Self {
Self {
gear_teeth: 96,
gear_reference_angle: 0.0,
symmetry_order: 8,
mirror: true,
enlarged_panel: None,
style: DiagramStyle::default(),
hover_text: Vec::new(),
facet_tier: Vec::new(),
}
}
}
#[derive(Default)]
struct WorkerMemory {
style: SolidStyle,
diagram: DiagramMemory,
solved_masts: Option<Vec<SolvedTier>>,
planes: Option<Vec<(Vec3, f32)>>,
camera: Option<CameraPose>,
size: Option<(u32, u32)>,
view_mode: Option<u8>,
generation: u64,
}
fn dim_style(style: SolidStyle) -> SolidStyle {
SolidStyle {
base_color: [120, 122, 128],
ambient: 0.4,
diffuse: 0.35,
..style
}
}
#[cfg(feature = "editor")]
fn escaping_tier_label(
design: &indicatrix_cut_core::Design,
solved: &[SolvedTier],
plane_index: usize,
) -> String {
design
.tier_for_plane_index(solved, plane_index)
.map_or_else(
|| format!("plane {plane_index}"),
|tier_index| {
design.tiers.get(tier_index).map_or_else(
|| format!("tier {}", tier_index + 1),
|tier| {
if tier.name.is_empty() {
format!("tier {}", tier_index + 1)
} else {
format!("{} (tier {})", tier.name, tier_index + 1)
}
},
)
},
)
}
#[cfg(feature = "editor")]
struct PlanJob {
design: indicatrix_cut_core::Design,
dirty: std::collections::BTreeSet<usize>,
last_solved: Option<Vec<SolvedTier>>,
camera: CameraPose,
size: (u32, u32),
selected_tier: Option<usize>,
n_d: f64,
view_mode: u8,
generation: u64,
show_preform: bool,
enlarged_panel: i32,
tier_cutoff: Option<usize>,
}
#[cfg(feature = "editor")]
struct PlannedFrame {
design: indicatrix_cut_core::Design,
planes: Vec<(Vec3, f32)>,
style: SolidStyle,
solved: Option<Vec<SolvedTier>>,
stale: bool,
unsolvable_status: Option<String>,
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
generation: u64,
n_d: f64,
enlarged_panel: i32,
}
#[cfg(feature = "editor")]
fn build_planned_frame(job: PlanJob, budget: std::time::Duration) -> PlannedFrame {
let PlanJob {
design,
dirty,
last_solved,
camera,
size,
selected_tier,
n_d,
view_mode,
generation,
show_preform,
enlarged_panel,
tier_cutoff,
} = job;
let plan = live_update::plan_preview(
&design,
last_solved.as_deref(),
&dirty,
budget,
&live_update::RealSolver,
tier_cutoff,
);
let pending_tier = match &plan.freshness {
live_update::Freshness::Stale { pending } => pending.iter().next().copied(),
_ => None,
};
let facet_map = FacetMap::from_design(&design, plan.solved.as_deref().unwrap_or(&[]));
let overlay = facet_map.overlay_flags(&design, n_d, selected_tier, pending_tier);
let unsolvable_status = match &plan.freshness {
live_update::Freshness::Unsolvable(err) => {
Some(format!("Preview cannot be solved: {err}."))
}
_ => None,
};
let is_unsolvable = unsolvable_status.is_some();
let preform_plane_count = facet_map.preform_plane_count();
let style = SolidStyle {
flagged: overlay.flagged,
pending: overlay.pending,
selected: overlay.selected,
preform_plane_count,
show_preform,
..SolidStyle::default()
};
let stale = matches!(plan.freshness, live_update::Freshness::Stale { .. });
let solved = if is_unsolvable {
last_solved
} else {
plan.solved
};
PlannedFrame {
design,
planes: plan.planes,
style,
solved,
stale,
unsolvable_status,
camera,
size,
view_mode,
generation,
n_d,
enlarged_panel,
}
}
#[cfg(feature = "editor")]
fn update_diagram_memory_from_design(
last_diagram: &mut DiagramMemory,
design: &indicatrix_cut_core::Design,
solved: Option<&[SolvedTier]>,
solid_style: &SolidStyle,
n_d: f64,
) {
let facet_map = FacetMap::from_design(design, solved.unwrap_or(&[]));
let facet_count = facet_map.facet_count();
let mut labels = vec![String::new(); facet_count];
let mut hover_text = vec![String::new(); facet_count];
let mut facet_tier = vec![None; facet_count];
let mut facet_index_on_gear = vec![0u32; facet_count];
for id in 0..facet_count {
labels[id] = facet_map.facet_label(id);
hover_text[id] = facet_map.hover_text(id, n_d);
facet_tier[id] = facet_map.tier_of(id);
facet_index_on_gear[id] = facet_map.index_on_gear(id);
}
last_diagram.style = DiagramStyle {
flagged: solid_style.flagged.clone(),
pending: solid_style.pending.clone(),
selected: solid_style.selected.clone(),
facet_labels: labels,
facet_index_on_gear,
meet_marker_pairs: facet_map.meeting_facet_pairs(design),
..DiagramStyle::default()
};
last_diagram.hover_text = hover_text;
last_diagram.facet_tier = facet_tier;
}
type DiagramOutputs = (
Option<slint::SharedPixelBuffer<slint::Rgba8Pixel>>,
bool,
Option<PickBuffer>,
Option<PickBuffer>,
Option<Vec<String>>,
Option<Vec<Option<usize>>>,
);
fn build_diagram_outputs(
mesh_cache: &mut MeshCache,
planes: &[(Vec3, f32)],
size: (u32, u32),
view_mode: u8,
last_diagram: &DiagramMemory,
) -> DiagramOutputs {
if view_mode != 3 {
return (None, false, None, None, None, None);
}
let (image, has_diagram, pick, tooth_pick) =
mesh_cache
.get_or_build(planes)
.map_or((None, false, None, None), |cached| {
let config = DiagramConfig {
width: size.0,
height: size.1,
gear_teeth: last_diagram.gear_teeth,
gear_reference_angle: last_diagram.gear_reference_angle,
symmetry_order: last_diagram.symmetry_order,
mirror: last_diagram.mirror,
};
let diagram_frame = last_diagram.enlarged_panel.map_or_else(
|| diagram2d::render_diagram(&cached.mesh, &config, &last_diagram.style),
|panel| {
diagram2d::render_diagram_single_panel(
&cached.mesh,
&config,
&last_diagram.style,
panel,
)
},
);
let image = to_diagram_pixel_buffer(&diagram_frame);
let tooth_pick = PickBuffer {
width: diagram_frame.width,
height: diagram_frame.height,
pick: diagram_frame.tooth,
};
let pick = PickBuffer {
width: diagram_frame.width,
height: diagram_frame.height,
pick: diagram_frame.pick,
};
(Some(image), true, Some(pick), Some(tooth_pick))
});
(
image,
has_diagram,
pick,
tooth_pick,
Some(last_diagram.hover_text.clone()),
Some(last_diagram.facet_tier.clone()),
)
}
const fn apply_reproject_gear(last_diagram: &mut DiagramMemory, gear: Option<(u32, f32)>) {
if let Some((gear_teeth, gear_reference_angle)) = gear {
last_diagram.gear_teeth = gear_teeth;
last_diagram.gear_reference_angle = gear_reference_angle;
}
}
type RequestState = (
Vec<(Vec3, f32)>,
CameraPose,
(u32, u32),
u8,
SolidStyle,
Option<Vec<SolvedTier>>,
bool,
Option<String>,
u64,
);
fn resolve_request_state(
memory: &mut WorkerMemory,
mesh_cache: &mut MeshCache,
request: RedrawRequest,
) -> RequestState {
match request {
RedrawRequest::Reproject {
planes,
camera,
size,
view_mode,
gear,
} => {
apply_reproject_gear(&mut memory.diagram, gear);
if memory.planes.as_deref() != Some(planes.as_slice()) {
memory.style = SolidStyle::default();
memory.diagram = DiagramMemory::default();
}
memory.planes = Some(planes.clone());
memory.camera = Some(camera);
memory.size = Some(size);
memory.view_mode = Some(view_mode);
(
planes,
camera,
size,
view_mode,
memory.style.clone(),
memory.solved_masts.clone(),
false,
None,
memory.generation,
)
}
RedrawRequest::UpdateFacetOverlay(overlay) => {
memory.style.hovered = overlay.hovered;
memory.style.selected_facet = overlay.selected_facet;
memory.style.multi_selected = overlay.multi_selected.clone();
memory.diagram.style.hovered = overlay.hovered;
memory.diagram.style.selected_facet = overlay.selected_facet;
memory.diagram.style.multi_selected = overlay.multi_selected;
(
memory.planes.clone().unwrap_or_default(),
memory.camera.unwrap_or(CameraPose {
yaw: 0.0,
pitch: 0.0,
distance: 5.0,
}),
memory.size.unwrap_or((1, 1)),
memory.view_mode.unwrap_or(0),
memory.style.clone(),
memory.solved_masts.clone(),
false,
None,
memory.generation,
)
}
#[cfg(feature = "editor")]
RedrawRequest::Planned(frame) => resolve_planned_state(memory, mesh_cache, *frame),
}
}
#[cfg(feature = "editor")]
fn resolve_planned_state(
memory: &mut WorkerMemory,
mesh_cache: &mut MeshCache,
frame: PlannedFrame,
) -> RequestState {
let PlannedFrame {
design,
planes,
style,
solved,
stale,
unsolvable_status,
camera,
size,
view_mode,
generation,
n_d,
enlarged_panel,
} = frame;
let closes = mesh_cache.get_or_build(&planes).is_some();
let unbounded_status = if unsolvable_status.is_none() && !closes {
match mesh_cache.status() {
Some(SolidStatus::Unbounded { escaping }) => {
let solved_ref = solved.as_deref().unwrap_or(&[]);
let names = escaping
.iter()
.map(|&plane_index| escaping_tier_label(&design, solved_ref, plane_index))
.collect::<Vec<_>>()
.join(", ");
Some(format!("Unbounded: {names} never close the solid."))
}
_ => None,
}
} else {
None
};
let holding_over = unsolvable_status.is_some() || unbounded_status.is_some();
let preview_status = unsolvable_status.or(unbounded_status);
let style = if holding_over {
dim_style(style)
} else {
style
};
memory.style = style.clone();
if solved.is_some() {
memory.solved_masts.clone_from(&solved);
}
memory.planes = Some(planes.clone());
memory.camera = Some(camera);
memory.size = Some(size);
memory.view_mode = Some(view_mode);
memory.generation = generation;
memory.diagram.gear_teeth = design.meta.gear_teeth_abs();
memory.diagram.gear_reference_angle = design.meta.gear_reference_angle as f32;
memory.diagram.symmetry_order = design.meta.symmetry_order;
memory.diagram.mirror = design.meta.mirror;
memory.diagram.enlarged_panel = panel_kind_from_index(enlarged_panel);
update_diagram_memory_from_design(&mut memory.diagram, &design, solved.as_deref(), &style, n_d);
(
planes,
camera,
size,
view_mode,
style,
solved,
stale,
preview_status,
generation,
)
}
fn render_request(
mesh_cache: &mut MeshCache,
rasterizer: &mut SolidRasterizer,
edges_rasterizer: &mut SolidRasterizer,
memory: &mut WorkerMemory,
request: RedrawRequest,
) -> PreviewFrame {
let (planes, camera_pose, size, view_mode, style, solved, stale, unsolvable_status, generation) =
resolve_request_state(memory, mesh_cache, request);
rasterizer.resize(size.0, size.1);
let camera = Camera::new(
camera_pose.yaw,
camera_pose.pitch,
camera_pose.distance,
42.0,
);
let (image, has_solid, status) = if let Some(cached) = mesh_cache.get_or_build(&planes) {
rasterizer.render_prepared(cached, &camera, &style);
(to_pixel_buffer(rasterizer), true, String::new())
} else if let Some(cached) = mesh_cache.last_closed() {
let dimmed = dim_style(style.clone());
rasterizer.render_prepared(cached, &camera, &dimmed);
(
to_pixel_buffer(rasterizer),
true,
mesh_cache.status_message(),
)
} else {
rasterizer.render(&SolidMesh::default(), &camera, &style);
(
to_pixel_buffer(rasterizer),
false,
mesh_cache.status_message(),
)
};
let status = unsolvable_status.unwrap_or(status);
let pick = PickBuffer {
width: rasterizer.width,
height: rasterizer.height,
pick: rasterizer.pick.clone(),
};
let edges_image = if view_mode == 2 {
mesh_cache.get_or_build(&planes).map(|cached| {
edges_rasterizer.resize(size.0, size.1);
render_edges_layer(edges_rasterizer, cached, &camera, &style);
to_pixel_buffer(edges_rasterizer)
})
} else {
None
};
let (
diagram_image,
has_diagram,
diagram_pick,
diagram_tooth_pick,
diagram_hover_text,
diagram_facet_tier,
) = build_diagram_outputs(mesh_cache, &planes, size, view_mode, &memory.diagram);
let hover_text = memory.diagram.hover_text.clone();
let facet_tier = memory.diagram.facet_tier.clone();
let mesh_bounding_radius = mesh_cache
.get_or_build(&planes)
.map(CachedMesh::bounding_radius)
.or_else(|| mesh_cache.last_closed().map(CachedMesh::bounding_radius))
.unwrap_or(DEFAULT_MESH_BOUNDING_RADIUS);
PreviewFrame {
image,
has_solid,
status,
solved,
stale,
pick,
edges_image,
diagram_image,
has_diagram,
diagram_pick,
diagram_tooth_pick,
diagram_hover_text,
diagram_facet_tier,
planes,
hover_text,
facet_tier,
generation,
mesh_bounding_radius,
}
}
pub struct SolidPreviewState {
sink: Arc<dyn PreviewSink>,
gate: Arc<RedrawGate<RedrawRequest>>,
wake: Mutex<Option<Sender<()>>>,
#[cfg(feature = "editor")]
plan_gate: Arc<RedrawGate<PlanJob>>,
#[cfg(feature = "editor")]
plan_wake: Mutex<Option<Sender<()>>>,
#[cfg(feature = "editor")]
self_weak: Mutex<Weak<Self>>,
#[cfg(feature = "editor")]
tier_cutoff: Mutex<Option<usize>>,
}
impl SolidPreviewState {
#[must_use]
pub fn new(sink: Arc<dyn PreviewSink>) -> Arc<Self> {
let state = Arc::new(Self {
sink,
gate: Arc::new(RedrawGate::new()),
wake: Mutex::new(None),
#[cfg(feature = "editor")]
plan_gate: Arc::new(RedrawGate::new()),
#[cfg(feature = "editor")]
plan_wake: Mutex::new(None),
#[cfg(feature = "editor")]
self_weak: Mutex::new(Weak::new()),
#[cfg(feature = "editor")]
tier_cutoff: Mutex::new(None),
});
#[cfg(feature = "editor")]
{
*state
.self_weak
.lock()
.unwrap_or_else(PoisonError::into_inner) = Arc::downgrade(&state);
}
state
}
pub fn request_redraw(
&self,
planes: Vec<(Vec3, f32)>,
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
) {
self.request_redraw_with_gear(planes, camera, size, view_mode, None);
}
pub fn request_redraw_with_gear(
&self,
planes: Vec<(Vec3, f32)>,
camera: CameraPose,
size: (u32, u32),
view_mode: u8,
gear: Option<(u32, f32)>,
) {
self.submit(RedrawRequest::Reproject {
planes,
camera,
size,
view_mode,
gear,
});
}
pub fn request_facet_overlay(&self, overlay: FacetOverlay) {
self.submit(RedrawRequest::UpdateFacetOverlay(overlay));
}
#[cfg(feature = "editor")]
pub fn request_replan(&self, request: ReplanRequest) {
let tier_cutoff = *self
.tier_cutoff
.lock()
.unwrap_or_else(PoisonError::into_inner);
self.submit_plan(PlanJob {
design: request.design,
dirty: request.dirty,
last_solved: request.last_solved,
camera: request.camera,
size: request.size,
selected_tier: request.selected_tier,
n_d: request.n_d,
view_mode: request.view_mode,
generation: request.generation,
show_preform: request.show_preform,
enlarged_panel: request.enlarged_panel,
tier_cutoff,
});
}
#[cfg(feature = "editor")]
pub fn set_tier_cutoff(&self, cutoff: Option<usize>) {
*self
.tier_cutoff
.lock()
.unwrap_or_else(PoisonError::into_inner) = cutoff;
}
fn submit(&self, request: RedrawRequest) {
let tx = {
let mut guard = self.wake.lock().unwrap_or_else(PoisonError::into_inner);
if guard.is_none() {
*guard = Some(self.spawn_worker());
}
guard
.clone()
.expect("just initialized above if it was empty")
};
if self.gate.submit(request).is_some() {
let _ = tx.send(());
}
}
fn spawn_worker(&self) -> Sender<()> {
let (tx, rx) = mpsc::channel::<()>();
let sink = Arc::clone(&self.sink);
let gate = Arc::clone(&self.gate);
std::thread::spawn(move || {
let mut mesh_cache = MeshCache::default();
let mut rasterizer = SolidRasterizer::new(1, 1);
let mut edges_rasterizer = SolidRasterizer::new(1, 1);
let mut memory = WorkerMemory::default();
for () in rx {
let Some(request) = gate.take() else {
continue;
};
let frame = render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
request,
);
sink.apply(frame);
}
});
tx
}
#[cfg(feature = "editor")]
fn submit_plan(&self, job: PlanJob) {
let tx = {
let mut guard = self
.plan_wake
.lock()
.unwrap_or_else(PoisonError::into_inner);
if guard.is_none() {
*guard = Some(self.spawn_plan_worker());
}
guard
.clone()
.expect("just initialized above if it was empty")
};
if self.plan_gate.submit(job).is_some() {
let _ = tx.send(());
}
}
#[cfg(feature = "editor")]
fn spawn_plan_worker(&self) -> Sender<()> {
let (tx, rx) = mpsc::channel::<()>();
let plan_gate = Arc::clone(&self.plan_gate);
let self_weak = self
.self_weak
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
std::thread::spawn(move || {
for () in rx {
let Some(job) = plan_gate.take() else {
continue;
};
let frame = build_planned_frame(job, live_update::DEFAULT_PREVIEW_BUDGET);
if let Some(state) = self_weak.upgrade() {
state.submit(RedrawRequest::Planned(Box::new(frame)));
}
}
});
tx
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
struct FakeSink {
calls: Mutex<Vec<(bool, String)>>,
}
impl FakeSink {
fn new() -> Arc<Self> {
Arc::new(Self {
calls: Mutex::new(Vec::new()),
})
}
fn calls(&self) -> Vec<(bool, String)> {
self.calls
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
}
impl PreviewSink for FakeSink {
fn apply(&self, frame: PreviewFrame) {
self.calls
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push((frame.has_solid, frame.status));
}
}
fn wait_for_settled<T: PartialEq + Clone>(mut f: impl FnMut() -> T, timeout: Duration) -> T {
let deadline = Instant::now() + timeout;
let mut last = f();
let mut stable_polls = 0u32;
loop {
std::thread::sleep(Duration::from_millis(15));
let current = f();
if current == last {
stable_polls += 1;
if stable_polls >= 4 {
return current;
}
} else {
stable_polls = 0;
last = current;
}
assert!(Instant::now() < deadline, "worker thread never settled");
}
}
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 = wait_for_settled(|| sink.calls().len(), Duration::from_secs(5));
assert_eq!(calls, 1);
assert!(sink.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 final_len = wait_for_settled(|| sink.calls().len(), Duration::from_secs(5));
assert!(
final_len < 10,
"expected coalescing to avoid one render per request, got {final_len}"
);
let calls = sink.calls();
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);
wait_for_settled(|| sink.calls().len(), Duration::from_secs(5));
let calls = sink.calls();
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);
wait_for_settled(|| sink.calls().len(), Duration::from_secs(5));
assert!(sink.calls().last().unwrap().0, "the first frame must close");
state.request_redraw(unbounded_planes(), CAMERA, (16, 16), 0);
let final_len = wait_for_settled(|| sink.calls().len(), Duration::from_secs(5));
assert!(final_len >= 2);
let (has_solid, status) = sink.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],
};
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()),
);
assert_eq!(planes, 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!(
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();
resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::UpdateFacetOverlay(FacetOverlay::default()),
);
resolve_request_state(
&mut memory,
&mut mesh_cache,
RedrawRequest::Reproject {
planes: box_planes(0.6),
camera: CAMERA,
size: (16, 16),
view_mode: 0,
gear: None,
},
);
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 {
planes: box_planes(0.6),
camera: CAMERA,
size: (16, 16),
view_mode: 0,
gear: None,
},
);
assert_eq!(
style.hovered,
Some(7),
"an orbit/zoom reproject at the SAME planes must not drop the hover"
);
}
#[cfg(feature = "editor")]
mod replan {
use super::*;
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{ConstraintTier, Design, PreformSpec, ScheduleMeta};
fn tier(name: &str, angle_deg: f64, constraint: MeetConstraint) -> ConstraintTier {
ConstraintTier {
angle_deg,
name: name.to_string(),
indices: vec![0.0],
constraint,
imported_meet: None,
original_notes: None,
detached: Vec::new(),
}
}
fn pinned_design() -> Design {
Design::new(
PreformSpec::block(1.0, 1.0, 1.0),
ScheduleMeta {
gear_teeth: 96,
..ScheduleMeta::default()
},
vec![
tier("Table", 0.0, MeetConstraint::ScaleReference(0.5)),
tier("Pavilion", -40.0, MeetConstraint::ScaleReference(0.6)),
],
)
}
fn closed_design() -> Design {
const GIRDLE_INDICES: [f64; 16] = [
0.0, 6.0, 12.0, 18.0, 24.0, 30.0, 36.0, 42.0, 48.0, 54.0, 60.0, 66.0, 72.0, 78.0,
84.0, 90.0,
];
const BREAK_INDICES: [f64; 16] = [
95.0, 1.0, 11.0, 13.0, 23.0, 25.0, 35.0, 37.0, 47.0, 49.0, 59.0, 61.0, 71.0, 73.0,
83.0, 85.0,
];
const MAIN_INDICES: [f64; 8] = [0.0, 12.0, 24.0, 36.0, 48.0, 60.0, 72.0, 84.0];
const STAR_INDICES: [f64; 8] = [6.0, 18.0, 30.0, 42.0, 54.0, 66.0, 78.0, 90.0];
fn rbc_tier(name: &str, angle_deg: f64, indices: &[f64], mast: f64) -> ConstraintTier {
ConstraintTier {
angle_deg,
name: name.to_string(),
indices: indices.to_vec(),
constraint: MeetConstraint::ScaleReference(mast),
imported_meet: None,
original_notes: None,
detached: Vec::new(),
}
}
Design::new(
PreformSpec::block(2.0, 1.0, 2.0),
ScheduleMeta {
gemcad_version: "GemCad 5.0".to_string(),
gear_teeth: 96,
gear_reference_angle: 0.0,
symmetry_order: 8,
mirror: true,
refractive_index: 1.54,
headers: Vec::new(),
footnotes: Vec::new(),
},
vec![
rbc_tier("Table", 0.0, &[], 0.32),
rbc_tier("Star", 15.0, &STAR_INDICES, 0.45),
rbc_tier("Crown Main", 34.5, &MAIN_INDICES, 0.59),
rbc_tier("Upper Girdle", 41.0, &BREAK_INDICES, 0.67),
rbc_tier("Girdle", 90.0, &GIRDLE_INDICES, 1.0),
rbc_tier("Pavilion Main", -41.0, &MAIN_INDICES, 0.67),
rbc_tier("Lower Girdle", -42.5, &BREAK_INDICES, 0.68),
rbc_tier("Culet", -0.0, &[], 0.88),
],
)
}
fn free_design() -> Design {
Design::new(
PreformSpec::block(2.0, 1.0, 2.0),
ScheduleMeta {
gear_teeth: 96,
..ScheduleMeta::default()
},
vec![
tier("C1", 30.0, MeetConstraint::ScaleReference(0.6)),
tier("C2", 40.0, MeetConstraint::MeetExisting),
],
)
}
fn plan_job(design: Design) -> PlanJob {
let n_d = design.effective_refractive_index();
PlanJob {
design,
dirty: std::collections::BTreeSet::new(),
last_solved: None,
camera: CAMERA,
size: (16, 16),
selected_tier: None,
n_d,
view_mode: 0,
generation: 0,
show_preform: true,
enlarged_panel: -1,
tier_cutoff: None,
}
}
#[test]
fn build_planned_frame_returns_the_solved_masts_for_a_pinned_design() {
let design = pinned_design();
let frame = build_planned_frame(plan_job(design), live_update::DEFAULT_PREVIEW_BUDGET);
assert_ne!(frame.planes, Vec::<(Vec3, f32)>::new());
assert_eq!(frame.solved.map(|s| s.len()), Some(2));
assert!(!frame.stale, "a pinned design never goes over budget");
}
#[test]
fn a_zero_budget_forces_stale_and_marks_the_dirty_tier_pending() {
let design = free_design();
let previous = design.solve().expect("fixture must solve");
let dirty = std::collections::BTreeSet::from([1]);
let design_for_facet_map = design.clone();
let frame = build_planned_frame(
PlanJob {
last_solved: Some(previous),
dirty,
..plan_job(design)
},
Duration::ZERO,
);
assert!(
frame.stale,
"a real resolve_dirty call can never finish within 0ns"
);
assert!(
frame.solved.is_some(),
"the fresh (late) result must still be chained forward"
);
let facet_map = FacetMap::from_design(
&design_for_facet_map,
frame.solved.as_deref().unwrap_or(&[]),
);
assert!(
facet_map
.facets_of_tier(1)
.iter()
.all(|&id| frame.style.pending[id as usize]),
"the edited tier's own facets must be marked pending"
);
}
#[test]
fn escaping_tier_label_names_the_owning_tier() {
let design = pinned_design();
let solved = design.solve().expect("every tier is pinned");
let preform_plane_count = design.preform.planes().len();
let label = escaping_tier_label(&design, &solved, preform_plane_count);
assert_eq!(label, "Table (tier 1)");
}
#[test]
fn escaping_tier_label_falls_back_to_a_raw_plane_for_a_preform_plane() {
let design = pinned_design();
let solved = design.solve().expect("every tier is pinned");
let label = escaping_tier_label(&design, &solved, 0);
assert_eq!(label, "plane 0");
}
#[test]
fn selected_tier_flags_reach_solid_style_selected() {
let design = pinned_design();
let design_for_facet_map = design.clone();
let frame = build_planned_frame(
PlanJob {
selected_tier: Some(0),
..plan_job(design)
},
live_update::DEFAULT_PREVIEW_BUDGET,
);
let style = frame.style;
let solved = design_for_facet_map.solve().expect("every tier is pinned");
let facet_map = FacetMap::from_design(&design_for_facet_map, &solved);
assert!(
facet_map
.facets_of_tier(0)
.iter()
.all(|&id| style.selected[id as usize]),
"tier 0's own facets must be marked selected"
);
assert!(
facet_map
.facets_of_tier(1)
.iter()
.all(|&id| !style.selected[id as usize]),
"tier 1 was never selected"
);
}
#[test]
fn tier_cutoff_truncates_the_planned_frame() {
let design = pinned_design();
let full = build_planned_frame(
plan_job(design.clone()),
live_update::DEFAULT_PREVIEW_BUDGET,
);
let truncated = build_planned_frame(
PlanJob {
tier_cutoff: Some(0),
..plan_job(design)
},
live_update::DEFAULT_PREVIEW_BUDGET,
);
assert!(
truncated.planes.len() < full.planes.len(),
"cutting off after tier 0 must drop tier 1's (\"Pavilion\") facet(s): \
full={}, truncated={}",
full.planes.len(),
truncated.planes.len()
);
}
#[test]
fn set_tier_cutoff_round_trips_through_the_cache() {
let state = SolidPreviewState::new(FakeSink::new());
let cached = || {
*state
.tier_cutoff
.lock()
.unwrap_or_else(PoisonError::into_inner)
};
assert_eq!(cached(), None);
state.set_tier_cutoff(Some(3));
assert_eq!(cached(), Some(3));
state.set_tier_cutoff(None);
assert_eq!(cached(), None);
}
fn planned_request(
design: Design,
view_mode: u8,
size: (u32, u32),
generation: u64,
) -> RedrawRequest {
let frame = build_planned_frame(
PlanJob {
size,
view_mode,
generation,
..plan_job(design)
},
live_update::DEFAULT_PREVIEW_BUDGET,
);
RedrawRequest::Planned(Box::new(frame))
}
#[test]
fn render_request_carries_solved_and_freshness_through_to_the_worker_frame() {
let design = closed_design();
let mut mesh_cache = MeshCache::default();
let mut rasterizer = SolidRasterizer::new(16, 16);
let mut edges_rasterizer = SolidRasterizer::new(16, 16);
let mut memory = WorkerMemory::default();
let frame = render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
planned_request(design, 0, (16, 16), 7),
);
assert!(frame.has_solid);
assert!(!frame.stale);
assert_eq!(frame.solved.map(|s| s.len()), Some(8));
assert_eq!(
frame.generation, 7,
"the request's own generation must reach the finished frame unchanged"
);
assert!(
frame.edges_image.is_none(),
"view_mode 0 never builds edges"
);
assert!(
frame.diagram_image.is_none(),
"view_mode 0 never builds the diagram"
);
}
#[test]
fn view_mode_both_also_produces_an_edges_image() {
let design = closed_design();
let mut mesh_cache = MeshCache::default();
let mut rasterizer = SolidRasterizer::new(16, 16);
let mut edges_rasterizer = SolidRasterizer::new(16, 16);
let mut memory = WorkerMemory::default();
let frame = render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
planned_request(design, 2, (16, 16), 0),
);
assert!(frame.edges_image.is_some());
assert!(
frame.diagram_image.is_none(),
"view_mode 2 never builds the diagram"
);
}
#[test]
fn view_mode_diagram_produces_the_diagram_image_and_its_side_tables() {
let design = closed_design();
let mut mesh_cache = MeshCache::default();
let mut rasterizer = SolidRasterizer::new(16, 16);
let mut edges_rasterizer = SolidRasterizer::new(16, 16);
let mut memory = WorkerMemory::default();
let frame = render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
planned_request(design, 3, (240, 120), 0),
);
assert!(frame.has_diagram);
assert!(frame.diagram_image.is_some());
let pick = frame
.diagram_pick
.expect("view_mode 3 must produce a diagram pick buffer");
assert_eq!((pick.width, pick.height), (240, 120));
let tooth_pick = frame
.diagram_tooth_pick
.expect("view_mode 3 must also produce a tooth pick buffer");
assert_eq!((tooth_pick.width, tooth_pick.height), (240, 120));
let hover_text = frame
.diagram_hover_text
.expect("view_mode 3 must produce a facet-id-indexed hover-text table");
assert_eq!(
hover_text.len(),
frame.diagram_facet_tier.as_ref().unwrap().len()
);
assert!(hover_text.iter().any(|t| t.contains("Table")));
}
#[test]
fn replan_frame_carries_a_positive_mesh_bounding_radius() {
let design = closed_design();
let mut mesh_cache = MeshCache::default();
let mut rasterizer = SolidRasterizer::new(16, 16);
let mut edges_rasterizer = SolidRasterizer::new(16, 16);
let mut memory = WorkerMemory::default();
let frame = render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
planned_request(design, 0, (16, 16), 0),
);
assert!(frame.has_solid);
assert!(frame.mesh_bounding_radius > 0.0);
}
#[test]
fn a_reproject_frame_carries_forward_the_last_replans_generation() {
let design = closed_design();
let mut mesh_cache = MeshCache::default();
let mut rasterizer = SolidRasterizer::new(16, 16);
let mut edges_rasterizer = SolidRasterizer::new(16, 16);
let mut memory = WorkerMemory::default();
let replan_frame = render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
planned_request(design, 0, (16, 16), 42),
);
assert_eq!(replan_frame.generation, 42);
let reproject_frame = render_request(
&mut mesh_cache,
&mut rasterizer,
&mut edges_rasterizer,
&mut memory,
RedrawRequest::Reproject {
planes: replan_frame.planes,
camera: CAMERA,
size: (16, 16),
view_mode: 0,
gear: None,
},
);
assert_eq!(
reproject_frame.generation, 42,
"a camera-follow reproject must not lose the last replan's generation"
);
}
}
}