tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main llimphi-voxel-studio/src/modelo.rs Rust · 637 líneas · 22.1 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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