tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1use super::*;
2
3/// Los niveles de composición, de lo más básico a lo más compuesto.
4#[derive(Clone, Copy, PartialEq, Eq)]
5pub(crate) enum Level {
6 Leyes,
7 Materiales,
8 Seres,
9 Biomas,
10 Mundos,
11 Escenas,
12 Interiores,
13}
14
15impl Level {
16 pub(crate) const ALL: [Level; 7] = [
17 Level::Leyes,
18 Level::Materiales,
19 Level::Seres,
20 Level::Biomas,
21 Level::Mundos,
22 Level::Escenas,
23 Level::Interiores,
24 ];
25
26 pub(crate) fn index(self) -> usize {
27 Self::ALL.iter().position(|&l| l == self).unwrap_or(0)
28 }
29 pub(crate) fn from_index(i: usize) -> Level {
30 Self::ALL[i.min(Self::ALL.len() - 1)]
31 }
32 pub(crate) fn label(self) -> &'static str {
33 match self {
34 Level::Leyes => "Leyes",
35 Level::Materiales => "Materiales",
36 Level::Seres => "Seres",
37 Level::Biomas => "Biomas",
38 Level::Mundos => "Mundos",
39 Level::Escenas => "Escenas",
40 Level::Interiores => "Interiores",
41 }
42 }
43 pub(crate) fn icon(self) -> Icon {
44 match self {
45 Level::Leyes => Icon::Droplet,
46 Level::Materiales => Icon::Leaf,
47 Level::Seres => Icon::User,
48 Level::Biomas => Icon::Mountain,
49 Level::Mundos => Icon::Globe,
50 Level::Escenas => Icon::Film,
51 Level::Interiores => Icon::Grid,
52 }
53 }
54 /// Color de acento del nivel (íconos coloridos).
55 pub(crate) fn color(self) -> Color {
56 match self {
57 Level::Leyes => Color::from_rgba8(150, 130, 230, 255),
58 Level::Materiales => Color::from_rgba8(220, 170, 80, 255),
59 Level::Seres => Color::from_rgba8(110, 200, 130, 255),
60 Level::Biomas => Color::from_rgba8(90, 200, 200, 255),
61 Level::Mundos => Color::from_rgba8(90, 150, 230, 255),
62 Level::Escenas => Color::from_rgba8(230, 120, 110, 255),
63 Level::Interiores => Color::from_rgba8(210, 150, 200, 255),
64 }
65 }
66 /// Sets de herramientas (pestañas del rail derecho): rótulo + ícono.
67 pub(crate) fn tools(self) -> Vec<(&'static str, Icon)> {
68 match self {
69 Level::Leyes => vec![("Parámetros", Icon::Gauge)],
70 Level::Materiales => vec![("Aspecto", Icon::Image), ("Leyes", Icon::Droplet)],
71 Level::Seres => vec![
72 ("Cuerpo", Icon::User),
73 ("Andar", Icon::Gauge),
74 ("Conducta", Icon::Settings),
75 ("Piel", Icon::Image),
76 ("Camiseta", Icon::Image),
77 ("Pantalón", Icon::Image),
78 (rotulo_vestuario(), Icon::User),
79 ],
80 Level::Biomas => vec![
81 ("Relieve", Icon::Mountain),
82 ("Materiales", Icon::Leaf),
83 ("Objetos", Icon::Grid),
84 ("Minerales", Icon::Gauge),
85 ("Casas", Icon::Globe),
86 ("Fauna", Icon::User),
87 ("Seres", Icon::User),
88 ],
89 Level::Mundos => vec![("Semilla", Icon::Globe), ("Biomas", Icon::Mountain)],
90 Level::Escenas => vec![
91 ("Escena", Icon::Film),
92 ("Reparto", Icon::User),
93 ("Cámara", Icon::Camera),
94 (rotulo_utileria(), Icon::Grid),
95 (rotulo_decorado(), Icon::Globe),
96 ("Video", Icon::Play),
97 ],
98 Level::Interiores => vec![("Sala", Icon::Grid), ("Mueble", Icon::User)],
99 }
100 }
101}
102
103/// Parte coloreable de un ser. Las tres últimas son del vestuario y nacen sin color
104/// propio (heredan el de fábrica): el primer toque de un slider se lo fija.
105#[derive(Clone, Copy)]
106pub(crate) enum Part {
107 Skin,
108 Shirt,
109 Pants,
110 Sombrero,
111 Falda,
112 Manto,
113}
114
115/// Perilla continua del vestuario de un ser.
116#[derive(Clone, Copy)]
117pub(crate) enum PrendaField {
118 Barba,
119 Falda,
120 Manto,
121}
122
123/// El rótulo de la pestaña del vestuario. Las pestañas son `&'static str` (el resto
124/// del estudio es castellano fijo); éste se resuelve una vez, con el idioma que
125/// eligió `rimay_localize::init` al arrancar.
126pub(crate) fn rotulo_vestuario() -> &'static str {
127 static R: std::sync::OnceLock<String> = std::sync::OnceLock::new();
128 R.get_or_init(|| rimay_localize::t("voxelstudio-tab-wardrobe")).as_str()
129}
130
131/// El rótulo de la pestaña de utilería (ver [`rotulo_vestuario`]).
132pub(crate) fn rotulo_utileria() -> &'static str {
133 static R: std::sync::OnceLock<String> = std::sync::OnceLock::new();
134 R.get_or_init(|| rimay_localize::t("voxelstudio-tab-props")).as_str()
135}
136
137/// El rótulo de la pestaña del decorado (ver [`rotulo_vestuario`]).
138pub(crate) fn rotulo_decorado() -> &'static str {
139 static R: std::sync::OnceLock<String> = std::sync::OnceLock::new();
140 R.get_or_init(|| rimay_localize::t("voxelstudio-tab-sets")).as_str()
141}
142
143/// Perilla continua de una construcción del decorado: dónde va, en columnas de mundo.
144#[derive(Clone, Copy)]
145pub(crate) enum DecoradoField {
146 X,
147 Z,
148}
149
150/// Perilla continua de una pieza de utilería.
151#[derive(Clone, Copy)]
152pub(crate) enum UtileriaField {
153 X,
154 Y,
155 Z,
156 Alto,
157 Desde,
158 Giro,
159}
160
161/// Perilla continua del cielo de noche de una escena.
162#[derive(Clone, Copy)]
163pub(crate) enum CieloField {
164 Estrellas,
165 Luna,
166}
167
168/// Parámetro `f32` del relieve de un bioma.
169#[derive(Clone, Copy)]
170pub(crate) enum BiomaField {
171 Base,
172 Dune,
173 Relief,
174 Mountains,
175 Water,
176 Rivers,
177 PeakAt,
178}
179
180#[derive(Clone)]
181pub(crate) enum Msg {
182 // Navegación de niveles + items.
183 SelectLevel(Level),
184 SelectItem(u64),
185 SelectTool(usize),
186 /// Mensaje del **sidebar unificado IZQUIERDO** (selector de niveles): ejes de
187 /// disposición, buscador, control y resize. Se reduce con
188 /// `RagSidebarState::update`; el diente abierto se re-fija a `level`.
189 SidebarLeft(RagSidebarMsg),
190 /// Mensaje del **sidebar unificado DERECHO** (selector de herramientas). Igual:
191 /// se reduce con `RagSidebarState::update` y el abierto se re-fija a `tool_tab`.
192 SidebarRight(RagSidebarMsg),
193 NewItem,
194 DupItem,
195 DelItem,
196 // Renombrar (campo de texto).
197 /// Evento de mouse del campo de nombre (click/arrastre). El `Press` además
198 /// lo enfoca. Ver [`text_input_view_full`].
199 RenameCampo(TextInputEvent),
200 RenameKey(KeyEvent),
201 // Leyes.
202 CycleLeyKind,
203 SetLeyParam(usize, f32),
204 // Leyes por ecuación (autorables).
205 LeyPreset,
206 SetEcuParam(usize, f32),
207 /// Empezar a editar la fórmula `i` (desde un botón): carga su fuente en el
208 /// buffer compartido y enfoca. Ver [`Msg::FormulaCampo`] para el mouse del
209 /// propio campo ya en edición.
210 FormulaFocus(usize),
211 /// Evento de mouse del campo de fórmula ya en edición (click→caret,
212 /// arrastre→selección). Ver [`text_input_view_full`].
213 FormulaCampo(TextInputEvent),
214 FormulaKey(KeyEvent),
215 AddCampo,
216 RemoveCampo,
217 AddEcuParam,
218 RemoveEcuParam,
219 CycleLabField,
220 ReseedLab,
221 // Vista de grafo de nodos de la ley.
222 ToggleLeyesView,
223 GraphDrag(NodeId, f32, f32),
224 GraphConnect(NodeId, PinIdx, NodeId, PinIdx),
225 GraphAddNode(NodeOp),
226 GraphDeleteNode(NodeId),
227 // Materiales.
228 CycleMatRole,
229 SetMatColor(usize, f32),
230 SetMatGrain(f32),
231 SetMatBrillo(f32),
232 SetMatRugosidad(f32),
233 SetMatBisel(f32),
234 CycleMatParent,
235 AddMatLey,
236 RemoveMatLey,
237 CycleMatLey(usize),
238 // Seres.
239 CycleSereAge,
240 CycleSereCuerpo,
241 /// Cicla la especie del ser por el bestiario (`true` = hacia atrás).
242 CycleSereBicho(bool),
243 SetSereColor(Part, usize, f32),
244 // Vestuario del ser (CharSpec::atuendo).
245 CycleSerePeinado,
246 CycleSereSombrero,
247 ToggleSereAnteojos,
248 SetSerePrenda(PrendaField, f32),
249 // Andares (capa 2) del ser-rig.
250 CycleAndarEstado,
251 SetAndarCadencia(f32),
252 SetAndarAmplitud(usize, f32),
253 // Conducta (capa 3) — parámetros + impulsos de steering autorables.
254 SetConducta(usize, f32),
255 CondAddImpulso,
256 CondRemoveImpulso,
257 CondCycleImpulso(usize),
258 /// Empezar a editar el peso del impulso `i` (desde un botón): carga su
259 /// fuente en el buffer y enfoca. Ver [`Msg::CondFormulaCampo`] para el mouse.
260 CondFormulaFocus(usize),
261 /// Evento de mouse del campo de peso ya en edición (click→caret,
262 /// arrastre→selección). Ver [`text_input_view_full`].
263 CondFormulaCampo(TextInputEvent),
264 CondFormulaKey(KeyEvent),
265 ToggleManada,
266 // Biomas.
267 SetBiomaField(BiomaField, f32),
268 CycleBiomaGround,
269 CycleBiomaCliff,
270 CycleBiomaPeak,
271 AddBiomaObjeto,
272 RemoveBiomaObjeto,
273 SetObjetoDensidad(usize, f32),
274 CycleObjetoForma(usize),
275 CycleObjetoMaterial(usize),
276 // Minerales del bioma (con todos sus factores).
277 AddBiomaMineral,
278 RemoveBiomaMineral,
279 CycleMineralTipo(usize),
280 CycleMineralAspecto(usize),
281 SetMineralAbundancia(usize, f32),
282 SetMineralProfMin(usize, f32),
283 SetMineralProfMax(usize, f32),
284 SetMineralColor(usize, usize, f32),
285 SetMineralDureza(usize, f32),
286 SetMineralBrillo(usize, f32),
287 // Casas del bioma (pueblo procedural).
288 AddBiomaCasa,
289 RemoveBiomaCasa,
290 CycleCasaTipo(usize),
291 SetCasaDensidad(usize, f32),
292 SetDensidadUrbana(f32),
293 CyclePuebloTipo,
294 AplicarPuebloTipo,
295 // Fauna del bioma (seres esparcidos por semilla).
296 AddBiomaFauna,
297 RemoveBiomaFauna,
298 CycleFaunaBicho(usize),
299 SetFaunaDensidad(usize, f32),
300 // Interiores (colocar muebles).
301 CycleMuebleBrush,
302 CycleMuebleFacing,
303 PlaceMuebleAt(f32, f32),
304 RemoveLastPieza,
305 ClearPiezas,
306 SetInteriorAncho(f32),
307 SetInteriorFondo(f32),
308 CycleInteriorPiso,
309 CycleInteriorMuro,
310 ToggleInteriorMuros,
311 ToggleInteriorVentanas,
312 CycleInteriorTecho,
313 ToggleInteriorTecho,
314 AddBiomaSere,
315 RemoveBiomaSere,
316 CycleBiomaSere(usize),
317 SetBiomaSereProb(usize, f32),
318 // Mundos.
319 /// Evento de mouse del campo de semilla (click/arrastre). El `Press` además
320 /// lo enfoca. Ver [`text_input_view_full`].
321 SeedCampo(TextInputEvent),
322 SeedKey(KeyEvent),
323 SeedRandom,
324 CycleMundoBioma,
325 // Escenas — utilería (estrella, flor, perla, pluma).
326 SelectUtileria(usize),
327 AddUtileria,
328 RemoveUtileria,
329 CycleUtileriaCosa,
330 /// Cicla quién la lleva: nadie → actor 0 → … → nadie.
331 CycleUtileriaLleva,
332 SetUtileria(UtileriaField, f32),
333 // Escenas — decorado (el palacio, el kiosko…).
334 SelectDecorado(usize),
335 AddDecorado,
336 RemoveDecorado,
337 CycleDecoradoCasa,
338 /// Otra variante del mismo estilo (otra semilla).
339 OtraVarianteDecorado,
340 SetDecorado(DecoradoField, f32),
341 // Escenas — cielo de noche.
342 /// Día ↔ noche (la noche nace con [`Cielo::noche`](llimphi_voxel::Cielo::noche)).
343 ToggleNoche,
344 SetCielo(CieloField, f32),
345 // Escenas — reparto y recorrido (filmografía).
346 SelectActor(usize),
347 AddActor,
348 RemoveActor,
349 CycleActorSere,
350 SelectKey(usize),
351 AddKey,
352 RemoveKey,
353 SetKeyPos(bool, f32), // true=gx, false=gz
354 SetKeyTime(f32),
355 CycleKeyClip,
356 PlaceKeyAt(f32, f32), // click en el terreno → mueve la clave seleccionada
357 // Escenas.
358 CycleSceneMundo,
359 SetSceneDur(f32),
360 TogglePlay,
361 Scrub(f32),
362 ToggleSceneCam,
363 AddShot,
364 RemoveShot,
365 ExportVideo,
366 ExportDone(Result<String, String>),
367 // Simulación de la ley Fluir (agua).
368 ToggleSim,
369 // Cámara.
370 Orbit(f32, f32),
371 Zoom(f32),
372 // IA.
373 /// Evento de mouse del campo de la IA (click/arrastre). El `Press` además
374 /// lo enfoca. Ver [`text_input_view_full`].
375 AiCampo(TextInputEvent),
376 AiKey(KeyEvent),
377 AiGenerate,
378 AiBioma(Bioma),
379 AiSere(CharSpec),
380 AiScene(SceneSpec),
381 // Persistencia.
382 Save,
383 Load,
384 // Archivos: ruta editable + abrir / guardar-como / combinar.
385 /// Evento de mouse del campo de ruta (click/arrastre). El `Press` además lo
386 /// enfoca. Ver [`text_input_view_full`].
387 FileCampo(TextInputEvent),
388 FileKey(KeyEvent),
389 Abrir,
390 GuardarComo,
391 Combinar,
392 /// Cargar un **mapa real** (GeoJSON del visor, +`.pgm` hermano opcional) como
393 /// mundo voxel del preview. Ruta = campo de archivo. Ver [`mapa_import`].
394 MundoDesdeMapa,
395 /// Volver del mundo-desde-mapa al mundo procedural del bioma.
396 QuitarMapa,
397 /// **Ser desde foto** (capa 3): la imagen del campo de archivo REGULA los rasgos
398 /// (piel/ropa) de un personaje ESTILIZADO del kit — no se voxeliza literal (eso da el
399 /// valle). Recorte de persona por u2net (daemon `pixel-verbo`). Ver [`rostro`].
400 SerDesdeFoto,
401 /// El worker terminó de leer la foto y de pedirle el recorte a u2net.
402 /// Lleva el path porque de ahí sale el nombre del personaje, y el worker
403 /// es quien sabe cuál se intentó.
404 RostroListo(String, Result<rostro::Rasgos, String>),
405 // Historia (DAG): versionar el proyecto y ramificar alternativas.
406 Versionar,
407 Ramificar,
408 /// Navegar: checkout de una versión (por su hash) → carga su proyecto.
409 IrAVersion([u8; 32]),
410 // Juego: entrar a la escena y moverse en primera persona.
411 EntrarSalir,
412 MoverJuego(EjeJuego, bool),
413 Saltar(bool),
414 AlternarVuelo,
415 RerollSemilla,
416 /// Dictar un suceso donde está parado el jugador.
417 DictarSuceso(TipoSuceso),
418 // Reloj.
419 Tick,
420}
421
422/// Ejes de movimiento en modo JUEGO (WASD + subir/bajar en vuelo).
423#[derive(Clone, Copy)]
424pub(crate) enum EjeJuego {
425 Fwd,
426 Back,
427 Left,
428 Right,
429 Arriba,
430 Abajo,
431}
432
433/// Snapshot de las teclas de movimiento pulsadas — lo lee el paso de física del
434/// canvas cada frame (como el `Input` de llimphi-voxel-app).
435#[derive(Clone, Copy, Default)]
436pub(crate) struct InputJuego {
437 pub(crate) fwd: bool,
438 pub(crate) back: bool,
439 pub(crate) left: bool,
440 pub(crate) right: bool,
441 pub(crate) arriba: bool,
442 pub(crate) abajo: bool,
443 pub(crate) salto: bool,
444}
445
446pub(crate) struct Model {
447 pub(crate) theme: Theme,
448 pub(crate) project: Project,
449 /// Nivel activo.
450 pub(crate) level: Level,
451 /// Item seleccionado (id) por nivel.
452 pub(crate) sel: [u64; 7],
453 /// Pestaña de herramientas activa (rail derecho).
454 pub(crate) tool_tab: usize,
455 /// Generación: se incrementa en cada edición para regenerar el preview.
456 pub(crate) gen: u64,
457 /// Cámara de órbita.
458 pub(crate) yaw: f32,
459 pub(crate) pitch: f32,
460 pub(crate) dist: f32,
461 /// Preview perezoso (se construye en la 1ª pintada GPU).
462 pub(crate) preview: Arc<Mutex<Option<WorldPreview>>>,
463 /// **Mundo desde mapa real** (nahual-geo-voxel): si está cargado, el preview de
464 /// Mundos usa ESTE grid en vez del procedural del bioma. `Arc` para clonarlo
465 /// barato cada frame de `view`. Se limpia con «quitar mapa».
466 pub(crate) mapa: Option<Arc<VoxelGrid>>,
467 /// Mensaje de estado.
468 pub(crate) status: String,
469 /// Campo de renombrado + foco.
470 pub(crate) name_input: TextInputState,
471 pub(crate) name_focused: bool,
472 /// Campo de semilla (mundos) + foco.
473 pub(crate) seed_input: TextInputState,
474 pub(crate) seed_focused: bool,
475 /// Campo de la IA + foco + "generando".
476 pub(crate) ai_input: TextInputState,
477 pub(crate) ai_focused: bool,
478 pub(crate) ai_busy: bool,
479 /// Ruta de archivo para abrir/guardar-como/combinar + foco.
480 pub(crate) file_input: TextInputState,
481 pub(crate) file_focused: bool,
482 /// La HISTORIA: DAG de versiones y alternativas del proyecto (el cimiento).
483 pub(crate) historia: Historia,
484 /// Modo JUEGO: entrar a la escena y moverse en primera persona.
485 pub(crate) jugando: bool,
486 /// Estado del jugador (compartido: lo avanza el canvas GPU cada frame).
487 pub(crate) jugador: Arc<Mutex<Player>>,
488 /// Teclas de movimiento pulsadas.
489 pub(crate) input_juego: InputJuego,
490 /// Vuelo libre (creativo) vs caminar con gravedad.
491 pub(crate) volar: bool,
492 /// Pedido de reposar al jugador al entrar a la escena.
493 pub(crate) respawn_jugador: Arc<Mutex<bool>>,
494 /// Reproducción de escenas.
495 pub(crate) time: f32,
496 pub(crate) playing: bool,
497 pub(crate) script_cam: bool,
498 pub(crate) exporting: bool,
499 /// Simulación de agua (ley Fluir) corriendo en el preview de mundo/bioma.
500 pub(crate) simulating: bool,
501 /// Estado del andar que se edita/previsualiza en Seres (0=quieto,1=caminar,2=correr).
502 pub(crate) andar_estado: usize,
503 /// Manada viva (conducta) corriendo en el preview de Seres.
504 pub(crate) manada: bool,
505 /// Reparto: actor y clave (waypoint) seleccionados al dirigir una escena.
506 pub(crate) actor_sel: usize,
507 pub(crate) key_sel: usize,
508 /// Utilería: la pieza seleccionada de la escena.
509 pub(crate) utileria_sel: usize,
510 /// Decorado: la construcción seleccionada de la escena.
511 pub(crate) decorado_sel: usize,
512 /// Semilla del random de mundos (LCG; no hay `Math.random`).
513 pub(crate) rng: u32,
514 /// Estado del **sidebar unificado IZQUIERDO** (widget `rag-sidebar`): el
515 /// selector de **niveles** (Leyes…Escenas). Selector **fijo** — un diente por
516 /// nivel, el abierto sigue a [`Model::level`] (nunca colapsa). Los ejes de
517 /// disposición, el buscador y el ancho del panel los reduce `RagSidebarState`.
518 pub(crate) sidebar_left: RagSidebarState,
519 /// Estado del **sidebar unificado DERECHO** (widget `rag-sidebar`): el selector
520 /// de **herramientas** del nivel. Selector **fijo** — un diente por tool, el
521 /// abierto sigue a [`Model::tool_tab`].
522 pub(crate) sidebar_right: RagSidebarState,
523 /// Laboratorio de la ley `Ecuacion` seleccionada (nivel Leyes). Perezoso.
524 pub(crate) lab: Option<LawLab>,
525 /// Generación estructural de la ley en edición: bump al cambiar campos/fórmulas
526 /// (para reconstruir el laboratorio, sin resetear al tocar sólo parámetros).
527 pub(crate) lab_gen: u64,
528 /// Edición de una fórmula: buffer compartido + foco + campo objetivo.
529 pub(crate) formula_input: TextInputState,
530 pub(crate) formula_focused: bool,
531 pub(crate) formula_target: usize,
532 /// Edición de la fórmula de **peso de un impulso** (nivel Seres › Conducta): buffer
533 /// propio + foco + índice del impulso objetivo (separado del de Leyes).
534 pub(crate) cond_formula_input: TextInputState,
535 pub(crate) cond_formula_focused: bool,
536 pub(crate) cond_formula_target: usize,
537 /// Nivel Leyes: modo de la vista central — `false` laboratorio (heatmap),
538 /// `true` grafo de nodos (la segunda superficie de autoría).
539 pub(crate) leyes_node_mode: bool,
540 /// Grafo de nodos de la ley (derivado de las fórmulas; estado de UI).
541 pub(crate) eq_graph: Option<EqGraph>,
542 /// `lab_gen` con el que se armó el grafo (para saber si el texto cambió afuera).
543 pub(crate) eq_graph_gen: u64,
544 /// Portapapeles del sistema para copiar/cortar/pegar en los campos.
545 pub(crate) clipboard: SystemClipboard,
546 /// Editor de interiores: mueble «pincel» y su orientación al colocar.
547 pub(crate) mueble_brush: Mueble,
548 pub(crate) mueble_facing: u8,
549 /// Editor de casas: preset de tipo de pueblo a aplicar sobre el bioma.
550 pub(crate) pueblo_brush: PuebloTipo,
551}
552
553impl Model {
554 /// Id del item seleccionado en el nivel activo (0 = ninguno).
555 pub(crate) fn selected(&self) -> u64 {
556 self.sel[self.level.index()]
557 }
558 pub(crate) fn set_selected(&mut self, id: u64) {
559 let i = self.level.index();
560 self.sel[i] = id;
561 }
562
563 /// Los items `(id, nombre)` del nivel activo, en orden.
564 pub(crate) fn items(&self) -> Vec<(u64, String)> {
565 match self.level {
566 Level::Leyes => self.project.leyes.iter().map(|x| (x.id, x.name.clone())).collect(),
567 Level::Materiales => self.project.materiales.iter().map(|x| (x.id, x.name.clone())).collect(),
568 Level::Seres => self.project.seres.iter().map(|x| (x.id, x.name.clone())).collect(),
569 Level::Biomas => self.project.biomas.iter().map(|x| (x.id, x.name.clone())).collect(),
570 Level::Mundos => self.project.mundos.iter().map(|x| (x.id, x.name.clone())).collect(),
571 Level::Escenas => self.project.escenas.iter().map(|x| (x.id, x.name.clone())).collect(),
572 Level::Interiores => self.project.interiores.iter().map(|x| (x.id, x.name.clone())).collect(),
573 }
574 }
575
576 /// Nombre del item seleccionado (para cargar el campo de renombrar).
577 pub(crate) fn selected_name(&self) -> String {
578 let id = self.selected();
579 self.items()
580 .into_iter()
581 .find(|(i, _)| *i == id)
582 .map(|(_, n)| n)
583 .unwrap_or_default()
584 }
585
586 /// MundoRender que el centro previsualiza (según nivel/selección), con un
587 /// fallback verde si no hay artefacto válido.
588 pub(crate) fn preview_render(&self) -> MundoRender {
589 let pick = match self.level {
590 Level::Mundos | Level::Escenas => {
591 let mundo = if self.level == Level::Mundos {
592 self.selected()
593 } else {
594 self.project.escenas.iter().find(|e| e.id == self.selected()).map(|e| e.mundo).unwrap_or(0)
595 };
596 self.project.render_mundo(mundo)
597 }
598 Level::Biomas => self.project.render_bioma(self.selected()),
599 // Seres: parados sobre el primer bioma disponible.
600 Level::Seres => self.project.biomas.first().and_then(|b| self.project.render_bioma(b.id)),
601 _ => None,
602 };
603 pick.unwrap_or_else(fallback_render)
604 }
605}
606
607/// MundoRender por defecto (pradera neutra) cuando no hay artefacto que mostrar.
608pub(crate) fn fallback_render() -> MundoRender {
609 let bioma = Bioma {
610 id: 0,
611 name: String::new(),
612 base: 0.22,
613 dune: 0.10,
614 relief: 0.5,
615 mountains: 0.4,
616 water_level: 0.30,
617 rivers: 0.20,
618 peak_at: 0.80,
619 ground: 0,
620 cliff: 0,
621 peak: None,
622 objetos: vec![],
623 minerales: vec![],
624 densidad_urbana: 0.4,
625 casas: vec![],
626 fauna: vec![],
627 seres: vec![],
628 };
629 let palette = BiomaPalette::flat(Material::Grass.color(), Material::Grass.grain());
630 MundoRender { bioma, seed: 1, palette }
631}
632
633/// Distancia de órbita inicial/clamp en función del lado del mundo.
634pub(crate) fn default_dist() -> f32 {
635 PREVIEW_DIM_XZ as f32 * 1.6
636}
637