use crate::chunks::animation::M2InterpolationType;
use crate::common::{C3Vector, M2Array};
use crate::error::Result;
use crate::io_ext::{ReadExt, WriteExt};
use std::io::{Read, Write};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct M2CompQuat {
pub x: i16,
pub y: i16,
pub z: i16,
pub w: i16,
}
impl M2CompQuat {
pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
let x = reader.read_i16_le()?;
let y = reader.read_i16_le()?;
let z = reader.read_i16_le()?;
let w = reader.read_i16_le()?;
Ok(Self { x: -x, y, z, w })
}
pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
writer.write_i16_le(self.x)?;
writer.write_i16_le(self.y)?;
writer.write_i16_le(self.z)?;
writer.write_i16_le(self.w)?;
Ok(())
}
pub fn to_float_quaternion(&self) -> (f32, f32, f32, f32) {
const SCALE: f32 = 1.0 / 32767.0;
(
self.x as f32 * SCALE,
self.y as f32 * SCALE,
self.z as f32 * SCALE,
self.w as f32 * SCALE,
)
}
pub fn from_float_quaternion(x: f32, y: f32, z: f32, w: f32) -> Self {
const SCALE: f32 = 32767.0;
Self {
x: (x * SCALE) as i16,
y: (y * SCALE) as i16,
z: (z * SCALE) as i16,
w: (w * SCALE) as i16,
}
}
}
#[derive(Debug, Clone)]
pub struct M2TrackBase {
pub interpolation_type: M2InterpolationType,
pub global_sequence: u16,
}
impl M2TrackBase {
pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
let interpolation_type = M2InterpolationType::from_u16(reader.read_u16_le()?);
let interpolation_type = interpolation_type.unwrap_or(M2InterpolationType::Linear);
let global_sequence = reader.read_u16_le()?;
Ok(Self {
interpolation_type,
global_sequence,
})
}
pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
writer.write_u16_le(self.interpolation_type as u16)?;
writer.write_u16_le(self.global_sequence)?;
Ok(())
}
pub fn uses_global_sequence(&self) -> bool {
self.global_sequence != 65535
}
}
#[derive(Debug, Clone)]
pub struct M2Track<T> {
pub base: M2TrackBase,
pub ranges: Option<M2Array<u32>>,
pub timestamps: M2Array<u32>,
pub values: M2Array<T>,
}
impl<T> M2Track<T> {
pub fn parse<R: Read>(reader: &mut R, version: u32) -> Result<Self> {
let base = M2TrackBase::parse(reader)?;
let (ranges, timestamps, values) = if version < 264 {
let ranges = M2Array::parse(reader)?;
let timestamps = M2Array::parse(reader)?;
let values = M2Array::parse(reader)?;
(Some(ranges), timestamps, values)
} else {
let timestamps = M2Array::parse(reader)?;
let values = M2Array::parse(reader)?;
(None, timestamps, values)
};
Ok(Self {
base,
ranges,
timestamps,
values,
})
}
pub fn write<W: Write>(&self, writer: &mut W, version: u32) -> Result<()> {
self.base.write(writer)?;
if version < 264 {
if let Some(ref ranges) = self.ranges {
ranges.write(writer)?;
} else {
M2Array::<u32>::new(0, 0).write(writer)?;
}
}
self.timestamps.write(writer)?;
self.values.write(writer)?;
Ok(())
}
pub fn has_data(&self) -> bool {
!self.timestamps.is_empty() && !self.values.is_empty()
}
pub fn is_static(&self) -> bool {
self.timestamps.count <= 1
}
pub fn interpolation_type(&self) -> M2InterpolationType {
self.base.interpolation_type
}
pub fn uses_global_sequence(&self) -> bool {
self.base.uses_global_sequence()
}
pub fn new() -> Self {
Self {
base: M2TrackBase {
interpolation_type: M2InterpolationType::None,
global_sequence: 65535, },
ranges: None, timestamps: M2Array::new(0, 0),
values: M2Array::new(0, 0),
}
}
pub fn new_with_interpolation(interpolation_type: M2InterpolationType) -> Self {
Self {
base: M2TrackBase {
interpolation_type,
global_sequence: 65535,
},
ranges: None, timestamps: M2Array::new(0, 0),
values: M2Array::new(0, 0),
}
}
}
impl<T> Default for M2Track<T> {
fn default() -> Self {
Self::new()
}
}
pub type M2TrackVec3 = M2Track<C3Vector>;
pub type M2TrackQuat = M2Track<M2CompQuat>;
pub type M2TrackFloat = M2Track<f32>;
pub type M2TrackUint16 = M2Track<u16>;
#[derive(Debug, Clone)]
pub struct M2TrackWithRanges<T> {
pub track: M2Track<T>,
pub interpolation_ranges: M2Array<u32>,
}
impl<T> M2TrackWithRanges<T> {
pub fn parse<R: Read>(reader: &mut R, version: u32) -> Result<Self> {
let track = M2Track::parse(reader, version)?;
let interpolation_ranges = M2Array::parse(reader)?;
Ok(Self {
track,
interpolation_ranges,
})
}
pub fn write<W: Write>(&self, writer: &mut W, version: u32) -> Result<()> {
self.track.write(writer, version)?;
self.interpolation_ranges.write(writer)?;
Ok(())
}
pub fn uses_interpolation_ranges(&self) -> bool {
matches!(
self.track.interpolation_type(),
M2InterpolationType::Bezier | M2InterpolationType::Hermite
) && !self.interpolation_ranges.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_interpolation_type_conversion() {
assert_eq!(
M2InterpolationType::from_u16(0),
Some(M2InterpolationType::None)
);
assert_eq!(
M2InterpolationType::from_u16(1),
Some(M2InterpolationType::Linear)
);
assert_eq!(
M2InterpolationType::from_u16(2),
Some(M2InterpolationType::Bezier)
);
assert_eq!(
M2InterpolationType::from_u16(3),
Some(M2InterpolationType::Hermite)
);
assert_eq!(M2InterpolationType::from_u16(999), None);
assert_eq!(M2InterpolationType::None as u16, 0);
assert_eq!(M2InterpolationType::Linear as u16, 1);
assert_eq!(M2InterpolationType::Bezier as u16, 2);
assert_eq!(M2InterpolationType::Hermite as u16, 3);
}
#[test]
fn test_compressed_quaternion_parse() {
let data = [
0x00, 0x40, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, ];
let mut cursor = Cursor::new(data);
let quat = M2CompQuat::parse(&mut cursor).unwrap();
assert_eq!(quat.x, -16384); assert_eq!(quat.y, 32767);
assert_eq!(quat.z, 0);
assert_eq!(quat.w, 16383);
}
#[test]
fn test_compressed_quaternion_float_conversion() {
let quat = M2CompQuat {
x: 16384, y: -16384, z: 0,
w: 32767, };
let (x, y, z, w) = quat.to_float_quaternion();
assert!((x - 0.5).abs() < 0.01);
assert!((y + 0.5).abs() < 0.01);
assert!(z.abs() < 0.001);
assert!((w - 1.0).abs() < 0.01);
}
#[test]
fn test_track_base_parse() {
let data = [
0x01, 0x00, 0xFF, 0xFF, ];
let mut cursor = Cursor::new(data);
let base = M2TrackBase::parse(&mut cursor).unwrap();
assert_eq!(base.interpolation_type, M2InterpolationType::Linear);
assert_eq!(base.global_sequence, 65535);
assert!(!base.uses_global_sequence());
}
#[test]
fn test_track_base_with_global_sequence() {
let data = [
0x02, 0x00, 0x05, 0x00, ];
let mut cursor = Cursor::new(data);
let base = M2TrackBase::parse(&mut cursor).unwrap();
assert_eq!(base.interpolation_type, M2InterpolationType::Bezier);
assert_eq!(base.global_sequence, 5);
assert!(base.uses_global_sequence());
}
#[test]
fn test_m2track_parse() {
let mut data = Vec::new();
data.extend_from_slice(&1u16.to_le_bytes()); data.extend_from_slice(&65535u16.to_le_bytes());
data.extend_from_slice(&3u32.to_le_bytes()); data.extend_from_slice(&100u32.to_le_bytes());
data.extend_from_slice(&3u32.to_le_bytes()); data.extend_from_slice(&200u32.to_le_bytes());
let mut cursor = Cursor::new(data);
let track: M2Track<f32> = M2Track::parse(&mut cursor, 264).unwrap();
assert_eq!(track.base.interpolation_type, M2InterpolationType::Linear);
assert_eq!(track.base.global_sequence, 65535);
assert_eq!(track.timestamps.count, 3);
assert_eq!(track.timestamps.offset, 100);
assert_eq!(track.values.count, 3);
assert_eq!(track.values.offset, 200);
assert!(track.has_data());
assert!(!track.is_static());
}
#[test]
fn test_m2track_new() {
let track: M2Track<f32> = M2Track::new();
assert_eq!(track.base.interpolation_type, M2InterpolationType::None);
assert_eq!(track.base.global_sequence, 65535);
assert!(!track.has_data());
assert!(track.is_static());
}
#[test]
fn test_m2track_with_interpolation() {
let track: M2Track<M2CompQuat> =
M2Track::new_with_interpolation(M2InterpolationType::Hermite);
assert_eq!(track.base.interpolation_type, M2InterpolationType::Hermite);
assert_eq!(track.base.global_sequence, 65535);
}
#[test]
fn test_compressed_quaternion_roundtrip() {
let original_x = 0.707;
let original_y = 0.0;
let original_z = 0.707;
let original_w = 0.0;
let quat =
M2CompQuat::from_float_quaternion(original_x, original_y, original_z, original_w);
let (recovered_x, recovered_y, recovered_z, recovered_w) = quat.to_float_quaternion();
assert!(
(recovered_x - original_x).abs() < 0.01,
"X component: expected {}, got {}",
original_x,
recovered_x
);
assert!(
(recovered_y - original_y).abs() < 0.01,
"Y component: expected {}, got {}",
original_y,
recovered_y
);
assert!(
(recovered_z - original_z).abs() < 0.01,
"Z component: expected {}, got {}",
original_z,
recovered_z
);
assert!(
(recovered_w - original_w).abs() < 0.01,
"W component: expected {}, got {}",
original_w,
recovered_w
);
}
}