use std::io::Write;
use std::path::Path;
use crate::data::PolyData;
use crate::types::VtkError;
pub struct GlbWriter;
impl GlbWriter {
pub fn write(path: &Path, poly_data: &PolyData) -> Result<(), VtkError> {
let file = std::fs::File::create(path)?;
let mut writer = std::io::BufWriter::new(file);
Self::write_to(&mut writer, poly_data)
}
pub fn write_to<W: Write>(writer: &mut W, poly_data: &PolyData) -> Result<(), VtkError> {
let (json, bin) = build_glb_data(poly_data)?;
write_glb(writer, &json, &bin)
}
}
fn build_glb_data(pd: &PolyData) -> Result<(Vec<u8>, Vec<u8>), VtkError> {
let n_points = pd.points.len();
if n_points == 0 {
return Err(VtkError::InvalidData("empty PolyData".into()));
}
let mut indices: Vec<u32> = Vec::new();
for cell in pd.polys.iter() {
if cell.len() < 3 {
continue;
}
for i in 1..cell.len() - 1 {
indices.push(cell[0] as u32);
indices.push(cell[i] as u32);
indices.push(cell[i + 1] as u32);
}
}
if indices.is_empty() {
return Err(VtkError::InvalidData("no triangles in PolyData".into()));
}
let mut bin = Vec::new();
let mut accessors = Vec::new();
let mut buffer_views = Vec::new();
let mut attributes = Vec::new();
let indices_offset = bin.len();
for &idx in &indices {
bin.extend_from_slice(&idx.to_le_bytes());
}
let indices_len = bin.len() - indices_offset;
pad_to_4(&mut bin);
let idx_max = indices.iter().copied().max().unwrap_or(0);
buffer_views.push(format!(
r#"{{"buffer":0,"byteOffset":{},"byteLength":{},"target":34963}}"#,
indices_offset, indices_len
));
accessors.push(format!(
r#"{{"bufferView":0,"componentType":5125,"count":{},"type":"SCALAR","max":[{}],"min":[0]}}"#,
indices.len(),
idx_max
));
let pos_offset = bin.len();
let mut min_pos = [f32::INFINITY; 3];
let mut max_pos = [f32::NEG_INFINITY; 3];
for i in 0..n_points {
let p = pd.points.get(i);
let pos = [p[0] as f32, p[1] as f32, p[2] as f32];
for k in 0..3 {
min_pos[k] = min_pos[k].min(pos[k]);
max_pos[k] = max_pos[k].max(pos[k]);
}
bin.extend_from_slice(&pos[0].to_le_bytes());
bin.extend_from_slice(&pos[1].to_le_bytes());
bin.extend_from_slice(&pos[2].to_le_bytes());
}
let pos_len = bin.len() - pos_offset;
pad_to_4(&mut bin);
buffer_views.push(format!(
r#"{{"buffer":0,"byteOffset":{},"byteLength":{},"byteStride":12,"target":34962}}"#,
pos_offset, pos_len
));
accessors.push(format!(
r#"{{"bufferView":1,"componentType":5126,"count":{},"type":"VEC3","max":[{},{},{}],"min":[{},{},{}]}}"#,
n_points,
max_pos[0], max_pos[1], max_pos[2],
min_pos[0], min_pos[1], min_pos[2]
));
attributes.push(r#""POSITION":1"#.to_string());
let has_normals = pd.point_data().normals().is_some();
if has_normals {
let normals = pd.point_data().normals().unwrap();
let norm_bv_idx = buffer_views.len();
let norm_acc_idx = accessors.len();
let norm_offset = bin.len();
let mut buf = [0.0f64; 3];
for i in 0..normals.num_tuples().min(n_points) {
normals.tuple_as_f64(i, &mut buf);
bin.extend_from_slice(&(buf[0] as f32).to_le_bytes());
bin.extend_from_slice(&(buf[1] as f32).to_le_bytes());
bin.extend_from_slice(&(buf[2] as f32).to_le_bytes());
}
let norm_len = bin.len() - norm_offset;
pad_to_4(&mut bin);
buffer_views.push(format!(
r#"{{"buffer":0,"byteOffset":{},"byteLength":{},"byteStride":12,"target":34962}}"#,
norm_offset, norm_len
));
accessors.push(format!(
r#"{{"bufferView":{},"componentType":5126,"count":{},"type":"VEC3"}}"#,
norm_bv_idx,
normals.num_tuples().min(n_points)
));
attributes.push(format!(r#""NORMAL":{}"#, norm_acc_idx));
}
let attrs_str = attributes.join(",");
let accessors_str = accessors.join(",");
let buffer_views_str = buffer_views.join(",");
let json_str = format!(
concat!(
r#"{{"asset":{{"version":"2.0","generator":"vtk-rs"}},"#,
r#""scene":0,"scenes":[{{"nodes":[0]}}],"#,
r#""nodes":[{{"mesh":0}}],"#,
r#""meshes":[{{"primitives":[{{"attributes":{{{}}},"indices":0,"mode":4}}]}}],"#,
r#""accessors":[{}],"bufferViews":[{}],"buffers":[{{"byteLength":{}}}]}}"#
),
attrs_str,
accessors_str,
buffer_views_str,
bin.len()
);
let json_bytes = json_str.into_bytes();
Ok((json_bytes, bin))
}
fn write_glb<W: Write>(writer: &mut W, json: &[u8], bin: &[u8]) -> Result<(), VtkError> {
let json_padded_len = (json.len() + 3) & !3;
let bin_padded_len = (bin.len() + 3) & !3;
let total_len = 12 + 8 + json_padded_len + 8 + bin_padded_len;
writer.write_all(b"glTF")?; writer.write_all(&2u32.to_le_bytes())?; writer.write_all(&(total_len as u32).to_le_bytes())?;
writer.write_all(&(json_padded_len as u32).to_le_bytes())?;
writer.write_all(&0x4E4F534Au32.to_le_bytes())?; writer.write_all(json)?;
for _ in 0..(json_padded_len - json.len()) {
writer.write_all(b" ")?;
}
writer.write_all(&(bin_padded_len as u32).to_le_bytes())?;
writer.write_all(&0x004E4942u32.to_le_bytes())?; writer.write_all(bin)?;
for _ in 0..(bin_padded_len - bin.len()) {
writer.write_all(&[0u8])?;
}
Ok(())
}
fn pad_to_4(buf: &mut Vec<u8>) {
while buf.len() % 4 != 0 {
buf.push(0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn write_triangle_glb() {
let pd = PolyData::from_triangles(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
vec![[0, 1, 2]],
);
let mut buf = Vec::new();
GlbWriter::write_to(&mut buf, &pd).unwrap();
assert_eq!(&buf[0..4], b"glTF");
assert_eq!(u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]), 2);
let total = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as usize;
assert_eq!(total, buf.len());
}
#[test]
fn write_quad_glb() {
let pd = PolyData::from_triangles(
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],
],
vec![[0, 1, 2], [0, 2, 3]],
);
let mut buf = Vec::new();
GlbWriter::write_to(&mut buf, &pd).unwrap();
assert_eq!(&buf[0..4], b"glTF");
}
#[test]
fn empty_poly_data_error() {
let pd = PolyData::new();
let mut buf = Vec::new();
assert!(GlbWriter::write_to(&mut buf, &pd).is_err());
}
#[test]
fn json_chunk_contains_asset() {
let pd = PolyData::from_triangles(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
vec![[0, 1, 2]],
);
let mut buf = Vec::new();
GlbWriter::write_to(&mut buf, &pd).unwrap();
let json_len = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]) as usize;
let json_str = std::str::from_utf8(&buf[20..20 + json_len]).unwrap();
assert!(json_str.contains("\"version\":\"2.0\""));
assert!(json_str.contains("\"generator\":\"vtk-rs\""));
assert!(json_str.contains("POSITION"));
}
}