//! **Muebles y objetos de casa**: un catálogo de modelitos voxel paramétricos //! (mesa, silla, cama, estante, lámpara, alfombra, cofre, estufa, sofá, maceta, //! barril, escritorio) que se **estampan** en una escena con [`stamp_mueble`]. //! //! A diferencia de la [`Forma`](crate::Forma) de flora (un solo color), un mueble //! combina **varios materiales** por pieza (madera + acero + tela…) y lleva el //! **brillo** de cada material en el alpha del voxel, así el acero de un cofre o la //! pantalla de una lámpara **glasean** con el mismo ray-march que las gemas. Se //! orientan con `facing` (0=+Z, 1=+X, 2=−Z, 3=−X). use serde::{Deserialize, Serialize}; use crate::worldgen::Material; use llimphi_3d::VoxelGrid; /// Catálogo de muebles y objetos de casa. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Mueble { /// Mesa: tapa de madera sobre cuatro patas. Mesa, /// Silla: asiento, respaldo y patas de madera. Silla, /// Cama: bastidor de madera, colchón claro y almohada. Cama, /// Estante: librero de madera con libros de colores. Estante, /// Lámpara de pie: base y poste de acero, pantalla cálida. Lampara, /// Alfombra: losa plana coloreada. Alfombra, /// Cofre: caja de madera con herrajes de acero. Cofre, /// Estufa: cuerpo de ladrillo con boca de fuego. Estufa, /// Sofá: bloque tapizado con brazos y respaldo. Sofa, /// Maceta: tiesto de cerámica con tierra y una plantita. Maceta, /// Barril: duela de madera con aros de acero. Barril, /// Escritorio: tapa de madera con un bloque de cajones. Escritorio, // --- Plantas de interior --- /// Ficus: árbol de interior en maceta (copa frondosa). Ficus, /// Palma de interior en maceta. Palma, /// Suculenta en maceta baja. Suculenta, /// Bonsái en maceta plana. Bonsai, /// Planta colgante: cesta a altura con enredaderas que caen. Colgante, /// Cactus de interior en maceta. Nopal, // --- Decoración de pared --- /// Cuadro: marco con lienzo coloreado, montado a altura. Cuadro, /// Espejo: marco con vidrio lustroso. Espejo, /// Reloj de pared: esfera clara con agujas. Reloj, /// Repisa flotante: estante con un par de objetos. Repisa, /// Aplique: brazo de acero con una luz cálida. Aplique, /// Tapiz: paño colgante de colores. Tapiz, // --- Elementos de casas y edificios modernos --- /// Escalera: tramo de peldaños de hormigón. Escalera, /// Baranda: barandal de vidrio con montantes de acero. Baranda, /// Ventanal: paño de vidrio de piso a techo (muro cortina). Ventanal, /// Columna: pilar cuadrado de hormigón. Columna, /// Aire acondicionado: split blanco de pared. Aire, /// Panel solar: placa azul inclinada sobre soporte. Solar, /// Televisor: pantalla plana negra sobre soporte. Tele, /// Isla de cocina: mueble bajo con mesada de mármol. Isla, /// Ducha: mampara de vidrio con base y ducha de acero. Ducha, /// Placard: armario empotrado alto. Placard, /// Farol: poste de acero con luz cálida (ilumina). Farol, /// Pileta: espejo de agua con borde de cerámica. Pileta, // --- Elementos de ciudad --- /// Semáforo: poste con luces roja/amarilla/verde. Semaforo, /// Banco de plaza: tablas sobre patas de acero. Banco, /// Fuente: pileta de piedra con chorro de agua. Fuente, /// Buzón azul sobre poste. Buzon, /// Papelera: tacho cilíndrico de acero. Basura, /// Parada de colectivo: marquesina de vidrio con banco. Parada, /// Hidrante: boca de incendio roja. Hidrante, /// Cabina telefónica de vidrio y techo azul. Cabina, /// Cartel: poste con letrero. Cartel, /// Auto: carrocería azul con ventanas de vidrio y ruedas. Auto, /// Estatua de mármol sobre pedestal. Estatua, /// Bolardo: poste bajo de hormigón. Bolardo, // --- Edificios grandes --- /// Rascacielos: torre alta de vidrio y acero. Rascacielos, /// Edificio: bloque de departamentos de ladrillo con ventanas. Edificio, /// Torre: torre delgada de hormigón con antena. Torre, /// Fábrica: nave baja con chimenea de ladrillo. Fabrica, /// Iglesia: nave con campanario y cruz. Iglesia, /// Galpón: nave ancha con techo de zinc. Galpon, /// Silo: cilindro de acero con cúpula. Silo, /// Estacionamiento: losas y columnas en varios niveles. Estacionamiento, /// Farola: alumbrado público alto (ilumina). Farola, /// Contenedor: volquete metálico. Contenedor, /// Grúa: torre grúa con pluma y contrapeso. Grua, /// Puente: tablero con barandas y pilas. Puente, // --- Transporte --- /// Avión: fuselaje con alas y cola. Avion, /// Helicóptero: cabina, cola y rotor. Helicoptero, /// Camioneta (pickup): cabina y caja abierta. Camioneta, /// Camión: cabina y caja de carga. Camion, /// Colectivo (bus): carrocería larga con ventanas. Colectivo, /// Taxi: auto amarillo con cartel. Taxi, /// Tren: locomotora sobre ruedas. Tren, /// Vagón: caja de tren sobre ruedas. Vagon, /// Moto: dos ruedas y manubrio. Moto, /// Bici: cuadro y dos ruedas. Bici, /// Barco: casco con cabina y mástil. Barco, /// Ambulancia: furgón blanco con cruz roja. Ambulancia, } impl Mueble { /// Todos los muebles (para ciclar en un editor). pub const ALL: [Mueble; 72] = [ Mueble::Mesa, Mueble::Silla, Mueble::Cama, Mueble::Estante, Mueble::Lampara, Mueble::Alfombra, Mueble::Cofre, Mueble::Estufa, Mueble::Sofa, Mueble::Maceta, Mueble::Barril, Mueble::Escritorio, Mueble::Ficus, Mueble::Palma, Mueble::Suculenta, Mueble::Bonsai, Mueble::Colgante, Mueble::Nopal, Mueble::Cuadro, Mueble::Espejo, Mueble::Reloj, Mueble::Repisa, Mueble::Aplique, Mueble::Tapiz, Mueble::Escalera, Mueble::Baranda, Mueble::Ventanal, Mueble::Columna, Mueble::Aire, Mueble::Solar, Mueble::Tele, Mueble::Isla, Mueble::Ducha, Mueble::Placard, Mueble::Farol, Mueble::Pileta, Mueble::Semaforo, Mueble::Banco, Mueble::Fuente, Mueble::Buzon, Mueble::Basura, Mueble::Parada, Mueble::Hidrante, Mueble::Cabina, Mueble::Cartel, Mueble::Auto, Mueble::Estatua, Mueble::Bolardo, Mueble::Rascacielos, Mueble::Edificio, Mueble::Torre, Mueble::Fabrica, Mueble::Iglesia, Mueble::Galpon, Mueble::Silo, Mueble::Estacionamiento, Mueble::Farola, Mueble::Contenedor, Mueble::Grua, Mueble::Puente, Mueble::Avion, Mueble::Helicoptero, Mueble::Camioneta, Mueble::Camion, Mueble::Colectivo, Mueble::Taxi, Mueble::Tren, Mueble::Vagon, Mueble::Moto, Mueble::Bici, Mueble::Barco, Mueble::Ambulancia, ]; /// ¿Es **arquitectura**? Las construcciones salen simétricas izquierda- /// derecha por regla ([`stamp_mueble`]); un mueble, un vehículo o una grúa /// no tienen por qué. pub fn es_construccion(self) -> bool { matches!( self, Mueble::Rascacielos | Mueble::Edificio | Mueble::Torre | Mueble::Fabrica | Mueble::Iglesia | Mueble::Galpon | Mueble::Silo | Mueble::Estacionamiento ) } /// Nombre legible (español). pub fn label(self) -> &'static str { match self { Mueble::Mesa => "mesa", Mueble::Silla => "silla", Mueble::Cama => "cama", Mueble::Estante => "estante", Mueble::Lampara => "lámpara", Mueble::Alfombra => "alfombra", Mueble::Cofre => "cofre", Mueble::Estufa => "estufa", Mueble::Sofa => "sofá", Mueble::Maceta => "maceta", Mueble::Barril => "barril", Mueble::Escritorio => "escritorio", Mueble::Ficus => "ficus", Mueble::Palma => "palma", Mueble::Suculenta => "suculenta", Mueble::Bonsai => "bonsái", Mueble::Colgante => "colgante", Mueble::Nopal => "nopal", Mueble::Cuadro => "cuadro", Mueble::Espejo => "espejo", Mueble::Reloj => "reloj", Mueble::Repisa => "repisa", Mueble::Aplique => "aplique", Mueble::Tapiz => "tapiz", Mueble::Escalera => "escalera", Mueble::Baranda => "baranda", Mueble::Ventanal => "ventanal", Mueble::Columna => "columna", Mueble::Aire => "aire", Mueble::Solar => "panel solar", Mueble::Tele => "televisor", Mueble::Isla => "isla", Mueble::Ducha => "ducha", Mueble::Placard => "placard", Mueble::Farol => "farol", Mueble::Pileta => "pileta", Mueble::Semaforo => "semáforo", Mueble::Banco => "banco", Mueble::Fuente => "fuente", Mueble::Buzon => "buzón", Mueble::Basura => "papelera", Mueble::Parada => "parada", Mueble::Hidrante => "hidrante", Mueble::Cabina => "cabina", Mueble::Cartel => "cartel", Mueble::Auto => "auto", Mueble::Estatua => "estatua", Mueble::Bolardo => "bolardo", Mueble::Rascacielos => "rascacielos", Mueble::Edificio => "edificio", Mueble::Torre => "torre", Mueble::Fabrica => "fábrica", Mueble::Iglesia => "iglesia", Mueble::Galpon => "galpón", Mueble::Silo => "silo", Mueble::Estacionamiento => "estacionamiento", Mueble::Farola => "farola", Mueble::Contenedor => "contenedor", Mueble::Grua => "grúa", Mueble::Puente => "puente", Mueble::Avion => "avión", Mueble::Helicoptero => "helicóptero", Mueble::Camioneta => "camioneta", Mueble::Camion => "camión", Mueble::Colectivo => "colectivo", Mueble::Taxi => "taxi", Mueble::Tren => "tren", Mueble::Vagon => "vagón", Mueble::Moto => "moto", Mueble::Bici => "bici", Mueble::Barco => "barco", Mueble::Ambulancia => "ambulancia", } } /// El mueble siguiente (cicla). pub fn next(self) -> Mueble { let i = Mueble::ALL.iter().position(|&m| m == self).unwrap_or(0); Mueble::ALL[(i + 1) % Mueble::ALL.len()] } /// Si el mueble **emite luz**, devuelve `(altura sobre la base, color RGB, /// alcance en voxels)` de su fuente puntual. `None` = no ilumina. Lo consume el /// editor de interiores para sembrar `PointLight`s cálidas. pub fn luz(self) -> Option<(f32, [f32; 3], f32)> { match self { Mueble::Lampara => Some((6.5, [1.55, 1.22, 0.72], 15.0)), // pantalla cálida Mueble::Aplique => Some((5.5, [1.5, 1.16, 0.7], 11.0)), // aplique de pared Mueble::Estufa => Some((1.5, [1.6, 0.72, 0.28], 9.0)), // boca de fuego Mueble::Farol => Some((7.5, [1.4, 1.3, 1.0], 16.0)), // farol de poste (luz fría) Mueble::Farola => Some((10.5, [1.5, 1.42, 1.15], 20.0)), // alumbrado público (alto) _ => None, } } } /// Rota un desplazamiento `(dx, dz)` según `facing` (giros de 90°). #[inline] fn rot(dx: i32, dz: i32, facing: u8) -> (i32, i32) { match facing & 3 { 0 => (dx, dz), 1 => (-dz, dx), 2 => (-dx, -dz), _ => (dz, -dx), } } /// Escribe un voxel del material `m` (con su brillo en el alpha), recortando fuera /// del grid. fn place(g: &mut VoxelGrid, x: i32, y: i32, z: i32, m: Material) { if x < 0 || y < 0 || z < 0 { return; } let dim = g.dim(); if (x as u32) < dim[0] && (y as u32) < dim[1] && (z as u32) < dim[2] { g.set_rgba(x as u32, y as u32, z as u32, m.color(), VoxelGrid::shine_to_alpha(m.brillo())); } } /// Hash barato de la posición de colocación. #[inline] fn hashxz(x: u32, z: u32) -> u32 { let mut h = x.wrapping_mul(0x9E37_79B9).wrapping_add(z.wrapping_mul(0x85EB_CA77)); h ^= h >> 15; h = h.wrapping_mul(0x2C1B_3C6D); h ^ (h >> 13) } /// **Color de carrocería** de un vehículo, elegido de una paleta por la posición de /// colocación (determinista → una flota estampada sale VARIADA, del mismo modo que /// la flora varía por posición de mundo). No usa la semilla del mundo: los vehículos /// siguen siendo props colocables, pero cada uno toma su color de dónde cae. fn vehicle_body(x: u32, z: u32) -> Material { const PAL: [Material; 6] = [ Material::Azul, Material::Lava, Material::Wheat, Material::Leaf, Material::Ceramica, Material::Grava, ]; PAL[(hashxz(x, z) % PAL.len() as u32) as usize] } /// **Estampa un mueble** con la base (centro del piso) en `(x, y, z)`, orientado /// según `facing`. Cada pieza combina sus materiales y lleva su brillo. pub fn stamp_mueble(g: &mut VoxelGrid, mueble: Mueble, x: u32, y: u32, z: u32, facing: u8) { let (bx, by, bz) = (x as i32, y as i32, z as i32); let body = vehicle_body(x, z); // color de carrocería (vehículos) // Coloca en coordenadas LOCALES (dx a lo ancho, dz al fondo, dy hacia arriba). // Se anota y se escribe al final, rotado por facing alrededor de la base: // así una construcción se puede espejar entera antes de escribirla. let mut piezas: Vec<(i32, i32, i32, Material)> = Vec::new(); let mut put = |dx: i32, dy: i32, dz: i32, m: Material| piezas.push((dx, dy, dz, m)); // Paleta de lomos de libro / telas. let books = [Material::Ladrillo, Material::Leaf, Material::Bloom, Material::Wheat]; match mueble { Mueble::Mesa => { for dx in -2..=2 { for dz in -2..=2 { put(dx, 3, dz, Material::Madera); // tapa } } for &(lx, lz) in &[(-2, -2), (2, -2), (-2, 2), (2, 2)] { for dy in 0..3 { put(lx, dy, lz, Material::Madera); // patas } } } Mueble::Silla => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 2, dz, Material::Madera); // asiento } } for &(lx, lz) in &[(-1, -1), (1, -1), (-1, 1), (1, 1)] { for dy in 0..2 { put(lx, dy, lz, Material::Madera); // patas } } for dx in -1..=1 { for dy in 3..=4 { put(dx, dy, -1, Material::Madera); // respaldo (al fondo) } } } Mueble::Cama => { for dx in -1..=1 { for dz in -2..=3 { put(dx, 1, dz, Material::Madera); // bastidor put(dx, 2, dz, Material::Yeso); // colchón } } for dz in -2..=3 { put(-2, 1, dz, Material::Madera); put(2, 1, dz, Material::Madera); } for dx in -1..=1 { put(dx, 3, 3, Material::Bloom); // almohada (cabecera +Z) } } Mueble::Estante => { for dy in 0..=6 { put(-2, dy, 0, Material::Madera); // laterales put(2, dy, 0, Material::Madera); } for &sy in &[0, 2, 4, 6] { for dx in -2..=2 { put(dx, sy, 0, Material::Madera); // estantes } } for (bi, &sy) in [1i32, 3, 5].iter().enumerate() { for dx in -1..=1 { let m = books[((dx + bi as i32).rem_euclid(4)) as usize]; // libros put(dx, sy, 0, m); } } } Mueble::Lampara => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Acero); // base } } for dy in 1..=5 { put(0, dy, 0, Material::Acero); // poste } for dx in -1..=1 { for dz in -1..=1 { put(dx, 6, dz, Material::Wheat); // pantalla cálida (lustrosa) } } put(0, 7, 0, Material::Wheat); } Mueble::Alfombra => { for dx in -2i32..=2 { for dz in -3i32..=3 { // Borde de un color, centro de otro. let borde = dx.abs() == 2 || dz.abs() == 3; put(dx, 0, dz, if borde { Material::Bloom } else { Material::Ceramica }); } } } Mueble::Cofre => { for dx in -1..=1 { for dz in -1..=1 { for dy in 0..=1 { put(dx, dy, dz, Material::Madera); // cuerpo } put(dx, 2, dz, Material::Madera); // tapa } } for dx in -1..=1 { put(dx, 0, -1, Material::Acero); // herrajes al frente put(dx, 2, -1, Material::Acero); } put(0, 1, -1, Material::Acero); // cerradura } Mueble::Estufa => { for dx in -1..=1 { for dz in -1..=1 { for dy in 0..=2 { put(dx, dy, dz, Material::Hormigon); // cuerpo } put(dx, 3, dz, Material::Acero); // hornalla de acero } } put(0, 1, -1, Material::Lava); // boca de fuego (al frente) put(0, 0, -1, Material::Lava); } Mueble::Sofa => { for dx in -2..=2 { put(dx, 0, 0, Material::Madera); // base put(dx, 0, 1, Material::Madera); put(dx, 1, 0, Material::Bloom); // asiento (tela) put(dx, 1, 1, Material::Bloom); put(dx, 2, -1, Material::Bloom); // respaldo put(dx, 3, -1, Material::Bloom); } for dz in -1..=1 { put(-2, 1, dz, Material::Bloom); // brazos put(-2, 2, dz, Material::Bloom); put(2, 1, dz, Material::Bloom); put(2, 2, dz, Material::Bloom); } } Mueble::Maceta => { for dx in -1i32..=1 { for dz in -1i32..=1 { put(dx, 0, dz, Material::Ceramica); // tiesto if dx.abs() == 1 || dz.abs() == 1 { put(dx, 1, dz, Material::Ceramica); // borde } } } put(0, 1, 0, Material::Tierra); // tierra put(0, 2, 0, Material::Leaf); // plantita put(0, 3, 0, Material::Leaf); put(1, 2, 0, Material::Leaf); put(-1, 2, 0, Material::Leaf); put(0, 2, 1, Material::Leaf); } Mueble::Barril => { for dy in 0..=3 { let m = if dy == 0 || dy == 3 { Material::Acero } else { Material::Madera }; for dx in -1..=1 { for dz in -1..=1 { if dx * dx + dz * dz <= 2 { put(dx, dy, dz, m); // duela octogonal + aros } } } } put(0, 4, 0, Material::Madera); // tapa } Mueble::Escritorio => { for dx in -2..=2 { for dz in -1..=1 { put(dx, 3, dz, Material::Madera); // tapa } } // Bloque de cajones a un lado. for dy in 0..=2 { for dz in -1..=1 { put(-2, dy, dz, Material::Madera); put(-1, dy, dz, Material::Madera); } } for dy in &[0, 2] { put(-2, *dy, -1, Material::Acero); // tiradores } for dy in 0..3 { put(2, dy, -1, Material::Madera); // pata del otro lado put(2, dy, 1, Material::Madera); } } // ---------------- Plantas de interior ---------------- Mueble::Ficus => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Ceramica); // maceta } } for dy in 1..=5 { put(0, dy, 0, Material::Madera); // tronco } for dx in -2..=2 { for dy in 5..=8 { for dz in -2..=2 { if dx * dx + (dy - 6) * (dy - 6) + dz * dz <= 5 { put(dx, dy, dz, Material::Leaf); // copa frondosa } } } } } Mueble::Palma => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Ceramica); } } for dy in 1..=6 { put(0, dy, 0, Material::Madera); // tronco fino } let ty = 6; put(0, ty + 1, 0, Material::Leaf); for (dxi, dzi) in [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (-1, -1)] { for s in 1..=2 { put(dxi * s, ty + 1 - s, dzi * s, Material::Leaf); // frondas que caen } } } Mueble::Suculenta => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Ceramica); // maceta baja } } put(0, 1, 0, Material::Leaf); // roseta for (dxi, dzi) in [(1, 0), (-1, 0), (0, 1), (0, -1)] { put(dxi, 1, dzi, Material::Leaf); } put(0, 2, 0, Material::Leaf); } Mueble::Bonsai => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Ceramica); // maceta plana ancha } } put(0, 1, 0, Material::Madera); // tronco corto put(0, 2, 0, Material::Madera); put(1, 2, 0, Material::Madera); for (dxi, dzi) in [(1, 0), (2, 0), (0, 1), (-1, 1)] { put(dxi, 3, dzi, Material::Leaf); // copas plato } put(0, 3, 0, Material::Leaf); } Mueble::Colgante => { // Cesta a altura + enredaderas que caen. for dx in -1..=1 { put(dx, 6, 0, Material::Ceramica); } for &dx in &[-1, 0, 1] { for dy in 2..=5 { if (dx + dy) % 2 == 0 { put(dx, dy, 0, Material::Leaf); // hebras verdes } } } } Mueble::Nopal => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Ceramica); } } for dy in 1..=4 { put(0, dy, 0, Material::Cactus); // columna } put(1, 2, 0, Material::Cactus); // brazos put(1, 3, 0, Material::Cactus); put(-1, 3, 0, Material::Cactus); } // ---------------- Decoración de pared ---------------- Mueble::Cuadro => { let (y0, y1) = (4i32, 7i32); for dy in y0..=y1 { for dx in -1i32..=1 { let borde = dx.abs() == 1 || dy == y0 || dy == y1; put(dx, dy, 0, if borde { Material::Madera } else { Material::Bloom }); } } } Mueble::Espejo => { let (y0, y1) = (4i32, 7i32); for dy in y0..=y1 { for dx in -1i32..=1 { let borde = dx.abs() == 1 || dy == y0 || dy == y1; put(dx, dy, 0, if borde { Material::Acero } else { Material::Vidrio }); } } } Mueble::Reloj => { let cy = 5i32; for dx in -1i32..=1 { for dy in (cy - 1)..=(cy + 1) { if dx.abs() + (dy - cy).abs() <= 2 { put(dx, dy, 0, Material::Yeso); // esfera } } } put(0, cy, 0, Material::Pizarra); // centro/agujas put(0, cy + 1, 0, Material::Pizarra); put(1, cy, 0, Material::Madera); // marco put(-1, cy, 0, Material::Madera); } Mueble::Repisa => { for dx in -2..=2 { put(dx, 5, 0, Material::Madera); // estante flotante } put(-1, 6, 0, Material::Bloom); // un libro put(-1, 7, 0, Material::Leaf); put(1, 6, 0, Material::Ceramica); // una maceta chica put(1, 7, 0, Material::Leaf); } Mueble::Aplique => { put(0, 4, 0, Material::Acero); // placa put(0, 5, 0, Material::Acero); put(0, 6, 0, Material::Acero); put(0, 5, 1, Material::Acero); // brazo put(0, 6, 1, Material::Wheat); // luz cálida (lustrosa) put(0, 4, 1, Material::Wheat); } Mueble::Tapiz => { for dy in 2..=7 { for dx in -1..=1 { let m = if (dx + dy) % 2 == 0 { Material::Bloom } else { Material::Leaf }; put(dx, dy, 0, m); // paño de colores } } } // ---------------- Casas y edificios modernos ---------------- Mueble::Escalera => { for s in 0..7i32 { for dx in -1..=1 { for k in 0..=s { put(dx, k, s, Material::Hormigon); // peldaños macizos que suben en +Z } } } } Mueble::Baranda => { for dx in -2..=2 { put(dx, 1, 0, Material::Vidrio); // paño de vidrio put(dx, 2, 0, Material::Vidrio); put(dx, 3, 0, Material::Acero); // pasamanos } for &dx in &[-2i32, 0, 2] { for dy in 0..3 { put(dx, dy, 0, Material::Acero); // montantes } } } Mueble::Ventanal => { for dy in 0..9 { for dx in -1..=1 { put(dx, dy, 0, Material::Vidrio); // paño de piso a techo } put(-2, dy, 0, Material::Acero); // parantes put(2, dy, 0, Material::Acero); } for dx in -2..=2 { put(dx, 0, 0, Material::Acero); // marco inf/sup put(dx, 8, 0, Material::Acero); } } Mueble::Columna => { for dy in 0..10 { for dx in 0..=1 { for dz in 0..=1 { put(dx, dy, dz, Material::Hormigon); // pilar 2×2 } } } } Mueble::Aire => { for dx in -2..=2 { for dy in 6..8 { put(dx, dy, 0, Material::Yeso); // caja del split } put(dx, 6, 0, Material::Acero); // rejilla } } Mueble::Solar => { for dx in -2..=2 { for dz in 0..4 { put(dx, 2 + dz, dz, Material::Azul); // placa azul inclinada } } for &dx in &[-2i32, 2] { for dy in 0..2 { put(dx, dy, 0, Material::Acero); // patas } } put(0, 0, 0, Material::Acero); } Mueble::Tele => { for dx in -2..=2 { for dy in 3..6 { put(dx, dy, 0, Material::Pizarra); // pantalla negra } } for dy in 0..3 { put(0, dy, 0, Material::Acero); // pie } put(-1, 0, 1, Material::Acero); // base put(1, 0, 1, Material::Acero); } Mueble::Isla => { for dx in -2..=2 { for dz in -1..=1 { for dy in 0..2 { put(dx, dy, dz, Material::Yeso); // bajomesada } put(dx, 2, dz, Material::Marmol); // mesada de mármol (lustrosa) } } } Mueble::Ducha => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Ceramica); // base } } for dy in 1..7 { for dx in -1..=1 { put(dx, dy, -1, Material::Vidrio); // mampara fondo } for dz in -1..=1 { put(-1, dy, dz, Material::Vidrio); // mampara lateral } } put(0, 6, -1, Material::Acero); // ducha put(0, 5, -1, Material::Acero); } Mueble::Placard => { for dx in -1..=1 { for dy in 0..7 { for dz in -1..=0 { put(dx, dy, dz, Material::Madera); // cuerpo alto } } } put(-1, 3, 0, Material::Acero); // manijas put(1, 3, 0, Material::Acero); } Mueble::Farol => { for dy in 0..7 { put(0, dy, 0, Material::Acero); // poste } for dx in -1..=1 { for dz in -1..=1 { put(dx, 7, dz, Material::Vidrio); // farol de vidrio put(dx, 9, dz, Material::Acero); // capucha } } put(0, 8, 0, Material::Wheat); // luz } Mueble::Pileta => { for dx in -3i32..=3 { for dz in -2i32..=2 { let borde = dx.abs() == 3 || dz.abs() == 2; put(dx, 0, dz, if borde { Material::Ceramica } else { Material::Water }); } } } // ---------------- Elementos de ciudad ---------------- Mueble::Semaforo => { for dy in 0..6 { put(0, dy, 0, Material::Acero); // poste } for dy in 5..9 { put(0, dy, 0, Material::Pizarra); // caja } put(0, 8, 1, Material::Lava); // rojo put(0, 7, 1, Material::Wheat); // amarillo put(0, 6, 1, Material::Leaf); // verde put(1, 7, 0, Material::Acero); // brazo } Mueble::Banco => { for dx in -2..=2 { put(dx, 1, 0, Material::Madera); // asiento put(dx, 2, -1, Material::Madera); // respaldo } for &dx in &[-2i32, 2] { put(dx, 0, 0, Material::Acero); // patas } } Mueble::Fuente => { for dx in -2i32..=2 { for dz in -2i32..=2 { if dx * dx + dz * dz <= 4 { put(dx, 0, dz, Material::Hormigon); // pileta de piedra if dx * dx + dz * dz <= 2 { put(dx, 1, dz, Material::Water); // agua } } } } put(0, 1, 0, Material::Hormigon); // columna put(0, 2, 0, Material::Hormigon); put(0, 3, 0, Material::Water); // chorro } Mueble::Buzon => { put(0, 0, 0, Material::Acero); // poste put(0, 1, 0, Material::Acero); for dx in -1..=1 { for dy in 2..4 { put(dx, dy, 0, Material::Azul); // caja azul } } put(1, 3, 0, Material::Azul); } Mueble::Basura => { for dy in 0..3 { for dx in -1i32..=1 { for dz in -1i32..=1 { if dx * dx + dz * dz <= 2 { put(dx, dy, dz, Material::Acero); // tacho } } } } for dx in -1i32..=1 { for dz in -1i32..=1 { if dx * dx + dz * dz <= 2 { put(dx, 3, dz, Material::Pizarra); // tapa } } } } Mueble::Parada => { for &(px, pz) in &[(-2i32, -1i32), (2, -1), (-2, 1), (2, 1)] { for dy in 0..5 { put(px, dy, pz, Material::Acero); // columnas } } for dx in -2..=2 { for dz in -1..=1 { put(dx, 5, dz, Material::Vidrio); // techo de vidrio } for dy in 1..4 { put(dx, dy, -1, Material::Vidrio); // fondo de vidrio } put(dx, 1, 0, Material::Madera); // banco } } Mueble::Hidrante => { for dy in 0..3 { put(0, dy, 0, Material::Lava); // cuerpo rojo } put(1, 1, 0, Material::Lava); // brazos put(-1, 1, 0, Material::Lava); put(0, 3, 0, Material::Lava); // tapa } Mueble::Cabina => { for dy in 0..7 { for dx in -1..=1 { put(dx, dy, -1, Material::Vidrio); // paredes de vidrio put(dx, dy, 1, Material::Vidrio); } put(-1, dy, 0, Material::Vidrio); for &(cx, cz) in &[(-1i32, -1i32), (1, -1), (-1, 1), (1, 1)] { put(cx, dy, cz, Material::Acero); // esquinas } } for dx in -1..=1 { for dz in -1..=1 { put(dx, 7, dz, Material::Azul); // techo azul } } } Mueble::Cartel => { for dy in 0..5 { put(0, dy, 0, Material::Acero); // poste } for dx in -1..=1 { for dy in 5..7 { put(dx, dy, 0, Material::Yeso); // letrero blanco } } put(0, 6, 0, Material::Azul); // franja } Mueble::Auto => { for dx in -3..=3 { for dz in -1..=1 { put(dx, 1, dz, body); // carrocería (color por posición) } } for dx in -1..=1 { for dz in -1..=1 { put(dx, 2, dz, Material::Vidrio); // habitáculo/ventanas } } for &(wx, wz) in &[(-2i32, -1i32), (2, -1), (-2, 1), (2, 1)] { put(wx, 0, wz, Material::Pizarra); // ruedas } } Mueble::Estatua => { for dx in -1..=1 { for dz in -1..=1 { put(dx, 0, dz, Material::Marmol); // pedestal put(dx, 1, dz, Material::Marmol); } } for dy in 2..5 { put(0, dy, 0, Material::Marmol); // figura } put(1, 3, 0, Material::Marmol); // brazos put(-1, 3, 0, Material::Marmol); } Mueble::Bolardo => { for dy in 0..4 { put(0, dy, 0, Material::Hormigon); // poste bajo } for &(dx, dz) in &[(1, 0), (-1, 0), (0, 1), (0, -1)] { put(dx, 0, dz, Material::Hormigon); // base ancha } put(0, 4, 0, Material::Acero); // tapa } // ---------------- Edificios grandes ---------------- Mueble::Rascacielos => { let h = 20i32; for dy in 0..h { for dx in -2..=1 { for dz in -2..=1 { let borde = dx == -2 || dx == 1 || dz == -2 || dz == 1; if borde { let esquina = (dx == -2 || dx == 1) && (dz == -2 || dz == 1); let banda = dy % 4 == 0; put(dx, dy, dz, if esquina || banda { Material::Acero } else { Material::Vidrio }); } } } } for dx in -2..=1 { for dz in -2..=1 { put(dx, h, dz, Material::Hormigon); // azotea } } put(0, h + 1, 0, Material::Acero); put(0, h + 2, 0, Material::Acero); // antena } Mueble::Edificio => { let h = 12i32; for dy in 0..h { for dx in -2i32..=2 { for dz in -2..=1 { let borde = dx == -2 || dx == 2 || dz == -2 || dz == 1; if borde { let ventana = dy > 0 && dy % 3 != 0 && dx.rem_euclid(2) == 0; put(dx, dy, dz, if ventana { Material::Vidrio } else { Material::Ladrillo }); } } } } for dx in -2..=2 { for dz in -2..=1 { put(dx, h, dz, Material::Hormigon); // azotea } } } Mueble::Torre => { let h = 22i32; for dy in 0..h { for dx in 0..=1 { for dz in 0..=1 { put(dx, dy, dz, Material::Hormigon); } } } let mut dy = 2; while dy < h { put(0, dy, 0, Material::Vidrio); // ventanas put(1, dy, 1, Material::Vidrio); dy += 3; } for k in 0..4 { put(0, h + k, 0, Material::Acero); // antena } } Mueble::Fabrica => { for dy in 0..6 { for dx in -3..=2 { for dz in -2..=1 { if dx == -3 || dx == 2 || dz == -2 || dz == 1 { put(dx, dy, dz, Material::Hormigon); } } } } for dx in -3..=2 { for dz in -2..=1 { put(dx, 6, dz, Material::Acero); // techo de zinc } } for dy in 0..12 { put(2, dy, 1, Material::Ladrillo); // chimenea } put(2, 12, 1, Material::Pizarra); } Mueble::Iglesia => { for dy in 0..6 { for dx in -2..=1 { for dz in -3..=2 { if dx == -2 || dx == 1 || dz == -3 || dz == 2 { put(dx, dy, dz, Material::Yeso); } } } } for dx in -2..=1 { for dz in -3..=2 { put(dx, 6, dz, Material::Teja); // techo } } for dy in 0..11 { for cdx in -2..=-1 { for cdz in -3..=-2 { put(cdx, dy, cdz, Material::Yeso); // campanario } } } put(-2, 11, -3, Material::Teja); // cruz put(-2, 12, -3, Material::Acero); put(-2, 13, -3, Material::Acero); put(-3, 13, -3, Material::Acero); put(-1, 13, -3, Material::Acero); } Mueble::Galpon => { for dy in 0..5 { for dx in -4..=3 { for dz in -3..=2 { if dx == -4 || dx == 3 || dz == -3 || dz == 2 { put(dx, dy, dz, Material::Hormigon); } } } } for dx in -4..=3 { for dz in -3..=2 { let slope = (dz + 3).min(2 - dz).min(3); put(dx, 5 + slope, dz, Material::Acero); // techo de zinc a dos aguas } } for dx in -1..=1 { for dy in 0..4 { put(dx, dy, 2, Material::Acero); // portón } } } Mueble::Silo => { for dy in 0..14 { for dx in -1i32..=1 { for dz in -1i32..=1 { if dx * dx + dz * dz <= 2 { put(dx, dy, dz, Material::Acero); // cilindro } } } } for dx in -1i32..=1 { for dz in -1i32..=1 { if dx * dx + dz * dz <= 2 { put(dx, 14, dz, Material::Yeso); // cúpula } } } put(0, 15, 0, Material::Yeso); } Mueble::Estacionamiento => { for lvl in 0..4 { let y = lvl * 3; for dx in -3..=2 { for dz in -2..=1 { put(dx, y, dz, Material::Hormigon); // losa } } for &(cx, cz) in &[(-3, -2), (2, -2), (-3, 1), (2, 1), (0, -2), (0, 1)] { for k in 1..3 { put(cx, y + k, cz, Material::Hormigon); // columnas } } } } // ---------------- Más ciudad ---------------- Mueble::Farola => { for dy in 0..10 { put(0, dy, 0, Material::Acero); // poste alto } for dx in 0..=2 { put(dx, 10, 0, Material::Acero); // brazo } put(2, 9, 0, Material::Wheat); // luminaria } Mueble::Contenedor => { for dy in 0..2 { for dx in -2..=2 { for dz in -1..=1 { if dx == -2 || dx == 2 || dz == -1 || dz == 1 || dy == 0 { put(dx, dy, dz, Material::Acero); // volquete } } } } for dx in -2..=2 { for dz in -1..=1 { put(dx, 2, dz, Material::Pizarra); // tapa } } } Mueble::Grua => { for dy in 0..18 { put(0, dy, 0, Material::Acero); // mástil } for dx in 0..=6 { put(dx, 17, 0, Material::Acero); // pluma } for dx in -3..=0 { put(dx, 17, 0, Material::Acero); // contrapluma } put(-3, 16, 0, Material::Hormigon); // contrapeso put(4, 16, 0, Material::Acero); // cable put(4, 15, 0, Material::Acero); // gancho } Mueble::Puente => { for dx in -4..=4 { for dz in -1..=1 { put(dx, 3, dz, Material::Hormigon); // tablero } put(dx, 4, -1, Material::Acero); // barandas put(dx, 4, 1, Material::Acero); } for &px in &[-3i32, 3] { for dy in 0..3 { for dz in -1..=1 { put(px, dy, dz, Material::Hormigon); // pilas } } } } // ---------------- Transporte ---------------- Mueble::Avion => { for dx in -4..=4 { for dy in 1..=2 { put(dx, dy, 0, Material::Yeso); // fuselaje } } put(4, 2, 0, Material::Vidrio); // cabina for dz in -3..=3 { put(0, 1, dz, body); // alas (color por posición) } put(-4, 3, 0, Material::Yeso); // cola vertical put(-4, 4, 0, Material::Yeso); put(-4, 3, 1, Material::Yeso); // cola horizontal put(-4, 3, -1, Material::Yeso); put(0, 0, 2, Material::Pizarra); // turbinas put(0, 0, -2, Material::Pizarra); } Mueble::Helicoptero => { for dx in -1..=1 { for dz in -1..=1 { for dy in 1..3 { put(dx, dy, dz, body); // cabina } } } put(1, 2, 0, Material::Vidrio); // burbuja for dx in -4..=-2 { put(dx, 2, 0, Material::Yeso); // cola } put(-4, 3, 0, Material::Pizarra); // rotor de cola for dx in -3..=3 { put(dx, 4, 0, Material::Pizarra); // rotor principal } for dz in -3..=3 { put(0, 4, dz, Material::Pizarra); } put(0, 3, 0, Material::Acero); // mástil for dx in -1..=1 { put(dx, 0, -1, Material::Acero); // patines put(dx, 0, 1, Material::Acero); } } Mueble::Camioneta => { for dx in -3..=3 { for dz in -1..=1 { put(dx, 1, dz, body); // chasis } } for dx in 1..=3 { for dz in -1..=1 { put(dx, 2, dz, if dz == 0 && dx == 2 { Material::Vidrio } else { body }); // cabina } } for dx in -3..=0 { put(dx, 2, -1, body); // caja put(dx, 2, 1, body); } for &(wx, wz) in &[(-2i32, -1i32), (2, -1), (-2, 1), (2, 1)] { put(wx, 0, wz, Material::Pizarra); // ruedas } } Mueble::Camion => { for dx in 2..=3 { for dz in -1..=1 { for dy in 1..3 { put(dx, dy, dz, body); // cabina } } } put(3, 2, 0, Material::Vidrio); for dx in -4..=1 { for dz in -1..=1 { for dy in 1..4 { if dx == -4 || dz == -1 || dz == 1 || dy == 3 { put(dx, dy, dz, Material::Yeso); // caja de carga } } } } for &(wx, wz) in &[(-3i32, -1i32), (0, -1), (3, -1), (-3, 1), (0, 1), (3, 1)] { put(wx, 0, wz, Material::Pizarra); } } Mueble::Colectivo => { for dx in -5..=5 { for dz in -1..=1 { for dy in 1..4 { if dx == -5 || dx == 5 || dz == -1 || dz == 1 { put(dx, dy, dz, if dy == 3 { Material::Vidrio } else { body }); } } } for dz in -1..=1 { put(dx, 4, dz, body); // techo } } for &wx in &[-4i32, -1, 4] { put(wx, 0, -1, Material::Pizarra); put(wx, 0, 1, Material::Pizarra); } } Mueble::Taxi => { for dx in -3..=3 { for dz in -1..=1 { put(dx, 1, dz, Material::Wheat); // amarillo } } for dx in -1..=1 { for dz in -1..=1 { put(dx, 2, dz, if dz == 0 { Material::Vidrio } else { Material::Wheat }); } } put(0, 3, 0, Material::Pizarra); // cartel de techo for &(wx, wz) in &[(-2i32, -1i32), (2, -1), (-2, 1), (2, 1)] { put(wx, 0, wz, Material::Pizarra); } } Mueble::Tren => { for dx in -5..=5 { for dz in -1..=1 { for dy in 1..3 { put(dx, dy, dz, Material::Lava); // cuerpo rojo } } } for dx in 3..=5 { for dz in -1..=1 { put(dx, 3, dz, if dz == 0 { Material::Vidrio } else { Material::Lava }); // cabina } } put(-5, 3, 0, Material::Pizarra); // chimenea let mut wx = -4; while wx <= 4 { put(wx, 0, -1, Material::Pizarra); put(wx, 0, 1, Material::Pizarra); wx += 2; } } Mueble::Vagon => { for dx in -4..=4 { for dz in -1..=1 { for dy in 1..3 { if dx == -4 || dx == 4 || dz == -1 || dz == 1 || dy == 2 { put(dx, dy, dz, body); } } } } let mut wx = -3; while wx <= 3 { put(wx, 0, -1, Material::Pizarra); put(wx, 0, 1, Material::Pizarra); wx += 3; } } Mueble::Moto => { put(1, 0, 0, Material::Pizarra); // rueda delantera put(1, 1, 0, Material::Pizarra); put(-1, 0, 0, Material::Pizarra); // rueda trasera put(-1, 1, 0, Material::Pizarra); put(0, 1, 0, body); // cuerpo put(0, 2, 0, body); // asiento put(1, 2, 0, Material::Acero); // manubrio } Mueble::Bici => { put(1, 0, 0, Material::Pizarra); // rueda delantera put(1, 1, 0, Material::Pizarra); put(-1, 0, 0, Material::Pizarra); // rueda trasera put(-1, 1, 0, Material::Pizarra); put(0, 1, 0, Material::Acero); // cuadro put(0, 2, 0, Material::Acero); // asiento put(1, 3, 0, Material::Acero); // manubrio } Mueble::Barco => { for dx in -4..=4 { put(dx, 1, 0, body); // casco superior } for dx in -3..=3 { put(dx, 0, 0, body); // casco inferior } for dx in -1..=1 { put(dx, 2, 0, Material::Yeso); // cabina put(dx, 3, 0, Material::Vidrio); } put(0, 4, 0, Material::Acero); // mástil put(0, 5, 0, Material::Acero); } Mueble::Ambulancia => { for dx in -3..=3 { for dz in -1..=1 { for dy in 1..4 { if dx == -3 || dx == 3 || dz == -1 || dz == 1 || dy == 3 { put(dx, dy, dz, Material::Yeso); // furgón blanco } } } } put(3, 2, 0, Material::Vidrio); // parabrisas put(0, 4, 0, Material::Lava); // baliza roja put(0, 2, 1, Material::Lava); // cruz roja put(0, 2, -1, Material::Lava); for &(wx, wz) in &[(-2i32, -1i32), (2, -1), (-2, 1), (2, 1)] { put(wx, 0, wz, Material::Pizarra); } } } // **Toda construcción es simétrica izquierda-derecha** (la regla de // `casa.rs`). El eje es el centro de lo dibujado y no `dx = 0`: varios // edificios tienen ancho par (`-2..=1`), y espejar en 0 los correría un // voxel. Cada pieza se escribe con su espejo en el mismo orden, así el // último que escribe escribe en los dos lados. let espejo = if mueble.es_construccion() && !piezas.is_empty() { let (lo, hi) = piezas.iter().fold((i32::MAX, i32::MIN), |(lo, hi), p| (lo.min(p.0), hi.max(p.0))); Some(lo + hi) } else { None }; for (dx, dy, dz, m) in piezas { let (rx, rz) = rot(dx, dz, facing); place(g, bx + rx, by + dy, bz + rz, m); if let Some(suma) = espejo { let (rx, rz) = rot(suma - dx, dz, facing); place(g, bx + rx, by + dy, bz + rz, m); } } } #[cfg(test)] mod tests { use super::*; /// Las construcciones de la lista salen simétricas izquierda-derecha, con /// las cuatro orientaciones: el espejo se hace en coords locales, antes de /// rotar, así que «izquierda» es la del que la mira de frente. #[test] fn toda_construccion_es_simetrica_izquierda_derecha() { let dim = [48u32, 48, 48]; let mut malas = Vec::new(); for m in Mueble::ALL.iter().copied().filter(|m| m.es_construccion()) { for facing in 0..4u8 { let mut g = VoxelGrid::new(dim); stamp_mueble(&mut g, m, 24, 1, 24, facing); let pts: Vec<(u32, u32, u32)> = (0..dim[0]) .flat_map(|x| (0..dim[1]).flat_map(move |y| (0..dim[2]).map(move |z| (x, y, z)))) .filter(|&(x, y, z)| g.get(x, y, z).is_some_and(|c| c[3] > 0)) .collect(); // El eje de espejo, en coords de grilla: a lo ancho del frente, // que con facing 0/2 es `x` y con 1/3 es `z`. let en_x = facing % 2 == 0; let (lo, hi) = pts.iter().fold((u32::MAX, 0), |(lo, hi), p| { let v = if en_x { p.0 } else { p.2 }; (lo.min(v), hi.max(v)) }); let distintos = pts .iter() .filter(|&&(x, y, z)| { let (ex, ez) = if en_x { (lo + hi - x, z) } else { (x, lo + hi - z) }; g.get(x, y, z) != g.get(ex, y, ez) }) .count(); if distintos > 0 { malas.push(format!("{} (facing {facing}): {distintos}", m.label())); } } } assert!(malas.is_empty(), "construcciones asimétricas: {malas:?}"); } fn solids(g: &VoxelGrid, dim: [u32; 3]) -> usize { let mut n = 0; for x in 0..dim[0] { for y in 0..dim[1] { for z in 0..dim[2] { if g.is_solid(x as i32, y as i32, z as i32) { n += 1; } } } } n } #[test] fn cada_mueble_estampa_voxels() { let dim = [24u32, 24, 24]; for m in Mueble::ALL { let mut g = VoxelGrid::new(dim); stamp_mueble(&mut g, m, 12, 2, 12, 0); assert!(solids(&g, dim) >= 5, "{} estampó muy poco", m.label()); } } #[test] fn facing_rota_la_silueta() { // La silla (asimétrica por el respaldo) cambia de huella al rotar. let dim = [24u32, 24, 24]; let mut a = VoxelGrid::new(dim); let mut b = VoxelGrid::new(dim); stamp_mueble(&mut a, Mueble::Silla, 12, 2, 12, 0); stamp_mueble(&mut b, Mueble::Silla, 12, 2, 12, 1); // El respaldo cae en una celda distinta según la orientación. Base (12,2,12), // respaldo local (0, 3, -1): facing 0 → (12,5,11); facing 1 → (13,5,12). let back0 = a.is_solid(12, 5, 11); // -Z de la base let back1 = b.is_solid(13, 5, 12); // rotado a +X assert!(back0, "respaldo en facing 0"); assert!(back1, "respaldo rotó con el facing 1"); } }