use super::*; /// Los niveles de composición, de lo más básico a lo más compuesto. #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum Level { Leyes, Materiales, Seres, Biomas, Mundos, Escenas, Interiores, } impl Level { pub(crate) const ALL: [Level; 7] = [ Level::Leyes, Level::Materiales, Level::Seres, Level::Biomas, Level::Mundos, Level::Escenas, Level::Interiores, ]; pub(crate) fn index(self) -> usize { Self::ALL.iter().position(|&l| l == self).unwrap_or(0) } pub(crate) fn from_index(i: usize) -> Level { Self::ALL[i.min(Self::ALL.len() - 1)] } pub(crate) fn label(self) -> &'static str { match self { Level::Leyes => "Leyes", Level::Materiales => "Materiales", Level::Seres => "Seres", Level::Biomas => "Biomas", Level::Mundos => "Mundos", Level::Escenas => "Escenas", Level::Interiores => "Interiores", } } pub(crate) fn icon(self) -> Icon { match self { Level::Leyes => Icon::Droplet, Level::Materiales => Icon::Leaf, Level::Seres => Icon::User, Level::Biomas => Icon::Mountain, Level::Mundos => Icon::Globe, Level::Escenas => Icon::Film, Level::Interiores => Icon::Grid, } } /// Color de acento del nivel (íconos coloridos). pub(crate) fn color(self) -> Color { match self { Level::Leyes => Color::from_rgba8(150, 130, 230, 255), Level::Materiales => Color::from_rgba8(220, 170, 80, 255), Level::Seres => Color::from_rgba8(110, 200, 130, 255), Level::Biomas => Color::from_rgba8(90, 200, 200, 255), Level::Mundos => Color::from_rgba8(90, 150, 230, 255), Level::Escenas => Color::from_rgba8(230, 120, 110, 255), Level::Interiores => Color::from_rgba8(210, 150, 200, 255), } } /// Sets de herramientas (pestañas del rail derecho): rótulo + ícono. pub(crate) fn tools(self) -> Vec<(&'static str, Icon)> { match self { Level::Leyes => vec![("Parámetros", Icon::Gauge)], Level::Materiales => vec![("Aspecto", Icon::Image), ("Leyes", Icon::Droplet)], Level::Seres => vec![ ("Cuerpo", Icon::User), ("Andar", Icon::Gauge), ("Conducta", Icon::Settings), ("Piel", Icon::Image), ("Camiseta", Icon::Image), ("Pantalón", Icon::Image), (rotulo_vestuario(), Icon::User), ], Level::Biomas => vec![ ("Relieve", Icon::Mountain), ("Materiales", Icon::Leaf), ("Objetos", Icon::Grid), ("Minerales", Icon::Gauge), ("Casas", Icon::Globe), ("Fauna", Icon::User), ("Seres", Icon::User), ], Level::Mundos => vec![("Semilla", Icon::Globe), ("Biomas", Icon::Mountain)], Level::Escenas => vec![ ("Escena", Icon::Film), ("Reparto", Icon::User), ("Cámara", Icon::Camera), (rotulo_utileria(), Icon::Grid), (rotulo_decorado(), Icon::Globe), ("Video", Icon::Play), ], Level::Interiores => vec![("Sala", Icon::Grid), ("Mueble", Icon::User)], } } } /// Parte coloreable de un ser. Las tres últimas son del vestuario y nacen sin color /// propio (heredan el de fábrica): el primer toque de un slider se lo fija. #[derive(Clone, Copy)] pub(crate) enum Part { Skin, Shirt, Pants, Sombrero, Falda, Manto, } /// Perilla continua del vestuario de un ser. #[derive(Clone, Copy)] pub(crate) enum PrendaField { Barba, Falda, Manto, } /// El rótulo de la pestaña del vestuario. Las pestañas son `&'static str` (el resto /// del estudio es castellano fijo); éste se resuelve una vez, con el idioma que /// eligió `rimay_localize::init` al arrancar. pub(crate) fn rotulo_vestuario() -> &'static str { static R: std::sync::OnceLock = std::sync::OnceLock::new(); R.get_or_init(|| rimay_localize::t("voxelstudio-tab-wardrobe")).as_str() } /// El rótulo de la pestaña de utilería (ver [`rotulo_vestuario`]). pub(crate) fn rotulo_utileria() -> &'static str { static R: std::sync::OnceLock = std::sync::OnceLock::new(); R.get_or_init(|| rimay_localize::t("voxelstudio-tab-props")).as_str() } /// El rótulo de la pestaña del decorado (ver [`rotulo_vestuario`]). pub(crate) fn rotulo_decorado() -> &'static str { static R: std::sync::OnceLock = std::sync::OnceLock::new(); R.get_or_init(|| rimay_localize::t("voxelstudio-tab-sets")).as_str() } /// Perilla continua de una construcción del decorado: dónde va, en columnas de mundo. #[derive(Clone, Copy)] pub(crate) enum DecoradoField { X, Z, } /// Perilla continua de una pieza de utilería. #[derive(Clone, Copy)] pub(crate) enum UtileriaField { X, Y, Z, Alto, Desde, Giro, } /// Perilla continua del cielo de noche de una escena. #[derive(Clone, Copy)] pub(crate) enum CieloField { Estrellas, Luna, } /// Parámetro `f32` del relieve de un bioma. #[derive(Clone, Copy)] pub(crate) enum BiomaField { Base, Dune, Relief, Mountains, Water, Rivers, PeakAt, } #[derive(Clone)] pub(crate) enum Msg { // Navegación de niveles + items. SelectLevel(Level), SelectItem(u64), SelectTool(usize), /// Mensaje del **sidebar unificado IZQUIERDO** (selector de niveles): ejes de /// disposición, buscador, control y resize. Se reduce con /// `RagSidebarState::update`; el diente abierto se re-fija a `level`. SidebarLeft(RagSidebarMsg), /// Mensaje del **sidebar unificado DERECHO** (selector de herramientas). Igual: /// se reduce con `RagSidebarState::update` y el abierto se re-fija a `tool_tab`. SidebarRight(RagSidebarMsg), NewItem, DupItem, DelItem, // Renombrar (campo de texto). /// Evento de mouse del campo de nombre (click/arrastre). El `Press` además /// lo enfoca. Ver [`text_input_view_full`]. RenameCampo(TextInputEvent), RenameKey(KeyEvent), // Leyes. CycleLeyKind, SetLeyParam(usize, f32), // Leyes por ecuación (autorables). LeyPreset, SetEcuParam(usize, f32), /// Empezar a editar la fórmula `i` (desde un botón): carga su fuente en el /// buffer compartido y enfoca. Ver [`Msg::FormulaCampo`] para el mouse del /// propio campo ya en edición. FormulaFocus(usize), /// Evento de mouse del campo de fórmula ya en edición (click→caret, /// arrastre→selección). Ver [`text_input_view_full`]. FormulaCampo(TextInputEvent), FormulaKey(KeyEvent), AddCampo, RemoveCampo, AddEcuParam, RemoveEcuParam, CycleLabField, ReseedLab, // Vista de grafo de nodos de la ley. ToggleLeyesView, GraphDrag(NodeId, f32, f32), GraphConnect(NodeId, PinIdx, NodeId, PinIdx), GraphAddNode(NodeOp), GraphDeleteNode(NodeId), // Materiales. CycleMatRole, SetMatColor(usize, f32), SetMatGrain(f32), SetMatBrillo(f32), SetMatRugosidad(f32), SetMatBisel(f32), CycleMatParent, AddMatLey, RemoveMatLey, CycleMatLey(usize), // Seres. CycleSereAge, CycleSereCuerpo, /// Cicla la especie del ser por el bestiario (`true` = hacia atrás). CycleSereBicho(bool), SetSereColor(Part, usize, f32), // Vestuario del ser (CharSpec::atuendo). CycleSerePeinado, CycleSereSombrero, ToggleSereAnteojos, SetSerePrenda(PrendaField, f32), // Andares (capa 2) del ser-rig. CycleAndarEstado, SetAndarCadencia(f32), SetAndarAmplitud(usize, f32), // Conducta (capa 3) — parámetros + impulsos de steering autorables. SetConducta(usize, f32), CondAddImpulso, CondRemoveImpulso, CondCycleImpulso(usize), /// Empezar a editar el peso del impulso `i` (desde un botón): carga su /// fuente en el buffer y enfoca. Ver [`Msg::CondFormulaCampo`] para el mouse. CondFormulaFocus(usize), /// Evento de mouse del campo de peso ya en edición (click→caret, /// arrastre→selección). Ver [`text_input_view_full`]. CondFormulaCampo(TextInputEvent), CondFormulaKey(KeyEvent), ToggleManada, // Biomas. SetBiomaField(BiomaField, f32), CycleBiomaGround, CycleBiomaCliff, CycleBiomaPeak, AddBiomaObjeto, RemoveBiomaObjeto, SetObjetoDensidad(usize, f32), CycleObjetoForma(usize), CycleObjetoMaterial(usize), // Minerales del bioma (con todos sus factores). AddBiomaMineral, RemoveBiomaMineral, CycleMineralTipo(usize), CycleMineralAspecto(usize), SetMineralAbundancia(usize, f32), SetMineralProfMin(usize, f32), SetMineralProfMax(usize, f32), SetMineralColor(usize, usize, f32), SetMineralDureza(usize, f32), SetMineralBrillo(usize, f32), // Casas del bioma (pueblo procedural). AddBiomaCasa, RemoveBiomaCasa, CycleCasaTipo(usize), SetCasaDensidad(usize, f32), SetDensidadUrbana(f32), CyclePuebloTipo, AplicarPuebloTipo, // Fauna del bioma (seres esparcidos por semilla). AddBiomaFauna, RemoveBiomaFauna, CycleFaunaBicho(usize), SetFaunaDensidad(usize, f32), // Interiores (colocar muebles). CycleMuebleBrush, CycleMuebleFacing, PlaceMuebleAt(f32, f32), RemoveLastPieza, ClearPiezas, SetInteriorAncho(f32), SetInteriorFondo(f32), CycleInteriorPiso, CycleInteriorMuro, ToggleInteriorMuros, ToggleInteriorVentanas, CycleInteriorTecho, ToggleInteriorTecho, AddBiomaSere, RemoveBiomaSere, CycleBiomaSere(usize), SetBiomaSereProb(usize, f32), // Mundos. /// Evento de mouse del campo de semilla (click/arrastre). El `Press` además /// lo enfoca. Ver [`text_input_view_full`]. SeedCampo(TextInputEvent), SeedKey(KeyEvent), SeedRandom, CycleMundoBioma, // Escenas — utilería (estrella, flor, perla, pluma). SelectUtileria(usize), AddUtileria, RemoveUtileria, CycleUtileriaCosa, /// Cicla quién la lleva: nadie → actor 0 → … → nadie. CycleUtileriaLleva, SetUtileria(UtileriaField, f32), // Escenas — decorado (el palacio, el kiosko…). SelectDecorado(usize), AddDecorado, RemoveDecorado, CycleDecoradoCasa, /// Otra variante del mismo estilo (otra semilla). OtraVarianteDecorado, SetDecorado(DecoradoField, f32), // Escenas — cielo de noche. /// Día ↔ noche (la noche nace con [`Cielo::noche`](llimphi_voxel::Cielo::noche)). ToggleNoche, SetCielo(CieloField, f32), // Escenas — reparto y recorrido (filmografía). SelectActor(usize), AddActor, RemoveActor, CycleActorSere, SelectKey(usize), AddKey, RemoveKey, SetKeyPos(bool, f32), // true=gx, false=gz SetKeyTime(f32), CycleKeyClip, PlaceKeyAt(f32, f32), // click en el terreno → mueve la clave seleccionada // Escenas. CycleSceneMundo, SetSceneDur(f32), TogglePlay, Scrub(f32), ToggleSceneCam, AddShot, RemoveShot, ExportVideo, ExportDone(Result), // Simulación de la ley Fluir (agua). ToggleSim, // Cámara. Orbit(f32, f32), Zoom(f32), // IA. /// Evento de mouse del campo de la IA (click/arrastre). El `Press` además /// lo enfoca. Ver [`text_input_view_full`]. AiCampo(TextInputEvent), AiKey(KeyEvent), AiGenerate, AiBioma(Bioma), AiSere(CharSpec), AiScene(SceneSpec), // Persistencia. Save, Load, // Archivos: ruta editable + abrir / guardar-como / combinar. /// Evento de mouse del campo de ruta (click/arrastre). El `Press` además lo /// enfoca. Ver [`text_input_view_full`]. FileCampo(TextInputEvent), FileKey(KeyEvent), Abrir, GuardarComo, Combinar, /// Cargar un **mapa real** (GeoJSON del visor, +`.pgm` hermano opcional) como /// mundo voxel del preview. Ruta = campo de archivo. Ver [`mapa_import`]. MundoDesdeMapa, /// Volver del mundo-desde-mapa al mundo procedural del bioma. QuitarMapa, /// **Ser desde foto** (capa 3): la imagen del campo de archivo REGULA los rasgos /// (piel/ropa) de un personaje ESTILIZADO del kit — no se voxeliza literal (eso da el /// valle). Recorte de persona por u2net (daemon `pixel-verbo`). Ver [`rostro`]. SerDesdeFoto, /// El worker terminó de leer la foto y de pedirle el recorte a u2net. /// Lleva el path porque de ahí sale el nombre del personaje, y el worker /// es quien sabe cuál se intentó. RostroListo(String, Result), // Historia (DAG): versionar el proyecto y ramificar alternativas. Versionar, Ramificar, /// Navegar: checkout de una versión (por su hash) → carga su proyecto. IrAVersion([u8; 32]), // Juego: entrar a la escena y moverse en primera persona. EntrarSalir, MoverJuego(EjeJuego, bool), Saltar(bool), AlternarVuelo, RerollSemilla, /// Dictar un suceso donde está parado el jugador. DictarSuceso(TipoSuceso), // Reloj. Tick, } /// Ejes de movimiento en modo JUEGO (WASD + subir/bajar en vuelo). #[derive(Clone, Copy)] pub(crate) enum EjeJuego { Fwd, Back, Left, Right, Arriba, Abajo, } /// Snapshot de las teclas de movimiento pulsadas — lo lee el paso de física del /// canvas cada frame (como el `Input` de llimphi-voxel-app). #[derive(Clone, Copy, Default)] pub(crate) struct InputJuego { pub(crate) fwd: bool, pub(crate) back: bool, pub(crate) left: bool, pub(crate) right: bool, pub(crate) arriba: bool, pub(crate) abajo: bool, pub(crate) salto: bool, } pub(crate) struct Model { pub(crate) theme: Theme, pub(crate) project: Project, /// Nivel activo. pub(crate) level: Level, /// Item seleccionado (id) por nivel. pub(crate) sel: [u64; 7], /// Pestaña de herramientas activa (rail derecho). pub(crate) tool_tab: usize, /// Generación: se incrementa en cada edición para regenerar el preview. pub(crate) gen: u64, /// Cámara de órbita. pub(crate) yaw: f32, pub(crate) pitch: f32, pub(crate) dist: f32, /// Preview perezoso (se construye en la 1ª pintada GPU). pub(crate) preview: Arc>>, /// **Mundo desde mapa real** (nahual-geo-voxel): si está cargado, el preview de /// Mundos usa ESTE grid en vez del procedural del bioma. `Arc` para clonarlo /// barato cada frame de `view`. Se limpia con «quitar mapa». pub(crate) mapa: Option>, /// Mensaje de estado. pub(crate) status: String, /// Campo de renombrado + foco. pub(crate) name_input: TextInputState, pub(crate) name_focused: bool, /// Campo de semilla (mundos) + foco. pub(crate) seed_input: TextInputState, pub(crate) seed_focused: bool, /// Campo de la IA + foco + "generando". pub(crate) ai_input: TextInputState, pub(crate) ai_focused: bool, pub(crate) ai_busy: bool, /// Ruta de archivo para abrir/guardar-como/combinar + foco. pub(crate) file_input: TextInputState, pub(crate) file_focused: bool, /// La HISTORIA: DAG de versiones y alternativas del proyecto (el cimiento). pub(crate) historia: Historia, /// Modo JUEGO: entrar a la escena y moverse en primera persona. pub(crate) jugando: bool, /// Estado del jugador (compartido: lo avanza el canvas GPU cada frame). pub(crate) jugador: Arc>, /// Teclas de movimiento pulsadas. pub(crate) input_juego: InputJuego, /// Vuelo libre (creativo) vs caminar con gravedad. pub(crate) volar: bool, /// Pedido de reposar al jugador al entrar a la escena. pub(crate) respawn_jugador: Arc>, /// Reproducción de escenas. pub(crate) time: f32, pub(crate) playing: bool, pub(crate) script_cam: bool, pub(crate) exporting: bool, /// Simulación de agua (ley Fluir) corriendo en el preview de mundo/bioma. pub(crate) simulating: bool, /// Estado del andar que se edita/previsualiza en Seres (0=quieto,1=caminar,2=correr). pub(crate) andar_estado: usize, /// Manada viva (conducta) corriendo en el preview de Seres. pub(crate) manada: bool, /// Reparto: actor y clave (waypoint) seleccionados al dirigir una escena. pub(crate) actor_sel: usize, pub(crate) key_sel: usize, /// Utilería: la pieza seleccionada de la escena. pub(crate) utileria_sel: usize, /// Decorado: la construcción seleccionada de la escena. pub(crate) decorado_sel: usize, /// Semilla del random de mundos (LCG; no hay `Math.random`). pub(crate) rng: u32, /// Estado del **sidebar unificado IZQUIERDO** (widget `rag-sidebar`): el /// selector de **niveles** (Leyes…Escenas). Selector **fijo** — un diente por /// nivel, el abierto sigue a [`Model::level`] (nunca colapsa). Los ejes de /// disposición, el buscador y el ancho del panel los reduce `RagSidebarState`. pub(crate) sidebar_left: RagSidebarState, /// Estado del **sidebar unificado DERECHO** (widget `rag-sidebar`): el selector /// de **herramientas** del nivel. Selector **fijo** — un diente por tool, el /// abierto sigue a [`Model::tool_tab`]. pub(crate) sidebar_right: RagSidebarState, /// Laboratorio de la ley `Ecuacion` seleccionada (nivel Leyes). Perezoso. pub(crate) lab: Option, /// Generación estructural de la ley en edición: bump al cambiar campos/fórmulas /// (para reconstruir el laboratorio, sin resetear al tocar sólo parámetros). pub(crate) lab_gen: u64, /// Edición de una fórmula: buffer compartido + foco + campo objetivo. pub(crate) formula_input: TextInputState, pub(crate) formula_focused: bool, pub(crate) formula_target: usize, /// Edición de la fórmula de **peso de un impulso** (nivel Seres › Conducta): buffer /// propio + foco + índice del impulso objetivo (separado del de Leyes). pub(crate) cond_formula_input: TextInputState, pub(crate) cond_formula_focused: bool, pub(crate) cond_formula_target: usize, /// Nivel Leyes: modo de la vista central — `false` laboratorio (heatmap), /// `true` grafo de nodos (la segunda superficie de autoría). pub(crate) leyes_node_mode: bool, /// Grafo de nodos de la ley (derivado de las fórmulas; estado de UI). pub(crate) eq_graph: Option, /// `lab_gen` con el que se armó el grafo (para saber si el texto cambió afuera). pub(crate) eq_graph_gen: u64, /// Portapapeles del sistema para copiar/cortar/pegar en los campos. pub(crate) clipboard: SystemClipboard, /// Editor de interiores: mueble «pincel» y su orientación al colocar. pub(crate) mueble_brush: Mueble, pub(crate) mueble_facing: u8, /// Editor de casas: preset de tipo de pueblo a aplicar sobre el bioma. pub(crate) pueblo_brush: PuebloTipo, } impl Model { /// Id del item seleccionado en el nivel activo (0 = ninguno). pub(crate) fn selected(&self) -> u64 { self.sel[self.level.index()] } pub(crate) fn set_selected(&mut self, id: u64) { let i = self.level.index(); self.sel[i] = id; } /// Los items `(id, nombre)` del nivel activo, en orden. pub(crate) fn items(&self) -> Vec<(u64, String)> { match self.level { Level::Leyes => self.project.leyes.iter().map(|x| (x.id, x.name.clone())).collect(), Level::Materiales => self.project.materiales.iter().map(|x| (x.id, x.name.clone())).collect(), Level::Seres => self.project.seres.iter().map(|x| (x.id, x.name.clone())).collect(), Level::Biomas => self.project.biomas.iter().map(|x| (x.id, x.name.clone())).collect(), Level::Mundos => self.project.mundos.iter().map(|x| (x.id, x.name.clone())).collect(), Level::Escenas => self.project.escenas.iter().map(|x| (x.id, x.name.clone())).collect(), Level::Interiores => self.project.interiores.iter().map(|x| (x.id, x.name.clone())).collect(), } } /// Nombre del item seleccionado (para cargar el campo de renombrar). pub(crate) fn selected_name(&self) -> String { let id = self.selected(); self.items() .into_iter() .find(|(i, _)| *i == id) .map(|(_, n)| n) .unwrap_or_default() } /// MundoRender que el centro previsualiza (según nivel/selección), con un /// fallback verde si no hay artefacto válido. pub(crate) fn preview_render(&self) -> MundoRender { let pick = match self.level { Level::Mundos | Level::Escenas => { let mundo = if self.level == Level::Mundos { self.selected() } else { self.project.escenas.iter().find(|e| e.id == self.selected()).map(|e| e.mundo).unwrap_or(0) }; self.project.render_mundo(mundo) } Level::Biomas => self.project.render_bioma(self.selected()), // Seres: parados sobre el primer bioma disponible. Level::Seres => self.project.biomas.first().and_then(|b| self.project.render_bioma(b.id)), _ => None, }; pick.unwrap_or_else(fallback_render) } } /// MundoRender por defecto (pradera neutra) cuando no hay artefacto que mostrar. pub(crate) fn fallback_render() -> MundoRender { let bioma = Bioma { id: 0, name: String::new(), base: 0.22, dune: 0.10, relief: 0.5, mountains: 0.4, water_level: 0.30, rivers: 0.20, peak_at: 0.80, ground: 0, cliff: 0, peak: None, objetos: vec![], minerales: vec![], densidad_urbana: 0.4, casas: vec![], fauna: vec![], seres: vec![], }; let palette = BiomaPalette::flat(Material::Grass.color(), Material::Grass.grain()); MundoRender { bioma, seed: 1, palette } } /// Distancia de órbita inicial/clamp en función del lado del mundo. pub(crate) fn default_dist() -> f32 { PREVIEW_DIM_XZ as f32 * 1.6 }