use super::{Reporter, WORKER_STACK_BYTES, group_thousands, panic_message, stages};
use crate::gui::{batch::batch_queue::local_lane_count, editor::CataloguePlanesSource};
use glam::DVec3;
use indicatrix::geometry::{
GpuFacetPlane,
stone_metrics::{
SolidMetrics, SolidStatus, build_solid_mesh_geom, measure_solid_with_vertices, mesh_volume,
},
tool::ToolPrimitive,
};
use indicatrix_cut_core::rough_plan::shape::hull::design_outline;
use indicatrix_vault::{
db::sqlite::Database,
model::{
entry::FullDiagramRecord,
solid_extents::{SolidExtents, SolidExtentsSource, StoredSolidExtents},
solid_hull::SolidHull,
},
};
use std::{
collections::BTreeMap,
panic::{AssertUnwindSafe, catch_unwind},
sync::{
Mutex, PoisonError,
atomic::{AtomicUsize, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
};
use tracing::{debug, warn};
pub(super) const SCAN_BAND: f32 = 0.35;
type Measured = (Option<SolidExtents>, SolidExtentsSource, Option<SolidHull>);
fn extents_of(metrics: &SolidMetrics) -> Option<SolidExtents> {
let extents = SolidExtents {
width_caliper: metrics.width_caliper,
length_caliper: metrics.length_caliper,
width_axis: metrics.width_axis,
length_axis: metrics.length_axis,
height: metrics.total_height,
volume: metrics.volume,
};
[
extents.width_caliper,
extents.length_caliper,
extents.width_axis,
extents.length_axis,
extents.height,
extents.volume,
]
.iter()
.all(|v| v.is_finite() && *v > 0.0)
.then_some(extents)
}
pub(super) enum Measurement {
Done(Measured),
ConcaveUnresolved(String),
}
pub(super) fn measure_record(full: &FullDiagramRecord) -> Measurement {
let resolved = crate::gui::editor::resolve_catalogue_planes(full);
if let Some(reason) = resolved.concave_error {
return Measurement::ConcaveUnresolved(reason);
}
Measurement::Done(measure_planes(
resolved.source,
resolved.preform_plane_count,
&resolved.planes,
&resolved.tools,
))
}
fn carved_stone(facets: &[(DVec3, f64)], tools: &[ToolPrimitive]) -> Option<(f64, Vec<DVec3>)> {
match build_solid_mesh_geom(facets, tools) {
SolidStatus::Closed(mesh) => {
let corners = mesh
.rings
.iter()
.flat_map(|(_, ring)| ring.iter().copied())
.collect();
Some((mesh_volume(&mesh), corners))
}
_ => None,
}
}
fn carve(
metrics: SolidMetrics,
verts: Vec<DVec3>,
facets: &[(DVec3, f64)],
tools: &[ToolPrimitive],
) -> Option<(SolidMetrics, Vec<DVec3>)> {
if tools.is_empty() {
return Some((metrics, verts));
}
let (volume, mesh_corners) = carved_stone(facets, tools)?;
let Some(outline) = design_outline(&mesh_corners) else {
return Some((SolidMetrics { volume, ..metrics }, verts));
};
let extents = outline.extents;
let carved = SolidMetrics {
volume,
width_caliper: extents.width_caliper,
length_caliper: extents.length_caliper,
width_axis: extents.width_axis,
length_axis: extents.length_axis,
total_height: extents.height,
vertex_count: outline.corners.len(),
..metrics
};
Some((carved, outline.corners))
}
fn measure_planes(
source: CataloguePlanesSource,
preform_plane_count: usize,
planes: &[GpuFacetPlane],
tools: &[ToolPrimitive],
) -> Measured {
let halfspaces: Vec<_> = planes
.iter()
.copied()
.map(GpuFacetPlane::to_halfspace_f64)
.collect();
match source {
CataloguePlanesSource::AngleTable => {
let measured = measure_solid_with_vertices(&halfspaces);
let extents = measured.as_ref().and_then(|(m, _)| extents_of(m));
(extents, SolidExtentsSource::AngleTable, None)
}
CataloguePlanesSource::DesignFile => {
let start = preform_plane_count.min(halfspaces.len());
let facets = &halfspaces[start..];
let measured = measure_solid_with_vertices(facets)
.and_then(|(metrics, verts)| carve(metrics, verts, facets, tools));
match measured {
Some((metrics, verts)) => {
let extents = extents_of(&metrics);
let vertices = verts
.into_iter()
.map(|v| [v.x as f32, v.y as f32, v.z as f32])
.collect();
let hull = SolidHull { vertices };
(extents, SolidExtentsSource::DesignFile, Some(hull))
}
None => (None, SolidExtentsSource::Unbounded, None),
}
}
}
}
pub(super) enum Loaded {
Ready {
measured: Measured,
epoch: u64,
},
ConcaveUnresolved,
Gone,
Failed,
}
fn measure_entry(db: &Mutex<Database>, entry_id: i64) -> Loaded {
let (epoch, full) = {
let guard = db.lock().unwrap_or_else(PoisonError::into_inner);
(
guard.solid_extents_epoch(),
guard.get_diagram_full(entry_id),
)
};
match full {
Ok(Some(full)) => match measure_record(&full) {
Measurement::Done(measured) => Loaded::Ready { measured, epoch },
Measurement::ConcaveUnresolved(reason) => {
warn!(
"Rough planner: design #{entry_id} skipped, its concave tiers do not \
resolve: {reason}"
);
Loaded::ConcaveUnresolved
}
},
Ok(None) => {
debug!("Rough planner: design #{entry_id} no longer exists");
Loaded::Gone
}
Err(e) => {
warn!("Rough planner: could not load design #{entry_id}: {e}");
Loaded::Failed
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Saved {
Stored,
Stale,
Failed,
}
fn save_measurement(
db: &Mutex<Database>,
entry_id: i64,
epoch: u64,
(extents, source, hull): &Measured,
) -> Saved {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs() as i64);
let guard = db.lock().unwrap_or_else(PoisonError::into_inner);
match guard.save_solid_extents_and_hull_if_current(
entry_id,
epoch,
*extents,
*source,
hull.as_ref(),
now,
) {
Ok(true) => Saved::Stored,
Ok(false) => {
debug!("Rough planner: design #{entry_id} changed while it was measured; not saved");
Saved::Stale
}
Err(e) => {
warn!("Rough planner: could not save the measurement of design #{entry_id}: {e}");
Saved::Failed
}
}
}
pub(super) fn merge_measured(
stored: &mut BTreeMap<i64, StoredSolidExtents>,
hulls: &mut BTreeMap<i64, SolidHull>,
measured: BTreeMap<i64, Measured>,
) {
for (entry_id, (extents, source, hull)) in measured {
stored.insert(entry_id, StoredSolidExtents { extents, source });
match hull {
Some(hull) => {
hulls.insert(entry_id, hull);
}
None => {
hulls.remove(&entry_id);
}
}
}
}
pub(super) fn ids_needing_scan(
ids: &[i64],
extents: &BTreeMap<i64, StoredSolidExtents>,
hulls: &BTreeMap<i64, SolidHull>,
) -> (Vec<i64>, bool) {
let mut missing = Vec::new();
let mut every_id_has_extents = true;
for &id in ids {
match extents.get(&id) {
None => {
missing.push(id);
every_id_has_extents = false;
}
Some(row) => {
let wants_hull = row.source == SolidExtentsSource::DesignFile
&& row.extents.is_some()
&& !hulls.contains_key(&id);
if wants_hull {
missing.push(id);
}
}
}
}
let outlines_only = every_id_has_extents && !missing.is_empty();
(missing, outlines_only)
}
#[derive(Default)]
pub(super) struct ScanOutcome {
pub cancelled: bool,
pub measured: BTreeMap<i64, Measured>,
pub save_failures: usize,
pub load_failures: usize,
pub panics: usize,
pub concave_unresolved: usize,
}
type MeasureFn<'a> = &'a (dyn Fn(&Mutex<Database>, i64) -> Loaded + Sync);
struct ScanShared<'a> {
db: &'a Mutex<Database>,
reporter: &'a Reporter,
missing: &'a [i64],
outlines_only: bool,
measure: MeasureFn<'a>,
next: AtomicUsize,
done: AtomicUsize,
outcome: Mutex<ScanOutcome>,
}
impl ScanShared<'_> {
fn record(&self, update: impl FnOnce(&mut ScanOutcome)) {
update(&mut self.outcome.lock().unwrap_or_else(PoisonError::into_inner));
}
fn store(&self, entry_id: i64, epoch: u64, measured: Measured) {
if save_measurement(self.db, entry_id, epoch, &measured) == Saved::Failed {
self.record(|outcome| {
outcome.save_failures += 1;
outcome.measured.insert(entry_id, measured);
});
}
}
}
fn scan_lane(shared: &ScanShared<'_>) {
stages::lower_thread_priority();
let total = shared.missing.len();
loop {
if shared.reporter.cancelled() {
break;
}
let index = shared.next.fetch_add(1, Ordering::Relaxed);
let Some(&entry_id) = shared.missing.get(index) else {
break;
};
let loaded = catch_unwind(AssertUnwindSafe(|| (shared.measure)(shared.db, entry_id)));
match loaded {
Ok(Loaded::Ready { measured, epoch }) => shared.store(entry_id, epoch, measured),
Ok(Loaded::ConcaveUnresolved) => {
shared.record(|outcome| outcome.concave_unresolved += 1);
}
Ok(Loaded::Gone) => {}
Ok(Loaded::Failed) => shared.record(|outcome| outcome.load_failures += 1),
Err(payload) => {
warn!(
"Rough planner: measuring design #{entry_id} panicked ({}); skipped",
panic_message(&*payload)
);
shared.record(|outcome| outcome.panics += 1);
}
}
let finished = shared.done.fetch_add(1, Ordering::Relaxed) + 1;
let fraction = SCAN_BAND * (finished as f32 / total.max(1) as f32);
let label = if shared.outlines_only {
format!(
"Measuring design outlines {} / {} (one-time)",
group_thousands(finished),
group_thousands(total)
)
} else {
format!(
"Measuring designs {} / {}",
group_thousands(finished),
group_thousands(total)
)
};
shared.reporter.report(&label, fraction, finished == total);
}
}
pub(super) fn scan_missing(
db: &Mutex<Database>,
reporter: &Reporter,
missing: &[i64],
outlines_only: bool,
) -> ScanOutcome {
scan_with(
db,
reporter,
missing,
outlines_only,
local_lane_count(),
&measure_entry,
)
}
fn scan_with(
db: &Mutex<Database>,
reporter: &Reporter,
missing: &[i64],
outlines_only: bool,
lanes: usize,
measure: MeasureFn<'_>,
) -> ScanOutcome {
if missing.is_empty() {
return ScanOutcome::default();
}
let shared = ScanShared {
db,
reporter,
missing,
outlines_only,
measure,
next: AtomicUsize::new(0),
done: AtomicUsize::new(0),
outcome: Mutex::new(ScanOutcome::default()),
};
let initial_label = if outlines_only {
format!(
"Measuring design outlines 0 / {} (one-time)",
group_thousands(missing.len())
)
} else {
format!("Measuring designs 0 / {}", group_thousands(missing.len()))
};
reporter.report(&initial_label, 0.0, true);
let lanes = lanes.min(missing.len()).max(1);
std::thread::scope(|scope| {
for lane in 0..lanes {
std::thread::Builder::new()
.name(format!("rough-plan-scan-{lane}"))
.stack_size(WORKER_STACK_BYTES)
.spawn_scoped(scope, || scan_lane(&shared))
.expect("the operating system could not start a measuring thread");
}
});
let mut outcome = shared
.outcome
.into_inner()
.unwrap_or_else(PoisonError::into_inner);
outcome.cancelled = reporter.cancelled();
if outcome.load_failures > 0
|| outcome.panics > 0
|| outcome.save_failures > 0
|| outcome.concave_unresolved > 0
{
warn!(
"Rough planner: of {} designs, {} could not be read, {} panicked while measured, {} \
had concave tiers that could not be resolved and {} measurements could not be saved",
missing.len(),
outcome.load_failures,
outcome.panics,
outcome.concave_unresolved,
outcome.save_failures
);
}
outcome
}
type Cached = (BTreeMap<i64, StoredSolidExtents>, BTreeMap<i64, SolidHull>);
pub(super) fn load_extents_and_hulls(db: &Mutex<Database>, ids: &[i64]) -> Result<Cached, String> {
let (extents, hulls) = {
let guard = db.lock().unwrap_or_else(PoisonError::into_inner);
(guard.solid_extents_for(ids), guard.solid_hulls_for(ids))
};
let extents = extents.map_err(|e| format!("Could not read cached extents: {e}"))?;
let hulls = hulls.map_err(|e| format!("Could not read cached hulls: {e}"))?;
Ok((extents, hulls))
}
#[cfg(test)]
mod cache_tests;
#[cfg(test)]
mod tests;