use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Write};
use std::path::Path;
use crate::camera::Camera;
use crate::geometry::Geometry;
use crate::objects::Object;
use crate::text_label::{HorizontalAlignment, VerticalAlignment, TextLabel};
use crate::text_overlay::TextOverlay;
use crate::transform::Transform;
use crate::world::World;
pub const MAGIC: [u8; 4] = [0x56, 0x54, 0x52, 0x00];
pub const FORMAT_VERSION: u16 = 3;
pub const ENGINE_VERSION_MAJOR: u16 = 0;
pub const ENGINE_VERSION_MINOR: u16 = 2;
pub const ENGINE_VERSION_PATCH: u16 = 0;
const NO_PARENT: u32 = u32::MAX;
#[derive(Debug)]
pub struct SceneData {
pub camera: Camera,
pub world: World,
pub text_overlay: TextOverlay,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VtrHeader {
pub format_version: u16,
pub engine_major: u16,
pub engine_minor: u16,
pub engine_patch: u16,
pub object_count: u32,
}
impl VtrHeader {
pub fn engine_version_string(&self) -> String {
format!("{}.{}.{}", self.engine_major, self.engine_minor, self.engine_patch)
}
}
#[derive(Debug)]
pub enum VtrError {
Io(io::Error),
InvalidMagic,
UnsupportedVersion { found: u16 },
InvalidUtf8(std::string::FromUtf8Error),
UnknownGeometryTag(u8),
TexturePathTooLong { len: usize },
LabelTextTooLong { len: usize },
FontIdTooLong { len: usize },
}
impl std::fmt::Display for VtrError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VtrError::Io(e) => write!(f, "I/O error: {e}"),
VtrError::InvalidMagic => {
write!(f, "Not a valid VTR file (magic bytes mismatch)")
}
VtrError::UnsupportedVersion { found } => {
write!(
f,
"Unsupported VTR format version {found} \
(this build supports versions 2-{FORMAT_VERSION})"
)
}
VtrError::InvalidUtf8(e) => write!(f, "Invalid UTF-8 in object name: {e}"),
VtrError::UnknownGeometryTag(tag) => {
write!(f, "Unknown geometry tag byte: {tag:#04x}")
}
VtrError::TexturePathTooLong { len } => {
write!(
f,
"texture_path is {len} bytes, which exceeds the maximum \
of {} bytes allowed by the VTR u16 length field",
u16::MAX
)
}
VtrError::LabelTextTooLong { len } => {
write!(
f,
"label text is {len} bytes, which exceeds the maximum \
of {} bytes allowed by the VTR u32 length field",
u32::MAX
)
}
VtrError::FontIdTooLong { len } => {
write!(
f,
"font_id is {len} bytes, which exceeds the maximum \
of {} bytes allowed by the VTR u16 length field",
u16::MAX
)
}
}
}
}
impl std::error::Error for VtrError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
VtrError::Io(e) => Some(e),
VtrError::InvalidUtf8(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for VtrError {
fn from(e: io::Error) -> Self {
VtrError::Io(e)
}
}
impl From<std::string::FromUtf8Error> for VtrError {
fn from(e: std::string::FromUtf8Error) -> Self {
VtrError::InvalidUtf8(e)
}
}
#[inline]
fn w_u16(w: &mut impl Write, v: u16) -> io::Result<()> {
w.write_all(&v.to_le_bytes())
}
#[inline]
fn w_u32(w: &mut impl Write, v: u32) -> io::Result<()> {
w.write_all(&v.to_le_bytes())
}
#[inline]
fn w_f32(w: &mut impl Write, v: f32) -> io::Result<()> {
w.write_all(&v.to_le_bytes())
}
#[inline]
fn w_i32(w: &mut impl Write, v: i32) -> io::Result<()> {
w.write_all(&v.to_le_bytes())
}
#[inline]
fn w_f32x3(w: &mut impl Write, v: [f32; 3]) -> io::Result<()> {
w_f32(w, v[0])?;
w_f32(w, v[1])?;
w_f32(w, v[2])
}
#[inline]
fn w_f32x4(w: &mut impl Write, v: [f32; 4]) -> io::Result<()> {
w_f32(w, v[0])?;
w_f32(w, v[1])?;
w_f32(w, v[2])?;
w_f32(w, v[3])
}
#[inline] fn r_u8 (r: &mut impl Read) -> io::Result<u8> {
let mut b=[0u8;1];
r.read_exact(&mut b)?;
Ok(b[0])
}
#[inline] fn r_u16(r: &mut impl Read) -> io::Result<u16> {
let mut b=[0u8;2];
r.read_exact(&mut b)?;
Ok(u16::from_le_bytes(b)) }
#[inline] fn r_u32(r: &mut impl Read) -> io::Result<u32> {
let mut b=[0u8;4];
r.read_exact(&mut b)?;
Ok(u32::from_le_bytes(b)) }
#[inline] fn r_i32(r: &mut impl Read) -> io::Result<i32> {
let mut b=[0u8;4];
r.read_exact(&mut b)?;
Ok(i32::from_le_bytes(b))
}
#[inline] fn r_f32(r: &mut impl Read) -> io::Result<f32> {
let mut b=[0u8;4];
r.read_exact(&mut b)?;
Ok(f32::from_le_bytes(b))
}
#[inline]
fn r_f32x3(r: &mut impl Read) -> io::Result<[f32; 3]> {
Ok([r_f32(r)?, r_f32(r)?, r_f32(r)?])
}
#[inline]
fn r_f32x4(r: &mut impl Read) -> io::Result<[f32; 4]> {
Ok([r_f32(r)?, r_f32(r)?, r_f32(r)?, r_f32(r)?])
}
mod tag {
pub const NONE: u8 = 0;
pub const CUBE: u8 = 1;
pub const BOX: u8 = 2;
pub const PLANE: u8 = 3;
pub const PYRAMID: u8 = 4;
pub const CAPSULE: u8 = 5;
pub const SPHERE: u8 = 6;
}
fn write_geometry(w: &mut impl Write, geom: &Option<Geometry>) -> io::Result<()> {
match geom {
None => w.write_all(&[tag::NONE]),
Some(Geometry::Cube { size }) => {
w.write_all(&[tag::CUBE])?;
w_f32(w, *size)
}
Some(Geometry::Box { width, height, depth }) => {
w.write_all(&[tag::BOX])?;
w_f32(w, *width)?;
w_f32(w, *height)?;
w_f32(w, *depth)
}
Some(Geometry::Plane { size }) => {
w.write_all(&[tag::PLANE])?;
w_f32(w, *size)
}
Some(Geometry::Pyramid { base_size, height }) => {
w.write_all(&[tag::PYRAMID])?;
w_f32(w, *base_size)?;
w_f32(w, *height)
}
Some(Geometry::Capsule { radius, height, subdivisions }) => {
w.write_all(&[tag::CAPSULE])?;
w_f32(w, *radius)?;
w_f32(w, *height)?;
w_u32(w, *subdivisions as u32)
}
Some(Geometry::Sphere { radius, subdivisions }) => {
w.write_all(&[tag::SPHERE])?;
w_f32(w, *radius)?;
w_u32(w, *subdivisions as u32)
}
}
}
fn read_geometry(r: &mut impl Read) -> Result<Option<Geometry>, VtrError> {
let mut buf = [0u8; 1];
r.read_exact(&mut buf)?;
match buf[0] {
tag::NONE => Ok(None),
tag::CUBE => Ok(Some(Geometry::Cube { size: r_f32(r)? })),
tag::BOX => Ok(Some(Geometry::Box {
width: r_f32(r)?,
height: r_f32(r)?,
depth: r_f32(r)?,
})),
tag::PLANE => Ok(Some(Geometry::Plane { size: r_f32(r)? })),
tag::PYRAMID => Ok(Some(Geometry::Pyramid {
base_size: r_f32(r)?,
height: r_f32(r)?,
})),
tag::CAPSULE => Ok(Some(Geometry::Capsule {
radius: r_f32(r)?,
height: r_f32(r)?,
subdivisions: r_u32(r)? as usize,
})),
tag::SPHERE => Ok(Some(Geometry::Sphere {
radius: r_f32(r)?,
subdivisions: r_u32(r)? as usize,
})),
unknown => Err(VtrError::UnknownGeometryTag(unknown)),
}
}
fn write_text_overlay(w: &mut impl Write, overlay: &TextOverlay) -> Result<(), VtrError> {
w_u32(w, overlay.next_id as u32)?;
w_u32(w, overlay.labels.len() as u32)?;
let mut ids: Vec<usize> = overlay.labels.keys().copied().collect();
ids.sort_unstable();
for id in ids {
let lbl = &overlay.labels[&id];
w_u32(w, lbl.id as u32)?;
w_f32(w, lbl.margin_x)?;
w_f32(w, lbl.margin_y)?;
w_f32(w, lbl.font_size)?;
w_f32x4(w, lbl.color)?;
w.write_all(&[lbl.visible as u8])?;
w_i32(w, lbl.zindex)?;
w.write_all(&[lbl.horizontal_alignment.to_u8()])?;
w.write_all(&[lbl.vertical_alignment.to_u8()])?;
let font_id_bytes = lbl.font_id.as_bytes();
if font_id_bytes.len() > u16::MAX as usize {
return Err(VtrError::FontIdTooLong { len: font_id_bytes.len() });
}
let font_id_len = font_id_bytes.len() as u16;
w_u16(w, font_id_len)?;
w.write_all(font_id_bytes)?;
let text_bytes = lbl.text.as_bytes();
if text_bytes.len() > u32::MAX as usize {
return Err(VtrError::LabelTextTooLong { len: text_bytes.len() });
}
w_u32(w, text_bytes.len() as u32)?;
w.write_all(text_bytes)?;
}
Ok(())
}
fn read_text_overlay(r: &mut impl Read, format_version: u16) -> Result<TextOverlay, VtrError> {
let next_id = r_u32(r)? as usize;
let label_count = r_u32(r)? as usize;
let mut labels = HashMap::with_capacity(label_count);
for _ in 0..label_count {
let id = r_u32(r)? as usize;
let x = r_f32(r)?;
let y = r_f32(r)?;
let font_size = r_f32(r)?;
let color = r_f32x4(r)?;
let visible = r_u8(r)? != 0;
let zindex = r_i32(r)?;
let horizontal_alignment = HorizontalAlignment::from_u8(r_u8(r)?);
let vertical_alignment = if format_version >= 3 {
VerticalAlignment::from_u8(r_u8(r)?)
} else {
VerticalAlignment::Top
};
let font_id_len = r_u16(r)? as usize;
let mut font_id_bytes = vec![0u8; font_id_len];
r.read_exact(&mut font_id_bytes)?;
let font_id = String::from_utf8(font_id_bytes)?;
let text_len = r_u32(r)? as usize;
let mut text_bytes = vec![0u8; text_len];
r.read_exact(&mut text_bytes)?;
let text = String::from_utf8(text_bytes)?;
labels.insert(id, TextLabel {
id, text, font_size, color, visible, font_id,
zindex, horizontal_alignment, vertical_alignment,
x,
y,
margin_x: x,
margin_y: y,
dirty: true,
position_dirty: false,
rasterized_w: 0,
rasterized_h: 0,
rasterized_font_size: 0.0,
rasterized_vp_w: 0.0,
rasterized_vp_h: 0.0,
});
}
Ok(TextOverlay { labels, next_id, fonts: Vec::new() })
}
pub fn read_header(r: &mut impl Read) -> Result<VtrHeader, VtrError> {
let mut magic = [0u8; 4];
r.read_exact(&mut magic)?;
if magic != MAGIC {
return Err(VtrError::InvalidMagic);
}
let format_version = r_u16(r)?;
if format_version < 2 || format_version > FORMAT_VERSION {
return Err(VtrError::UnsupportedVersion { found: format_version });
}
let engine_major = r_u16(r)?;
let engine_minor = r_u16(r)?;
let engine_patch = r_u16(r)?;
let _flags = r_u32(r)?; let object_count = r_u32(r)?;
Ok(VtrHeader { format_version, engine_major, engine_minor, engine_patch, object_count })
}
pub fn write(w: &mut impl Write, camera: &Camera, world: &World) -> Result<(), VtrError> {
write_scene(w, camera, world, &TextOverlay { labels: HashMap::new(), next_id: 0, fonts: Vec::new() })
}
pub fn write_scene(w: &mut impl Write, camera: &Camera, world: &World, overlay: &TextOverlay) -> Result<(), VtrError> {
w.write_all(&MAGIC)?;
w_u16(w, FORMAT_VERSION)?;
w_u16(w, ENGINE_VERSION_MAJOR)?;
w_u16(w, ENGINE_VERSION_MINOR)?;
w_u16(w, ENGINE_VERSION_PATCH)?;
w_u32(w, 0)?; w_u32(w, world.objects.len() as u32)?;
w_f32x3(w, camera.eye)?;
w_f32x3(w, camera.target)?;
w_f32x3(w, camera.up)?;
w_f32(w, camera.aspect)?;
w_f32(w, camera.fov)?;
w_f32(w, camera.znear)?;
w_f32(w, camera.zfar)?;
w_f32(w, camera.lr_rot)?;
w_f32(w, camera.ud_rot)?;
w_u32(w, world.roots.len() as u32)?;
for &root_id in &world.roots {
w_u32(w, root_id as u32)?;
}
let mut ids: Vec<usize> = world.objects.keys().copied().collect();
ids.sort_unstable();
for id in ids {
let obj = &world.objects[&id];
w_u32(w, id as u32)?;
w_u32(w, obj.parent.map(|p| p as u32).unwrap_or(NO_PARENT))?;
let name_bytes = obj.name.as_bytes();
w_u16(w, name_bytes.len() as u16)?;
w.write_all(name_bytes)?;
let bytes = obj.str_id.as_bytes();
w_u16(w, bytes.len() as u16)?;
w.write_all(bytes)?;
w_f32x3(w, obj.transform.position)?;
w_f32x3(w, obj.transform.rotation)?;
w_f32x3(w, obj.transform.scale)?;
w_f32x4(w, obj.color)?;
write_geometry(w, &obj.geometry)?;
match &obj.texture_path {
Some(tp) => {
let tp_bytes = tp.as_bytes();
if tp_bytes.len() > u16::MAX as usize {
return Err(VtrError::TexturePathTooLong { len: tp_bytes.len() });
}
w_u16(w, tp_bytes.len() as u16)?;
w.write_all(tp_bytes)?;
}
None => w_u16(w, 0)?,
}
w_u32(w, obj.children.len() as u32)?;
for &child_id in &obj.children {
w_u32(w, child_id as u32)?;
}
}
write_text_overlay(w, overlay)?;
w.flush()?;
Ok(())
}
pub fn read(r: &mut impl Read) -> Result<SceneData, VtrError> {
let header = read_header(r)?;
let object_count = header.object_count as usize;
let camera = Camera {
eye: r_f32x3(r)?,
target: r_f32x3(r)?,
up: r_f32x3(r)?,
aspect: r_f32(r)?,
fov: r_f32(r)?,
znear: r_f32(r)?,
zfar: r_f32(r)?,
lr_rot: r_f32(r)?,
ud_rot: r_f32(r)?,
};
let roots_count = r_u32(r)? as usize;
let mut roots = Vec::with_capacity(roots_count);
for _ in 0..roots_count {
roots.push(r_u32(r)? as usize);
}
let mut objects: HashMap<usize, Object> = HashMap::with_capacity(object_count);
let mut max_id: usize = 0;
for _ in 0..object_count {
let id = r_u32(r)? as usize;
let parent_raw = r_u32(r)?;
let parent = if parent_raw == NO_PARENT { None } else { Some(parent_raw as usize) };
let name_len = r_u16(r)? as usize;
let mut name_bytes = vec![0u8; name_len];
r.read_exact(&mut name_bytes)?;
let name = String::from_utf8(name_bytes)?;
let str_id_len = r_u16(r)? as usize;
let mut sid_bytes = vec![0u8; str_id_len];
r.read_exact(&mut sid_bytes)?;
let str_id = String::from_utf8(sid_bytes)?;
let position = r_f32x3(r)?;
let rotation = r_f32x3(r)?;
let scale = r_f32x3(r)?;
let color = r_f32x4(r)?;
let geometry = read_geometry(r)?;
let tp_len = r_u16(r)? as usize;
let texture_path = if tp_len > 0 {
let mut tp_bytes = vec![0u8; tp_len];
r.read_exact(&mut tp_bytes)?;
Some(String::from_utf8(tp_bytes)?)
} else {
None
};
let children_count = r_u32(r)? as usize;
let mut children = Vec::with_capacity(children_count);
for _ in 0..children_count {
children.push(r_u32(r)? as usize);
}
if id > max_id { max_id = id; }
objects.insert(id, Object {
name,
str_id,
transform: Transform { position, rotation, scale },
geometry,
color,
children,
parent,
texture_path,
});
}
let next_id = if objects.is_empty() { 0 } else { max_id + 1 };
let world = World::from_parts(objects, roots, next_id);
let text_overlay = if header.format_version >= 3 {
read_text_overlay(r, header.format_version)?
} else {
TextOverlay { labels: HashMap::new(), next_id: 0, fonts: Vec::new() }
};
Ok(SceneData { camera, world, text_overlay })
}
pub fn write_to_file(path: &Path, camera: &Camera, world: &World) -> Result<(), VtrError> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
write(&mut writer, camera, world)
}
pub fn write_scene_to_file(path: &Path, camera: &Camera, world: &World, overlay: &TextOverlay) -> Result<(), VtrError> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
write_scene(&mut writer, camera, world, overlay)
}
pub fn read_from_file(path: &Path) -> Result<SceneData, VtrError> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
read(&mut reader)
}
pub fn header_from_file(path: &Path) -> Result<VtrHeader, VtrError> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
read_header(&mut reader)
}