use crate::{Mesh, RecognitionError, RecognitionResult};
use std::fmt::Write;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DebugObjExport {
pub obj: String,
pub mtl: String,
}
const COLORS: &[[f64; 3]] = &[
[0.894, 0.102, 0.110],
[0.216, 0.494, 0.722],
[0.302, 0.686, 0.290],
[0.596, 0.306, 0.639],
[1.000, 0.498, 0.000],
[1.000, 1.000, 0.200],
[0.651, 0.337, 0.157],
[0.969, 0.506, 0.749],
];
pub fn export_debug_obj(
mesh: &Mesh,
result: &RecognitionResult,
) -> Result<DebugObjExport, RecognitionError> {
validate_mesh_indices(mesh)?;
let mut owner = vec![None; mesh.triangles.len()];
for (region_index, region) in result.regions.iter().enumerate() {
for &triangle in ®ion.triangle_indices {
claim(&mut owner, triangle, region_index, "recognized region")?;
}
}
let unresolved_owner = result.regions.len();
for &triangle in &result.unresolved_triangles {
claim(
&mut owner,
triangle,
unresolved_owner,
"unresolved partition",
)?;
}
let mut obj = String::new();
obj.push_str("# cadmesh-analytic recognition debug export\n");
obj.push_str("mtllib analytic_regions.mtl\n");
obj.push_str("o analytic_recognition_debug\n");
for vertex in &mesh.vertices {
writeln!(
&mut obj,
"v {:.17} {:.17} {:.17}",
vertex.x, vertex.y, vertex.z
)
.unwrap();
}
let mut mtl = String::from("# cadmesh-analytic deterministic debug palette\n");
for (region_index, region) in result.regions.iter().enumerate() {
let material = format!("region_{region_index:03}");
let group = format!(
"region_{region_index:03}_{}",
region.surface.surface_type().name()
);
let color = COLORS[region_index % COLORS.len()];
write_material(&mut mtl, &material, color);
writeln!(&mut obj, "\ng {group}\nusemtl {material}").unwrap();
writeln!(
&mut obj,
"# orientation={} triangles={} rms_error={:.17} max_error={:.17}",
region.orientation,
region.triangle_indices.len(),
region.metrics.rms_error,
region.metrics.max_error
)
.unwrap();
write_faces(&mut obj, mesh, ®ion.triangle_indices);
}
if !result.unresolved_triangles.is_empty() {
write_material(&mut mtl, "unresolved", [0.55, 0.55, 0.55]);
obj.push_str("\ng unresolved\nusemtl unresolved\n");
write_faces(&mut obj, mesh, &result.unresolved_triangles);
}
let unassigned: Vec<_> = owner
.iter()
.enumerate()
.filter_map(|(triangle, assigned)| assigned.is_none().then_some(triangle))
.collect();
if !unassigned.is_empty() {
write_material(&mut mtl, "unassigned", [0.08, 0.08, 0.08]);
obj.push_str("\ng unassigned\nusemtl unassigned\n");
write_faces(&mut obj, mesh, &unassigned);
}
Ok(DebugObjExport { obj, mtl })
}
fn validate_mesh_indices(mesh: &Mesh) -> Result<(), RecognitionError> {
if mesh.vertices.iter().any(|vertex| !vertex.is_finite()) {
return Err(RecognitionError::InvalidMesh(
"debug export requires finite vertex coordinates".into(),
));
}
for (triangle_index, triangle) in mesh.triangles.iter().enumerate() {
for &vertex in triangle {
if vertex as usize >= mesh.vertices.len() {
return Err(RecognitionError::InvalidMesh(format!(
"triangle {triangle_index} references missing vertex {vertex}"
)));
}
}
}
Ok(())
}
fn claim(
owner: &mut [Option<usize>],
triangle: usize,
claimant: usize,
partition: &str,
) -> Result<(), RecognitionError> {
let slot = owner.get_mut(triangle).ok_or_else(|| {
RecognitionError::InvalidSelection(format!(
"{partition} references missing triangle {triangle}"
))
})?;
if let Some(previous) = *slot {
return Err(RecognitionError::InvalidSelection(format!(
"triangle {triangle} is assigned more than once (owners {previous} and {claimant})"
)));
}
*slot = Some(claimant);
Ok(())
}
fn write_faces(out: &mut String, mesh: &Mesh, triangle_indices: &[usize]) {
for &triangle_index in triangle_indices {
let triangle = mesh.triangles[triangle_index];
writeln!(
out,
"f {} {} {}",
triangle[0] + 1,
triangle[1] + 1,
triangle[2] + 1
)
.unwrap();
}
}
fn write_material(out: &mut String, name: &str, color: [f64; 3]) {
writeln!(
out,
"\nnewmtl {name}\nKd {:.3} {:.3} {:.3}\nKa 0.000 0.000 0.000\nd 1.000",
color[0], color[1], color[2]
)
.unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
AnalyticSurface, FitDiagnostics, FitMetrics, FitPath, PlaneSurface, SurfaceRegion, Vec3,
};
fn result(region_triangles: Vec<usize>, unresolved: Vec<usize>) -> RecognitionResult {
RecognitionResult {
regions: vec![SurfaceRegion {
surface: AnalyticSurface::Plane(PlaneSurface {
origin: Vec3::ZERO,
normal: Vec3::Z,
}),
orientation: 1,
triangle_indices: region_triangles,
metrics: FitMetrics::default(),
confidence: 1.0,
diagnostics: FitDiagnostics {
path: FitPath::GenericRecognition,
supplied_surface: None,
fixed_parameters: Default::default(),
parameters_refined: false,
generic_classification_skipped: false,
exact_parameters_reused: false,
hypotheses_generated: 1,
candidates_evaluated: 1,
reason: "test".into(),
rejected_competitors: Vec::new(),
..Default::default()
},
}],
unresolved_triangles: unresolved,
unresolved_diagnostics: Vec::new(),
}
}
#[test]
fn exports_every_triangle_in_a_diagnostic_partition() {
let mesh = Mesh::new(
vec![Vec3::ZERO, Vec3::X, Vec3::Y, Vec3::new(1.0, 1.0, 0.0)],
vec![[0, 1, 2], [1, 3, 2]],
);
let export = export_debug_obj(&mesh, &result(vec![0], vec![1])).unwrap();
assert!(export.obj.contains("g region_000_plane"));
assert!(export.obj.contains("g unresolved"));
assert_eq!(
export
.obj
.lines()
.filter(|line| line.starts_with("f "))
.count(),
2
);
assert!(export.mtl.contains("newmtl region_000"));
assert!(export.mtl.contains("newmtl unresolved"));
}
#[test]
fn rejects_overlapping_result_partitions() {
let mesh = Mesh::new(vec![Vec3::ZERO, Vec3::X, Vec3::Y], vec![[0, 1, 2]]);
let error = export_debug_obj(&mesh, &result(vec![0], vec![0])).unwrap_err();
assert!(error.to_string().contains("assigned more than once"));
}
}