use super::{types::FaceMask, AppearanceRequest, RepresentationPolicy};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
pub(super) const STALE: &str = "Face selection is stale: the evaluated surface geometry changed";
pub(super) const DIRECT_BODY: &str =
"Face masks currently apply to converted occurrence bodies only; this object already has a direct tessellated Body";
const QUANTUM_PER_METRE: f64 = 1e6;
const MASK_ORDINAL_BUDGET: usize = 500_000;
pub(super) fn validate(request: &AppearanceRequest) -> Result<BTreeMap<u32, &FaceMask>, String> {
let mut masks = BTreeMap::new();
if request.face_masks.is_empty() { return Ok(masks); }
if request.representation_policy != RepresentationPolicy::EvaluatedOccurrence {
return Err("Face masks require the evaluatedOccurrence representation policy".into());
}
if request.face_masks.len() > request.product_ids.len() {
return Err("Face masks exceed the appearance scope".into());
}
let scope: BTreeSet<u32> = request.product_ids.iter().copied().collect();
let mut ordinals = 0usize;
for mask in &request.face_masks {
if !scope.contains(&mask.product_id) {
return Err("Face mask targets a product outside the appearance scope".into());
}
if mask.surface_fingerprint.len() != 64 || !mask.surface_fingerprint.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err("Face mask fingerprint must be a hex SHA-256 digest".into());
}
ordinals = ordinals.saturating_add(mask.triangles.len());
if ordinals > MASK_ORDINAL_BUDGET { return Err("Face masks exceed their triangle budget".into()); }
if masks.insert(mask.product_id, mask).is_some() {
return Err("Duplicate face mask for one product".into());
}
}
Ok(masks)
}
pub(super) fn fingerprint(global_id: &str, points: &[[f64; 3]], scale: f64, indices: &[u32]) -> Result<String, String> {
let mut hasher = Sha256::new();
hasher.update(b"ifc-lite evaluated surface v1\0");
hasher.update(global_id.as_bytes());
hasher.update([0u8]);
hasher.update((points.len() as u64).to_le_bytes());
for point in points {
for &value in point {
let quantised = value * scale * QUANTUM_PER_METRE;
if !quantised.is_finite() || quantised.abs() > 9.0e18 {
return Err("Evaluated surface coordinate exceeds the fingerprint range".into());
}
hasher.update((quantised.round() as i64).to_le_bytes());
}
}
hasher.update((indices.len() as u64).to_le_bytes());
for &index in indices { hasher.update(index.to_le_bytes()); }
Ok(format!("{:x}", hasher.finalize()))
}
pub(super) struct Partition {
pub masked: Vec<u32>,
pub retained: Vec<u32>,
pub triangles: Vec<u32>,
}
pub(super) fn partition(mask: &FaceMask, fingerprint: &str, indices: &[u32]) -> Result<Option<Partition>, String> {
if mask.surface_fingerprint != fingerprint { return Err(STALE.into()); }
let count = indices.len() / 3;
let mut triangles = mask.triangles.clone();
triangles.sort_unstable();
triangles.dedup();
if triangles.is_empty() { return Err("Face selection contains no triangles".into()); }
if triangles.last().is_some_and(|&last| last as usize >= count) {
return Err("Face selection references a triangle outside the evaluated surface".into());
}
if triangles.len() == count { return Ok(None); }
let mut masked = Vec::with_capacity(triangles.len() * 3);
let mut retained = Vec::with_capacity((count - triangles.len()) * 3);
let mut next = triangles.iter().peekable();
for (ordinal, corners) in indices.chunks_exact(3).enumerate() {
if next.peek().is_some_and(|&&selected| selected as usize == ordinal) {
masked.extend_from_slice(corners);
next.next();
} else {
retained.extend_from_slice(corners);
}
}
Ok(Some(Partition { masked, retained, triangles }))
}
#[cfg(test)]
#[path = "evaluated_mask_tests.rs"]
mod tests;