use crate::gpu::GpuContext;
use crate::mesh::{Mesh, Vertex3d};
use glam::{Quat, Vec3};
use std::path::Path;
#[derive(Debug)]
pub enum GeometryError {
Io(std::io::Error),
UnknownFormat(String),
ParseError(String),
}
impl std::fmt::Display for GeometryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GeometryError::Io(e) => write!(f, "IO error: {}", e),
GeometryError::UnknownFormat(ext) => {
write!(f, "Unknown geometry format: '{}'", ext)
}
GeometryError::ParseError(msg) => write!(f, "Parse error: {}", msg),
}
}
}
impl std::error::Error for GeometryError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
GeometryError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for GeometryError {
fn from(e: std::io::Error) -> Self {
GeometryError::Io(e)
}
}
#[derive(Clone, Debug)]
pub struct RawGeometry {
pub vertices: Vec<Vertex3d>,
pub indices: Vec<u32>,
}
impl RawGeometry {
pub fn new(vertices: Vec<Vertex3d>, indices: Vec<u32>) -> Self {
Self { vertices, indices }
}
pub fn bounds(&self) -> (Vec3, Vec3) {
let mut min = Vec3::splat(f32::INFINITY);
let mut max = Vec3::splat(f32::NEG_INFINITY);
for v in &self.vertices {
let p = Vec3::from(v.position);
min = min.min(p);
max = max.max(p);
}
(min, max)
}
pub fn center(&self) -> Vec3 {
let (min, max) = self.bounds();
(min + max) * 0.5
}
pub fn size(&self) -> Vec3 {
let (min, max) = self.bounds();
max - min
}
pub fn translate(&mut self, offset: Vec3) {
for v in &mut self.vertices {
v.position[0] += offset.x;
v.position[1] += offset.y;
v.position[2] += offset.z;
}
}
pub fn scale(&mut self, factor: f32) {
for v in &mut self.vertices {
v.position[0] *= factor;
v.position[1] *= factor;
v.position[2] *= factor;
}
}
pub fn scale_xyz(&mut self, factors: Vec3) {
for v in &mut self.vertices {
v.position[0] *= factors.x;
v.position[1] *= factors.y;
v.position[2] *= factors.z;
}
}
pub fn rotate(&mut self, rotation: Quat) {
for v in &mut self.vertices {
let pos = Vec3::from(v.position);
let rotated_pos = rotation * pos;
v.position = rotated_pos.into();
let normal = Vec3::from(v.normal);
let rotated_normal = rotation * normal;
v.normal = rotated_normal.into();
}
}
pub fn recenter(&mut self) {
let center = self.center();
self.translate(-center);
}
pub fn normalize(&mut self) {
let size = self.size();
let max_dim = size.x.max(size.y).max(size.z);
if max_dim > 0.0 {
self.scale(1.0 / max_dim);
}
}
pub fn recalculate_normals(&mut self) {
for v in &mut self.vertices {
v.normal = [0.0, 0.0, 0.0];
}
for tri in self.indices.chunks(3) {
if tri.len() < 3 {
continue;
}
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
let p0 = Vec3::from(self.vertices[i0].position);
let p1 = Vec3::from(self.vertices[i1].position);
let p2 = Vec3::from(self.vertices[i2].position);
let edge1 = p1 - p0;
let edge2 = p2 - p0;
let face_normal = edge1.cross(edge2);
for &i in &[i0, i1, i2] {
self.vertices[i].normal[0] += face_normal.x;
self.vertices[i].normal[1] += face_normal.y;
self.vertices[i].normal[2] += face_normal.z;
}
}
for v in &mut self.vertices {
let n = Vec3::from(v.normal);
let normalized = n.normalize_or_zero();
v.normal = normalized.into();
}
}
pub fn upload(&self, gpu: &GpuContext) -> Mesh {
Mesh::new(gpu, &self.vertices, &self.indices)
}
}
pub struct GeometryLoader<'a> {
gpu: &'a GpuContext,
pending: PendingGeometry,
}
#[derive(Clone)]
pub struct PendingGeometry {
result: Result<RawGeometry, String>,
center: bool,
normalize: bool,
smooth_normals: bool,
scale_factor: Option<f32>,
translation: Option<Vec3>,
rotation: Option<Quat>,
}
impl PendingGeometry {
pub fn from_file(path: impl AsRef<Path>) -> Self {
let path = path.as_ref();
let result = Self::load_file(path).map_err(|e| e.to_string());
Self {
result,
center: false,
normalize: false,
smooth_normals: false,
scale_factor: None,
translation: None,
rotation: None,
}
}
pub fn from_stl(path: impl AsRef<Path>) -> Self {
let result = Self::load_stl_file(path.as_ref()).map_err(|e| e.to_string());
Self {
result,
center: false,
normalize: false,
smooth_normals: false,
scale_factor: None,
translation: None,
rotation: None,
}
}
pub fn from_stl_bytes(bytes: &[u8]) -> Self {
let result = Self::parse_stl_bytes(bytes).map_err(|e| e.to_string());
Self {
result,
center: false,
normalize: false,
smooth_normals: false,
scale_factor: None,
translation: None,
rotation: None,
}
}
pub fn from_raw(geometry: RawGeometry) -> Self {
Self {
result: Ok(geometry),
center: false,
normalize: false,
smooth_normals: false,
scale_factor: None,
translation: None,
rotation: None,
}
}
pub fn centered(mut self) -> Self {
self.center = true;
self
}
pub fn normalized(mut self) -> Self {
self.normalize = true;
self
}
pub fn smooth_normals(mut self) -> Self {
self.smooth_normals = true;
self
}
pub fn scaled(mut self, factor: f32) -> Self {
self.scale_factor = Some(factor);
self
}
pub fn translated(mut self, offset: Vec3) -> Self {
self.translation = Some(offset);
self
}
pub fn upright(mut self) -> Self {
self.rotation = Some(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2));
self
}
pub fn rotated_by(mut self, rotation: Quat) -> Self {
self.rotation = Some(rotation);
self
}
pub fn upload(self, gpu: &GpuContext) -> Result<Mesh, GeometryError> {
let mut geometry = self.result.map_err(|s| GeometryError::ParseError(s))?;
if self.center {
geometry.recenter();
}
if let Some(rotation) = self.rotation {
geometry.rotate(rotation);
}
if self.normalize {
geometry.normalize();
}
if let Some(scale) = self.scale_factor {
geometry.scale(scale);
}
if self.smooth_normals {
geometry.recalculate_normals();
}
if let Some(offset) = self.translation {
geometry.translate(offset);
}
Ok(geometry.upload(gpu))
}
fn load_file(path: &Path) -> Result<RawGeometry, GeometryError> {
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_lowercase())
.unwrap_or_default();
match ext.as_str() {
"stl" => Self::load_stl_file(path),
_ => Err(GeometryError::UnknownFormat(ext)),
}
}
fn load_stl_file(path: &Path) -> Result<RawGeometry, GeometryError> {
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
Self::parse_stl(&mut reader)
}
fn parse_stl<R: std::io::Read + std::io::Seek>(
reader: &mut R,
) -> Result<RawGeometry, GeometryError> {
let stl = stl_io::read_stl(reader)
.map_err(|e| GeometryError::ParseError(format!("STL parse error: {}", e)))?;
let mut vertices = Vec::with_capacity(stl.faces.len() * 3);
let mut indices = Vec::with_capacity(stl.faces.len() * 3);
for (i, face) in stl.faces.iter().enumerate() {
let normal: [f32; 3] = face.normal.into();
for &vertex_idx in &face.vertices {
let vertex = &stl.vertices[vertex_idx];
let position: [f32; 3] = (*vertex).into();
vertices.push(Vertex3d::new(
position,
normal,
[0.0, 0.0], ));
}
let base = (i * 3) as u32;
indices.extend_from_slice(&[base, base + 1, base + 2]);
}
Ok(RawGeometry::new(vertices, indices))
}
fn parse_stl_bytes(bytes: &[u8]) -> Result<RawGeometry, GeometryError> {
let mut cursor = std::io::Cursor::new(bytes);
Self::parse_stl(&mut cursor)
}
}
impl<'a> GeometryLoader<'a> {
pub fn from_file(gpu: &'a GpuContext, path: impl AsRef<Path>) -> Self {
Self {
gpu,
pending: PendingGeometry::from_file(path),
}
}
pub fn from_stl(gpu: &'a GpuContext, path: impl AsRef<Path>) -> Self {
Self {
gpu,
pending: PendingGeometry::from_stl(path),
}
}
pub fn from_stl_bytes(gpu: &'a GpuContext, bytes: &[u8]) -> Self {
Self {
gpu,
pending: PendingGeometry::from_stl_bytes(bytes),
}
}
pub fn from_raw(gpu: &'a GpuContext, geometry: RawGeometry) -> Self {
Self {
gpu,
pending: PendingGeometry::from_raw(geometry),
}
}
pub fn centered(mut self) -> Self {
self.pending = self.pending.centered();
self
}
pub fn normalized(mut self) -> Self {
self.pending = self.pending.normalized();
self
}
pub fn smooth_normals(mut self) -> Self {
self.pending = self.pending.smooth_normals();
self
}
pub fn scaled(mut self, factor: f32) -> Self {
self.pending = self.pending.scaled(factor);
self
}
pub fn translated(mut self, offset: Vec3) -> Self {
self.pending = self.pending.translated(offset);
self
}
pub fn upright(mut self) -> Self {
self.pending = self.pending.upright();
self
}
pub fn rotated_by(mut self, rotation: Quat) -> Self {
self.pending = self.pending.rotated_by(rotation);
self
}
pub fn build(self) -> Result<Mesh, GeometryError> {
self.pending.upload(self.gpu)
}
pub fn unwrap(self) -> Mesh {
self.build().expect("Failed to load geometry")
}
pub fn expect(self, msg: &str) -> Mesh {
self.build().expect(msg)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_geometry_bounds() {
let vertices = vec![
Vertex3d::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0]),
Vertex3d::new([1.0, 2.0, 3.0], [0.0, 1.0, 0.0], [0.0, 0.0]),
Vertex3d::new([-1.0, -1.0, -1.0], [0.0, 1.0, 0.0], [0.0, 0.0]),
];
let indices = vec![0, 1, 2];
let geom = RawGeometry::new(vertices, indices);
let (min, max) = geom.bounds();
assert_eq!(min, Vec3::new(-1.0, -1.0, -1.0));
assert_eq!(max, Vec3::new(1.0, 2.0, 3.0));
}
#[test]
fn raw_geometry_center() {
let vertices = vec![
Vertex3d::new([0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0]),
Vertex3d::new([2.0, 4.0, 6.0], [0.0, 1.0, 0.0], [0.0, 0.0]),
];
let indices = vec![0, 1, 0];
let geom = RawGeometry::new(vertices, indices);
assert_eq!(geom.center(), Vec3::new(1.0, 2.0, 3.0));
}
#[test]
fn raw_geometry_recenter() {
let vertices = vec![
Vertex3d::new([2.0, 2.0, 2.0], [0.0, 1.0, 0.0], [0.0, 0.0]),
Vertex3d::new([4.0, 4.0, 4.0], [0.0, 1.0, 0.0], [0.0, 0.0]),
];
let indices = vec![0, 1, 0];
let mut geom = RawGeometry::new(vertices, indices);
geom.recenter();
let center = geom.center();
assert!((center.x).abs() < 0.001);
assert!((center.y).abs() < 0.001);
assert!((center.z).abs() < 0.001);
}
}