use super::Rgba;
use ifc_lite_core::{DecodedEntity, EntityDecoder};
use ifc_lite_geometry::Mesh;
#[derive(Debug, Clone)]
pub struct FullIndexedColourMap {
pub geometry_id: u32,
pub colours: Vec<Rgba>,
pub triangle_palette: Vec<usize>,
}
impl FullIndexedColourMap {
pub(crate) fn distinct_used(&self) -> usize {
let mut seen = self.triangle_palette.clone();
seen.sort_unstable();
seen.dedup();
seen.len()
}
pub fn dominant(&self) -> Rgba {
let mut counts: rustc_hash::FxHashMap<usize, u32> = rustc_hash::FxHashMap::default();
for &p in &self.triangle_palette {
*counts.entry(p).or_insert(0) += 1;
}
let idx = counts
.iter()
.max_by_key(|(_, c)| *c)
.map(|(&i, _)| i)
.unwrap_or(0);
self.colours.get(idx).copied().unwrap_or(Rgba::new(0.8, 0.8, 0.8, 1.0))
}
}
pub fn resolve_indexed_colour_map_full(
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<FullIndexedColourMap> {
let geometry_id = entity.get_ref(0)?;
let opacity = entity
.get(1)
.and_then(|a| a.as_float())
.map(|v| v as f32)
.unwrap_or(1.0)
.clamp(0.0, 1.0);
let colours_id = entity.get_ref(2)?;
let index_attr = entity.get(3)?;
let index_list = index_attr.as_list()?;
if index_list.is_empty() {
return None;
}
let colours_entity = decoder.decode_by_id(colours_id).ok()?;
let colour_list = colours_entity.get(0)?.as_list()?;
let colours: Vec<Rgba> = colour_list
.iter()
.filter_map(|c| {
let rgb = c.as_list()?;
let r = rgb.first().and_then(|v| v.as_float())? as f32;
let g = rgb.get(1).and_then(|v| v.as_float())? as f32;
let b = rgb.get(2).and_then(|v| v.as_float())? as f32;
Some(Rgba::new(r, g, b, opacity))
})
.collect();
if colours.is_empty() {
return None;
}
let max_idx = colours.len() - 1;
let triangle_palette: Vec<usize> = index_list
.iter()
.map(|v| {
let one_based = v.as_int().unwrap_or(1).max(1) as usize;
(one_based - 1).min(max_idx)
})
.collect();
Some(FullIndexedColourMap {
geometry_id,
colours,
triangle_palette,
})
}
pub fn split_mesh_by_indexed_colour(
mesh: &Mesh,
map: &FullIndexedColourMap,
) -> Option<Vec<(Rgba, Mesh)>> {
let tri_count = mesh.indices.len() / 3;
if tri_count == 0 || tri_count != map.triangle_palette.len() {
return None;
}
if map.distinct_used() < 2 {
return None; }
let has_normals = mesh.normals.len() == mesh.positions.len();
let rtc_applied = mesh.rtc_applied;
let origin = mesh.origin;
#[derive(Default)]
struct Group {
positions: Vec<f32>,
normals: Vec<f32>,
indices: Vec<u32>,
}
let mut groups: Vec<Option<Group>> = (0..map.colours.len()).map(|_| None).collect();
for (tri, &palette) in map.triangle_palette.iter().enumerate() {
let tri_in_range = (0..3).all(|k| {
let vi = mesh.indices[tri * 3 + k] as usize;
vi * 3 + 2 < mesh.positions.len()
});
if !tri_in_range {
continue;
}
let group = groups[palette].get_or_insert_with(Group::default);
for k in 0..3 {
let vi = mesh.indices[tri * 3 + k] as usize;
let base = vi * 3;
let new_index = (group.positions.len() / 3) as u32;
group.positions.push(mesh.positions[base]);
group.positions.push(mesh.positions[base + 1]);
group.positions.push(mesh.positions[base + 2]);
if has_normals {
group.normals.push(mesh.normals[base]);
group.normals.push(mesh.normals[base + 1]);
group.normals.push(mesh.normals[base + 2]);
}
group.indices.push(new_index);
}
}
let out: Vec<(Rgba, Mesh)> = groups
.into_iter()
.enumerate()
.filter_map(|(palette, group)| {
let group = group?;
if group.indices.is_empty() {
return None;
}
let mesh = Mesh {
positions: group.positions,
normals: group.normals,
indices: group.indices,
rtc_applied,
origin,
};
Some((map.colours[palette], mesh))
})
.collect();
(out.len() >= 2).then_some(out)
}
#[cfg(test)]
mod tests {
use super::{split_mesh_by_indexed_colour, FullIndexedColourMap};
use crate::style::Rgba;
use ifc_lite_geometry::Mesh;
#[test]
fn split_drops_out_of_range_triangle_without_partial_geometry() {
let positions: Vec<f32> = (0..6).flat_map(|i| [i as f32, 0.0, 0.0]).collect();
let mesh = Mesh {
positions,
normals: Vec::new(),
indices: vec![0, 1, 2, 3, 4, 5, 0, 1, 99],
rtc_applied: false,
origin: [0.0; 3], };
let map = FullIndexedColourMap {
geometry_id: 1,
colours: vec![Rgba::new(1.0, 0.0, 0.0, 1.0), Rgba::new(0.0, 1.0, 0.0, 1.0)],
triangle_palette: vec![0, 1, 0],
};
let parts = split_mesh_by_indexed_colour(&mesh, &map)
.expect("two valid palette groups survive after dropping the OOB triangle");
let total_tris: usize = parts
.iter()
.map(|(_, m)| {
assert_eq!(m.indices.len() % 3, 0, "index buffer must be whole triangles");
assert_eq!(m.positions.len() % 3, 0, "positions must be whole vertices");
m.indices.len() / 3
})
.sum();
assert_eq!(
total_tris, 2,
"the out-of-range triangle must be dropped, not partially emitted"
);
}
}