use axiolid_mesh::{AttributeChannel, Blend};
use ifc_model::{EntityId, Value};
use crate::error::{GeometryError, GeometryResult};
use crate::lower::session::LoweringSession;
pub const UV_CHANNEL: &str = "uv";
const TYPE: &str = "IFCINDEXEDTRIANGLETEXTUREMAP";
const TEX_COORDS: usize = 2;
const TEX_COORD_INDEX: usize = 3;
pub(super) fn channels(
session: &mut LoweringSession<'_>,
face_set: EntityId,
triangle_count: usize,
) -> GeometryResult<Vec<AttributeChannel>> {
let maps = session.triangle_texture_maps(face_set).to_vec();
let mut out = Vec::with_capacity(maps.len());
for map in maps {
if let Some(values) = channel(session, face_set, map, triangle_count)? {
let name = match out.len() {
0 => UV_CHANNEL.to_string(),
n => format!("{UV_CHANNEL}{n}"),
};
out.push(AttributeChannel::corner_indexed(
name,
values.0,
2,
Blend::Linear,
values.1,
));
}
}
Ok(out)
}
fn channel(
session: &mut LoweringSession<'_>,
face_set: EntityId,
map: EntityId,
triangle_count: usize,
) -> GeometryResult<Option<(Vec<f64>, Vec<u32>)>> {
let entity = session.entity(face_set, map)?;
let index = match entity.attributes.get(TEX_COORD_INDEX) {
None | Some(Value::Null) => return Ok(None),
Some(value) => triangles(map, value)?,
};
if index.len() > triangle_count {
return Err(invalid(
map,
"TexCoordIndex",
format!(
"maps {} triangles, but the face set has {triangle_count}",
index.len()
),
));
}
let list = entity
.attributes
.get(TEX_COORDS)
.and_then(Value::as_ref_id)
.ok_or_else(|| missing(map, TYPE, "TexCoords"))?;
let values = texture_vertices(session, map, list)?;
let pool = values.len() / 2;
let mut corners = Vec::with_capacity(triangle_count * 3);
for triangle in &index {
for &raw in triangle {
let slot = usize::try_from(raw)
.ok()
.filter(|&s| (1..=pool).contains(&s));
let Some(slot) = slot else {
return Err(invalid(
map,
"TexCoordIndex",
format!("holds {raw}; TexCoordsList has {pool} entries (1-based)"),
));
};
corners.push((slot - 1) as u32);
}
}
corners.resize(triangle_count * 3, AttributeChannel::UNMAPPED);
Ok(Some((values, corners)))
}
fn triangles(map: EntityId, value: &Value) -> GeometryResult<Vec<[i64; 3]>> {
let rows = value
.unwrap_typed()
.as_list()
.ok_or_else(|| invalid(map, "TexCoordIndex", "is not a list".to_string()))?;
rows.iter()
.map(|row| {
let row = row.unwrap_typed().as_list();
let ints: Option<Vec<i64>> = row.map(|r| {
r.iter()
.map(|v| v.unwrap_typed().as_i64())
.collect::<Option<Vec<_>>>()
})?;
match ints.as_deref() {
Some(&[a, b, c]) => Some([a, b, c]),
_ => None,
}
})
.collect::<Option<Vec<_>>>()
.ok_or_else(|| {
invalid(
map,
"TexCoordIndex",
"every entry must be exactly three integers".to_string(),
)
})
}
fn texture_vertices(
session: &mut LoweringSession<'_>,
map: EntityId,
list: EntityId,
) -> GeometryResult<Vec<f64>> {
let entity = session.entity(map, list)?;
let rows = entity
.attributes
.first()
.and_then(|v| v.unwrap_typed().as_list())
.ok_or_else(|| missing(list, "IFCTEXTUREVERTEXLIST", "TexCoordsList"))?;
let mut out = Vec::with_capacity(rows.len() * 2);
for row in rows {
let pair = row.unwrap_typed().as_list().and_then(|p| match p {
[s, t] => Some([s.unwrap_typed().as_f64()?, t.unwrap_typed().as_f64()?]),
_ => None,
});
let Some([s, t]) = pair else {
return Err(GeometryError::Degenerate {
entity: list,
type_name: "IFCTEXTUREVERTEXLIST".to_string(),
detail: "TexCoordsList entries must be (s, t) pairs of numbers".to_string(),
});
};
out.extend([s, t]);
}
Ok(out)
}
fn invalid(map: EntityId, attribute: &str, detail: String) -> GeometryError {
GeometryError::Degenerate {
entity: map,
type_name: TYPE.to_string(),
detail: format!("{attribute} {detail}"),
}
}
fn missing(entity: EntityId, type_name: &str, attribute: &'static str) -> GeometryError {
GeometryError::MissingAttribute {
entity,
type_name: type_name.to_string(),
attribute,
}
}
#[cfg(test)]
mod tests;