use std::collections::BTreeSet;
use axiolid_mesh::{AttributeChannel, TriMesh};
use ifc_model::{EntityId, Model, Value};
use super::UV_CHANNEL;
use crate::error::GeometryError;
use crate::lower::session::LoweringSession;
use crate::lower::tessellated::lower_triangulated_face_set_node;
use crate::solid::testkit::{entity, int_grid, list, model, n, r};
use crate::transform::Transform;
use crate::units::UnitScale;
const COORD_INDEX: [[i64; 3]; 12] = [
[1, 6, 5],
[1, 2, 6],
[6, 2, 7],
[7, 2, 3],
[7, 8, 6],
[6, 8, 5],
[5, 8, 1],
[1, 8, 4],
[4, 2, 1],
[2, 4, 3],
[4, 8, 7],
[7, 3, 4],
];
const COORD_LIST: [[f64; 3]; 8] = [
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 2.0],
[1.0, 0.0, 2.0],
[1.0, 1.0, 2.0],
[0.0, 1.0, 2.0],
];
const TEX_INDEX: [[i64; 3]; 12] = [
[1, 4, 3],
[1, 2, 4],
[3, 1, 4],
[4, 1, 2],
[8, 7, 6],
[6, 7, 5],
[4, 3, 2],
[2, 3, 1],
[5, 8, 7],
[8, 5, 6],
[2, 4, 3],
[3, 1, 2],
];
const TEX_LIST: [[f64; 2]; 8] = [
[0.0, -0.5],
[1.0, -0.5],
[0.0, 1.5],
[1.0, 1.5],
[0.0, 0.0],
[0.0, 1.0],
[1.0, 0.0],
[1.0, 1.0],
];
const FACE_SET: u64 = 2;
fn rows<const W: usize>(rows: &[[i64; W]]) -> Value {
let slices: Vec<&[i64]> = rows.iter().map(|row| row.as_slice()).collect();
int_grid(&slices)
}
fn figure_415(tex_index: Option<Value>, extra: Vec<(u64, ifc_model::Entity)>) -> Model {
let points = list(
COORD_LIST
.iter()
.map(|p| list(p.iter().map(|&v| n(v)).collect()))
.collect(),
);
let tex = list(
TEX_LIST
.iter()
.map(|p| list(p.iter().map(|&v| n(v)).collect()))
.collect(),
);
let mut entities = vec![
(1, entity("IFCCARTESIANPOINTLIST3D", vec![points])),
(
FACE_SET,
entity(
"IFCTRIANGULATEDFACESET",
vec![
r(1),
Value::Null,
Value::Bool(true),
rows(&COORD_INDEX),
Value::Null,
],
),
),
(3, entity("IFCTEXTUREVERTEXLIST", vec![tex])),
(4, entity("IFCIMAGETEXTURE", vec![Value::Null; 6])),
(
5,
entity(
"IFCINDEXEDTRIANGLETEXTUREMAP",
vec![
list(vec![r(4)]),
r(FACE_SET),
r(3),
tex_index.unwrap_or(Value::Null),
],
),
),
];
entities.extend(extra);
model(entities)
}
fn untextured() -> Model {
let points = list(
COORD_LIST
.iter()
.map(|p| list(p.iter().map(|&v| n(v)).collect()))
.collect(),
);
model(vec![
(1, entity("IFCCARTESIANPOINTLIST3D", vec![points])),
(
FACE_SET,
entity(
"IFCTRIANGULATEDFACESET",
vec![
r(1),
Value::Null,
Value::Bool(true),
rows(&COORD_INDEX),
Value::Null,
],
),
),
])
}
fn lower(model: &Model) -> Result<TriMesh, GeometryError> {
let scale = UnitScale::default();
let mut session = LoweringSession::new(model, &scale);
let node =
lower_triangulated_face_set_node(&mut session, EntityId(FACE_SET), Transform::identity())?;
let lowered = session.finish(node)?;
match lowered.graph.get(lowered.root) {
Some(axiolid_model::GeometryNode::TriMesh(mesh)) => Ok(mesh.clone()),
other => panic!("expected a TriMesh, got {other:?}"),
}
}
fn uv(mesh: &TriMesh) -> &AttributeChannel {
mesh.attributes
.iter()
.find(|c| c.name == UV_CHANNEL)
.expect("a uv channel")
}
#[test]
fn the_specification_box_carries_a_coordinate_per_corner() {
let mesh = lower(&figure_415(Some(rows(&TEX_INDEX)), vec![])).unwrap();
mesh.validate_structure().expect("valid mesh");
assert_eq!(mesh.positions.len(), 8, "positions stay shared");
assert!(
axiolid_mesh::EdgeAdjacency::build(&mesh).is_closed_two_manifold(),
"UV seams must not open the box"
);
let channel = uv(&mesh);
assert!(channel.is_corner_indexed());
for (corner, tex) in TEX_INDEX.iter().flatten().enumerate() {
let want = TEX_LIST[(*tex - 1) as usize];
assert_eq!(
channel.at_corner(&mesh.indices, corner),
Some(want.as_slice()),
"corner {corner}"
);
}
}
#[test]
fn one_position_carries_a_different_coordinate_in_each_face() {
let mesh = lower(&figure_415(Some(rows(&TEX_INDEX)), vec![])).unwrap();
let channel = uv(&mesh);
for vertex in 0..8u32 {
let seen: BTreeSet<[u64; 2]> = mesh
.indices
.iter()
.enumerate()
.filter(|(_, &v)| v == vertex)
.map(|(corner, _)| {
let value = channel.at_corner(&mesh.indices, corner).unwrap();
[value[0].to_bits(), value[1].to_bits()]
})
.collect();
assert_eq!(seen.len(), 3, "vertex {vertex}");
}
}
#[test]
fn a_shorter_index_leaves_the_trailing_triangles_unmapped() {
let mesh = lower(&figure_415(Some(rows(&TEX_INDEX[..4])), vec![])).unwrap();
mesh.validate_structure().expect("valid mesh");
let channel = uv(&mesh);
assert!(
channel.at_corner(&mesh.indices, 11).is_some(),
"triangle 4 mapped"
);
for corner in 12..36 {
assert_eq!(
channel.at_corner(&mesh.indices, corner),
None,
"corner {corner}"
);
}
}
#[test]
fn an_omitted_index_adds_no_channel() {
let mesh = lower(&figure_415(None, vec![])).unwrap();
assert!(mesh.attributes.is_empty());
}
#[test]
fn an_untextured_face_set_gets_no_channel() {
assert!(lower(&untextured()).unwrap().attributes.is_empty());
}
#[test]
fn more_mapped_triangles_than_the_face_set_has_is_an_error() {
let mut long = TEX_INDEX.to_vec();
long.push([1, 2, 3]);
let err = lower(&figure_415(Some(rows(&long)), vec![])).unwrap_err();
assert!(err.to_string().contains("maps 13 triangles"), "{err}");
}
#[test]
fn an_index_outside_the_texture_vertices_is_an_error() {
for bad in [0, 9] {
let mut index = TEX_INDEX;
index[3][1] = bad;
let err = lower(&figure_415(Some(rows(&index)), vec![])).unwrap_err();
assert!(
err.to_string().contains(&format!("holds {bad}")),
"{bad}: {err}"
);
}
}
#[test]
fn a_second_map_gets_its_own_channel() {
let second = entity(
"IFCINDEXEDTRIANGLETEXTUREMAP",
vec![list(vec![r(4)]), r(FACE_SET), r(3), rows(&TEX_INDEX[..1])],
);
let mesh = lower(&figure_415(Some(rows(&TEX_INDEX)), vec![(6, second)])).unwrap();
let names: Vec<&str> = mesh.attributes.iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, ["uv", "uv1"]);
mesh.validate_structure().expect("valid mesh");
}
#[test]
fn the_frame_does_not_move_texture_coordinates() {
let model = figure_415(Some(rows(&TEX_INDEX)), vec![]);
let scale = UnitScale::default();
let mut session = LoweringSession::new(&model, &scale);
let frame = Transform::translation([10.0, 20.0, 30.0]);
let node = lower_triangulated_face_set_node(&mut session, EntityId(FACE_SET), frame).unwrap();
let lowered = session.finish(node).unwrap();
let Some(axiolid_model::GeometryNode::TriMesh(mesh)) = lowered.graph.get(lowered.root) else {
panic!("expected a TriMesh");
};
assert_eq!(
uv(mesh).at_corner(&mesh.indices, 0),
Some([0.0, -0.5].as_slice())
);
}