use brepkit_math::vec::Point3;
use brepkit_operations::tessellate::{
EdgeLines, sample_solid_edges, tessellate_solid_grouped_with_tolerance,
};
use brepkit_topology::Topology;
use brepkit_topology::explorer::solid_faces;
use brepkit_topology::solid::SolidId;
use crate::error::RenderError;
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub face_id: u32,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
pub struct EdgeVertex {
pub position: [f32; 3],
}
pub struct RenderMesh {
pub vertices: Vec<Vertex>,
pub indices: Vec<u32>,
pub edge_vertices: Vec<EdgeVertex>,
pub center: Point3,
}
impl RenderMesh {
pub fn build(topo: &Topology, solid: SolidId, deflection: f64) -> Result<Self, RenderError> {
let angular_tol = brepkit_math::chord::DEFAULT_ANGULAR_TOL;
let (mesh, face_offsets) =
tessellate_solid_grouped_with_tolerance(topo, solid, deflection, angular_tol)?;
let faces = solid_faces(topo, solid)?;
let center = aabb_center(&mesh.positions);
let positions_rtc: Vec<[f32; 3]> = mesh
.positions
.iter()
.map(|&p| {
let d = p - center;
#[allow(clippy::cast_possible_truncation)]
[d.x() as f32, d.y() as f32, d.z() as f32]
})
.collect();
let normals: Vec<[f32; 3]> = mesh
.normals
.iter()
.map(|&n| {
#[allow(clippy::cast_possible_truncation)]
[n.x() as f32, n.y() as f32, n.z() as f32]
})
.collect();
if mesh.indices.len() % 3 != 0 {
return Err(RenderError::MeshData(format!(
"index buffer length {} is not divisible by 3",
mesh.indices.len()
)));
}
let tri_count = mesh.indices.len() / 3;
if face_offsets.len() != faces.len() + 1 {
return Err(RenderError::MeshData(format!(
"face_offsets has {} entries, expected {} (one per face + sentinel)",
face_offsets.len(),
faces.len() + 1
)));
}
let mut vertices: Vec<Vertex> = Vec::with_capacity(tri_count * 3);
let mut indices: Vec<u32> = Vec::with_capacity(tri_count * 3);
let mut tri_face_ids = vec![0_u32; tri_count];
for (i, face) in faces.iter().enumerate() {
let start = face_offsets[i] as usize / 3;
let end = face_offsets[i + 1] as usize / 3;
#[allow(clippy::cast_possible_truncation)]
let id = face.index() as u32 + 1;
for slot in tri_face_ids
.iter_mut()
.take(end.min(tri_count))
.skip(start.min(tri_count))
{
*slot = id;
}
}
for t in 0..tri_count {
let face_id = tri_face_ids[t];
for k in 0..3 {
let vi = mesh.indices[t * 3 + k] as usize;
let (Some(&position), Some(&normal)) = (positions_rtc.get(vi), normals.get(vi))
else {
return Err(RenderError::MeshData(format!(
"triangle index {vi} is out of range ({} vertices)",
positions_rtc.len()
)));
};
#[allow(clippy::cast_possible_truncation)]
let idx = vertices.len() as u32;
vertices.push(Vertex {
position,
normal,
face_id,
});
indices.push(idx);
}
}
let edges = sample_solid_edges(topo, solid, deflection)?;
let edge_vertices = build_edge_lines(&edges, center);
Ok(Self {
vertices,
indices,
edge_vertices,
center,
})
}
}
fn build_edge_lines(edges: &EdgeLines, center: Point3) -> Vec<EdgeVertex> {
let mut out = Vec::new();
let n_edges = edges.offsets.len();
for e in 0..n_edges {
let start = edges.offsets[e];
let end = edges
.offsets
.get(e + 1)
.copied()
.unwrap_or(edges.positions.len());
let pts = &edges.positions[start..end];
for w in pts.windows(2) {
for &p in w {
let d = p - center;
#[allow(clippy::cast_possible_truncation)]
out.push(EdgeVertex {
position: [d.x() as f32, d.y() as f32, d.z() as f32],
});
}
}
}
out
}
fn aabb_center(positions: &[Point3]) -> Point3 {
if positions.is_empty() {
return Point3::new(0.0, 0.0, 0.0);
}
let mut min = [f64::INFINITY; 3];
let mut max = [f64::NEG_INFINITY; 3];
for p in positions {
let c = [p.x(), p.y(), p.z()];
for i in 0..3 {
if c[i] < min[i] {
min[i] = c[i];
}
if c[i] > max[i] {
max[i] = c[i];
}
}
}
Point3::new(
(min[0] + max[0]) * 0.5,
(min[1] + max[1]) * 0.5,
(min[2] + max[2]) * 0.5,
)
}