//! # foreign-vox — puente al formato **MagicaVoxel `.vox`** //! //! Lee y escribe modelos voxel en el formato de MagicaVoxel (estructura de //! *chunks* tipo-RIFF: `MAIN` → `SIZE`/`XYZI`/`RGBA`) y los expone como un //! [`VoxModel`] **neutral**: dimensiones + lista de voxels `(x,y,z,índice)` + //! una paleta de 256 colores RGBA indexada por el índice del voxel. //! //! Sin dependencias (sólo `std`): el formato son bytes planos *little-endian*. //! **No** conoce el motor — el conversor a `VoxelGrid` vive en `llimphi-voxel` //! (CLAUDE.md regla #4: lo ajeno entra por el puente, el núcleo trabaja nativo). //! //! Convención de color: el índice `i` de un voxel (1..255; `0` = vacío) indexa //! [`VoxModel::palette`]`[i]`. Al leer el chunk `RGBA` (256 entradas crudas) se //! aplica el corrimiento documentado por MagicaVoxel (`palette[i] = crudo[i-1]`). //! Si el archivo no trae `RGBA`, se usa una paleta por defecto (rampa HSV) — los //! exportes reales de MagicaVoxel siempre incluyen `RGBA`, así que es un fallback. use std::fmt; /// Un voxel: posición en la grilla del modelo (`0..size`) + índice de color /// (`1..255`) en la [`VoxModel::palette`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Voxel { pub x: u8, pub y: u8, pub z: u8, pub i: u8, } /// Un modelo voxel: dimensiones, voxels y paleta (indexada por `voxel.i`). #[derive(Debug, Clone)] pub struct VoxModel { /// Dimensiones `[x, y, z]` (en el espacio del `.vox`, donde `z` es arriba). pub size: [u32; 3], /// Voxels ocupados. pub voxels: Vec, /// 256 colores RGBA; `palette[voxel.i]` es el color del voxel (`[0]` vacío). pub palette: [[u8; 4]; 256], } impl VoxModel { /// Modelo vacío de las dimensiones dadas con la paleta por defecto. pub fn new(size: [u32; 3]) -> Self { Self { size, voxels: Vec::new(), palette: default_palette() } } /// Color RGBA de un voxel (vía su índice en la paleta). pub fn color(&self, v: &Voxel) -> [u8; 4] { self.palette[v.i as usize] } } /// Error de parseo de un `.vox`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum VoxError { /// Faltan los 4 bytes mágicos `"VOX "` al inicio. BadMagic, /// El buffer se corta antes de lo que un chunk declara. Truncated, /// No hay ningún par `SIZE`+`XYZI` (ningún modelo). NoModel, } impl fmt::Display for VoxError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { VoxError::BadMagic => write!(f, "no es un .vox (faltan los bytes 'VOX ')"), VoxError::Truncated => write!(f, ".vox truncado (un chunk declara más bytes de los que hay)"), VoxError::NoModel => write!(f, ".vox sin modelos (ningún par SIZE+XYZI)"), } } } impl std::error::Error for VoxError {} /// Una **rotación** del grafo de escena: la matriz 3×3 de MagicaVoxel, que siempre /// es una *permutación con signo* de los ejes (los 24 giros de un cubo, más las /// reflexiones que el formato admite). Se lee del byte `"_r"` de un `nTRN`. /// /// Fila `a` = qué eje de origen y con qué signo aporta al eje de destino `a`, así /// que `apply` es un producto matriz·vector con enteros exactos (no hay redondeo: /// una rotación de MagicaVoxel nunca inventa un voxel intermedio). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Rot(pub [[i8; 3]; 3]); impl Rot { /// Sin rotación (el `_r` = 4 de MagicaVoxel, y lo que se asume si falta). pub const IDENTITY: Rot = Rot([[1, 0, 0], [0, 1, 0], [0, 0, 1]]); /// Desempaca el byte `"_r"`: bits 0-1 = eje no nulo de la fila 0, bits 2-3 = /// el de la fila 1 (el de la fila 2 es el que sobra), bits 4/5/6 = signo de /// cada fila. Un byte inválido (dos filas sobre el mismo eje) cae a identidad /// en vez de romper el archivo entero. pub fn from_byte(b: u8) -> Rot { let r0 = (b & 0b11) as usize; let r1 = ((b >> 2) & 0b11) as usize; if r0 > 2 || r1 > 2 || r0 == r1 { return Rot::IDENTITY; } let r2 = 3 - r0 - r1; let sign = |bit: u8| if b & (1 << bit) != 0 { -1i8 } else { 1i8 }; let mut m = [[0i8; 3]; 3]; m[0][r0] = sign(4); m[1][r1] = sign(5); m[2][r2] = sign(6); Rot(m) } /// Aplica la rotación a un punto (en el espacio del `.vox`, z-arriba). pub fn apply(&self, p: [i32; 3]) -> [i32; 3] { let m = &self.0; let row = |a: usize| { m[a][0] as i32 * p[0] + m[a][1] as i32 * p[1] + m[a][2] as i32 * p[2] }; [row(0), row(1), row(2)] } /// Composición `self ∘ hija` — la del padre por la del hijo, que es el orden /// en que el grafo las acumula al bajar. pub fn compose(&self, child: &Rot) -> Rot { let mut m = [[0i8; 3]; 3]; for a in 0..3 { for c in 0..3 { let mut acc = 0i32; for k in 0..3 { acc += self.0[a][k] as i32 * child.0[k][c] as i32; } m[a][c] = acc as i8; } } Rot(m) } /// Empaqueta la rotación de vuelta al byte `"_r"` del formato (inverso de /// [`from_byte`](Self::from_byte)) — lo que hace falta para **escribir** una /// escena que MagicaVoxel abra con las piezas giradas. pub fn to_byte(&self) -> u8 { let fila = |a: usize| (0..3).find(|&j| self.0[a][j] != 0).unwrap_or(a); let (r0, r1) = (fila(0), fila(1)); let mut b = (r0 as u8) | ((r1 as u8) << 2); for (a, bit) in [(0usize, 4u8), (1, 5), (2, 6)] { if self.0[a][fila(a)] < 0 { b |= 1 << bit; } } b } /// El tamaño de una caja tras rotarla (permuta las dimensiones; el signo no /// cambia el tamaño). pub fn apply_size(&self, s: [u32; 3]) -> [u32; 3] { let p = self.apply([s[0] as i32, s[1] as i32, s[2] as i32]); [p[0].unsigned_abs(), p[1].unsigned_abs(), p[2].unsigned_abs()] } } impl Default for Rot { fn default() -> Self { Rot::IDENTITY } } /// Una **colocación** de un modelo en la escena: índice del modelo en /// [`Scene::models`] + la traslación y la rotación de mundo acumuladas por el /// grafo de escena (`nTRN`→`nGRP`→`nSHP`). El origen del `.vox` queda en `[0,0,0]`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Placement { /// Índice del modelo en [`Scene::models`]. pub model: usize, /// Traslación de mundo (en voxels, eje `.vox`: z-arriba) acumulada por el grafo. pub translation: [i32; 3], /// Rotación de mundo acumulada. `Rot::IDENTITY` si el `.vox` no la trae. /// Gira alrededor del **centro** del modelo, que es donde `translation` lo ubica. pub rotation: Rot, } /// Una escena `.vox`: los modelos crudos + sus colocaciones según el grafo de /// escena. Si el archivo es viejo (sólo `SIZE`/`XYZI`, sin `nTRN/nGRP/nSHP`), /// `placements` queda vacío y cada modelo se entiende en el origen. #[derive(Clone, Debug)] pub struct Scene { /// Modelos crudos, uno por par `SIZE`+`XYZI`, en orden de archivo. pub models: Vec, /// Colocaciones derivadas del grafo de escena (vacío si no hay grafo). pub placements: Vec, } /// Nodo del grafo de escena de MagicaVoxel (lo justo para ubicar modelos). enum Node { /// `nTRN`: aplica una traslación y una rotación (frame 0) y baja a `child`. Transform { child: i32, translation: [i32; 3], rotation: Rot }, /// `nGRP`: agrupa varios hijos bajo la misma transformación. Group { children: Vec }, /// `nSHP`: hoja — referencia a uno o más modelos por índice. Shape { models: Vec }, } /// Lee un `.vox` y devuelve todos sus modelos (uno por par `SIZE`+`XYZI`). La /// paleta `RGBA`, si existe, se aplica a todos. Ignora el grafo de escena — para /// escenas multi-modelo con posición usa [`parse_scene`]. pub fn parse(bytes: &[u8]) -> Result, VoxError> { Ok(parse_scene(bytes)?.models) } /// Lee un `.vox` completo: modelos **y** grafo de escena (`nTRN/nGRP/nSHP`), /// resolviendo la traslación **y la rotación** de mundo de cada modelo colocado /// (una pieza girada en MagicaVoxel entra girada, no derecha). pub fn parse_scene(bytes: &[u8]) -> Result { if bytes.len() < 8 || &bytes[0..4] != b"VOX " { return Err(VoxError::BadMagic); } // Tras el header (8 bytes) viene el chunk MAIN; sus hijos son el resto. No // hace falta interpretar MAIN: barremos los chunks linealmente desde su // contenido en adelante (SIZE/XYZI/RGBA no tienen hijos). let mut pos = 8usize; // Saltar el header del chunk MAIN (id + nContent + nChildren = 12 bytes) y su // contenido (que es 0 en la práctica). let main_n = read_u32(bytes, pos + 4)? as usize; pos += 12 + main_n; let mut sizes: Vec<[u32; 3]> = Vec::new(); let mut groups: Vec> = Vec::new(); let mut palette: Option<[[u8; 4]; 256]> = None; let mut nodes: std::collections::HashMap = std::collections::HashMap::new(); while pos + 12 <= bytes.len() { let id = &bytes[pos..pos + 4]; let n = read_u32(bytes, pos + 4)? as usize; let m = read_u32(bytes, pos + 8)? as usize; let content_start = pos + 12; let content_end = content_start.checked_add(n).ok_or(VoxError::Truncated)?; if content_end > bytes.len() { return Err(VoxError::Truncated); } let content = &bytes[content_start..content_end]; match id { b"SIZE" => { if content.len() < 12 { return Err(VoxError::Truncated); } sizes.push([ read_u32(content, 0)?, read_u32(content, 4)?, read_u32(content, 8)?, ]); } b"XYZI" => { let count = read_u32(content, 0)? as usize; let mut vs = Vec::with_capacity(count); let need = 4 + count * 4; if content.len() < need { return Err(VoxError::Truncated); } for k in 0..count { let o = 4 + k * 4; vs.push(Voxel { x: content[o], y: content[o + 1], z: content[o + 2], i: content[o + 3] }); } groups.push(vs); } b"RGBA" => { if content.len() < 256 * 4 { return Err(VoxError::Truncated); } let mut pal = [[0u8; 4]; 256]; // Corrimiento MagicaVoxel: el índice de voxel `c` (1..255) → la // `c-1`-ésima entrada cruda. `palette[0]` queda vacío. for c in 1..256usize { let o = (c - 1) * 4; pal[c] = [content[o], content[o + 1], content[o + 2], content[o + 3]]; } palette = Some(pal); } b"nTRN" => { if let Ok(node) = parse_ntrn(content) { nodes.insert(node.0, node.1); } } b"nGRP" => { if let Ok(node) = parse_ngrp(content) { nodes.insert(node.0, node.1); } } b"nSHP" => { if let Ok(node) = parse_nshp(content) { nodes.insert(node.0, node.1); } } _ => {} // PACK, MATL, LAYR, rOBJ … no afectan geometría ni colocación. } pos = content_end + m; } if sizes.is_empty() || groups.is_empty() { return Err(VoxError::NoModel); } let pal = palette.unwrap_or_else(default_palette); let n_models = sizes.len().min(groups.len()); let models: Vec = (0..n_models) .map(|k| VoxModel { size: sizes[k], voxels: groups[k].clone(), palette: pal }) .collect(); // Recorrer el grafo desde el nodo raíz (id 0, siempre un nTRN) acumulando // traslaciones hasta cada nSHP → una colocación por modelo referido. let mut placements = Vec::new(); if nodes.contains_key(&0) { let mut stack = vec![(0i32, [0i32; 3], Rot::IDENTITY)]; // Cota dura por si un .vox trae ciclos (no debería): a lo sumo |nodos| visitas. let mut budget = nodes.len() * 4 + 8; while let Some((id, off, rot)) = stack.pop() { if budget == 0 { break; } budget -= 1; match nodes.get(&id) { Some(Node::Transform { child, translation, rotation }) => { // La traslación del hijo se expresa en el marco YA rotado del padre. let t = rot.apply(*translation); let acc = [off[0] + t[0], off[1] + t[1], off[2] + t[2]]; stack.push((*child, acc, rot.compose(rotation))); } Some(Node::Group { children }) => { for c in children { stack.push((*c, off, rot)); } } Some(Node::Shape { models: ms }) => { for &mid in ms { if (mid as usize) < models.len() { placements.push(Placement { model: mid as usize, translation: off, rotation: rot, }); } } } None => {} } } } Ok(Scene { models, placements }) } /// Serializa un modelo a bytes `.vox` (header + `MAIN` con `SIZE`+`XYZI`+`RGBA`). /// Útil para **exportar** una escena voxel y editarla en MagicaVoxel, y para las /// pruebas de ida y vuelta. pub fn write(model: &VoxModel) -> Vec { // SIZE. let mut size = Vec::with_capacity(12); for d in model.size { size.extend_from_slice(&d.to_le_bytes()); } // XYZI. let mut xyzi = Vec::with_capacity(4 + model.voxels.len() * 4); xyzi.extend_from_slice(&(model.voxels.len() as u32).to_le_bytes()); for v in &model.voxels { xyzi.extend_from_slice(&[v.x, v.y, v.z, v.i]); } // RGBA: crudo[j] = palette[j+1] (inverso del corrimiento de lectura). let mut rgba = Vec::with_capacity(256 * 4); for j in 0..256usize { let c = j + 1; rgba.extend_from_slice(&model.palette.get(c).copied().unwrap_or([0, 0, 0, 0])); } let mut children = Vec::new(); children.extend_from_slice(&chunk(b"SIZE", &size)); children.extend_from_slice(&chunk(b"XYZI", &xyzi)); children.extend_from_slice(&chunk(b"RGBA", &rgba)); let mut out = Vec::new(); out.extend_from_slice(b"VOX "); out.extend_from_slice(&150u32.to_le_bytes()); // versión // MAIN: contenido vacío, hijos = los chunks de arriba. out.extend_from_slice(b"MAIN"); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(children.len() as u32).to_le_bytes()); out.extend_from_slice(&children); out } /// Codifica un chunk **sin hijos**: `id` + `len(content)` + `0` + `content`. fn chunk(id: &[u8; 4], content: &[u8]) -> Vec { let mut c = Vec::with_capacity(12 + content.len()); c.extend_from_slice(id); c.extend_from_slice(&(content.len() as u32).to_le_bytes()); c.extend_from_slice(&0u32.to_le_bytes()); c.extend_from_slice(content); c } /// Lee un `u32` little-endian en `off`, o [`VoxError::Truncated`] si no entra. fn read_u32(b: &[u8], off: usize) -> Result { b.get(off..off + 4) .map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]])) .ok_or(VoxError::Truncated) } // --- Lectura del grafo de escena (nTRN/nGRP/nSHP) ------------------------------- // El formato extendido de MagicaVoxel usa, dentro del contenido de cada nodo, // primitivas: int32 LE, STRING (int32 len + bytes) y DICT (int32 nPairs + pares de // STRING). Los lectores de abajo avanzan un cursor `pos`. /// Lee un `i32` LE en el cursor y lo avanza 4 bytes. fn take_i32(b: &[u8], pos: &mut usize) -> Result { let v = read_u32(b, *pos)? as i32; *pos += 4; Ok(v) } /// Lee una `STRING` del `.vox` (int32 len + bytes) en el cursor y lo avanza. fn take_string<'a>(b: &'a [u8], pos: &mut usize) -> Result<&'a [u8], VoxError> { let len = read_u32(b, *pos)? as usize; *pos += 4; let s = b.get(*pos..*pos + len).ok_or(VoxError::Truncated)?; *pos += len; Ok(s) } /// Lee un `DICT` (int32 nPairs + pares STRING/STRING) en el cursor; devuelve los /// pares como bytes crudos (clave, valor). fn take_dict(b: &[u8], pos: &mut usize) -> Result, Vec)>, VoxError> { let n = read_u32(b, *pos)? as usize; *pos += 4; let mut out = Vec::with_capacity(n); for _ in 0..n { let k = take_string(b, pos)?.to_vec(); let v = take_string(b, pos)?.to_vec(); out.push((k, v)); } Ok(out) } /// Extrae la traslación `"_t" = "x y z"` de un dict de frame (default `[0,0,0]`). fn translation_from_dict(d: &[(Vec, Vec)]) -> [i32; 3] { for (k, v) in d { if k.as_slice() == b"_t" { let s = core::str::from_utf8(v).unwrap_or(""); let mut it = s.split_whitespace().map(|x| x.parse::().unwrap_or(0)); return [it.next().unwrap_or(0), it.next().unwrap_or(0), it.next().unwrap_or(0)]; } } [0, 0, 0] } /// Extrae la rotación `"_r" = ""` de un dict de frame (default: identidad). fn rotation_from_dict(d: &[(Vec, Vec)]) -> Rot { for (k, v) in d { if k.as_slice() == b"_r" { let s = core::str::from_utf8(v).unwrap_or(""); return match s.trim().parse::() { Ok(b) => Rot::from_byte(b), Err(_) => Rot::IDENTITY, }; } } Rot::IDENTITY } /// `nTRN`: nodeId, dict, childId, reservedId, layerId, numFrames, frames[]. Tomamos /// la traslación del frame 0. fn parse_ntrn(content: &[u8]) -> Result<(i32, Node), VoxError> { let mut pos = 0; let id = take_i32(content, &mut pos)?; let _attrs = take_dict(content, &mut pos)?; let child = take_i32(content, &mut pos)?; let _reserved = take_i32(content, &mut pos)?; let _layer = take_i32(content, &mut pos)?; let n_frames = take_i32(content, &mut pos)?.max(0) as usize; let mut translation = [0; 3]; let mut rotation = Rot::IDENTITY; for f in 0..n_frames { let frame = take_dict(content, &mut pos)?; if f == 0 { translation = translation_from_dict(&frame); rotation = rotation_from_dict(&frame); } } Ok((id, Node::Transform { child, translation, rotation })) } /// `nGRP`: nodeId, dict, numChildren, childIds[]. fn parse_ngrp(content: &[u8]) -> Result<(i32, Node), VoxError> { let mut pos = 0; let id = take_i32(content, &mut pos)?; let _attrs = take_dict(content, &mut pos)?; let n = take_i32(content, &mut pos)?.max(0) as usize; let mut children = Vec::with_capacity(n); for _ in 0..n { children.push(take_i32(content, &mut pos)?); } Ok((id, Node::Group { children })) } /// `nSHP`: nodeId, dict, numModels, (modelId, dict)[]. fn parse_nshp(content: &[u8]) -> Result<(i32, Node), VoxError> { let mut pos = 0; let id = take_i32(content, &mut pos)?; let _attrs = take_dict(content, &mut pos)?; let n = take_i32(content, &mut pos)?.max(0) as usize; let mut models = Vec::with_capacity(n); for _ in 0..n { models.push(take_i32(content, &mut pos)?); let _model_attrs = take_dict(content, &mut pos)?; } Ok((id, Node::Shape { models })) } /// Paleta por defecto **oficial de MagicaVoxel** (sólo se usa si el `.vox` no trae /// chunk `RGBA`). Es la tabla canónica que publica el formato (ephtracy/voxel-model): /// los exportes reales casi siempre incluyen `RGBA`, pero los modelos viejos / /// generados a mano que omiten la paleta ahora abren con los colores correctos en /// vez de una rampa inventada. `[0]` = vacío. Ver [`DEFAULT_PALETTE_ABGR`]. fn default_palette() -> [[u8; 4]; 256] { let mut p = [[0u8; 4]; 256]; for (c, &v) in DEFAULT_PALETTE_ABGR.iter().enumerate() { // La tabla viene en 0xAABBGGRR (igual que el spec); la desempacamos a RGBA. p[c] = [ (v & 0xff) as u8, ((v >> 8) & 0xff) as u8, ((v >> 16) & 0xff) as u8, ((v >> 24) & 0xff) as u8, ]; } p } /// Paleta default canónica de MagicaVoxel, en `0xAABBGGRR` por entrada (formato del /// spec). `[0]` = transparente. Es una rampa de matices ×3 niveles + gradientes de /// grises/RGB al final — idéntica a la que muestra MagicaVoxel al abrir un modelo /// sin paleta propia. #[rustfmt::skip] const DEFAULT_PALETTE_ABGR: [u32; 256] = [ 0x00000000, 0xffffffff, 0xffccffff, 0xff99ffff, 0xff66ffff, 0xff33ffff, 0xff00ffff, 0xffffccff, 0xffccccff, 0xff99ccff, 0xff66ccff, 0xff33ccff, 0xff00ccff, 0xffff99ff, 0xffcc99ff, 0xff9999ff, 0xff6699ff, 0xff3399ff, 0xff0099ff, 0xffff66ff, 0xffcc66ff, 0xff9966ff, 0xff6666ff, 0xff3366ff, 0xff0066ff, 0xffff33ff, 0xffcc33ff, 0xff9933ff, 0xff6633ff, 0xff3333ff, 0xff0033ff, 0xffff00ff, 0xffcc00ff, 0xff9900ff, 0xff6600ff, 0xff3300ff, 0xff0000ff, 0xffffffcc, 0xffccffcc, 0xff99ffcc, 0xff66ffcc, 0xff33ffcc, 0xff00ffcc, 0xffffcccc, 0xffcccccc, 0xff99cccc, 0xff66cccc, 0xff33cccc, 0xff00cccc, 0xffff99cc, 0xffcc99cc, 0xff9999cc, 0xff6699cc, 0xff3399cc, 0xff0099cc, 0xffff66cc, 0xffcc66cc, 0xff9966cc, 0xff6666cc, 0xff3366cc, 0xff0066cc, 0xffff33cc, 0xffcc33cc, 0xff9933cc, 0xff6633cc, 0xff3333cc, 0xff0033cc, 0xffff00cc, 0xffcc00cc, 0xff9900cc, 0xff6600cc, 0xff3300cc, 0xff0000cc, 0xffffff99, 0xffccff99, 0xff99ff99, 0xff66ff99, 0xff33ff99, 0xff00ff99, 0xffffcc99, 0xffcccc99, 0xff99cc99, 0xff66cc99, 0xff33cc99, 0xff00cc99, 0xffff9999, 0xffcc9999, 0xff999999, 0xff669999, 0xff339999, 0xff009999, 0xffff6699, 0xffcc6699, 0xff996699, 0xff666699, 0xff336699, 0xff006699, 0xffff3399, 0xffcc3399, 0xff993399, 0xff663399, 0xff333399, 0xff003399, 0xffff0099, 0xffcc0099, 0xff990099, 0xff660099, 0xff330099, 0xff000099, 0xffffff66, 0xffccff66, 0xff99ff66, 0xff66ff66, 0xff33ff66, 0xff00ff66, 0xffffcc66, 0xffcccc66, 0xff99cc66, 0xff66cc66, 0xff33cc66, 0xff00cc66, 0xffff9966, 0xffcc9966, 0xff999966, 0xff669966, 0xff339966, 0xff009966, 0xffff6666, 0xffcc6666, 0xff996666, 0xff666666, 0xff336666, 0xff006666, 0xffff3366, 0xffcc3366, 0xff993366, 0xff663366, 0xff333366, 0xff003366, 0xffff0066, 0xffcc0066, 0xff990066, 0xff660066, 0xff330066, 0xff000066, 0xffffff33, 0xffccff33, 0xff99ff33, 0xff66ff33, 0xff33ff33, 0xff00ff33, 0xffffcc33, 0xffcccc33, 0xff99cc33, 0xff66cc33, 0xff33cc33, 0xff00cc33, 0xffff9933, 0xffcc9933, 0xff999933, 0xff669933, 0xff339933, 0xff009933, 0xffff6633, 0xffcc6633, 0xff996633, 0xff666633, 0xff336633, 0xff006633, 0xffff3333, 0xffcc3333, 0xff993333, 0xff663333, 0xff333333, 0xff003333, 0xffff0033, 0xffcc0033, 0xff990033, 0xff660033, 0xff330033, 0xff000033, 0xffffff00, 0xffccff00, 0xff99ff00, 0xff66ff00, 0xff33ff00, 0xff00ff00, 0xffffcc00, 0xffcccc00, 0xff99cc00, 0xff66cc00, 0xff33cc00, 0xff00cc00, 0xffff9900, 0xffcc9900, 0xff999900, 0xff669900, 0xff339900, 0xff009900, 0xffff6600, 0xffcc6600, 0xff996600, 0xff666600, 0xff336600, 0xff006600, 0xffff3300, 0xffcc3300, 0xff993300, 0xff663300, 0xff333300, 0xff003300, 0xffff0000, 0xffcc0000, 0xff990000, 0xff660000, 0xff330000, 0xff0000ee, 0xff0000dd, 0xff0000bb, 0xff0000aa, 0xff000088, 0xff000077, 0xff000055, 0xff000044, 0xff000022, 0xff000011, 0xff00ee00, 0xff00dd00, 0xff00bb00, 0xff00aa00, 0xff008800, 0xff007700, 0xff005500, 0xff004400, 0xff002200, 0xff001100, 0xffee0000, 0xffdd0000, 0xffbb0000, 0xffaa0000, 0xff880000, 0xff770000, 0xff550000, 0xff440000, 0xff220000, 0xff110000, 0xffeeeeee, 0xffdddddd, 0xffbbbbbb, 0xffaaaaaa, 0xff888888, 0xff777777, 0xff555555, 0xff444444, 0xff222222, 0xff111111, ]; /// Serializa una **escena multi-modelo**: los modelos + un grafo `nTRN/nGRP/nSHP` /// que los ubica (traslación y rotación de cada colocación). Es el inverso de /// [`parse_scene`], y lo que hace falta para **devolverle** al editor ajeno una /// escena compuesta acá (una casa amueblada, un reparto, un set entero). /// /// La paleta del archivo es la del **primer** modelo — el formato tiene una sola por /// archivo, así que quien compone tiene que traer los modelos ya reconciliados (es lo /// que hace `grid_to_model` del lado del motor, que arma una paleta común). /// Sin colocaciones, cae a [`write`] del primer modelo (un `.vox` clásico). pub fn write_scene(models: &[VoxModel], placements: &[Placement]) -> Vec { if models.is_empty() { return Vec::new(); } if placements.is_empty() { return write(&models[0]); } let mut children = Vec::new(); for m in models { let mut size = Vec::with_capacity(12); for d in m.size { size.extend_from_slice(&d.to_le_bytes()); } let mut xyzi = Vec::with_capacity(4 + m.voxels.len() * 4); xyzi.extend_from_slice(&(m.voxels.len() as u32).to_le_bytes()); for v in &m.voxels { xyzi.extend_from_slice(&[v.x, v.y, v.z, v.i]); } children.extend_from_slice(&chunk(b"SIZE", &size)); children.extend_from_slice(&chunk(b"XYZI", &xyzi)); } let mut rgba = Vec::with_capacity(256 * 4); for j in 0..256usize { rgba.extend_from_slice(&models[0].palette.get(j + 1).copied().unwrap_or([0, 0, 0, 0])); } children.extend_from_slice(&chunk(b"RGBA", &rgba)); // Grafo: nTRN(0) → nGRP(1) → { nTRN(2+2k) → nSHP(3+2k) } por colocación. let i32b = |v: i32| v.to_le_bytes().to_vec(); let string = |txt: &str| { let mut o = (txt.len() as u32).to_le_bytes().to_vec(); o.extend_from_slice(txt.as_bytes()); o }; let dict = |pares: &[(&str, String)]| { let mut o = (pares.len() as u32).to_le_bytes().to_vec(); for (k, v) in pares { o.extend_from_slice(&string(k)); o.extend_from_slice(&string(v)); } o }; let ntrn = |id: i32, child: i32, frame: Vec| { let mut c = Vec::new(); c.extend_from_slice(&i32b(id)); c.extend_from_slice(&dict(&[])); c.extend_from_slice(&i32b(child)); c.extend_from_slice(&i32b(-1)); c.extend_from_slice(&i32b(0)); c.extend_from_slice(&i32b(1)); c.extend_from_slice(&frame); c }; children.extend_from_slice(&chunk(b"nTRN", &ntrn(0, 1, dict(&[])))); let mut grp = Vec::new(); grp.extend_from_slice(&i32b(1)); grp.extend_from_slice(&dict(&[])); grp.extend_from_slice(&i32b(placements.len() as i32)); for k in 0..placements.len() { grp.extend_from_slice(&i32b(2 + 2 * k as i32)); } children.extend_from_slice(&chunk(b"nGRP", &grp)); for (k, p) in placements.iter().enumerate() { let (id_t, id_s) = (2 + 2 * k as i32, 3 + 2 * k as i32); let t = format!("{} {} {}", p.translation[0], p.translation[1], p.translation[2]); let mut pares: Vec<(&str, String)> = vec![("_t", t)]; if p.rotation != Rot::IDENTITY { pares.push(("_r", p.rotation.to_byte().to_string())); } children.extend_from_slice(&chunk(b"nTRN", &ntrn(id_t, id_s, dict(&pares)))); let mut shp = Vec::new(); shp.extend_from_slice(&i32b(id_s)); shp.extend_from_slice(&dict(&[])); shp.extend_from_slice(&i32b(1)); shp.extend_from_slice(&i32b(p.model as i32)); shp.extend_from_slice(&dict(&[])); children.extend_from_slice(&chunk(b"nSHP", &shp)); } let mut out = Vec::new(); out.extend_from_slice(b"VOX "); out.extend_from_slice(&150u32.to_le_bytes()); out.extend_from_slice(b"MAIN"); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&(children.len() as u32).to_le_bytes()); out.extend_from_slice(&children); out } #[cfg(test)] mod tests { use super::*; fn modelo_demo() -> VoxModel { let mut m = VoxModel::new([4, 5, 6]); m.palette[1] = [200, 30, 30, 255]; m.palette[2] = [30, 180, 60, 255]; m.voxels = vec![ Voxel { x: 0, y: 0, z: 0, i: 1 }, Voxel { x: 3, y: 4, z: 5, i: 2 }, Voxel { x: 1, y: 2, z: 3, i: 1 }, ]; m } #[test] fn ida_y_vuelta() { let m = modelo_demo(); let bytes = write(&m); let back = parse(&bytes).expect("parse"); assert_eq!(back.len(), 1); let r = &back[0]; assert_eq!(r.size, [4, 5, 6]); assert_eq!(r.voxels, m.voxels); // Los colores de los índices usados sobreviven el corrimiento RGBA. assert_eq!(r.palette[1], [200, 30, 30, 255]); assert_eq!(r.palette[2], [30, 180, 60, 255]); } #[test] fn rechaza_no_vox() { assert_eq!(parse(b"NOPE....").unwrap_err(), VoxError::BadMagic); } #[test] fn parsea_bytes_a_mano_sin_rgba() { // .vox mínimo escrito a mano: header + MAIN(vacío) + SIZE(1,1,1) + // XYZI(1 voxel en 0,0,0 índice 1). Sin RGBA → paleta por defecto. let mut b = Vec::new(); b.extend_from_slice(b"VOX "); b.extend_from_slice(&150u32.to_le_bytes()); // MAIN: n=0, m = bytes de los dos chunks hijos (24+16=... lo calculamos). let size_chunk = { let mut c = Vec::new(); c.extend_from_slice(b"SIZE"); c.extend_from_slice(&12u32.to_le_bytes()); c.extend_from_slice(&0u32.to_le_bytes()); c.extend_from_slice(&1u32.to_le_bytes()); c.extend_from_slice(&1u32.to_le_bytes()); c.extend_from_slice(&1u32.to_le_bytes()); c }; let xyzi_chunk = { let mut c = Vec::new(); c.extend_from_slice(b"XYZI"); c.extend_from_slice(&8u32.to_le_bytes()); // 4 (count) + 4 (un voxel) c.extend_from_slice(&0u32.to_le_bytes()); c.extend_from_slice(&1u32.to_le_bytes()); // count c.extend_from_slice(&[0, 0, 0, 1]); // voxel c }; let children_len = size_chunk.len() + xyzi_chunk.len(); b.extend_from_slice(b"MAIN"); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&(children_len as u32).to_le_bytes()); b.extend_from_slice(&size_chunk); b.extend_from_slice(&xyzi_chunk); let models = parse(&b).expect("parse"); assert_eq!(models.len(), 1); assert_eq!(models[0].size, [1, 1, 1]); assert_eq!(models[0].voxels, vec![Voxel { x: 0, y: 0, z: 0, i: 1 }]); assert_ne!(models[0].palette[1], [0, 0, 0, 0]); // default no vacío } #[test] fn escena_multi_modelo_con_traslacion() { // .vox extendido a mano: dos modelos + grafo // nTRN(0)→nGRP(1)→{ nTRN(2)→nSHP(3:model0), nTRN(4,_t="5 0 0")→nSHP(5:model1) }. // Esperado: model0 en [0,0,0], model1 en [5,0,0]. let i32b = |v: i32| v.to_le_bytes().to_vec(); let s = |txt: &str| { let mut o = (txt.len() as u32).to_le_bytes().to_vec(); o.extend_from_slice(txt.as_bytes()); o }; let dict0 = || 0u32.to_le_bytes().to_vec(); // dict vacío let dict_t = |t: &str| { let mut o = 1u32.to_le_bytes().to_vec(); o.extend_from_slice(&s("_t")); o.extend_from_slice(&s(t)); o }; let cat = |parts: &[Vec]| parts.concat(); let size = { let mut c = Vec::new(); c.extend_from_slice(&i32b(2)); c.extend_from_slice(&i32b(2)); c.extend_from_slice(&i32b(2)); c }; let xyzi = { let mut c = 1u32.to_le_bytes().to_vec(); c.extend_from_slice(&[0, 0, 0, 1]); c }; // nTRN(id, child, frame): id, dict0, child, -1, -1, 1, frame. let ntrn = |id: i32, child: i32, frame: Vec| { cat(&[i32b(id), dict0(), i32b(child), i32b(-1), i32b(-1), i32b(1), frame]) }; let nshp = |id: i32, model: i32| { // id, dict0, numModels=1, (modelId, dict0) cat(&[i32b(id), dict0(), i32b(1), i32b(model), dict0()]) }; let ngrp = |id: i32, kids: &[i32]| { let mut c = cat(&[i32b(id), dict0(), i32b(kids.len() as i32)]); for &k in kids { c.extend_from_slice(&i32b(k)); } c }; let children = cat(&[ chunk(b"SIZE", &size), chunk(b"XYZI", &xyzi), chunk(b"SIZE", &size), chunk(b"XYZI", &xyzi), chunk(b"nTRN", &ntrn(0, 1, dict0())), chunk(b"nGRP", &ngrp(1, &[2, 4])), chunk(b"nTRN", &ntrn(2, 3, dict0())), chunk(b"nSHP", &nshp(3, 0)), chunk(b"nTRN", &ntrn(4, 5, dict_t("5 0 0"))), chunk(b"nSHP", &nshp(5, 1)), ]); let mut b = Vec::new(); b.extend_from_slice(b"VOX "); b.extend_from_slice(&150u32.to_le_bytes()); b.extend_from_slice(b"MAIN"); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&(children.len() as u32).to_le_bytes()); b.extend_from_slice(&children); let scene = parse_scene(&b).expect("parse_scene"); assert_eq!(scene.models.len(), 2); assert_eq!(scene.placements.len(), 2, "dos colocaciones"); let mut by_model: std::collections::HashMap = Default::default(); for p in &scene.placements { by_model.insert(p.model, p.translation); } assert_eq!(by_model.get(&0), Some(&[0, 0, 0])); assert_eq!(by_model.get(&1), Some(&[5, 0, 0])); } #[test] fn el_byte_de_rotacion_se_desempaca() { // 4 = identidad (el que escribe MagicaVoxel cuando no giraste nada). assert_eq!(Rot::from_byte(4), Rot::IDENTITY); // Un cuarto de vuelta alrededor de z: x→y, y→−x. // fila0 toma el eje 1 con signo −, fila1 toma el eje 0 con +, fila2 el 2 con +. // bits: r0=1, r1=0 (<<2 = 0), signo fila0 negativo (bit 4). let r = Rot::from_byte(0b001_0001); assert_eq!(r.apply([1, 0, 0]), [0, 1, 0], "x va a +y"); assert_eq!(r.apply([0, 1, 0]), [-1, 0, 0], "y va a −x"); assert_eq!(r.apply([0, 0, 1]), [0, 0, 1], "z queda"); // Cuatro cuartos de vuelta = identidad. let cuatro = r.compose(&r).compose(&r).compose(&r); assert_eq!(cuatro, Rot::IDENTITY, "cuatro cuartos vuelven al principio"); // Un byte imposible (dos filas sobre el mismo eje) no rompe el archivo. assert_eq!(Rot::from_byte(0b0000_0000), Rot::IDENTITY); } #[test] fn la_rotacion_permuta_el_tamano_de_la_caja() { let r = Rot::from_byte(0b001_0001); // cuarto de vuelta en z assert_eq!(r.apply_size([2, 5, 7]), [5, 2, 7]); assert_eq!(Rot::IDENTITY.apply_size([2, 5, 7]), [2, 5, 7]); } #[test] fn escena_con_rotacion_del_grafo() { // Mismo esqueleto que el test de traslación, pero el 2º nTRN trae _r. // nTRN(0)→nGRP(1)→{ nTRN(2)→nSHP(3:m0), nTRN(4,_t="5 0 0",_r=cuarto)→nSHP(5:m1) }. let i32b = |v: i32| v.to_le_bytes().to_vec(); let s = |txt: &str| { let mut o = (txt.len() as u32).to_le_bytes().to_vec(); o.extend_from_slice(txt.as_bytes()); o }; let dict0 = || 0u32.to_le_bytes().to_vec(); let dict_tr = |t: &str, r: &str| { let mut o = 2u32.to_le_bytes().to_vec(); o.extend_from_slice(&s("_t")); o.extend_from_slice(&s(t)); o.extend_from_slice(&s("_r")); o.extend_from_slice(&s(r)); o }; let cat = |parts: &[Vec]| parts.concat(); let size = cat(&[i32b(2), i32b(2), i32b(2)]); let xyzi = { let mut c = 1u32.to_le_bytes().to_vec(); c.extend_from_slice(&[0, 0, 0, 1]); c }; let ntrn = |id: i32, child: i32, frame: Vec| { cat(&[i32b(id), dict0(), i32b(child), i32b(-1), i32b(-1), i32b(1), frame]) }; let nshp = |id: i32, model: i32| cat(&[i32b(id), dict0(), i32b(1), i32b(model), dict0()]); let ngrp = |id: i32, kids: &[i32]| { let mut c = cat(&[i32b(id), dict0(), i32b(kids.len() as i32)]); for &k in kids { c.extend_from_slice(&i32b(k)); } c }; let children = cat(&[ chunk(b"SIZE", &size), chunk(b"XYZI", &xyzi), chunk(b"SIZE", &size), chunk(b"XYZI", &xyzi), chunk(b"nTRN", &ntrn(0, 1, dict0())), chunk(b"nGRP", &ngrp(1, &[2, 4])), chunk(b"nTRN", &ntrn(2, 3, dict0())), chunk(b"nSHP", &nshp(3, 0)), chunk(b"nTRN", &ntrn(4, 5, dict_tr("5 0 0", "17"))), // 17 = 0b0010001 chunk(b"nSHP", &nshp(5, 1)), ]); let mut b = Vec::new(); b.extend_from_slice(b"VOX "); b.extend_from_slice(&150u32.to_le_bytes()); b.extend_from_slice(b"MAIN"); b.extend_from_slice(&0u32.to_le_bytes()); b.extend_from_slice(&(children.len() as u32).to_le_bytes()); b.extend_from_slice(&children); let scene = parse_scene(&b).expect("parse_scene"); let mut by_model: std::collections::HashMap = Default::default(); for p in &scene.placements { by_model.insert(p.model, *p); } let p0 = by_model[&0]; let p1 = by_model[&1]; assert_eq!(p0.rotation, Rot::IDENTITY, "el que no giró sigue derecho"); assert_eq!(p0.translation, [0, 0, 0]); assert_eq!(p1.rotation, Rot::from_byte(17), "la rotación del nodo llega a la colocación"); assert_eq!(p1.translation, [5, 0, 0], "la traslación del propio nodo NO se auto-rota"); } #[test] fn el_byte_de_rotacion_va_y_vuelve() { // Las 24 rotaciones (y las reflexiones que el formato admite) sobreviven el // viaje byte → matriz → byte. for b in 0u8..128 { let r = Rot::from_byte(b); if r == Rot::IDENTITY && b != 4 { continue; // bytes imposibles: caen a identidad a propósito } assert_eq!(Rot::from_byte(r.to_byte()), r, "byte {b}"); } } #[test] fn escena_escrita_se_relee_igual() { // Componer acá y devolverle la escena al editor ajeno: los dos modelos, sus // traslaciones y la rotación de uno tienen que volver idénticos. let pieza = |i: u8| { let mut m = VoxModel::new([2, 3, 4]); m.palette[1] = [10, 20, 30, 255]; m.palette[2] = [200, 100, 50, 255]; m.voxels = vec![Voxel { x: 0, y: 1, z: 2, i }, Voxel { x: 1, y: 0, z: 0, i: 1 }]; m }; let rot = Rot::from_byte(17); let placements = vec![ Placement { model: 0, translation: [3, -4, 5], rotation: Rot::IDENTITY }, Placement { model: 1, translation: [-9, 0, 2], rotation: rot }, ]; let bytes = write_scene(&[pieza(1), pieza(2)], &placements); let vuelta = parse_scene(&bytes).expect("releer la escena escrita"); assert_eq!(vuelta.models.len(), 2); assert_eq!(vuelta.models[0].size, [2, 3, 4]); assert_eq!(vuelta.models[1].voxels, pieza(2).voxels); assert_eq!(vuelta.models[0].palette[2], [200, 100, 50, 255], "la paleta viaja"); let mut por_modelo: std::collections::HashMap = Default::default(); for p in &vuelta.placements { por_modelo.insert(p.model, *p); } assert_eq!(por_modelo[&0], placements[0]); assert_eq!(por_modelo[&1], placements[1], "traslación Y rotación sobreviven"); } #[test] fn escena_sin_colocaciones_cae_al_vox_clasico() { let m = modelo_demo(); let bytes = write_scene(std::slice::from_ref(&m), &[]); assert_eq!(bytes, write(&m)); } #[test] fn vox_viejo_sin_grafo_no_tiene_placements() { // Un .vox de un solo modelo sin nTRN/nGRP/nSHP → placements vacío. let m = modelo_demo(); let scene = parse_scene(&write(&m)).expect("parse_scene"); assert_eq!(scene.models.len(), 1); assert!(scene.placements.is_empty()); } #[test] fn paleta_default_es_la_oficial() { let p = default_palette(); assert_eq!(p[0], [0, 0, 0, 0]); // índice 0 = vacío/transparente assert_eq!(p[1], [255, 255, 255, 255]); // 0xffffffff = blanco assert_eq!(p[6], [255, 255, 0, 255]); // 0xff00ffff (AABBGGRR) = amarillo assert_eq!(p[255], [17, 17, 17, 255]); // 0xff111111 = gris oscuro // Toda entrada usable (1..256) es opaca y no vacía. for c in 1..256 { assert_eq!(p[c][3], 255, "alpha en índice {c}"); } } }