ifc_lite_processing/style/
indexed_colour.rs1use super::Rgba;
21use ifc_lite_core::{DecodedEntity, EntityDecoder};
22use ifc_lite_geometry::Mesh;
23
24#[derive(Debug, Clone)]
28pub struct FullIndexedColourMap {
29 pub geometry_id: u32,
31 pub colours: Vec<Rgba>,
33 pub triangle_palette: Vec<usize>,
35}
36
37impl FullIndexedColourMap {
38 pub(crate) fn distinct_used(&self) -> usize {
40 let mut seen = self.triangle_palette.clone();
41 seen.sort_unstable();
42 seen.dedup();
43 seen.len()
44 }
45
46 pub fn dominant(&self) -> Rgba {
49 let mut counts: rustc_hash::FxHashMap<usize, u32> = rustc_hash::FxHashMap::default();
50 for &p in &self.triangle_palette {
51 *counts.entry(p).or_insert(0) += 1;
52 }
53 let idx = counts
54 .iter()
55 .max_by_key(|(_, c)| *c)
56 .map(|(&i, _)| i)
57 .unwrap_or(0);
58 self.colours.get(idx).copied().unwrap_or(Rgba::new(0.8, 0.8, 0.8, 1.0))
59 }
60}
61
62pub fn resolve_indexed_colour_map_full(
70 entity: &DecodedEntity,
71 decoder: &mut EntityDecoder,
72) -> Option<FullIndexedColourMap> {
73 let geometry_id = entity.get_ref(0)?;
74 let opacity = entity
75 .get(1)
76 .and_then(|a| a.as_float())
77 .map(|v| v as f32)
78 .unwrap_or(1.0)
79 .clamp(0.0, 1.0);
80 let colours_id = entity.get_ref(2)?;
81 let index_attr = entity.get(3)?;
82 let index_list = index_attr.as_list()?;
83 if index_list.is_empty() {
84 return None;
85 }
86
87 let colours_entity = decoder.decode_by_id(colours_id).ok()?;
88 let colour_list = colours_entity.get(0)?.as_list()?;
89 let colours: Vec<Rgba> = colour_list
90 .iter()
91 .filter_map(|c| {
92 let rgb = c.as_list()?;
93 let r = rgb.first().and_then(|v| v.as_float())? as f32;
94 let g = rgb.get(1).and_then(|v| v.as_float())? as f32;
95 let b = rgb.get(2).and_then(|v| v.as_float())? as f32;
96 Some(Rgba::new(r, g, b, opacity))
97 })
98 .collect();
99 if colours.is_empty() {
100 return None;
101 }
102
103 let max_idx = colours.len() - 1;
104 let triangle_palette: Vec<usize> = index_list
105 .iter()
106 .map(|v| {
107 let one_based = v.as_int().unwrap_or(1).max(1) as usize;
108 (one_based - 1).min(max_idx)
109 })
110 .collect();
111
112 Some(FullIndexedColourMap {
113 geometry_id,
114 colours,
115 triangle_palette,
116 })
117}
118
119pub fn split_mesh_by_indexed_colour(
127 mesh: &Mesh,
128 map: &FullIndexedColourMap,
129) -> Option<Vec<(Rgba, Mesh)>> {
130 let tri_count = mesh.indices.len() / 3;
131 if tri_count == 0 || tri_count != map.triangle_palette.len() {
132 return None;
133 }
134 if map.distinct_used() < 2 {
135 return None; }
137
138 let has_normals = mesh.normals.len() == mesh.positions.len();
139 let rtc_applied = mesh.rtc_applied;
140 let origin = mesh.origin;
141 let local_bounds = mesh.local_bounds;
142 let local_to_world = mesh.local_to_world;
143
144 #[derive(Default)]
146 struct Group {
147 positions: Vec<f32>,
148 normals: Vec<f32>,
149 indices: Vec<u32>,
150 }
151 let mut groups: Vec<Option<Group>> = (0..map.colours.len()).map(|_| None).collect();
152
153 for (tri, &palette) in map.triangle_palette.iter().enumerate() {
154 let tri_in_range = (0..3).all(|k| {
158 let vi = mesh.indices[tri * 3 + k] as usize;
159 vi * 3 + 2 < mesh.positions.len()
160 });
161 if !tri_in_range {
162 continue;
163 }
164
165 let group = groups[palette].get_or_insert_with(Group::default);
166 for k in 0..3 {
167 let vi = mesh.indices[tri * 3 + k] as usize;
168 let base = vi * 3;
169 let new_index = (group.positions.len() / 3) as u32;
170 group.positions.push(mesh.positions[base]);
171 group.positions.push(mesh.positions[base + 1]);
172 group.positions.push(mesh.positions[base + 2]);
173 if has_normals {
174 group.normals.push(mesh.normals[base]);
175 group.normals.push(mesh.normals[base + 1]);
176 group.normals.push(mesh.normals[base + 2]);
177 }
178 group.indices.push(new_index);
179 }
180 }
181
182 let out: Vec<(Rgba, Mesh)> = groups
183 .into_iter()
184 .enumerate()
185 .filter_map(|(palette, group)| {
186 let group = group?;
187 if group.indices.is_empty() {
188 return None;
189 }
190 let mesh = Mesh {
191 positions: group.positions,
192 normals: group.normals,
193 indices: group.indices,
194 rtc_applied,
195 origin,
196 instance_meta: None,
197 local_bounds,
204 local_to_world,
205 };
206 Some((map.colours[palette], mesh))
207 })
208 .collect();
209
210 (out.len() >= 2).then_some(out)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::{split_mesh_by_indexed_colour, FullIndexedColourMap};
216 use crate::style::Rgba;
217 use ifc_lite_geometry::Mesh;
218
219 #[test]
220 fn split_drops_out_of_range_triangle_without_partial_geometry() {
221 let positions: Vec<f32> = (0..6).flat_map(|i| [i as f32, 0.0, 0.0]).collect();
225 let mesh = Mesh {
226 positions,
227 normals: Vec::new(),
228 indices: vec![0, 1, 2, 3, 4, 5, 0, 1, 99],
229 rtc_applied: false,
230 origin: [0.0; 3],
231 instance_meta: None,
232 local_bounds: None,
233 local_to_world: None,
234 };
235 let map = FullIndexedColourMap {
236 geometry_id: 1,
237 colours: vec![Rgba::new(1.0, 0.0, 0.0, 1.0), Rgba::new(0.0, 1.0, 0.0, 1.0)],
238 triangle_palette: vec![0, 1, 0],
240 };
241
242 let parts = split_mesh_by_indexed_colour(&mesh, &map)
243 .expect("two valid palette groups survive after dropping the OOB triangle");
244
245 let total_tris: usize = parts
246 .iter()
247 .map(|(_, m)| {
248 assert_eq!(m.indices.len() % 3, 0, "index buffer must be whole triangles");
249 assert_eq!(m.positions.len() % 3, 0, "positions must be whole vertices");
250 m.indices.len() / 3
251 })
252 .sum();
253 assert_eq!(
254 total_tris, 2,
255 "the out-of-range triangle must be dropped, not partially emitted"
256 );
257 }
258}