#![allow(dead_code)]
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum StlMode {
Ascii,
Binary,
}
#[derive(Debug, Clone)]
pub struct StlExportConfig {
pub mode: StlMode,
pub solid_name: String,
pub precision: usize,
}
impl Default for StlExportConfig {
fn default() -> Self {
Self {
mode: StlMode::Ascii,
solid_name: "oxihuman".to_string(),
precision: 6,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct StlTriangle {
pub normal: [f32; 3],
pub v0: [f32; 3],
pub v1: [f32; 3],
pub v2: [f32; 3],
pub attribute: u16,
}
pub type StlValidationResult = Result<(), String>;
#[allow(dead_code)]
pub fn default_stl_config() -> StlExportConfig {
StlExportConfig::default()
}
#[allow(dead_code)]
pub fn export_to_stl_ascii(triangles: &[StlTriangle], cfg: &StlExportConfig) -> String {
let name = stl_solid_name(cfg);
let mut out = format!("solid {}\n", name);
for tri in triangles {
let n = tri.normal;
out.push_str(&format!(
" facet normal {:.prec$} {:.prec$} {:.prec$}\n",
n[0],
n[1],
n[2],
prec = cfg.precision
));
out.push_str(" outer loop\n");
for v in &[tri.v0, tri.v1, tri.v2] {
out.push_str(&stl_ascii_line(v[0], v[1], v[2], cfg.precision));
}
out.push_str(" endloop\n");
out.push_str(" endfacet\n");
}
out.push_str(&format!("endsolid {}\n", name));
out
}
#[allow(dead_code)]
pub fn export_to_stl_binary(triangles: &[StlTriangle], cfg: &StlExportConfig) -> Vec<u8> {
let mut bytes: Vec<u8> = Vec::with_capacity(stl_file_size(triangles.len()));
let header = stl_header_bytes(&cfg.solid_name);
bytes.extend_from_slice(&header);
let count = triangles.len() as u32;
bytes.extend_from_slice(&count.to_le_bytes());
for tri in triangles {
for &val in tri.normal.iter().chain(tri.v0.iter()).chain(tri.v1.iter()).chain(tri.v2.iter()) {
bytes.extend_from_slice(&val.to_le_bytes());
}
bytes.extend_from_slice(&tri.attribute.to_le_bytes());
}
bytes
}
#[allow(dead_code)]
pub fn stl_triangle_from_positions(v0: [f32; 3], v1: [f32; 3], v2: [f32; 3]) -> StlTriangle {
let normal = stl_normal_from_vertices(v0, v1, v2);
StlTriangle { normal, v0, v1, v2, attribute: 0 }
}
#[allow(dead_code)]
pub fn stl_header_bytes(solid_name: &str) -> [u8; 80] {
let mut header = [0u8; 80];
let prefix = format!("OxiHuman STL: {}", solid_name);
let bytes = prefix.as_bytes();
let len = bytes.len().min(80);
header[..len].copy_from_slice(&bytes[..len]);
header
}
#[allow(dead_code)]
pub fn stl_face_count(triangles: &[StlTriangle]) -> usize {
triangles.len()
}
#[allow(dead_code)]
pub fn stl_file_size(n: usize) -> usize {
84 + n * 50
}
#[allow(dead_code)]
pub fn validate_stl_triangles(triangles: &[StlTriangle]) -> StlValidationResult {
if triangles.is_empty() {
return Err("triangle list is empty".to_string());
}
for (i, tri) in triangles.iter().enumerate() {
let n = tri.normal;
let len_sq = n[0] * n[0] + n[1] * n[1] + n[2] * n[2];
if len_sq < 1e-10 {
return Err(format!("triangle {} has a zero-length normal", i));
}
}
Ok(())
}
#[allow(dead_code)]
pub fn stl_normal_from_vertices(v0: [f32; 3], v1: [f32; 3], v2: [f32; 3]) -> [f32; 3] {
let ab = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
let ac = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
let cross = [
ab[1] * ac[2] - ab[2] * ac[1],
ab[2] * ac[0] - ab[0] * ac[2],
ab[0] * ac[1] - ab[1] * ac[0],
];
let len = (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt();
if len < 1e-10 {
return [0.0, 0.0, 0.0];
}
[cross[0] / len, cross[1] / len, cross[2] / len]
}
#[allow(dead_code)]
pub fn stl_ascii_line(x: f32, y: f32, z: f32, precision: usize) -> String {
format!(" vertex {:.prec$} {:.prec$} {:.prec$}\n", x, y, z, prec = precision)
}
#[allow(dead_code)]
pub fn stl_solid_name(cfg: &StlExportConfig) -> &str {
if cfg.solid_name.is_empty() {
"oxihuman"
} else {
&cfg.solid_name
}
}
#[allow(dead_code)]
pub fn stl_merge_triangles(a: &[StlTriangle], b: &[StlTriangle]) -> Vec<StlTriangle> {
let mut merged = Vec::with_capacity(a.len() + b.len());
merged.extend_from_slice(a);
merged.extend_from_slice(b);
merged
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_triangle() -> StlTriangle {
stl_triangle_from_positions(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
)
}
#[test]
fn test_default_stl_config() {
let cfg = default_stl_config();
assert_eq!(cfg.mode, StlMode::Ascii);
assert_eq!(cfg.solid_name, "oxihuman");
assert_eq!(cfg.precision, 6);
}
#[test]
fn test_stl_normal_from_vertices_z_up() {
let n = stl_normal_from_vertices(
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
);
assert!((n[2] - 1.0).abs() < 1e-5, "expected Z-up normal");
}
#[test]
fn test_stl_triangle_from_positions() {
let tri = sample_triangle();
assert!((tri.normal[2] - 1.0).abs() < 1e-5);
assert_eq!(tri.attribute, 0);
}
#[test]
fn test_export_to_stl_ascii_contains_solid() {
let cfg = default_stl_config();
let tris = vec![sample_triangle()];
let ascii = export_to_stl_ascii(&tris, &cfg);
assert!(ascii.starts_with("solid oxihuman"));
assert!(ascii.ends_with("endsolid oxihuman\n"));
}
#[test]
fn test_export_to_stl_ascii_contains_facet() {
let cfg = default_stl_config();
let tris = vec![sample_triangle()];
let ascii = export_to_stl_ascii(&tris, &cfg);
assert!(ascii.contains("facet normal"));
assert!(ascii.contains("outer loop"));
assert!(ascii.contains("endloop"));
assert!(ascii.contains("endfacet"));
}
#[test]
fn test_export_to_stl_binary_length() {
let cfg = default_stl_config();
let tris = vec![sample_triangle(); 3];
let bytes = export_to_stl_binary(&tris, &cfg);
assert_eq!(bytes.len(), stl_file_size(3));
}
#[test]
fn test_stl_file_size() {
assert_eq!(stl_file_size(0), 84);
assert_eq!(stl_file_size(1), 134);
assert_eq!(stl_file_size(10), 584);
}
#[test]
fn test_stl_face_count() {
let tris = vec![sample_triangle(); 5];
assert_eq!(stl_face_count(&tris), 5);
}
#[test]
fn test_stl_header_bytes_length() {
let hdr = stl_header_bytes("test");
assert_eq!(hdr.len(), 80);
}
#[test]
fn test_stl_header_bytes_content() {
let hdr = stl_header_bytes("mysolid");
let text = std::str::from_utf8(&hdr[..22]).expect("should succeed");
assert!(text.contains("mysolid"));
}
#[test]
fn test_validate_stl_triangles_ok() {
let tris = vec![sample_triangle()];
assert!(validate_stl_triangles(&tris).is_ok());
}
#[test]
fn test_validate_stl_triangles_empty() {
assert!(validate_stl_triangles(&[]).is_err());
}
#[test]
fn test_validate_stl_triangles_zero_normal() {
let bad = StlTriangle {
normal: [0.0, 0.0, 0.0],
v0: [0.0, 0.0, 0.0],
v1: [1.0, 0.0, 0.0],
v2: [0.0, 1.0, 0.0],
attribute: 0,
};
assert!(validate_stl_triangles(&[bad]).is_err());
}
#[test]
fn test_stl_merge_triangles() {
let a = vec![sample_triangle()];
let b = vec![sample_triangle(), sample_triangle()];
let merged = stl_merge_triangles(&a, &b);
assert_eq!(merged.len(), 3);
}
#[test]
fn test_stl_ascii_line_format() {
let line = stl_ascii_line(1.0, 2.0, 3.0, 3);
assert_eq!(line, " vertex 1.000 2.000 3.000\n");
}
#[test]
fn test_stl_solid_name_fallback() {
let mut cfg = default_stl_config();
cfg.solid_name = String::new();
assert_eq!(stl_solid_name(&cfg), "oxihuman");
}
#[test]
fn test_stl_binary_triangle_count_field() {
let tris = vec![sample_triangle(); 7];
let cfg = default_stl_config();
let bytes = export_to_stl_binary(&tris, &cfg);
let count = u32::from_le_bytes([bytes[80], bytes[81], bytes[82], bytes[83]]);
assert_eq!(count, 7);
}
}