use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::Path;
use crate::ids::{FaceId, HalfEdgeId, VertexId};
use crate::storage::{Face, HalfEdge, MeshStorage, Vertex};
use crate::traversal::{FaceHalfEdges, FaceVertices};
#[derive(Debug)]
pub enum ObjError {
Io(std::io::Error),
Parse { line: usize, msg: String },
IndexOutOfRange {
line: usize,
idx: i64,
vertex_count: usize,
},
NotTriangular { line: usize, face_verts: usize },
}
impl fmt::Display for ObjError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "IO 错误: {}", e),
Self::Parse { line, msg } => write!(f, "第 {} 行解析错误: {}", line, msg),
Self::IndexOutOfRange {
line,
idx,
vertex_count,
} => write!(
f,
"第 {} 行索引 {} 越界(当前顶点数 {})",
line, idx, vertex_count
),
Self::NotTriangular { line, face_verts } => {
write!(f, "第 {} 行面顶点数 {} ≠ 3,仅支持三角面", line, face_verts)
}
}
}
}
impl std::error::Error for ObjError {}
impl From<std::io::Error> for ObjError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
pub fn build_mesh_from_vertices_and_faces(
vertices: &[[f64; 3]],
faces: &[[u32; 3]],
) -> MeshStorage {
let mut mesh = MeshStorage::new();
mesh.reserve(vertices.len(), faces.len() * 6, faces.len());
let v_ids: Vec<VertexId> = vertices
.iter()
.map(|p| mesh.add_vertex(Vertex::new(*p)))
.collect();
let mut edge_map: HashMap<(u32, u32), HalfEdgeId> = HashMap::new();
for face_idx in faces {
let [i0, i1, i2] = *face_idx;
let v0 = v_ids[i0 as usize];
let v1 = v_ids[i1 as usize];
let v2 = v_ids[i2 as usize];
let h0 = mesh.add_halfedge(HalfEdge::new(v1)); let h1 = mesh.add_halfedge(HalfEdge::new(v2)); let h2 = mesh.add_halfedge(HalfEdge::new(v0));
let f_id = mesh.add_face(Face::new());
for (he, next, prev) in [(h0, h1, h2), (h1, h2, h0), (h2, h0, h1)] {
let h = mesh.get_halfedge_mut(he).unwrap();
h.next = Some(next);
h.prev = Some(prev);
h.face = Some(f_id);
}
mesh.get_face_mut(f_id).unwrap().halfedge = Some(h0);
edge_map.insert((i0, i1), h0);
edge_map.insert((i1, i2), h1);
edge_map.insert((i2, i0), h2);
if mesh.get_vertex(v0).unwrap().halfedge.is_none() {
mesh.get_vertex_mut(v0).unwrap().halfedge = Some(h0);
}
if mesh.get_vertex(v1).unwrap().halfedge.is_none() {
mesh.get_vertex_mut(v1).unwrap().halfedge = Some(h1);
}
if mesh.get_vertex(v2).unwrap().halfedge.is_none() {
mesh.get_vertex_mut(v2).unwrap().halfedge = Some(h2);
}
}
let directed_edges: Vec<(u32, u32, HalfEdgeId)> =
edge_map.iter().map(|((a, b), h)| (*a, *b, *h)).collect();
let mut boundary_twins: Vec<HalfEdgeId> = Vec::new();
for (a, b, h) in &directed_edges {
if mesh.get_halfedge(*h).unwrap().twin.is_some() {
continue;
}
if let Some(reverse_h) = edge_map.get(&(*b, *a)) {
mesh.get_halfedge_mut(*h).unwrap().twin = Some(*reverse_h);
mesh.get_halfedge_mut(*reverse_h).unwrap().twin = Some(*h);
} else {
let origin_v = v_ids[*a as usize]; let twin_id = mesh.add_halfedge(HalfEdge::new(origin_v));
mesh.get_halfedge_mut(*h).unwrap().twin = Some(twin_id);
mesh.get_halfedge_mut(twin_id).unwrap().twin = Some(*h);
boundary_twins.push(twin_id);
}
}
for bh in &boundary_twins {
let mut cur = mesh.get_halfedge(*bh).unwrap().twin.unwrap();
let max_iter = mesh.halfedge_count() + 1;
let mut next_bh = None;
for _ in 0..max_iter {
let prev = match mesh.get_halfedge(cur).and_then(|h| h.prev) {
Some(p) => p,
None => break,
};
let prev_twin = match mesh.get_halfedge(prev).and_then(|h| h.twin) {
Some(t) => t,
None => break,
};
if mesh
.get_halfedge(prev_twin)
.map(|h| h.face.is_none())
.unwrap_or(false)
{
next_bh = Some(prev_twin);
break;
}
cur = prev_twin;
}
if let Some(n) = next_bh {
mesh.get_halfedge_mut(*bh).unwrap().next = Some(n);
}
let mut cur = *bh;
let mut prev_bh = None;
for _ in 0..max_iter {
let twin = match mesh.get_halfedge(cur).and_then(|h| h.twin) {
Some(t) => t,
None => break,
};
let twin_next = match mesh.get_halfedge(twin).and_then(|h| h.next) {
Some(n) => n,
None => break,
};
let twin_next_twin = match mesh.get_halfedge(twin_next).and_then(|h| h.twin) {
Some(t) => t,
None => break,
};
if mesh
.get_halfedge(twin_next_twin)
.map(|h| h.face.is_none())
.unwrap_or(false)
{
prev_bh = Some(twin_next_twin);
break;
}
cur = twin_next;
}
if let Some(p) = prev_bh {
mesh.get_halfedge_mut(*bh).unwrap().prev = Some(p);
}
}
mesh
}
pub fn build_mesh_from_polygons(vertices: &[[f64; 3]], faces: &[Vec<u32>]) -> MeshStorage {
let mut mesh = MeshStorage::new();
let total_he: usize = faces
.iter()
.map(|f| f.len())
.sum::<usize>()
.saturating_mul(2);
mesh.reserve(vertices.len(), total_he, faces.len());
let v_ids: Vec<VertexId> = vertices
.iter()
.map(|p| mesh.add_vertex(Vertex::new(*p)))
.collect();
let mut edge_map: HashMap<(u32, u32), HalfEdgeId> = HashMap::new();
for face_idx in faces {
let k = face_idx.len();
if k < 3 {
continue; }
let mut he_ids: Vec<HalfEdgeId> = Vec::with_capacity(k);
for i in 0..k {
let v_from = v_ids[face_idx[i] as usize];
let v_to = v_ids[face_idx[(i + 1) % k] as usize];
let h = mesh.add_halfedge(HalfEdge::new(v_to)); he_ids.push(h);
if mesh.get_vertex(v_from).unwrap().halfedge.is_none() {
mesh.get_vertex_mut(v_from).unwrap().halfedge = Some(h);
}
}
let f_id = mesh.add_face(Face::new());
for i in 0..k {
let next = he_ids[(i + 1) % k];
let prev = he_ids[(i + k - 1) % k];
let h = mesh.get_halfedge_mut(he_ids[i]).unwrap();
h.next = Some(next);
h.prev = Some(prev);
h.face = Some(f_id);
}
mesh.get_face_mut(f_id).unwrap().halfedge = Some(he_ids[0]);
for i in 0..k {
let a = face_idx[i];
let b = face_idx[(i + 1) % k];
edge_map.insert((a, b), he_ids[i]);
}
}
let directed_edges: Vec<(u32, u32, HalfEdgeId)> =
edge_map.iter().map(|((a, b), h)| (*a, *b, *h)).collect();
let mut boundary_twins: Vec<HalfEdgeId> = Vec::new();
for (a, b, h) in &directed_edges {
if mesh.get_halfedge(*h).unwrap().twin.is_some() {
continue;
}
if let Some(reverse_h) = edge_map.get(&(*b, *a)) {
mesh.get_halfedge_mut(*h).unwrap().twin = Some(*reverse_h);
mesh.get_halfedge_mut(*reverse_h).unwrap().twin = Some(*h);
} else {
let origin_v = v_ids[*a as usize];
let twin_id = mesh.add_halfedge(HalfEdge::new(origin_v));
mesh.get_halfedge_mut(*h).unwrap().twin = Some(twin_id);
mesh.get_halfedge_mut(twin_id).unwrap().twin = Some(*h);
boundary_twins.push(twin_id);
}
}
for bh in &boundary_twins {
let mut cur = mesh.get_halfedge(*bh).unwrap().twin.unwrap();
let max_iter = mesh.halfedge_count() + 1;
let mut next_bh = None;
for _ in 0..max_iter {
let prev = match mesh.get_halfedge(cur).and_then(|h| h.prev) {
Some(p) => p,
None => break,
};
let prev_twin = match mesh.get_halfedge(prev).and_then(|h| h.twin) {
Some(t) => t,
None => break,
};
if mesh
.get_halfedge(prev_twin)
.map(|h| h.face.is_none())
.unwrap_or(false)
{
next_bh = Some(prev_twin);
break;
}
cur = prev_twin;
}
if let Some(n) = next_bh {
mesh.get_halfedge_mut(*bh).unwrap().next = Some(n);
}
let mut cur = *bh;
let mut prev_bh = None;
for _ in 0..max_iter {
let twin = match mesh.get_halfedge(cur).and_then(|h| h.twin) {
Some(t) => t,
None => break,
};
let twin_next = match mesh.get_halfedge(twin).and_then(|h| h.next) {
Some(n) => n,
None => break,
};
let twin_next_twin = match mesh.get_halfedge(twin_next).and_then(|h| h.twin) {
Some(t) => t,
None => break,
};
if mesh
.get_halfedge(twin_next_twin)
.map(|h| h.face.is_none())
.unwrap_or(false)
{
prev_bh = Some(twin_next_twin);
break;
}
cur = twin_next;
}
if let Some(p) = prev_bh {
mesh.get_halfedge_mut(*bh).unwrap().prev = Some(p);
}
}
mesh
}
pub fn load_obj<P: AsRef<Path>>(path: P) -> Result<MeshStorage, ObjError> {
let text = fs::read_to_string(path)?;
parse_obj(&text)
}
pub fn parse_obj(text: &str) -> Result<MeshStorage, ObjError> {
let mut vertices: Vec<[f64; 3]> = Vec::new();
let mut faces: Vec<Vec<u32>> = Vec::new();
for (line_no, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut tokens = line.split_whitespace();
let kind = match tokens.next() {
Some(k) => k,
None => continue,
};
match kind {
"v" => {
let coords: Vec<f64> = tokens
.take(3)
.map(|t| {
t.parse::<f64>().map_err(|_| ObjError::Parse {
line: line_no + 1,
msg: format!("无法解析顶点坐标: {}", t),
})
})
.collect::<Result<Vec<_>, _>>()?;
if coords.len() != 3 {
return Err(ObjError::Parse {
line: line_no + 1,
msg: "顶点行缺少坐标分量".into(),
});
}
vertices.push([coords[0], coords[1], coords[2]]);
}
"f" => {
let verts: Vec<i64> = tokens
.map(|t| {
let v_part = t.split('/').next().unwrap_or(t);
v_part.parse::<i64>().map_err(|_| ObjError::Parse {
line: line_no + 1,
msg: format!("无法解析面索引: {}", t),
})
})
.collect::<Result<Vec<_>, _>>()?;
if verts.len() < 3 {
return Err(ObjError::NotTriangular {
line: line_no + 1,
face_verts: verts.len(),
});
}
let to_zero = |i: i64| -> Result<u32, ObjError> {
let zero_based = if i > 0 {
(i - 1) as usize
} else if i < 0 {
let n = vertices.len() as i64;
if n + i < 0 {
return Err(ObjError::IndexOutOfRange {
line: line_no + 1,
idx: i,
vertex_count: vertices.len(),
});
}
(n + i) as usize
} else {
return Err(ObjError::Parse {
line: line_no + 1,
msg: "面索引不能为 0".into(),
});
};
if zero_based >= vertices.len() {
return Err(ObjError::IndexOutOfRange {
line: line_no + 1,
idx: i,
vertex_count: vertices.len(),
});
}
Ok(zero_based as u32)
};
let indices: Vec<u32> = verts
.iter()
.map(|&i| to_zero(i))
.collect::<Result<_, _>>()?;
faces.push(indices);
}
_ => {
}
}
}
Ok(build_mesh_from_polygons(&vertices, &faces))
}
pub fn save_obj<P: AsRef<Path>>(mesh: &MeshStorage, path: P) -> Result<(), ObjError> {
let text = format_obj(mesh);
fs::write(path, text)?;
Ok(())
}
pub fn format_obj(mesh: &MeshStorage) -> String {
let mut v_index: HashMap<VertexId, u32> = HashMap::new();
let mut out = String::new();
for (next_idx, v_id) in (1u32..).zip(mesh.vertex_ids()) {
v_index.insert(v_id, next_idx);
let p = mesh.get_vertex(v_id).unwrap().position;
out.push_str(&format!("v {:.6} {:.6} {:.6}\n", p[0], p[1], p[2]));
}
for f_id in mesh.face_ids() {
let verts: Vec<u32> = FaceHalfEdges::new(mesh, f_id)
.filter_map(|he| mesh.get_halfedge(he))
.map(|h| h.vertex)
.filter_map(|v| v_index.get(&v).copied())
.collect();
if verts.len() < 3 {
continue; }
out.push('f');
for v in &verts {
out.push(' ');
out.push_str(&v.to_string());
}
out.push('\n');
}
out
}
#[derive(Debug)]
pub enum PlyError {
Io(std::io::Error),
Parse { line: usize, msg: String },
Unsupported(String),
}
impl fmt::Display for PlyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "IO error: {e}"),
Self::Parse { line, msg } => write!(f, "PLY parse error at line {line}: {msg}"),
Self::Unsupported(s) => write!(f, "Unsupported PLY feature: {s}"),
}
}
}
impl std::error::Error for PlyError {}
impl From<std::io::Error> for PlyError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
pub fn load_ply<P: AsRef<Path>>(path: P) -> Result<MeshStorage, PlyError> {
let text = fs::read_to_string(path)?;
parse_ply(&text)
}
pub fn parse_ply(text: &str) -> Result<MeshStorage, PlyError> {
let mut lines = text.lines().enumerate();
let mut vertex_count: usize = 0;
let mut _face_count: usize = 0;
let mut in_header = true;
let mut vertices: Vec<[f64; 3]> = Vec::new();
let mut faces: Vec<Vec<u32>> = Vec::new();
for (line_no, raw) in &mut lines {
let line = raw.trim();
if line.is_empty() || (in_header && line.starts_with("comment")) {
continue;
}
if in_header {
if line == "end_header" {
in_header = false;
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 && parts[0] == "element" {
if parts[1] == "vertex" {
vertex_count = parts[2].parse().map_err(|_| PlyError::Parse {
line: line_no + 1,
msg: "invalid vertex count".into(),
})?;
} else if parts[1] == "face" {
_face_count = parts[2].parse().map_err(|_| PlyError::Parse {
line: line_no + 1,
msg: "invalid face count".into(),
})?;
}
}
} else {
if vertices.len() < vertex_count {
let coords: Vec<f64> = line
.split_whitespace()
.take(3)
.map(|s| {
s.parse::<f64>().map_err(|_| PlyError::Parse {
line: line_no + 1,
msg: format!("invalid vertex coordinate: {s}"),
})
})
.collect::<Result<_, _>>()?;
if coords.len() >= 3 {
vertices.push([coords[0], coords[1], coords[2]]);
}
} else {
let indices: Vec<u32> = line
.split_whitespace()
.skip(1) .map(|s| {
s.parse::<u32>().map_err(|_| PlyError::Parse {
line: line_no + 1,
msg: format!("invalid face index: {s}"),
})
})
.collect::<Result<_, _>>()?;
if indices.len() >= 3 {
faces.push(indices);
}
}
}
}
Ok(build_mesh_from_polygons(&vertices, &faces))
}
pub fn format_ply(mesh: &MeshStorage) -> String {
let v_ids: Vec<VertexId> = mesh.vertex_ids().collect();
let f_ids: Vec<FaceId> = mesh.face_ids().collect();
let mut index_map: std::collections::HashMap<VertexId, usize> =
std::collections::HashMap::new();
for (i, &v) in v_ids.iter().enumerate() {
index_map.insert(v, i);
}
let mut out = String::new();
out.push_str("ply\n");
out.push_str("format ascii 1.0\n");
out.push_str(&format!("element vertex {}\n", v_ids.len()));
out.push_str("property float x\n");
out.push_str("property float y\n");
out.push_str("property float z\n");
out.push_str(&format!("element face {}\n", f_ids.len()));
out.push_str("property list uchar int vertex_indices\n");
out.push_str("end_header\n");
for &v in &v_ids {
let p = mesh.get_vertex(v).unwrap().position;
out.push_str(&format!("{:.6} {:.6} {:.6}\n", p[0], p[1], p[2]));
}
for f in &f_ids {
let verts: Vec<usize> = FaceVertices::new(mesh, *f)
.filter_map(|v| index_map.get(&v).copied())
.collect();
if verts.len() < 3 {
continue;
}
out.push_str(&verts.len().to_string());
for vi in &verts {
out.push(' ');
out.push_str(&vi.to_string());
}
out.push('\n');
}
out
}
pub fn save_ply<P: AsRef<Path>>(mesh: &MeshStorage, path: P) -> Result<(), PlyError> {
let text = format_ply(mesh);
fs::write(path, text)?;
Ok(())
}
#[derive(Debug)]
pub enum StlError {
Io(std::io::Error),
Parse {
line: usize,
msg: String,
},
BadBinarySize {
actual: usize,
expected: usize,
},
NotTriangular {
face_verts: usize,
},
}
impl fmt::Display for StlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "IO error: {e}"),
Self::Parse { line, msg } => write!(f, "STL parse error at line {line}: {msg}"),
Self::BadBinarySize { actual, expected } => write!(
f,
"STL binary size mismatch: actual {actual} bytes, expected {expected} bytes"
),
Self::NotTriangular { face_verts } => {
write!(f, "STL face vertex count {face_verts} ≠ 3")
}
}
}
}
impl std::error::Error for StlError {}
impl From<std::io::Error> for StlError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
pub fn load_stl<P: AsRef<Path>>(path: P) -> Result<MeshStorage, StlError> {
let bytes = fs::read(path)?;
parse_stl_bytes(&bytes)
}
pub fn parse_stl_bytes(bytes: &[u8]) -> Result<MeshStorage, StlError> {
if bytes.len() >= 84 {
let n = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]) as usize;
let expected = 84 + 50 * n;
if bytes.len() == expected {
return parse_stl_binary(bytes, n);
}
}
let text = std::str::from_utf8(bytes).map_err(|_| StlError::Parse {
line: 0,
msg: "file is not valid UTF-8".into(),
})?;
parse_stl_ascii(text)
}
pub fn parse_stl_ascii(text: &str) -> Result<MeshStorage, StlError> {
use std::collections::HashMap;
let mut vertices: Vec<[f64; 3]> = Vec::new();
let mut faces: Vec<[u32; 3]> = Vec::new();
let mut dedup: HashMap<[u64; 3], u32> = HashMap::new();
let mut current_face: Vec<u32> = Vec::with_capacity(3);
let mut in_facet = false;
for (i, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.is_empty() {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
match parts[0] {
"facet" => {
in_facet = true;
current_face.clear();
}
"vertex" if in_facet => {
if parts.len() < 4 {
return Err(StlError::Parse {
line: i + 1,
msg: "vertex line needs 3 coordinates".into(),
});
}
let coords: [f64; 3] = [
parts[1].parse().map_err(|_| StlError::Parse {
line: i + 1,
msg: format!("invalid x: {}", parts[1]),
})?,
parts[2].parse().map_err(|_| StlError::Parse {
line: i + 1,
msg: format!("invalid y: {}", parts[2]),
})?,
parts[3].parse().map_err(|_| StlError::Parse {
line: i + 1,
msg: format!("invalid z: {}", parts[3]),
})?,
];
let key = [
coords[0].to_bits(),
coords[1].to_bits(),
coords[2].to_bits(),
];
let idx = *dedup.entry(key).or_insert_with(|| {
let k = vertices.len() as u32;
vertices.push(coords);
k
});
current_face.push(idx);
}
"endfacet" => {
if current_face.len() != 3 {
return Err(StlError::NotTriangular {
face_verts: current_face.len(),
});
}
let [a, b, c] = [current_face[0], current_face[1], current_face[2]];
faces.push([a, b, c]);
in_facet = false;
}
_ => {}
}
}
Ok(build_mesh_from_vertices_and_faces(&vertices, &faces))
}
pub fn parse_stl_binary(bytes: &[u8], n_faces: usize) -> Result<MeshStorage, StlError> {
use std::collections::HashMap;
let expected = 84 + 50 * n_faces;
if bytes.len() != expected {
return Err(StlError::BadBinarySize {
actual: bytes.len(),
expected,
});
}
let mut vertices: Vec<[f64; 3]> = Vec::with_capacity(n_faces * 3);
let mut faces: Vec<[u32; 3]> = Vec::with_capacity(n_faces);
let mut dedup: HashMap<[u32; 3], u32> = HashMap::new();
for i in 0..n_faces {
let base = 84 + i * 50;
let mut tri = [0u32; 3];
for (j, tri_j) in tri.iter_mut().enumerate() {
let off = base + 12 + j * 12;
let x =
f32::from_le_bytes([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]]);
let y = f32::from_le_bytes([
bytes[off + 4],
bytes[off + 5],
bytes[off + 6],
bytes[off + 7],
]);
let z = f32::from_le_bytes([
bytes[off + 8],
bytes[off + 9],
bytes[off + 10],
bytes[off + 11],
]);
let key = [x.to_bits(), y.to_bits(), z.to_bits()];
let idx = *dedup.entry(key).or_insert_with(|| {
let k = vertices.len() as u32;
vertices.push([x as f64, y as f64, z as f64]);
k
});
*tri_j = idx;
}
faces.push(tri);
}
Ok(build_mesh_from_vertices_and_faces(&vertices, &faces))
}
pub fn save_stl_ascii<P: AsRef<Path>>(mesh: &MeshStorage, path: P) -> Result<(), StlError> {
let text = format_stl_ascii(mesh);
fs::write(path, text)?;
Ok(())
}
pub fn format_stl_ascii(mesh: &MeshStorage) -> String {
use crate::geometry::face_normal;
let mut out = String::with_capacity(mesh.face_count() * 80);
out.push_str("solid halfedge\n");
for f in mesh.face_ids() {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
continue;
}
let p0 = mesh
.get_vertex(verts[0])
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let p1 = mesh
.get_vertex(verts[1])
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let p2 = mesh
.get_vertex(verts[2])
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let n = face_normal(mesh, f).unwrap_or([0.0, 0.0, 1.0]);
out.push_str(&format!(
" facet normal {:.6} {:.6} {:.6}\n outer loop\n",
n[0], n[1], n[2]
));
for p in [p0, p1, p2] {
out.push_str(&format!(
" vertex {:.6} {:.6} {:.6}\n",
p[0], p[1], p[2]
));
}
out.push_str(" endloop\n endfacet\n");
}
out.push_str("endsolid halfedge\n");
out
}
pub fn save_stl_binary<P: AsRef<Path>>(mesh: &MeshStorage, path: P) -> Result<(), StlError> {
let bytes = format_stl_binary(mesh);
fs::write(path, bytes)?;
Ok(())
}
pub fn format_stl_binary(mesh: &MeshStorage) -> Vec<u8> {
use crate::geometry::face_normal;
let mut out: Vec<u8> = Vec::with_capacity(84 + mesh.face_count() * 50);
out.extend_from_slice(b"halfedge binary stl".as_slice());
while out.len() < 80 {
out.push(0);
}
let n = mesh.face_count() as u32;
out.extend_from_slice(&n.to_le_bytes());
for f in mesh.face_ids() {
let verts: Vec<VertexId> = crate::traversal::FaceVertices::new(mesh, f).collect();
if verts.len() != 3 {
out.extend_from_slice(&[0u8; 50]);
continue;
}
let p0 = mesh
.get_vertex(verts[0])
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let p1 = mesh
.get_vertex(verts[1])
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let p2 = mesh
.get_vertex(verts[2])
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let n = face_normal(mesh, f).unwrap_or([0.0, 0.0, 1.0]);
for c in &n {
out.extend_from_slice(&(*c as f32).to_le_bytes());
}
for p in [p0, p1, p2] {
for c in &p {
out.extend_from_slice(&(*c as f32).to_le_bytes());
}
}
out.extend_from_slice(&[0u8, 0u8]);
}
out
}
#[derive(Debug)]
pub enum MeshError {
Obj(ObjError),
Ply(PlyError),
Stl(StlError),
UnsupportedFormat(String),
}
impl fmt::Display for MeshError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Obj(e) => write!(f, "OBJ error: {e}"),
Self::Ply(e) => write!(f, "PLY error: {e}"),
Self::Stl(e) => write!(f, "STL error: {e}"),
Self::UnsupportedFormat(s) => write!(f, "unsupported format: {s}"),
}
}
}
impl std::error::Error for MeshError {}
impl From<ObjError> for MeshError {
fn from(e: ObjError) -> Self {
Self::Obj(e)
}
}
impl From<PlyError> for MeshError {
fn from(e: PlyError) -> Self {
Self::Ply(e)
}
}
impl From<StlError> for MeshError {
fn from(e: StlError) -> Self {
Self::Stl(e)
}
}
pub fn load_mesh<P: AsRef<Path>>(path: P) -> Result<MeshStorage, MeshError> {
let path = path.as_ref();
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.unwrap_or_default();
match ext.as_str() {
"obj" => Ok(load_obj(path)?),
"ply" => Ok(load_ply(path)?),
"stl" => Ok(load_stl(path)?),
other => Err(MeshError::UnsupportedFormat(other.into())),
}
}
pub fn save_mesh<P: AsRef<Path>>(mesh: &MeshStorage, path: P) -> Result<(), MeshError> {
let path = path.as_ref();
let ext = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_lowercase())
.unwrap_or_default();
match ext.as_str() {
"obj" => {
save_obj(mesh, path)?;
Ok(())
}
"ply" => {
save_ply(mesh, path)?;
Ok(())
}
"stl" => {
save_stl_ascii(mesh, path)?;
Ok(())
}
other => Err(MeshError::UnsupportedFormat(other.into())),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::validate::check_topology;
fn make_quad_data() -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
let vertices = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
];
let faces = vec![[0, 1, 2], [0, 2, 3]];
(vertices, faces)
}
#[test]
fn build_mesh_basic_quad() {
let (verts, faces) = make_quad_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
assert_eq!(mesh.vertex_count(), 4);
assert_eq!(mesh.face_count(), 2);
assert_eq!(mesh.halfedge_count(), 10);
}
#[test]
fn build_mesh_passes_full_validation() {
let (verts, faces) = make_quad_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
assert!(
check_topology(&mesh).is_ok(),
"构建的网格应通过完整校验: {:?}",
check_topology(&mesh)
);
}
#[test]
fn build_mesh_closed_tetrahedron() {
let vertices = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
let faces = vec![
[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2], ];
let mesh = build_mesh_from_vertices_and_faces(&vertices, &faces);
assert_eq!(mesh.vertex_count(), 4);
assert_eq!(mesh.face_count(), 4);
assert_eq!(mesh.halfedge_count(), 12);
assert!(check_topology(&mesh).is_ok());
}
#[test]
fn obj_roundtrip_quad() {
let (verts, faces) = make_quad_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
let text = format_obj(&mesh);
let mesh2 = parse_obj(&text).unwrap();
assert_eq!(mesh2.vertex_count(), mesh.vertex_count());
assert_eq!(mesh2.face_count(), mesh.face_count());
assert_eq!(mesh2.halfedge_count(), mesh.halfedge_count());
assert!(check_topology(&mesh2).is_ok());
}
#[test]
fn obj_roundtrip_tetrahedron() {
let vertices = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
let faces = vec![[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]];
let mesh = build_mesh_from_vertices_and_faces(&vertices, &faces);
let text = format_obj(&mesh);
let mesh2 = parse_obj(&text).unwrap();
assert_eq!(mesh2.vertex_count(), 4);
assert_eq!(mesh2.face_count(), 4);
assert_eq!(mesh2.halfedge_count(), 12);
assert!(check_topology(&mesh2).is_ok());
}
#[test]
fn obj_parse_skips_comments_and_other_lines() {
let text = r#"
# 这是一个测试 OBJ
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 0.0 1.0 0.0
vt 0.0 0.0
vn 0.0 0.0 1.0
f 1 2 3
g mesh
usemtl default
"#;
let mesh = parse_obj(text).unwrap();
assert_eq!(mesh.vertex_count(), 3);
assert_eq!(mesh.face_count(), 1);
assert_eq!(mesh.halfedge_count(), 6); }
#[test]
fn obj_parse_supports_v_vt_vn_format() {
let text = r#"
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 0.0 1.0 0.0
f 1/1/1 2/2/1 3/3/1
"#;
let mesh = parse_obj(text).unwrap();
assert_eq!(mesh.vertex_count(), 3);
assert_eq!(mesh.face_count(), 1);
}
#[test]
fn obj_parse_negative_indices() {
let text = r#"
v 0.0 0.0 0.0
v 1.0 0.0 0.0
v 0.0 1.0 0.0
f -3 -2 -1
"#;
let mesh = parse_obj(text).unwrap();
assert_eq!(mesh.face_count(), 1);
assert!(check_topology(&mesh).is_ok());
}
#[test]
fn obj_parse_quadrilateral_face_succeeds() {
let text = r#"
v 0 0 0
v 1 0 0
v 1 1 0
v 0 1 0
f 1 2 3 4
"#;
let mesh = parse_obj(text).expect("四边形面 OBJ 解析应成功");
assert_eq!(mesh.vertex_count(), 4);
assert_eq!(mesh.face_count(), 1);
}
#[test]
fn obj_parse_out_of_range_index_fails() {
let text = r#"
v 0 0 0
v 1 0 0
v 0 1 0
f 1 2 5
"#;
let result = parse_obj(text);
match result {
Err(ObjError::IndexOutOfRange { idx, .. }) => assert_eq!(idx, 5),
other => panic!("期望 IndexOutOfRange 错误,实际: {:?}", other),
}
}
#[test]
fn obj_save_load_file_roundtrip() {
let (verts, faces) = make_quad_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
let path = std::env::temp_dir().join("halfedge_test_quad.obj");
save_obj(&mesh, &path).unwrap();
let loaded = load_obj(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(loaded.vertex_count(), mesh.vertex_count());
assert_eq!(loaded.face_count(), mesh.face_count());
assert_eq!(loaded.halfedge_count(), mesh.halfedge_count());
assert!(check_topology(&loaded).is_ok());
}
#[test]
fn auto_detect_obj() {
let mesh = crate::test_util::build_icosphere(0);
let path = std::env::temp_dir().join("halfedge_autodetect.obj");
save_mesh(&mesh, &path).unwrap();
let loaded = load_mesh(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(loaded.face_count(), mesh.face_count());
}
#[test]
fn auto_detect_ply() {
let mesh = crate::test_util::build_icosphere(0);
let path = std::env::temp_dir().join("halfedge_autodetect.ply");
save_mesh(&mesh, &path).unwrap();
let loaded = load_mesh(&path).unwrap();
let _ = std::fs::remove_file(&path);
assert_eq!(loaded.face_count(), mesh.face_count());
}
#[test]
fn auto_detect_unsupported() {
let path = std::env::temp_dir().join("halfedge_autodetect.off");
let err = load_mesh(&path).unwrap_err();
assert!(matches!(err, MeshError::UnsupportedFormat(_)));
}
fn make_tetra_data() -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
let vertices = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
];
let faces = vec![
[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3], ];
(vertices, faces)
}
#[test]
fn stl_ascii_roundtrip() {
let (verts, faces) = make_tetra_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
let text = format_stl_ascii(&mesh);
let parsed = parse_stl_ascii(&text).expect("ASCII STL 往返解析失败");
assert_eq!(parsed.vertex_count(), mesh.vertex_count());
assert_eq!(parsed.face_count(), mesh.face_count());
}
#[test]
fn stl_binary_roundtrip() {
let (verts, faces) = make_tetra_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
let bytes = format_stl_binary(&mesh);
assert_eq!(bytes.len(), 84 + 50 * 4);
let parsed = parse_stl_bytes(&bytes).expect("二进制 STL 往返解析失败");
assert_eq!(parsed.vertex_count(), mesh.vertex_count());
assert_eq!(parsed.face_count(), mesh.face_count());
}
#[test]
fn stl_ascii_parses_minimum_solid() {
let text = "solid x\n\
facet normal 0 0 1\n\
outer loop\n\
vertex 0 0 0\n\
vertex 1 0 0\n\
vertex 0 1 0\n\
endloop\n\
endfacet\n\
endsolid x\n";
let mesh = parse_stl_ascii(text).expect("最小 ASCII STL 解析失败");
assert_eq!(mesh.vertex_count(), 3);
assert_eq!(mesh.face_count(), 1);
}
#[test]
fn stl_ascii_rejects_non_triangular() {
let text = "solid x\n\
facet normal 0 0 1\n\
outer loop\n\
vertex 0 0 0\n\
vertex 1 0 0\n\
vertex 1 1 0\n\
vertex 0 1 0\n\
endloop\n\
endfacet\n\
endsolid x\n";
let err = parse_stl_ascii(text).unwrap_err();
assert!(matches!(err, StlError::NotTriangular { .. }));
}
#[test]
fn stl_binary_detects_size_mismatch() {
let mut bytes = vec![0u8; 84];
bytes.extend_from_slice(&3u32.to_le_bytes()); bytes.extend_from_slice(&[0u8; 50]); let err = parse_stl_binary(&bytes, 3).unwrap_err();
assert!(matches!(err, StlError::BadBinarySize { .. }));
}
#[test]
fn stl_file_roundtrip_ascii() {
let (verts, faces) = make_tetra_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
let path = std::env::temp_dir().join("halfedge_stl_ascii.stl");
save_stl_ascii(&mesh, &path).expect("保存 STL 文件失败");
let loaded = load_stl(&path).expect("加载 STL 文件失败");
let _ = std::fs::remove_file(&path);
assert_eq!(loaded.vertex_count(), mesh.vertex_count());
assert_eq!(loaded.face_count(), mesh.face_count());
}
#[test]
fn stl_file_roundtrip_binary() {
let (verts, faces) = make_tetra_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
let path = std::env::temp_dir().join("halfedge_stl_bin.stl");
save_stl_binary(&mesh, &path).expect("保存二进制 STL 失败");
let loaded = load_stl(&path).expect("加载二进制 STL 失败");
let _ = std::fs::remove_file(&path);
assert_eq!(loaded.vertex_count(), mesh.vertex_count());
assert_eq!(loaded.face_count(), mesh.face_count());
}
#[test]
fn auto_detect_stl() {
let (verts, faces) = make_tetra_data();
let mesh = build_mesh_from_vertices_and_faces(&verts, &faces);
let path = std::env::temp_dir().join("halfedge_autodetect.stl");
save_mesh(&mesh, &path).expect("save_mesh(.stl) 失败");
let loaded = load_mesh(&path).expect("load_mesh(.stl) 失败");
let _ = std::fs::remove_file(&path);
assert_eq!(loaded.face_count(), mesh.face_count());
}
}