use std::collections::HashMap;
use super::{Mesh, StlConversionError};
use brep_kernel::{tessellate_brep_watertight, BrepSolid, StepColors};
pub(super) struct ColorAssignment {
pub body_names: Vec<String>,
pub colors: StepColors,
pub bodies: usize,
pub faces: usize,
pub unmatched: usize,
}
const MAX_RINGS: i32 = 16;
const MATCH_FRACTION: f64 = 0.01;
pub(super) fn assign(
mesh: &Mesh,
triangle_colors: &[Option<[u8; 3]>],
solids: &mut [BrepSolid],
part_name: &str,
) -> Result<ColorAssignment, StlConversionError> {
if triangle_colors.len() != mesh.triangles.len() {
return Err(StlConversionError(format!(
"triangle_colors has {} entries for {} source triangles",
triangle_colors.len(),
mesh.triangles.len()
)));
}
let body_names: Vec<String> = if solids.len() == 1 {
vec![part_name.to_string()]
} else {
(1..=solids.len()).map(|index| format!("{part_name} {index}")).collect()
};
let mut face_names: Vec<Vec<String>> = Vec::with_capacity(solids.len());
for (solid, body_name) in solids.iter_mut().zip(&body_names) {
let mut names = Vec::new();
for face in solid.shells.iter_mut().flat_map(|shell| shell.faces.iter_mut()) {
let name = format!("{body_name}:Face_{}", names.len());
face.name = Some(name.clone());
names.push(name);
}
face_names.push(names);
}
let (low, high) = bounds(mesh);
let diagonal = distance(low, high).max(1e-9);
let chord = (diagonal * 1e-3).max(1e-6);
let mut grid = Grid::new(low, high);
for (solid_index, solid) in solids.iter().enumerate() {
let Ok(tessellated) = tessellate_brep_watertight(solid, chord) else {
continue;
};
for (triangle, corners) in tessellated.indices.chunks_exact(3).enumerate() {
let face = tessellated.face_ids.get(triangle).copied().unwrap_or(0);
let point = |corner: u32| {
let at = corner as usize * 3;
[tessellated.positions[at], tessellated.positions[at + 1], tessellated.positions[at + 2]]
};
grid.insert([point(corners[0]), point(corners[1]), point(corners[2])], solid_index, face);
}
}
grid.finish();
let mut body_votes: Vec<HashMap<Option<[u8; 3]>, usize>> = vec![HashMap::new(); solids.len()];
let mut face_votes: HashMap<(usize, u32), HashMap<Option<[u8; 3]>, usize>> = HashMap::new();
let mut unmatched = 0usize;
for (corners, color) in mesh.triangles.iter().zip(triangle_colors) {
let a = mesh.vertices[corners[0] as usize];
let b = mesh.vertices[corners[1] as usize];
let c = mesh.vertices[corners[2] as usize];
let centroid = [
(a.x + b.x + c.x) / 3.0,
(a.y + b.y + c.y) / 3.0,
(a.z + b.z + c.z) / 3.0,
];
match grid.nearest(centroid) {
Some((solid, face)) => {
*body_votes[solid].entry(*color).or_insert(0) += 1;
*face_votes.entry((solid, face)).or_default().entry(*color).or_insert(0) += 1;
}
None => unmatched += 1,
}
}
let mut colors = StepColors::new();
let mut bodies = 0usize;
let mut faces = 0usize;
for (solid_index, votes) in body_votes.iter().enumerate() {
let body = plurality(votes);
if let Some(rgb) = body {
colors.set(&body_names[solid_index], rgb);
bodies += 1;
}
for (face_index, name) in face_names[solid_index].iter().enumerate() {
let Some(votes) = face_votes.get(&(solid_index, face_index as u32)) else {
continue;
};
let face = plurality(votes);
if let Some(rgb) = face {
if face != body {
colors.set(name, rgb);
faces += 1;
}
}
}
}
Ok(ColorAssignment { body_names, colors, bodies, faces, unmatched })
}
fn plurality(votes: &HashMap<Option<[u8; 3]>, usize>) -> Option<[u8; 3]> {
let mut best: Option<(usize, Option<[u8; 3]>)> = None;
for (candidate, count) in votes {
let better = match best {
None => true,
Some((best_count, best_candidate)) => {
*count > best_count
|| (*count == best_count
&& match (candidate, best_candidate) {
(Some(_), None) => true,
(None, _) => false,
(Some(a), Some(b)) => *a < b,
})
}
};
if better {
best = Some((*count, *candidate));
}
}
best.and_then(|(_, candidate)| candidate)
}
fn bounds(mesh: &Mesh) -> ([f64; 3], [f64; 3]) {
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
for vertex in &mesh.vertices {
for (axis, value) in [vertex.x, vertex.y, vertex.z].into_iter().enumerate() {
low[axis] = low[axis].min(value);
high[axis] = high[axis].max(value);
}
}
if low[0] > high[0] {
return ([0.0; 3], [0.0; 3]);
}
(low, high)
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn distance(a: [f64; 3], b: [f64; 3]) -> f64 {
dot(sub(a, b), sub(a, b)).sqrt()
}
fn point_triangle_distance_squared(p: [f64; 3], [a, b, c]: [[f64; 3]; 3]) -> f64 {
let ab = sub(b, a);
let ac = sub(c, a);
let ap = sub(p, a);
let d1 = dot(ab, ap);
let d2 = dot(ac, ap);
let closest = if d1 <= 0.0 && d2 <= 0.0 {
a
} else {
let bp = sub(p, b);
let d3 = dot(ab, bp);
let d4 = dot(ac, bp);
if d3 >= 0.0 && d4 <= d3 {
b
} else {
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = if (d1 - d3).abs() > 0.0 { d1 / (d1 - d3) } else { 0.0 };
[a[0] + v * ab[0], a[1] + v * ab[1], a[2] + v * ab[2]]
} else {
let cp = sub(p, c);
let d5 = dot(ab, cp);
let d6 = dot(ac, cp);
if d6 >= 0.0 && d5 <= d6 {
c
} else {
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = if (d2 - d6).abs() > 0.0 { d2 / (d2 - d6) } else { 0.0 };
[a[0] + w * ac[0], a[1] + w * ac[1], a[2] + w * ac[2]]
} else {
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let denominator = (d4 - d3) + (d5 - d6);
let w = if denominator > 0.0 { (d4 - d3) / denominator } else { 0.0 };
[b[0] + w * (c[0] - b[0]), b[1] + w * (c[1] - b[1]), b[2] + w * (c[2] - b[2])]
} else {
let denominator = va + vb + vc;
if denominator.abs() > 0.0 {
let v = vb / denominator;
let w = vc / denominator;
[a[0] + ab[0] * v + ac[0] * w, a[1] + ab[1] * v + ac[1] * w, a[2] + ab[2] * v + ac[2] * w]
} else {
a
}
}
}
}
}
}
};
let d = sub(p, closest);
dot(d, d)
}
struct Grid {
origin: [f64; 3],
cell: f64,
triangles: Vec<([[f64; 3]; 3], usize, u32)>,
cells: HashMap<(i32, i32, i32), Vec<u32>>,
extent: f64,
reach_squared: f64,
}
impl Grid {
fn new(low: [f64; 3], high: [f64; 3]) -> Self {
let extent = distance(low, high);
let reach = (extent * MATCH_FRACTION).max(1e-9);
Self {
origin: low,
cell: 1.0,
triangles: Vec::new(),
cells: HashMap::new(),
extent,
reach_squared: reach * reach,
}
}
fn insert(&mut self, corners: [[f64; 3]; 3], solid: usize, face: u32) {
self.triangles.push((corners, solid, face));
}
fn finish(&mut self) {
let count = self.triangles.len().max(1) as f64;
let per_side = count.powf(0.4).max(1.0);
let longest_edge = self
.triangles
.iter()
.map(|(c, _, _)| distance(c[0], c[1]).max(distance(c[1], c[2])).max(distance(c[2], c[0])))
.fold(0.0, f64::max);
self.cell = (self.extent / per_side).max(longest_edge * 0.5).max(self.extent * 1e-6).max(1e-9);
for (index, (corners, _, _)) in self.triangles.iter().enumerate() {
let (low, high) = self.cell_span(*corners);
for x in low[0]..=high[0] {
for y in low[1]..=high[1] {
for z in low[2]..=high[2] {
self.cells.entry((x, y, z)).or_default().push(index as u32);
}
}
}
}
}
fn coordinate(&self, point: [f64; 3]) -> (i32, i32, i32) {
let c = |axis: usize| ((point[axis] - self.origin[axis]) / self.cell).floor().clamp(-1e6, 1e6) as i32;
(c(0), c(1), c(2))
}
fn cell_span(&self, corners: [[f64; 3]; 3]) -> ([i32; 3], [i32; 3]) {
let mut low = [i32::MAX; 3];
let mut high = [i32::MIN; 3];
for corner in corners {
let (x, y, z) = self.coordinate(corner);
for (axis, value) in [x, y, z].into_iter().enumerate() {
low[axis] = low[axis].min(value);
high[axis] = high[axis].max(value);
}
}
(low, high)
}
fn nearest(&self, point: [f64; 3]) -> Option<(usize, u32)> {
if self.triangles.is_empty() {
return None;
}
let (cx, cy, cz) = self.coordinate(point);
let mut best: Option<(f64, usize, u32)> = None;
for ring in 0..=MAX_RINGS {
if (ring - 1).max(0) as f64 * self.cell > self.reach_squared.sqrt() {
break;
}
for x in cx - ring..=cx + ring {
for y in cy - ring..=cy + ring {
for z in cz - ring..=cz + ring {
if (x - cx).abs() != ring && (y - cy).abs() != ring && (z - cz).abs() != ring {
continue;
}
let Some(bucket) = self.cells.get(&(x, y, z)) else {
continue;
};
for index in bucket {
let (corners, solid, face) = &self.triangles[*index as usize];
let d = point_triangle_distance_squared(point, *corners);
if best.is_none_or(|(best_d, _, _)| d < best_d) {
best = Some((d, *solid, *face));
}
}
}
}
}
if let Some((d, solid, face)) = best {
let reach = ring as f64 * self.cell;
if d <= reach * reach {
return Some((solid, face));
}
}
}
best.filter(|(d, _, _)| *d <= self.reach_squared).map(|(_, solid, face)| (solid, face))
}
}