feat(ai): add NavMesh binary serialization
Add serialize/deserialize methods directly on NavMesh with a proper binary header (magic "VNAV" + version u32) for format validation. Includes tests for roundtrip, invalid magic, empty mesh, header format, and unsupported version. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,128 @@ impl NavMesh {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize the NavMesh to a binary format with header.
|
||||||
|
///
|
||||||
|
/// Format:
|
||||||
|
/// - Magic: "VNAV" (4 bytes)
|
||||||
|
/// - Version: u32 (little-endian)
|
||||||
|
/// - num_vertices: u32
|
||||||
|
/// - vertices: [f32; 3] * num_vertices
|
||||||
|
/// - num_triangles: u32
|
||||||
|
/// - triangles: for each triangle: indices [u32; 3] + neighbors [i32; 3] (-1 for None)
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
// Magic
|
||||||
|
data.extend_from_slice(b"VNAV");
|
||||||
|
// Version
|
||||||
|
data.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
|
||||||
|
// Vertices
|
||||||
|
data.extend_from_slice(&(self.vertices.len() as u32).to_le_bytes());
|
||||||
|
for v in &self.vertices {
|
||||||
|
data.extend_from_slice(&v.x.to_le_bytes());
|
||||||
|
data.extend_from_slice(&v.y.to_le_bytes());
|
||||||
|
data.extend_from_slice(&v.z.to_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triangles
|
||||||
|
data.extend_from_slice(&(self.triangles.len() as u32).to_le_bytes());
|
||||||
|
for tri in &self.triangles {
|
||||||
|
for &idx in &tri.indices {
|
||||||
|
data.extend_from_slice(&(idx as u32).to_le_bytes());
|
||||||
|
}
|
||||||
|
for &nb in &tri.neighbors {
|
||||||
|
let val: i32 = match nb {
|
||||||
|
Some(i) => i as i32,
|
||||||
|
None => -1,
|
||||||
|
};
|
||||||
|
data.extend_from_slice(&val.to_le_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize a NavMesh from binary data with header validation.
|
||||||
|
pub fn deserialize(data: &[u8]) -> Result<Self, String> {
|
||||||
|
if data.len() < 8 {
|
||||||
|
return Err("data too short for header".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate magic
|
||||||
|
if &data[0..4] != b"VNAV" {
|
||||||
|
return Err(format!(
|
||||||
|
"invalid magic: expected VNAV, got {:?}",
|
||||||
|
&data[0..4]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read version
|
||||||
|
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||||
|
if version != 1 {
|
||||||
|
return Err(format!("unsupported version: {}", version));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate rest to offset-based reading
|
||||||
|
let mut offset = 8usize;
|
||||||
|
|
||||||
|
let read_u32 = |off: &mut usize| -> Result<u32, String> {
|
||||||
|
if *off + 4 > data.len() {
|
||||||
|
return Err("unexpected end of data".to_string());
|
||||||
|
}
|
||||||
|
let val = u32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]);
|
||||||
|
*off += 4;
|
||||||
|
Ok(val)
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_i32 = |off: &mut usize| -> Result<i32, String> {
|
||||||
|
if *off + 4 > data.len() {
|
||||||
|
return Err("unexpected end of data".to_string());
|
||||||
|
}
|
||||||
|
let val = i32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]);
|
||||||
|
*off += 4;
|
||||||
|
Ok(val)
|
||||||
|
};
|
||||||
|
|
||||||
|
let read_f32 = |off: &mut usize| -> Result<f32, String> {
|
||||||
|
if *off + 4 > data.len() {
|
||||||
|
return Err("unexpected end of data".to_string());
|
||||||
|
}
|
||||||
|
let val = f32::from_le_bytes([data[*off], data[*off + 1], data[*off + 2], data[*off + 3]]);
|
||||||
|
*off += 4;
|
||||||
|
Ok(val)
|
||||||
|
};
|
||||||
|
|
||||||
|
let vc = read_u32(&mut offset)? as usize;
|
||||||
|
let mut vertices = Vec::with_capacity(vc);
|
||||||
|
for _ in 0..vc {
|
||||||
|
let x = read_f32(&mut offset)?;
|
||||||
|
let y = read_f32(&mut offset)?;
|
||||||
|
let z = read_f32(&mut offset)?;
|
||||||
|
vertices.push(Vec3::new(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
let tc = read_u32(&mut offset)? as usize;
|
||||||
|
let mut triangles = Vec::with_capacity(tc);
|
||||||
|
for _ in 0..tc {
|
||||||
|
let i0 = read_u32(&mut offset)? as usize;
|
||||||
|
let i1 = read_u32(&mut offset)? as usize;
|
||||||
|
let i2 = read_u32(&mut offset)? as usize;
|
||||||
|
let n0 = read_i32(&mut offset)?;
|
||||||
|
let n1 = read_i32(&mut offset)?;
|
||||||
|
let n2 = read_i32(&mut offset)?;
|
||||||
|
let to_opt = |v: i32| -> Option<usize> {
|
||||||
|
if v < 0 { None } else { Some(v as usize) }
|
||||||
|
};
|
||||||
|
triangles.push(NavTriangle {
|
||||||
|
indices: [i0, i1, i2],
|
||||||
|
neighbors: [to_opt(n0), to_opt(n1), to_opt(n2)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NavMesh::new(vertices, triangles))
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the shared edge (portal) vertices between two adjacent triangles.
|
/// Return the shared edge (portal) vertices between two adjacent triangles.
|
||||||
/// Returns (left, right) vertices of the portal edge as seen from `from_tri` looking toward `to_tri`.
|
/// Returns (left, right) vertices of the portal edge as seen from `from_tri` looking toward `to_tri`.
|
||||||
/// Returns None if the triangles are not adjacent.
|
/// Returns None if the triangles are not adjacent.
|
||||||
@@ -312,4 +434,64 @@ mod tests {
|
|||||||
let result = deserialize_navmesh(&[]);
|
let result = deserialize_navmesh(&[]);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_method_serialize_deserialize_roundtrip() {
|
||||||
|
let nm = make_simple_navmesh();
|
||||||
|
let data = nm.serialize();
|
||||||
|
let restored = NavMesh::deserialize(&data).expect("should deserialize");
|
||||||
|
assert_eq!(nm.vertices.len(), restored.vertices.len());
|
||||||
|
assert_eq!(nm.triangles.len(), restored.triangles.len());
|
||||||
|
for (a, b) in nm.vertices.iter().zip(restored.vertices.iter()) {
|
||||||
|
assert!((a.x - b.x).abs() < 1e-6);
|
||||||
|
assert!((a.y - b.y).abs() < 1e-6);
|
||||||
|
assert!((a.z - b.z).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
for (a, b) in nm.triangles.iter().zip(restored.triangles.iter()) {
|
||||||
|
assert_eq!(a.indices, b.indices);
|
||||||
|
assert_eq!(a.neighbors, b.neighbors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_invalid_magic() {
|
||||||
|
let data = b"XXXX\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
|
||||||
|
assert!(NavMesh::deserialize(data).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_too_short() {
|
||||||
|
let data = b"VNA";
|
||||||
|
assert!(NavMesh::deserialize(data).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_empty_navmesh() {
|
||||||
|
let nm = NavMesh::new(vec![], vec![]);
|
||||||
|
let data = nm.serialize();
|
||||||
|
let restored = NavMesh::deserialize(&data).expect("should deserialize empty");
|
||||||
|
assert!(restored.vertices.is_empty());
|
||||||
|
assert!(restored.triangles.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_header_format() {
|
||||||
|
let nm = NavMesh::new(vec![], vec![]);
|
||||||
|
let data = nm.serialize();
|
||||||
|
// Check magic
|
||||||
|
assert_eq!(&data[0..4], b"VNAV");
|
||||||
|
// Check version = 1
|
||||||
|
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
|
||||||
|
assert_eq!(version, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_unsupported_version() {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
data.extend_from_slice(b"VNAV");
|
||||||
|
data.extend_from_slice(&99u32.to_le_bytes());
|
||||||
|
data.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
data.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
assert!(NavMesh::deserialize(&data).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user