Skip to main content

brep_ransac/
debug_export.rs

1//! Lightweight, dependency-free geometry export for recognition diagnostics.
2
3use crate::{Mesh, RecognitionError, RecognitionResult};
4use std::fmt::Write;
5
6/// In-memory Wavefront OBJ geometry and its companion material library.
7///
8/// Save [`obj`](Self::obj) as `analytic_regions.obj` and
9/// [`mtl`](Self::mtl) as `analytic_regions.mtl` in the same directory. The OBJ
10/// contains every input triangle exactly once, partitioned into one group per
11/// recognized region plus `unresolved` and `unassigned` groups when needed.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct DebugObjExport {
14    /// Wavefront OBJ geometry and grouping text.
15    pub obj: String,
16    /// Companion Wavefront material-library text.
17    pub mtl: String,
18}
19
20const COLORS: &[[f64; 3]] = &[
21    [0.894, 0.102, 0.110],
22    [0.216, 0.494, 0.722],
23    [0.302, 0.686, 0.290],
24    [0.596, 0.306, 0.639],
25    [1.000, 0.498, 0.000],
26    [1.000, 1.000, 0.200],
27    [0.651, 0.337, 0.157],
28    [0.969, 0.506, 0.749],
29];
30
31/// Export an input mesh and recognition result as Wavefront OBJ/MTL text.
32///
33/// Region colors are deterministic and intended only for debugging. OBJ groups
34/// retain primitive type and region number; unresolved triangles are gray and
35/// triangles absent from the result are black. Invalid indices or overlapping
36/// result partitions are rejected rather than producing misleading geometry.
37pub fn export_debug_obj(
38    mesh: &Mesh,
39    result: &RecognitionResult,
40) -> Result<DebugObjExport, RecognitionError> {
41    validate_mesh_indices(mesh)?;
42
43    // `None` means not mentioned by the recognition result, `Some(n)` is a
44    // recognized region, and `Some(region_count)` is the unresolved partition.
45    let mut owner = vec![None; mesh.triangles.len()];
46    for (region_index, region) in result.regions.iter().enumerate() {
47        for &triangle in &region.triangle_indices {
48            claim(&mut owner, triangle, region_index, "recognized region")?;
49        }
50    }
51    let unresolved_owner = result.regions.len();
52    for &triangle in &result.unresolved_triangles {
53        claim(
54            &mut owner,
55            triangle,
56            unresolved_owner,
57            "unresolved partition",
58        )?;
59    }
60
61    let mut obj = String::new();
62    obj.push_str("# cadmesh-analytic recognition debug export\n");
63    obj.push_str("mtllib analytic_regions.mtl\n");
64    obj.push_str("o analytic_recognition_debug\n");
65    for vertex in &mesh.vertices {
66        writeln!(
67            &mut obj,
68            "v {:.17} {:.17} {:.17}",
69            vertex.x, vertex.y, vertex.z
70        )
71        .unwrap();
72    }
73
74    let mut mtl = String::from("# cadmesh-analytic deterministic debug palette\n");
75    for (region_index, region) in result.regions.iter().enumerate() {
76        let material = format!("region_{region_index:03}");
77        let group = format!(
78            "region_{region_index:03}_{}",
79            region.surface.surface_type().name()
80        );
81        let color = COLORS[region_index % COLORS.len()];
82        write_material(&mut mtl, &material, color);
83        writeln!(&mut obj, "\ng {group}\nusemtl {material}").unwrap();
84        writeln!(
85            &mut obj,
86            "# orientation={} triangles={} rms_error={:.17} max_error={:.17}",
87            region.orientation,
88            region.triangle_indices.len(),
89            region.metrics.rms_error,
90            region.metrics.max_error
91        )
92        .unwrap();
93        write_faces(&mut obj, mesh, &region.triangle_indices);
94    }
95
96    if !result.unresolved_triangles.is_empty() {
97        write_material(&mut mtl, "unresolved", [0.55, 0.55, 0.55]);
98        obj.push_str("\ng unresolved\nusemtl unresolved\n");
99        write_faces(&mut obj, mesh, &result.unresolved_triangles);
100    }
101
102    let unassigned: Vec<_> = owner
103        .iter()
104        .enumerate()
105        .filter_map(|(triangle, assigned)| assigned.is_none().then_some(triangle))
106        .collect();
107    if !unassigned.is_empty() {
108        write_material(&mut mtl, "unassigned", [0.08, 0.08, 0.08]);
109        obj.push_str("\ng unassigned\nusemtl unassigned\n");
110        write_faces(&mut obj, mesh, &unassigned);
111    }
112
113    Ok(DebugObjExport { obj, mtl })
114}
115
116fn validate_mesh_indices(mesh: &Mesh) -> Result<(), RecognitionError> {
117    if mesh.vertices.iter().any(|vertex| !vertex.is_finite()) {
118        return Err(RecognitionError::InvalidMesh(
119            "debug export requires finite vertex coordinates".into(),
120        ));
121    }
122    for (triangle_index, triangle) in mesh.triangles.iter().enumerate() {
123        for &vertex in triangle {
124            if vertex as usize >= mesh.vertices.len() {
125                return Err(RecognitionError::InvalidMesh(format!(
126                    "triangle {triangle_index} references missing vertex {vertex}"
127                )));
128            }
129        }
130    }
131    Ok(())
132}
133
134fn claim(
135    owner: &mut [Option<usize>],
136    triangle: usize,
137    claimant: usize,
138    partition: &str,
139) -> Result<(), RecognitionError> {
140    let slot = owner.get_mut(triangle).ok_or_else(|| {
141        RecognitionError::InvalidSelection(format!(
142            "{partition} references missing triangle {triangle}"
143        ))
144    })?;
145    if let Some(previous) = *slot {
146        return Err(RecognitionError::InvalidSelection(format!(
147            "triangle {triangle} is assigned more than once (owners {previous} and {claimant})"
148        )));
149    }
150    *slot = Some(claimant);
151    Ok(())
152}
153
154fn write_faces(out: &mut String, mesh: &Mesh, triangle_indices: &[usize]) {
155    for &triangle_index in triangle_indices {
156        let triangle = mesh.triangles[triangle_index];
157        writeln!(
158            out,
159            "f {} {} {}",
160            triangle[0] + 1,
161            triangle[1] + 1,
162            triangle[2] + 1
163        )
164        .unwrap();
165    }
166}
167
168fn write_material(out: &mut String, name: &str, color: [f64; 3]) {
169    writeln!(
170        out,
171        "\nnewmtl {name}\nKd {:.3} {:.3} {:.3}\nKa 0.000 0.000 0.000\nd 1.000",
172        color[0], color[1], color[2]
173    )
174    .unwrap();
175}
176
177// BREP private tests: 25f095040f2f22f9