use super::GeometryRouter;
use crate::BoolFailure;
use rustc_hash::FxHashMap;
#[derive(Debug, Default, Clone, Copy)]
pub struct ClassificationStats {
pub rectangular: usize,
pub diagonal: usize,
pub non_rectangular: usize,
}
#[derive(Debug, Clone, Default)]
pub struct HostOpeningDiagnostic {
pub host_type: String,
pub openings: Vec<OpeningDiagnostic>,
pub csg_failure_count: usize,
pub first_failure_label: Option<String>,
pub tris_before: Option<usize>,
pub tris_after: Option<usize>,
pub rect_boxes_processed: usize,
pub host_bounds: Option<((f32, f32, f32), (f32, f32, f32))>,
}
#[derive(Debug, Clone)]
pub struct OpeningDiagnostic {
pub opening_id: u32,
pub kind: OpeningKindDiag,
pub vertex_count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpeningKindDiag {
Rectangular,
Diagonal,
NonRectangular,
}
impl OpeningKindDiag {
pub fn as_str(self) -> &'static str {
match self {
OpeningKindDiag::Rectangular => "Rectangular",
OpeningKindDiag::Diagonal => "Diagonal",
OpeningKindDiag::NonRectangular => "NonRectangular",
}
}
}
impl GeometryRouter {
pub fn take_csg_failures(&self) -> FxHashMap<u32, Vec<BoolFailure>> {
let pending = crate::diagnostics::take_pending_mapped_bool_failures();
if !pending.is_empty() {
self.csg_failures
.borrow_mut()
.entry(0)
.or_default()
.extend(pending);
}
std::mem::take(&mut *self.csg_failures.borrow_mut())
}
pub(crate) fn push_layer_slice_diag(&self, element_id: u32, reason: &'static str) {
self.layer_slice_diag.borrow_mut().push((element_id, reason));
}
pub fn take_layer_slice_diag(&self) -> Vec<(u32, &'static str)> {
std::mem::take(&mut *self.layer_slice_diag.borrow_mut())
}
pub fn csg_failure_product_count(&self) -> usize {
self.csg_failures.borrow().len()
}
pub fn csg_failure_total(&self) -> usize {
self.csg_failures.borrow().values().map(|v| v.len()).sum()
}
pub(crate) fn record_void_consumed_host(&self, product_id: u32) {
self.voids_consumed_hosts.borrow_mut().insert(product_id);
}
pub fn host_consumed_by_void(&self, product_id: u32) -> bool {
self.voids_consumed_hosts.borrow().contains(&product_id)
}
pub(crate) fn record_csg_failures(&self, product_id: u32, failures: Vec<BoolFailure>) {
if failures.is_empty() {
return;
}
let attributed: Vec<BoolFailure> = failures
.into_iter()
.map(|f| f.with_product_id(product_id))
.collect();
self.csg_failures
.borrow_mut()
.entry(product_id)
.or_default()
.extend(attributed);
}
pub fn take_classification_stats(&self) -> ClassificationStats {
std::mem::take(&mut *self.classification_stats.borrow_mut())
}
pub fn take_host_opening_diagnostics(&self) -> FxHashMap<u32, HostOpeningDiagnostic> {
std::mem::take(&mut *self.host_opening_diagnostics.borrow_mut())
}
pub(crate) fn record_rect_fast(&self, s: &crate::rect_fast::RectFastStats) {
let mut acc = self.rect_fast_stats.borrow_mut();
acc.fired += s.fired;
acc.openings_cut += s.openings_cut;
acc.defer_host_not_box += s.defer_host_not_box;
acc.defer_not_through += s.defer_not_through;
acc.defer_off_face += s.defer_off_face;
acc.defer_near_edge += s.defer_near_edge;
acc.defer_no_openings += s.defer_no_openings;
acc.defer_too_many_openings += s.defer_too_many_openings;
}
pub fn take_rect_fast_stats(&self) -> crate::rect_fast::RectFastStats {
std::mem::take(&mut *self.rect_fast_stats.borrow_mut())
}
pub fn host_opening_diagnostic_count(&self) -> usize {
self.host_opening_diagnostics.borrow().len()
}
pub(crate) fn bump_classification(&self, kind: ClassificationKind) {
let mut s = self.classification_stats.borrow_mut();
match kind {
ClassificationKind::Rectangular => s.rectangular += 1,
ClassificationKind::Diagonal => s.diagonal += 1,
ClassificationKind::NonRectangular => s.non_rectangular += 1,
}
}
pub(crate) fn record_host_opening_diagnostic(
&self,
host_id: u32,
host_type: &str,
openings: Vec<OpeningDiagnostic>,
) {
let mut log = self.host_opening_diagnostics.borrow_mut();
let entry = log.entry(host_id).or_default();
if entry.host_type.is_empty() {
entry.host_type = host_type.to_string();
}
entry.openings.extend(openings);
}
pub(crate) fn record_host_cut_effect(
&self,
host_id: u32,
tris_before: usize,
tris_after: usize,
rect_boxes_processed: usize,
host_bounds: ((f32, f32, f32), (f32, f32, f32)),
) {
let mut log = self.host_opening_diagnostics.borrow_mut();
let entry = log.entry(host_id).or_default();
entry.tris_before = Some(tris_before);
entry.tris_after = Some(tris_after);
entry.rect_boxes_processed = rect_boxes_processed;
entry.host_bounds = Some(host_bounds);
}
pub(crate) fn record_host_failure_summary(&self, host_id: u32, failures: &[BoolFailure]) {
if failures.is_empty() {
return;
}
let mut log = self.host_opening_diagnostics.borrow_mut();
let entry = log.entry(host_id).or_default();
entry.csg_failure_count += failures.len();
if entry.first_failure_label.is_none() {
let label = match &failures[0].reason {
crate::diagnostics::BoolFailureReason::OperandTooLarge { .. } => "OperandTooLarge",
crate::diagnostics::BoolFailureReason::EmptyOperand => "EmptyOperand",
crate::diagnostics::BoolFailureReason::DegenerateOperand => "DegenerateOperand",
crate::diagnostics::BoolFailureReason::NoBoundsOverlap => "NoBoundsOverlap",
crate::diagnostics::BoolFailureReason::KernelOutputInvalid => "KernelOutputInvalid",
crate::diagnostics::BoolFailureReason::SolidSolidDifferenceSkipped => {
"SolidSolidDifferenceSkipped"
}
crate::diagnostics::BoolFailureReason::PolygonalBoundedHalfSpaceFallback => {
"PolygonalBoundedHalfSpaceFallback"
}
crate::diagnostics::BoolFailureReason::CutterUnionUnavailable => {
"CutterUnionUnavailable"
}
crate::diagnostics::BoolFailureReason::UnknownBooleanOperator(_) => {
"UnknownBooleanOperator"
}
crate::diagnostics::BoolFailureReason::ManifoldOutputDegenerate { .. } => {
"ManifoldOutputDegenerate"
}
crate::diagnostics::BoolFailureReason::KernelError(_) => "KernelError",
crate::diagnostics::BoolFailureReason::DifferenceEmptiedHost => {
"DifferenceEmptiedHost"
}
};
entry.first_failure_label = Some(label.to_string());
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum ClassificationKind {
Rectangular,
Diagonal,
NonRectangular,
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassificationSummary {
pub rectangular: u64,
pub diagonal: u64,
pub non_rectangular: u64,
pub total: u64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReasonCount {
pub reason: String,
pub count: u64,
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RectFastSummary {
pub fired: u64,
pub openings_cut: u64,
pub defer_host_not_box: u64,
pub defer_not_through: u64,
pub defer_off_face: u64,
pub defer_near_edge: u64,
pub defer_no_openings: u64,
pub defer_too_many_openings: u64,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostBbox {
pub min: [f32; 3],
pub max: [f32; 3],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorstHost {
pub product_id: u32,
pub ifc_type: String,
pub openings: u64,
pub csg_failures: u64,
pub first_failure_label: Option<String>,
pub bbox: Option<HostBbox>,
pub triangle_count: Option<u64>,
}
pub const GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION: u32 = 2;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryDiagnostics {
#[serde(default)]
pub schema_version: u32,
pub total_csg_failures: u64,
pub products_with_failures: u64,
pub hosts_with_openings: u64,
pub classification: ClassificationSummary,
pub failures_by_reason: Vec<ReasonCount>,
pub silent_no_ops: u64,
pub rect_fast: RectFastSummary,
pub worst_hosts: Vec<WorstHost>,
}
impl Default for GeometryDiagnostics {
fn default() -> Self {
Self {
schema_version: GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION,
total_csg_failures: 0,
products_with_failures: 0,
hosts_with_openings: 0,
classification: ClassificationSummary::default(),
failures_by_reason: Vec::new(),
silent_no_ops: 0,
rect_fast: RectFastSummary::default(),
worst_hosts: Vec::new(),
}
}
}
impl GeometryDiagnostics {
pub fn has_issues(&self) -> bool {
self.total_csg_failures > 0 || self.silent_no_ops > 0
}
pub fn is_empty(&self) -> bool {
self.total_csg_failures == 0
&& self.hosts_with_openings == 0
&& self.classification.total == 0
&& self.silent_no_ops == 0
&& self.rect_fast.fired == 0
}
}
pub fn aggregate_diagnostics(
classification: ClassificationStats,
csg_failures: &FxHashMap<u32, Vec<BoolFailure>>,
host_diags: &FxHashMap<u32, HostOpeningDiagnostic>,
rect_fast: crate::rect_fast::RectFastStats,
worst_hosts_limit: usize,
) -> GeometryDiagnostics {
let total_csg_failures = csg_failures.values().map(Vec::len).sum::<usize>() as u64;
let products_with_failures = csg_failures.len() as u64;
let hosts_with_openings = host_diags.len() as u64;
let classification = ClassificationSummary {
rectangular: classification.rectangular as u64,
diagonal: classification.diagonal as u64,
non_rectangular: classification.non_rectangular as u64,
total: (classification.rectangular
+ classification.diagonal
+ classification.non_rectangular) as u64,
};
let mut by_reason: FxHashMap<&'static str, u64> = FxHashMap::default();
for fails in csg_failures.values() {
for f in fails {
*by_reason.entry(f.reason.label()).or_insert(0) += 1;
}
}
let mut failures_by_reason: Vec<ReasonCount> = by_reason
.into_iter()
.map(|(reason, count)| ReasonCount { reason: reason.to_string(), count })
.collect();
failures_by_reason
.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.reason.cmp(&b.reason)));
let silent_no_ops = host_diags
.values()
.filter(|hd| {
matches!((hd.tris_before, hd.tris_after), (Some(b), Some(a)) if b == a)
&& hd.rect_boxes_processed > 0
&& hd.csg_failure_count == 0
})
.count() as u64;
let mut worst: Vec<(&u32, &HostOpeningDiagnostic)> = host_diags
.iter()
.filter(|(_, hd)| hd.csg_failure_count > 0)
.collect();
worst.sort_by(|a, b| {
b.1.csg_failure_count.cmp(&a.1.csg_failure_count).then_with(|| a.0.cmp(b.0))
});
let worst_hosts: Vec<WorstHost> = worst
.into_iter()
.take(worst_hosts_limit)
.map(|(pid, hd)| WorstHost {
product_id: *pid,
ifc_type: hd.host_type.clone(),
openings: hd.openings.len() as u64,
csg_failures: hd.csg_failure_count as u64,
first_failure_label: hd.first_failure_label.clone(),
bbox: hd.host_bounds.map(|(min, max)| HostBbox {
min: [min.0, min.1, min.2],
max: [max.0, max.1, max.2],
}),
triangle_count: hd.tris_after.or(hd.tris_before).map(|t| t as u64),
})
.collect();
GeometryDiagnostics {
schema_version: GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION,
total_csg_failures,
products_with_failures,
hosts_with_openings,
classification,
failures_by_reason,
silent_no_ops,
rect_fast: RectFastSummary {
fired: rect_fast.fired,
openings_cut: rect_fast.openings_cut,
defer_host_not_box: rect_fast.defer_host_not_box,
defer_not_through: rect_fast.defer_not_through,
defer_off_face: rect_fast.defer_off_face,
defer_near_edge: rect_fast.defer_near_edge,
defer_no_openings: rect_fast.defer_no_openings,
defer_too_many_openings: rect_fast.defer_too_many_openings,
},
worst_hosts,
}
}
#[cfg(test)]
mod diagnostics_contract_tests;