use super::super::run::{rotate_into_caliper_frame, to_caliper_frame};
use glam::DVec3;
use indicatrix::geometry::{
GpuFacetPlane,
stone_metrics::{SolidStatus, build_solid_mesh_geom},
tool::ToolPrimitive,
};
use indicatrix_cut_core::rough_plan::shape::hull::design_outline;
use indicatrix_vault::db::sqlite::Database;
use std::{
collections::HashMap,
sync::{Arc, Mutex, PoisonError},
};
use tracing::warn;
const MERGE_FRACTION: f64 = 1e-7;
const COLLINEAR_SIN: f64 = 1e-6;
#[derive(Debug, Clone)]
pub(super) struct DesignFacet {
pub(super) normal: DVec3,
pub(super) ring: Vec<DVec3>,
pub(super) edge_drawn: Option<Vec<bool>>,
}
impl DesignFacet {
#[cfg(test)]
#[must_use]
pub(super) const fn new(normal: DVec3, ring: Vec<DVec3>) -> Self {
Self {
normal,
ring,
edge_drawn: None,
}
}
}
#[derive(Debug, Clone)]
pub(super) struct DesignMesh {
pub(super) facets: Vec<DesignFacet>,
}
impl DesignMesh {
#[must_use]
pub(super) fn caliper_width(&self) -> f64 {
let (mut low, mut high) = (f64::INFINITY, f64::NEG_INFINITY);
for facet in &self.facets {
for point in &facet.ring {
low = low.min(point.x);
high = high.max(point.x);
}
}
if high >= low { high - low } else { 0.0 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum MeshMiss {
Gone,
Unreadable,
}
pub(super) trait MeshSource {
fn mesh(&self, entry_id: i64) -> Result<Arc<DesignMesh>, MeshMiss>;
}
#[must_use]
pub(super) fn simplify_ring(ring: &[DVec3]) -> Vec<DVec3> {
let Some(&first) = ring.first() else {
return Vec::new();
};
let (mut low, mut high) = (first, first);
for &point in ring {
low = low.min(point);
high = high.max(point);
}
let merge = (high - low).length().max(1e-12) * MERGE_FRACTION;
let mut distinct: Vec<DVec3> = Vec::with_capacity(ring.len());
for &point in ring {
if distinct
.last()
.is_none_or(|&last| (point - last).length() > merge)
{
distinct.push(point);
}
}
while distinct.len() > 1 && (distinct[0] - distinct[distinct.len() - 1]).length() <= merge {
distinct.pop();
}
if distinct.len() < 3 {
return distinct;
}
let count = distinct.len();
let mut corners = Vec::with_capacity(count);
let mut previous = distinct[count - 1];
for (i, ¤t) in distinct.iter().enumerate() {
let next = distinct[(i + 1) % count];
let before = current - previous;
let after = next - current;
let collinear =
before.cross(after).length() <= COLLINEAR_SIN * before.length() * after.length();
if !collinear {
corners.push(current);
previous = current;
}
}
corners
}
pub(super) fn simplified_flags(
ring: &[DVec3],
simplified: &[DVec3],
visible: &[bool],
) -> Vec<bool> {
let mut source = Vec::with_capacity(simplified.len());
let mut cursor = 0;
for corner in simplified {
let Some(offset) = ring[cursor..].iter().position(|p| p == corner) else {
return vec![true; simplified.len()];
};
source.push(cursor + offset);
cursor += offset + 1;
}
let count = ring.len();
(0..source.len())
.map(|i| {
let (from, to) = (source[i], source[(i + 1) % source.len()]);
let mut segment = from;
loop {
if visible.get(segment).copied().unwrap_or(true) {
return true;
}
segment = (segment + 1) % count;
if segment == to {
return false;
}
}
})
.collect()
}
fn caliper_centre(corners: &[[f64; 3]], width_dir: [f64; 2]) -> DVec3 {
let mut low = DVec3::splat(f64::INFINITY);
let mut high = DVec3::splat(f64::NEG_INFINITY);
for &corner in corners {
let turned = DVec3::from_array(to_caliper_frame(corner, width_dir));
low = low.min(turned);
high = high.max(turned);
}
(low + high) * 0.5
}
#[must_use]
pub(super) fn design_mesh_from_planes(
planes: &[(DVec3, f64)],
tools: &[ToolPrimitive],
) -> Option<DesignMesh> {
let SolidStatus::Closed(mesh) = build_solid_mesh_geom(planes, tools) else {
return None;
};
let corners_of = |mesh: &indicatrix::geometry::stone_metrics::SolidMesh| -> Vec<[f64; 3]> {
mesh.rings
.iter()
.flat_map(|(_, ring)| ring.iter().map(DVec3::to_array))
.collect()
};
let mesh_corners = corners_of(&mesh);
let corners = if tools.is_empty() {
mesh_corners
} else {
let points: Vec<DVec3> = mesh_corners
.iter()
.copied()
.map(DVec3::from_array)
.collect();
design_outline(&points).map_or(mesh_corners, |outline| {
outline.corners.iter().map(DVec3::to_array).collect()
})
};
let stored: Vec<[f64; 3]> = corners
.iter()
.map(|corner| corner.map(|c| f64::from(c as f32)))
.collect();
let width_dir = rotate_into_caliper_frame(&stored)?.width_dir;
let centre = caliper_centre(&corners, width_dir);
let to_caliper = |v: DVec3| DVec3::from_array(to_caliper_frame(v.to_array(), width_dir));
let facets: Vec<DesignFacet> = mesh
.rings
.iter()
.enumerate()
.filter_map(|(index, (id, ring))| {
let normal = match &mesh.piece_normals {
Some(normals) => normals.get(index)?.normalize(),
None => planes.get(*id)?.0,
};
let ring: Vec<DVec3> = ring.iter().map(|&p| to_caliper(p) - centre).collect();
let corners = simplify_ring(&ring);
if corners.len() < 3 {
return None;
}
let edge_drawn = mesh.edge_visible.as_ref().map(|visible| {
simplified_flags(
&ring,
&corners,
visible.get(index).map_or(&[], Vec::as_slice),
)
});
Some(DesignFacet {
normal: to_caliper(normal),
ring: corners,
edge_drawn,
})
})
.collect();
(!facets.is_empty()).then_some(DesignMesh { facets })
}
type Slot = Arc<Mutex<Option<Option<Arc<DesignMesh>>>>>;
#[derive(Default)]
struct MeshCache {
slots: Mutex<HashMap<i64, Slot>>,
}
impl MeshCache {
fn get_or_load<E>(
&self,
id: i64,
load: impl FnOnce() -> Result<Option<DesignMesh>, E>,
) -> Result<Arc<DesignMesh>, MeshMiss> {
let slot = Arc::clone(
self.slots
.lock()
.unwrap_or_else(PoisonError::into_inner)
.entry(id)
.or_default(),
);
let mut held = slot.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(known) = held.as_ref() {
return known.clone().ok_or(MeshMiss::Gone);
}
let Ok(loaded) = load() else {
return Err(MeshMiss::Unreadable);
};
let built = loaded.map(Arc::new);
*held = Some(built.clone());
drop(held);
built.ok_or(MeshMiss::Gone)
}
fn ids(&self) -> Vec<i64> {
self.slots
.lock()
.unwrap_or_else(PoisonError::into_inner)
.keys()
.copied()
.collect()
}
fn remove(&self, id: i64) {
self.slots
.lock()
.unwrap_or_else(PoisonError::into_inner)
.remove(&id);
}
}
pub(super) struct MeshLibrary {
db: Arc<Mutex<Database>>,
cache: MeshCache,
stamps: Mutex<HashMap<i64, Option<i64>>>,
checked: Mutex<u64>,
}
impl MeshLibrary {
#[must_use]
pub(super) fn new(db: Arc<Mutex<Database>>) -> Self {
Self {
db,
cache: MeshCache::default(),
stamps: Mutex::default(),
checked: Mutex::default(),
}
}
pub(super) fn refresh(&self, epoch: u64) {
let mut checked = self.checked.lock().unwrap_or_else(PoisonError::into_inner);
if *checked >= epoch {
return;
}
self.drop_stale_meshes();
*checked = epoch;
}
fn drop_stale_meshes(&self) {
let ids = self.cache.ids();
if ids.is_empty() {
return;
}
let stale: Vec<i64> = {
let db = self.db.lock().unwrap_or_else(PoisonError::into_inner);
let stamps = self.stamps.lock().unwrap_or_else(PoisonError::into_inner);
ids.into_iter()
.filter(|id| {
stamps.get(id).is_none_or(|known| {
!db.entry_updated_at(*id).is_ok_and(|now| now == *known)
})
})
.collect()
};
let mut stamps = self.stamps.lock().unwrap_or_else(PoisonError::into_inner);
for id in stale {
self.cache.remove(id);
stamps.remove(&id);
}
}
fn load(&self, entry_id: i64) -> Result<Option<DesignMesh>, ()> {
let (stamp, full) = {
let db = self.db.lock().unwrap_or_else(PoisonError::into_inner);
(
db.entry_updated_at(entry_id).ok().flatten(),
db.get_diagram_full(entry_id),
)
};
let full = match full {
Ok(Some(full)) => full,
Ok(None) => return Ok(None),
Err(error) => {
warn!("Rough planner: could not read design #{entry_id}: {error}");
return Err(());
}
};
let resolved = crate::gui::editor::resolve_catalogue_planes(&full);
if let Some(reason) = &resolved.concave_error {
warn!(
"Rough planner: design #{entry_id} has concave tiers that do not resolve: {reason}"
);
return Err(());
}
let halfspaces: Vec<(DVec3, f64)> = resolved
.planes
.iter()
.copied()
.map(GpuFacetPlane::to_halfspace_f64)
.collect();
let start = resolved.preform_plane_count.min(halfspaces.len());
let Some(mesh) = design_mesh_from_planes(&halfspaces[start..], &resolved.tools) else {
warn!("Rough planner: design #{entry_id} has no closed solid to draw");
return Err(());
};
self.stamps
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(entry_id, stamp);
Ok(Some(mesh))
}
}
impl MeshSource for MeshLibrary {
fn mesh(&self, entry_id: i64) -> Result<Arc<DesignMesh>, MeshMiss> {
self.cache.get_or_load(entry_id, || self.load(entry_id))
}
}
#[cfg(test)]
mod tests;