gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-voxel-studio/src/actualizar.rs Rust · 1572 lines · 62.7 KB History Who touched what raw

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

1use super::*;
2
3/// El `update` de la app. Vive afuera del bloque `impl App` porque un trait
4/// admite UN solo bloque por tipo: para partir un método de mil cuatrocientas
5/// líneas hay que sacarle el cuerpo y dejar el impl delegando. Se llama
6/// distinto que el método para que el delegador no se llame a sí mismo.
7pub(crate) fn actualizar(mut model: Model, msg: Msg, handle: &Handle<Msg>) -> Model {
8    match msg {
9        Msg::SelectLevel(l) => {
10            model.level = l;
11            model.tool_tab = 0;
12            model.name_focused = false;
13            model.seed_focused = false;
14            model.formula_focused = false;
15            model.cond_formula_focused = false;
16            model.gen += 1;
17            // Selector fijo: el diente del nivel activo queda abierto; el de
18            // herramientas vuelve al primer tool. Se limpia el filtro de ambos.
19            pin_sidebars(&mut model);
20            model.sidebar_left.search.clear();
21            model.sidebar_right.search.clear();
22            sync_inputs(&mut model);
23            if model.level == Level::Mundos {
24                refrescar_mapa(&mut model); // al entrar a Mundos, refleja su mapa (o no)
25            }
26        }
27        Msg::SelectItem(id) => {
28            model.set_selected(id);
29            model.time = 0.0;
30            model.cond_formula_focused = false;
31            model.gen += 1;
32            sync_inputs(&mut model);
33            if model.level == Level::Mundos {
34                refrescar_mapa(&mut model); // el mundo elegido puede venir de un mapa
35            }
36        }
37        Msg::SelectTool(i) => {
38            model.tool_tab = i;
39            model.cond_formula_focused = false;
40            pin_sidebars(&mut model);
41            model.sidebar_right.search.clear();
42        }
43        Msg::SidebarLeft(sm) => {
44            // Selector fijo de niveles: tras reducir ejes/buscador/resize, el
45            // diente del nivel activo sigue abierto (nunca colapsa).
46            llimphi_widget_rag_sidebar::persistencia::update_persistente(&mut model.sidebar_left, sm, "voxel-studio", "izq");
47            pin_sidebars(&mut model);
48        }
49        Msg::SidebarRight(sm) => {
50            llimphi_widget_rag_sidebar::persistencia::update_persistente(&mut model.sidebar_right, sm, "voxel-studio", "der");
51            pin_sidebars(&mut model);
52        }
53        Msg::NewItem => {
54            let id = new_item(&mut model);
55            model.set_selected(id);
56            model.gen += 1;
57            model.status = format!("nuevo en {}", model.level.label());
58            sync_inputs(&mut model);
59        }
60        Msg::DupItem => {
61            if let Some(id) = duplicate_item(&mut model) {
62                model.set_selected(id);
63                model.gen += 1;
64                model.status = "duplicado".into();
65                sync_inputs(&mut model);
66            }
67        }
68        Msg::DelItem => {
69            delete_selected(&mut model);
70            // Reseleccionar el primero que quede.
71            let first = model.items().first().map(|(i, _)| *i).unwrap_or(0);
72            model.set_selected(first);
73            model.gen += 1;
74            model.status = "borrado".into();
75            sync_inputs(&mut model);
76        }
77        Msg::RenameCampo(ev) => {
78            // El press/click enfoca este campo (y desenfoca los demás); el
79            // arrastre extiende la selección.
80            if matches!(ev, TextInputEvent::Press(_)) {
81                model.name_focused = true;
82                model.seed_focused = false;
83                model.ai_focused = false;
84            }
85            model.name_input.handle(ev, &mut model.clipboard);
86            let txt = model.name_input.text();
87            set_selected_name(&mut model, txt);
88        }
89        Msg::RenameKey(ev) => {
90            // `handle` cubre escribir, copiar/cortar/pegar y acentos.
91            model.name_input.handle(TextInputEvent::Key(ev), &mut model.clipboard);
92            let txt = model.name_input.text();
93            set_selected_name(&mut model, txt);
94        }
95        Msg::CycleLeyKind => {
96            if let Some(l) = model.project.leyes.iter_mut().find(|x| x.id == model.sel[0]) {
97                l.kind = l.kind.next();
98            }
99        }
100        Msg::SetLeyParam(i, v) => {
101            if let Some(l) = model.project.leyes.iter_mut().find(|x| x.id == model.sel[0]) {
102                l.kind.set_param(i, v);
103            }
104        }
105        Msg::LeyPreset => {
106            // Cicla la ley `Ecuacion` seleccionada por un catálogo de leyes de
107            // fábrica (autoradas, editables). Cambio estructural → reconstruye el lab.
108            if let Some(l) = sel_ley_mut(&mut model) {
109                if matches!(l.kind, LeyKind::Ecuacion { .. }) {
110                    l.kind = next_preset(&l.kind);
111                    model.formula_focused = false;
112                    model.lab_gen += 1;
113                }
114            }
115        }
116        Msg::SetEcuParam(i, v) => {
117            if let Some(l) = sel_ley_mut(&mut model) {
118                l.kind.set_ecuacion_param(i, v);
119            }
120        }
121        Msg::FormulaFocus(i) => {
122            let src = sel_ley(&model)
123                .and_then(|l| match &l.kind {
124                    LeyKind::Ecuacion { fuentes, .. } => fuentes.get(i).cloned(),
125                    _ => None,
126                })
127                .unwrap_or_default();
128            model.formula_input.set_text(src);
129            model.formula_focused = true;
130            model.formula_target = i;
131            model.name_focused = false;
132            model.seed_focused = false;
133            model.ai_focused = false;
134        }
135        Msg::FormulaCampo(ev) => {
136            // El campo sólo se dibuja mientras se edita esa fórmula, así que
137            // el mouse (click→caret, arrastre→selección) va directo al buffer.
138            model.formula_input.handle(ev, &mut model.clipboard);
139        }
140        Msg::FormulaKey(ev) => {
141            // Enter confirma (desenfoca); el resto edita el buffer y se escribe en vivo.
142            if ev.state == KeyState::Pressed && matches!(&ev.key, Key::Named(NamedKey::Enter)) {
143                model.formula_focused = false;
144            } else {
145                // `handle` cubre copiar/cortar/pegar y acentos.
146                model.formula_input.handle(TextInputEvent::Key(ev), &mut model.clipboard);
147                let txt = model.formula_input.text();
148                let target = model.formula_target;
149                if let Some(l) = sel_ley_mut(&mut model) {
150                    if let LeyKind::Ecuacion { fuentes, .. } = &mut l.kind {
151                        if let Some(f) = fuentes.get_mut(target) {
152                            *f = txt;
153                        }
154                    }
155                }
156                model.lab_gen += 1;
157            }
158        }
159        Msg::AddCampo => {
160            if let Some(l) = sel_ley_mut(&mut model) {
161                if let LeyKind::Ecuacion { campos, fuentes, .. } = &mut l.kind {
162                    let name = format!("c{}", campos.len());
163                    campos.push(FieldDef::new(name, 0.0, 0.0, 1.0));
164                    fuentes.push("0".to_string());
165                    model.lab_gen += 1;
166                }
167            }
168        }
169        Msg::RemoveCampo => {
170            if let Some(l) = sel_ley_mut(&mut model) {
171                if let LeyKind::Ecuacion { campos, fuentes, .. } = &mut l.kind {
172                    if campos.len() > 1 {
173                        campos.pop();
174                        fuentes.pop();
175                        model.formula_focused = false;
176                        model.lab_gen += 1;
177                    }
178                }
179            }
180        }
181        Msg::AddEcuParam => {
182            if let Some(l) = sel_ley_mut(&mut model) {
183                if let LeyKind::Ecuacion { params, .. } = &mut l.kind {
184                    let name = format!("p{}", params.len());
185                    params.push(ParamDef::new(name, 0.1, 0.0, 1.0));
186                    model.lab_gen += 1;
187                }
188            }
189        }
190        Msg::RemoveEcuParam => {
191            if let Some(l) = sel_ley_mut(&mut model) {
192                if let LeyKind::Ecuacion { params, .. } = &mut l.kind {
193                    params.pop();
194                    model.lab_gen += 1;
195                }
196            }
197        }
198        Msg::CycleLabField => {
199            if let Some(lab) = &mut model.lab {
200                let n = lab.engine.fields().len().max(1);
201                lab.vis = (lab.vis + 1) % n;
202            }
203        }
204        Msg::ReseedLab => {
205            if let Some(lab) = &mut model.lab {
206                lab.reseed();
207            }
208        }
209        Msg::ToggleLeyesView => {
210            model.leyes_node_mode = !model.leyes_node_mode;
211            model.formula_focused = false;
212            if model.leyes_node_mode {
213                rebuild_eq_graph(&mut model);
214            } else {
215                model.eq_graph = None;
216            }
217        }
218        Msg::GraphDrag(id, dx, dy) => {
219            if let Some(g) = &mut model.eq_graph {
220                g.drag(id, dx, dy); // layout puro: no recompila
221            }
222        }
223        Msg::GraphConnect(from, _fp, to, tp) => {
224            let ok = model.eq_graph.as_mut().map(|g| g.connect(from, to, tp)).unwrap_or(false);
225            if ok {
226                sync_graph_to_fuentes(&mut model);
227            }
228        }
229        Msg::GraphAddNode(op) => {
230            if let Some(g) = &mut model.eq_graph {
231                g.add(op, 40.0, 40.0); // suelto; entra en la fórmula al conectarlo
232            }
233        }
234        Msg::GraphDeleteNode(id) => {
235            let had = model.eq_graph.is_some();
236            if let Some(g) = &mut model.eq_graph {
237                g.delete(id);
238            }
239            if had {
240                sync_graph_to_fuentes(&mut model);
241            }
242        }
243        Msg::CycleMatRole => {
244            if let Some(m) = sel_material_mut(&mut model) {
245                m.role = match m.role {
246                    MatRole::Terreno => MatRole::Objeto(Forma::Columnar),
247                    MatRole::Objeto(_) => MatRole::Terreno,
248                };
249                model.gen += 1;
250            }
251        }
252        Msg::SetMatColor(ch, v) => {
253            if let Some(m) = sel_material_mut(&mut model) {
254                let mut c = m.color.unwrap_or([0.6, 0.6, 0.6]);
255                if ch < 3 {
256                    c[ch] = v.clamp(0.0, 1.0);
257                }
258                m.color = Some(c);
259                model.gen += 1;
260            }
261        }
262        Msg::SetMatGrain(v) => {
263            if let Some(m) = sel_material_mut(&mut model) {
264                m.grain = Some(v.clamp(0.0, 1.0));
265                model.gen += 1;
266            }
267        }
268        Msg::SetMatBrillo(v) => {
269            if let Some(m) = sel_material_mut(&mut model) {
270                m.brillo = Some(v.clamp(0.0, 1.0));
271                model.gen += 1;
272            }
273        }
274        Msg::SetMatRugosidad(v) => {
275            if let Some(m) = sel_material_mut(&mut model) {
276                m.rugosidad = Some(v.clamp(0.0, 1.5));
277                // No cambia el grid (look de shader) → sin bump de gen.
278            }
279        }
280        Msg::SetMatBisel(v) => {
281            if let Some(m) = sel_material_mut(&mut model) {
282                m.bisel = Some(v.clamp(0.0, 1.0));
283            }
284        }
285        Msg::CycleMatParent => {
286            let id = model.sel[1];
287            // Lista de candidatos a padre (todos menos sí mismo).
288            let cands: Vec<u64> = model.project.materiales.iter().map(|m| m.id).filter(|&i| i != id).collect();
289            if let Some(m) = sel_material_mut(&mut model) {
290                m.parent = next_option(m.parent, &cands);
291                model.gen += 1;
292            }
293        }
294        Msg::AddMatLey => {
295            let first_ley = model.project.leyes.first().map(|l| l.id);
296            if let (Some(ley), Some(m)) = (first_ley, sel_material_mut(&mut model)) {
297                m.leyes.push(LeyUso { ley, params: vec![] });
298            }
299        }
300        Msg::RemoveMatLey => {
301            if let Some(m) = sel_material_mut(&mut model) {
302                m.leyes.pop();
303            }
304        }
305        Msg::CycleMatLey(i) => {
306            // Cicla a qué ley del proyecto apunta la ley `i` del material (para
307            // poder señalar una ley Ecuacion y que corra sobre el material).
308            let ids: Vec<u64> = model.project.leyes.iter().map(|l| l.id).collect();
309            if !ids.is_empty() {
310                if let Some(m) = sel_material_mut(&mut model) {
311                    if let Some(u) = m.leyes.get_mut(i) {
312                        let pos = ids.iter().position(|&id| id == u.ley).unwrap_or(0);
313                        u.ley = ids[(pos + 1) % ids.len()];
314                        u.params.clear();
315                    }
316                }
317                model.gen += 1;
318            }
319        }
320        Msg::CycleSereAge => {
321            if let Some(c) = sel_sere_mut(&mut model) {
322                c.age = c.age.next();
323            }
324        }
325        Msg::CycleSereCuerpo => {
326            if let Some(c) = sel_sere_mut(&mut model) {
327                c.cycle_cuerpo();
328                model.gen += 1;
329            }
330        }
331        Msg::CycleSereBicho(atras) => {
332            if let Some(c) = sel_sere_mut(&mut model) {
333                c.cycle_bicho(atras);
334                model.gen += 1;
335            }
336        }
337        Msg::SetSereColor(part, ch, v) => {
338            if let Some(c) = sel_sere_mut(&mut model) {
339                // Una prenda sin color propio arranca del que se le ve (el de fábrica),
340                // no de negro: si no, el primer toque del slider la apaga entera.
341                let remera = c.shirt;
342                let a = &mut c.atuendo;
343                let rgb = match part {
344                    Part::Skin => &mut c.skin,
345                    Part::Shirt => &mut c.shirt,
346                    Part::Pants => &mut c.pants,
347                    Part::Sombrero => {
348                        let de_fabrica = a.color_sombrero_efectivo(remera);
349                        a.color_sombrero.get_or_insert(de_fabrica)
350                    }
351                    Part::Falda => {
352                        let de_fabrica = a.color_falda_efectivo(remera);
353                        a.color_falda.get_or_insert(de_fabrica)
354                    }
355                    Part::Manto => {
356                        let de_fabrica = a.color_manto_efectivo(remera);
357                        a.color_manto.get_or_insert(de_fabrica)
358                    }
359                };
360                if ch < 3 {
361                    rgb[ch] = v.clamp(0.0, 1.0);
362                }
363            }
364        }
365        Msg::CycleSerePeinado => {
366            if let Some(c) = sel_sere_mut(&mut model) {
367                c.atuendo.peinado = c.atuendo.peinado.next();
368            }
369        }
370        Msg::CycleSereSombrero => {
371            if let Some(c) = sel_sere_mut(&mut model) {
372                c.atuendo.sombrero = c.atuendo.sombrero.next();
373            }
374        }
375        Msg::ToggleSereAnteojos => {
376            if let Some(c) = sel_sere_mut(&mut model) {
377                c.atuendo.anteojos = !c.atuendo.anteojos;
378            }
379        }
380        Msg::SetSerePrenda(campo, v) => {
381            if let Some(c) = sel_sere_mut(&mut model) {
382                let v = v.clamp(0.0, 1.0);
383                match campo {
384                    PrendaField::Barba => c.atuendo.barba = v,
385                    PrendaField::Falda => c.atuendo.falda = v,
386                    PrendaField::Manto => c.atuendo.manto = v,
387                }
388            }
389        }
390        Msg::CycleAndarEstado => {
391            model.andar_estado = (model.andar_estado + 1) % 3;
392        }
393        Msg::SetAndarCadencia(v) => {
394            let est = model.andar_estado;
395            if let Some(c) = sel_sere_mut(&mut model) {
396                if let Some(m) = &mut c.cuerpo {
397                    m.andares.estado_mut(est).cadencia = v.clamp(0.0, 18.0);
398                }
399            }
400        }
401        Msg::SetAndarAmplitud(seg, v) => {
402            let est = model.andar_estado;
403            if let Some(c) = sel_sere_mut(&mut model) {
404                if let Some(m) = &mut c.cuerpo {
405                    if let Some(o) = m.andares.estado_mut(est).osc.get_mut(seg) {
406                        o.amplitud = v.clamp(0.0, 1.5);
407                    }
408                }
409            }
410        }
411        Msg::SetConducta(i, v) => {
412            if let Some(c) = sel_sere_mut(&mut model) {
413                c.conducta.set(i, v);
414            }
415        }
416        Msg::CondAddImpulso => {
417            if let Some(c) = sel_sere_mut(&mut model) {
418                materializar_impulsos(c);
419                c.conducta.impulsos.push(Impulso::new(Primitiva::Deambular, "1"));
420            }
421        }
422        Msg::CondRemoveImpulso => {
423            if let Some(c) = sel_sere_mut(&mut model) {
424                materializar_impulsos(c);
425                if c.conducta.impulsos.len() > 1 {
426                    c.conducta.impulsos.pop();
427                }
428                model.cond_formula_focused = false;
429            }
430        }
431        Msg::CondCycleImpulso(i) => {
432            if let Some(c) = sel_sere_mut(&mut model) {
433                materializar_impulsos(c);
434                if let Some(imp) = c.conducta.impulsos.get_mut(i) {
435                    imp.primitiva = imp.primitiva.next();
436                }
437            }
438        }
439        Msg::CondFormulaFocus(i) => {
440            let src = sel_sere(&model)
441                .map(|c| c.conducta.impulsos_efectivos())
442                .and_then(|imps| imps.get(i).map(|imp| imp.peso.clone()))
443                .unwrap_or_default();
444            model.cond_formula_input.set_text(src);
445            model.cond_formula_focused = true;
446            model.cond_formula_target = i;
447            model.formula_focused = false;
448            model.name_focused = false;
449            model.seed_focused = false;
450            model.ai_focused = false;
451        }
452        Msg::CondFormulaCampo(ev) => {
453            // El campo sólo se dibuja mientras se edita ese peso: el mouse va
454            // directo al buffer (click→caret, arrastre→selección).
455            model.cond_formula_input.handle(ev, &mut model.clipboard);
456        }
457        Msg::CondFormulaKey(ev) => {
458            if ev.state == KeyState::Pressed && matches!(&ev.key, Key::Named(NamedKey::Enter)) {
459                model.cond_formula_focused = false;
460            } else {
461                model.cond_formula_input.handle(TextInputEvent::Key(ev), &mut model.clipboard);
462                let txt = model.cond_formula_input.text();
463                let target = model.cond_formula_target;
464                if let Some(c) = sel_sere_mut(&mut model) {
465                    materializar_impulsos(c);
466                    if let Some(imp) = c.conducta.impulsos.get_mut(target) {
467                        imp.peso = txt;
468                    }
469                }
470            }
471        }
472        Msg::ToggleManada => {
473            model.manada = !model.manada;
474            model.gen += 1; // repone el terreno y (re)inicia/limpia la manada
475            model.status = if model.manada { "manada viva".into() } else { "manada quieta".into() };
476        }
477        Msg::SetBiomaField(f, v) => {
478            if let Some(b) = sel_bioma_mut(&mut model) {
479                match f {
480                    BiomaField::Base => b.base = v.clamp(0.0, 0.9),
481                    BiomaField::Dune => b.dune = v.clamp(0.0, 0.4),
482                    BiomaField::Relief => b.relief = v.clamp(0.0, 1.0),
483                    BiomaField::Mountains => b.mountains = v.clamp(0.0, 1.0),
484                    BiomaField::Water => b.water_level = v.clamp(0.0, 0.9),
485                    BiomaField::Rivers => b.rivers = v.clamp(0.0, 1.0),
486                    BiomaField::PeakAt => b.peak_at = v.clamp(0.0, 1.0),
487                }
488                model.gen += 1;
489            }
490        }
491        Msg::CycleBiomaGround => {
492            let mats = terreno_ids(&model);
493            if let Some(b) = sel_bioma_mut(&mut model) {
494                b.ground = next_in(&mats, b.ground);
495                model.gen += 1;
496            }
497        }
498        Msg::CycleBiomaCliff => {
499            let mats = terreno_ids(&model);
500            if let Some(b) = sel_bioma_mut(&mut model) {
501                b.cliff = next_in(&mats, b.cliff);
502                model.gen += 1;
503            }
504        }
505        Msg::CycleBiomaPeak => {
506            let mats = terreno_ids(&model);
507            if let Some(b) = sel_bioma_mut(&mut model) {
508                b.peak = next_option(b.peak, &mats);
509                model.gen += 1;
510            }
511        }
512        Msg::AddBiomaObjeto => {
513            let obj = objeto_ids(&model);
514            if let (Some(&mat), Some(b)) = (obj.first(), sel_bioma_mut(&mut model)) {
515                b.objetos.push(llimphi_voxel::ObjetoUso { material: mat, densidad: 0.01, forma: Forma::Columnar });
516                model.gen += 1;
517            }
518        }
519        Msg::RemoveBiomaObjeto => {
520            if let Some(b) = sel_bioma_mut(&mut model) {
521                b.objetos.pop();
522                model.gen += 1;
523            }
524        }
525        Msg::SetObjetoDensidad(i, v) => {
526            if let Some(b) = sel_bioma_mut(&mut model) {
527                if let Some(o) = b.objetos.get_mut(i) {
528                    o.densidad = v.clamp(0.0, 0.05);
529                }
530                model.gen += 1;
531            }
532        }
533        Msg::CycleObjetoForma(i) => {
534            if let Some(b) = sel_bioma_mut(&mut model) {
535                if let Some(o) = b.objetos.get_mut(i) {
536                    o.forma = o.forma.next();
537                }
538                model.gen += 1;
539            }
540        }
541        Msg::CycleObjetoMaterial(i) => {
542            let ids = objeto_ids(&model);
543            if let Some(b) = sel_bioma_mut(&mut model) {
544                if let Some(o) = b.objetos.get_mut(i) {
545                    if let Some(pos) = ids.iter().position(|&m| m == o.material) {
546                        o.material = ids[(pos + 1) % ids.len().max(1)];
547                    } else if let Some(&first) = ids.first() {
548                        o.material = first;
549                    }
550                }
551                model.gen += 1;
552            }
553        }
554        Msg::AddBiomaMineral => {
555            if let Some(b) = sel_bioma_mut(&mut model) {
556                b.minerales.push(MineralUso::of(Mineral::Hierro));
557                model.gen += 1;
558            }
559        }
560        Msg::RemoveBiomaMineral => {
561            if let Some(b) = sel_bioma_mut(&mut model) {
562                b.minerales.pop();
563                model.gen += 1;
564            }
565        }
566        Msg::CycleMineralTipo(i) => {
567            if let Some(b) = sel_bioma_mut(&mut model) {
568                if let Some(mu) = b.minerales.get_mut(i) {
569                    let next = mu.mineral.next();
570                    mu.reset_to(next);
571                }
572                model.gen += 1;
573            }
574        }
575        Msg::CycleMineralAspecto(i) => {
576            if let Some(b) = sel_bioma_mut(&mut model) {
577                if let Some(mu) = b.minerales.get_mut(i) {
578                    mu.aspecto = mu.aspecto.next();
579                }
580                model.gen += 1;
581            }
582        }
583        Msg::SetMineralAbundancia(i, v) => {
584            if let Some(b) = sel_bioma_mut(&mut model) {
585                if let Some(mu) = b.minerales.get_mut(i) {
586                    mu.abundancia = v.clamp(0.0, 1.0);
587                }
588                model.gen += 1;
589            }
590        }
591        Msg::SetMineralProfMin(i, v) => {
592            if let Some(b) = sel_bioma_mut(&mut model) {
593                if let Some(mu) = b.minerales.get_mut(i) {
594                    mu.prof_min = v.clamp(0.0, mu.prof_max);
595                }
596                model.gen += 1;
597            }
598        }
599        Msg::SetMineralProfMax(i, v) => {
600            if let Some(b) = sel_bioma_mut(&mut model) {
601                if let Some(mu) = b.minerales.get_mut(i) {
602                    mu.prof_max = v.clamp(mu.prof_min, 1.0);
603                }
604                model.gen += 1;
605            }
606        }
607        Msg::SetMineralColor(i, ch, v) => {
608            if let Some(b) = sel_bioma_mut(&mut model) {
609                if let Some(mu) = b.minerales.get_mut(i) {
610                    if let Some(c) = mu.color.get_mut(ch) {
611                        *c = (v.clamp(0.0, 1.0) * 255.0) as u8;
612                    }
613                }
614                model.gen += 1;
615            }
616        }
617        Msg::SetMineralDureza(i, v) => {
618            if let Some(b) = sel_bioma_mut(&mut model) {
619                if let Some(mu) = b.minerales.get_mut(i) {
620                    mu.dureza = v.clamp(1.0, 10.0);
621                }
622                model.gen += 1;
623            }
624        }
625        Msg::SetMineralBrillo(i, v) => {
626            if let Some(b) = sel_bioma_mut(&mut model) {
627                if let Some(mu) = b.minerales.get_mut(i) {
628                    mu.brillo = v.clamp(0.0, 1.0);
629                }
630                model.gen += 1;
631            }
632        }
633        Msg::AddBiomaCasa => {
634            if let Some(b) = sel_bioma_mut(&mut model) {
635                b.casas.push(CasaUso { tipo: CasaTipo::Cabana, densidad: 0.25 });
636                model.gen += 1;
637            }
638        }
639        Msg::RemoveBiomaCasa => {
640            if let Some(b) = sel_bioma_mut(&mut model) {
641                b.casas.pop();
642                model.gen += 1;
643            }
644        }
645        Msg::CycleCasaTipo(i) => {
646            if let Some(b) = sel_bioma_mut(&mut model) {
647                if let Some(cu) = b.casas.get_mut(i) {
648                    cu.tipo = cu.tipo.next();
649                }
650                model.gen += 1;
651            }
652        }
653        Msg::SetCasaDensidad(i, v) => {
654            if let Some(b) = sel_bioma_mut(&mut model) {
655                if let Some(cu) = b.casas.get_mut(i) {
656                    cu.densidad = v.clamp(0.0, 1.0);
657                }
658                model.gen += 1;
659            }
660        }
661        Msg::SetDensidadUrbana(v) => {
662            if let Some(b) = sel_bioma_mut(&mut model) {
663                b.densidad_urbana = v.clamp(0.05, 0.95);
664                model.gen += 1;
665            }
666        }
667        Msg::CyclePuebloTipo => {
668            model.pueblo_brush = model.pueblo_brush.next();
669            model.status = format!("preset de pueblo: {}", model.pueblo_brush.label());
670        }
671        Msg::AplicarPuebloTipo => {
672            let pt = model.pueblo_brush;
673            if let Some(b) = sel_bioma_mut(&mut model) {
674                b.casas = pt.casas();
675                b.densidad_urbana = pt.densidad();
676                model.gen += 1;
677                model.status = format!("aplicado preset {}", pt.label());
678            }
679        }
680        Msg::AddBiomaFauna => {
681            if let Some(b) = sel_bioma_mut(&mut model) {
682                b.fauna.push(BichoUso { bicho: Bicho::Vaca, densidad: 0.3 });
683                model.gen += 1;
684            }
685        }
686        Msg::RemoveBiomaFauna => {
687            if let Some(b) = sel_bioma_mut(&mut model) {
688                b.fauna.pop();
689                model.gen += 1;
690            }
691        }
692        Msg::CycleFaunaBicho(i) => {
693            if let Some(b) = sel_bioma_mut(&mut model) {
694                if let Some(fu) = b.fauna.get_mut(i) {
695                    fu.bicho = fu.bicho.next();
696                }
697                model.gen += 1;
698            }
699        }
700        Msg::SetFaunaDensidad(i, v) => {
701            if let Some(b) = sel_bioma_mut(&mut model) {
702                if let Some(fu) = b.fauna.get_mut(i) {
703                    fu.densidad = v.clamp(0.0, 1.0);
704                }
705                model.gen += 1;
706            }
707        }
708        Msg::CycleMuebleBrush => {
709            model.mueble_brush = model.mueble_brush.next();
710            model.status = format!("mueble: {}", model.mueble_brush.label());
711        }
712        Msg::CycleMuebleFacing => {
713            model.mueble_facing = (model.mueble_facing + 1) % 4;
714        }
715        Msg::PlaceMuebleAt(gx, gz) => {
716            let (mb, fc) = (model.mueble_brush, model.mueble_facing);
717            if let Some(it) = sel_interior_mut(&mut model) {
718                it.colocar(mb, gx as u32, gz as u32, fc);
719                model.gen += 1;
720            }
721        }
722        Msg::RemoveLastPieza => {
723            if let Some(it) = sel_interior_mut(&mut model) {
724                it.piezas.pop();
725                model.gen += 1;
726            }
727        }
728        Msg::ClearPiezas => {
729            if let Some(it) = sel_interior_mut(&mut model) {
730                it.piezas.clear();
731                model.gen += 1;
732            }
733        }
734        Msg::SetInteriorAncho(v) => {
735            if let Some(it) = sel_interior_mut(&mut model) {
736                it.ancho = v.clamp(6.0, 60.0) as u32;
737                model.gen += 1;
738            }
739        }
740        Msg::SetInteriorFondo(v) => {
741            if let Some(it) = sel_interior_mut(&mut model) {
742                it.fondo = v.clamp(6.0, 60.0) as u32;
743                model.gen += 1;
744            }
745        }
746        Msg::CycleInteriorPiso => {
747            if let Some(it) = sel_interior_mut(&mut model) {
748                it.piso = it.piso.next();
749                model.gen += 1;
750            }
751        }
752        Msg::CycleInteriorMuro => {
753            if let Some(it) = sel_interior_mut(&mut model) {
754                it.muro = it.muro.next();
755                model.gen += 1;
756            }
757        }
758        Msg::ToggleInteriorMuros => {
759            if let Some(it) = sel_interior_mut(&mut model) {
760                it.muros = !it.muros;
761                model.gen += 1;
762            }
763        }
764        Msg::ToggleInteriorVentanas => {
765            if let Some(it) = sel_interior_mut(&mut model) {
766                it.ventanas = !it.ventanas;
767                model.gen += 1;
768            }
769        }
770        Msg::CycleInteriorTecho => {
771            if let Some(it) = sel_interior_mut(&mut model) {
772                it.techo = it.techo.next();
773                model.gen += 1;
774            }
775        }
776        Msg::ToggleInteriorTecho => {
777            if let Some(it) = sel_interior_mut(&mut model) {
778                it.con_techo = !it.con_techo;
779                model.gen += 1;
780            }
781        }
782        Msg::AddBiomaSere => {
783            let sids = sere_ids(&model);
784            if let (Some(&sere), Some(b)) = (sids.first(), sel_bioma_mut(&mut model)) {
785                b.seres.push(llimphi_voxel::SereUso { sere, probabilidad: 0.5 });
786                model.gen += 1;
787            }
788        }
789        Msg::RemoveBiomaSere => {
790            if let Some(b) = sel_bioma_mut(&mut model) {
791                b.seres.pop();
792                model.gen += 1;
793            }
794        }
795        Msg::CycleBiomaSere(i) => {
796            let sids = sere_ids(&model);
797            if let Some(b) = sel_bioma_mut(&mut model) {
798                if let Some(u) = b.seres.get_mut(i) {
799                    u.sere = next_in(&sids, u.sere);
800                }
801                model.gen += 1;
802            }
803        }
804        Msg::SetBiomaSereProb(i, v) => {
805            if let Some(b) = sel_bioma_mut(&mut model) {
806                if let Some(u) = b.seres.get_mut(i) {
807                    u.probabilidad = v.clamp(0.0, 1.0);
808                }
809                model.gen += 1;
810            }
811        }
812        Msg::SeedCampo(ev) => {
813            if matches!(ev, TextInputEvent::Press(_)) {
814                model.seed_focused = true;
815                model.name_focused = false;
816                model.ai_focused = false;
817            }
818            model.seed_input.handle(ev, &mut model.clipboard);
819            if let Ok(s) = model.seed_input.text().trim().parse::<u32>() {
820                if let Some(m) = sel_mundo_mut(&mut model) {
821                    m.seed = s;
822                    model.gen += 1;
823                }
824            }
825        }
826        Msg::SeedKey(ev) => {
827            model.seed_input.handle(TextInputEvent::Key(ev), &mut model.clipboard);
828            if let Ok(s) = model.seed_input.text().trim().parse::<u32>() {
829                if let Some(m) = sel_mundo_mut(&mut model) {
830                    m.seed = s;
831                    model.gen += 1;
832                }
833            }
834        }
835        Msg::SeedRandom => {
836            model.rng = model.rng.wrapping_mul(1664525).wrapping_add(1013904223);
837            let s = model.rng % 100_000;
838            if let Some(m) = sel_mundo_mut(&mut model) {
839                m.seed = s;
840            }
841            model.seed_input.set_text(&s.to_string());
842            model.gen += 1;
843        }
844        Msg::CycleMundoBioma => {
845            let bids = bioma_ids(&model);
846            if let Some(m) = sel_mundo_mut(&mut model) {
847                let cur = m.biomas.first().copied().unwrap_or(0);
848                m.biomas = vec![next_in(&bids, cur)];
849                model.gen += 1;
850            }
851        }
852        Msg::SelectUtileria(i) => model.utileria_sel = i,
853        Msg::AddUtileria => {
854            // Nace donde más se usa: en la mano del primer actor, desde el instante
855            // que se está mirando. Sin actores, flotando en el centro del mundo.
856            let dim = world_dim(PREVIEW_DIM_XZ);
857            let t = model.time;
858            if let Some(s) = sel_scene_mut(&mut model) {
859                let centro = [dim[0] as f32 * 0.5, dim[1] as f32 * 0.6, dim[2] as f32 * 0.5];
860                let mut u = UtileriaSpec::en(Utileria::Estrella, centro);
861                u.desde = t.min(s.duration);
862                u.lleva = (!s.actors.is_empty()).then_some(0);
863                s.utileria.push(u);
864                model.utileria_sel = s.utileria.len() - 1;
865            }
866        }
867        Msg::RemoveUtileria => {
868            let i = model.utileria_sel;
869            if let Some(s) = sel_scene_mut(&mut model) {
870                if i < s.utileria.len() {
871                    s.utileria.remove(i);
872                }
873                model.utileria_sel = i.min(s.utileria.len().saturating_sub(1));
874            }
875        }
876        Msg::CycleUtileriaCosa => {
877            let i = model.utileria_sel;
878            if let Some(u) = sel_scene_mut(&mut model).and_then(|s| s.utileria.get_mut(i)) {
879                let k = Utileria::ALL.iter().position(|&x| x == u.cosa).unwrap_or(0);
880                u.cosa = Utileria::ALL[(k + 1) % Utileria::ALL.len()];
881            }
882        }
883        Msg::CycleUtileriaLleva => {
884            let i = model.utileria_sel;
885            if let Some(s) = sel_scene_mut(&mut model) {
886                let n = s.actors.len();
887                if let Some(u) = s.utileria.get_mut(i) {
888                    u.lleva = match u.lleva {
889                        None if n > 0 => Some(0),
890                        Some(a) if a + 1 < n => Some(a + 1),
891                        _ => None,
892                    };
893                }
894            }
895        }
896        Msg::SetUtileria(campo, v) => {
897            let i = model.utileria_sel;
898            let dim = world_dim(PREVIEW_DIM_XZ);
899            if let Some(s) = sel_scene_mut(&mut model) {
900                let dur = s.duration;
901                if let Some(u) = s.utileria.get_mut(i) {
902                    match campo {
903                        UtileriaField::X => u.pos[0] = v.clamp(0.0, dim[0] as f32),
904                        UtileriaField::Y => u.pos[1] = v.clamp(0.0, dim[1] as f32),
905                        UtileriaField::Z => u.pos[2] = v.clamp(0.0, dim[2] as f32),
906                        UtileriaField::Alto => u.alto = v.clamp(0.02, 4.0),
907                        UtileriaField::Desde => u.desde = v.clamp(0.0, dur),
908                        UtileriaField::Giro => u.giro = v.clamp(0.0, 1.0),
909                    }
910                }
911            }
912        }
913        Msg::SelectActor(i) => {
914            model.actor_sel = i;
915            model.key_sel = 0;
916        }
917        Msg::AddActor => {
918            let dim = world_dim(PREVIEW_DIM_XZ);
919            let (cz, gx0, gx1) = (dim[2] as f32 * 0.5, dim[0] as f32 * 0.35, dim[0] as f32 * 0.65);
920            let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0);
921            if let Some(s) = sel_scene_mut(&mut model) {
922                s.actors.push(ActorSpec {
923                    character: 0,
924                    keys: vec![
925                        ActorKeySpec { t: 0.0, gx: gx0, gz: cz, clip: Some(Clip::Walk), face: None },
926                        ActorKeySpec { t: dur, gx: gx1, gz: cz, clip: Some(Clip::Walk), face: None },
927                    ],
928                    frame_rate: None,
929                });
930                model.actor_sel = s.actors.len() - 1;
931                model.key_sel = 0;
932                model.gen += 1;
933            }
934        }
935        Msg::RemoveActor => {
936            let a = model.actor_sel;
937            if let Some(s) = sel_scene_mut(&mut model) {
938                if s.actors.len() > 1 && a < s.actors.len() {
939                    s.actors.remove(a);
940                }
941            }
942            model.actor_sel = model.actor_sel.saturating_sub(1);
943            model.key_sel = 0;
944            model.gen += 1;
945        }
946        Msg::CycleActorSere => {
947            let a = model.actor_sel;
948            let n = model.project.seres.len().max(1);
949            if let Some(s) = sel_scene_mut(&mut model) {
950                if let Some(act) = s.actors.get_mut(a) {
951                    act.character = (act.character + 1) % n;
952                }
953                model.gen += 1;
954            }
955        }
956        Msg::SelectKey(i) => model.key_sel = i,
957        Msg::AddKey => {
958            let a = model.actor_sel;
959            let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0);
960            let dimx = world_dim(PREVIEW_DIM_XZ)[0] as f32;
961            if let Some(s) = sel_scene_mut(&mut model) {
962                if let Some(act) = s.actors.get_mut(a) {
963                    let mut k = act.keys.last().cloned().unwrap_or(ActorKeySpec {
964                        t: 0.0,
965                        gx: dimx * 0.5,
966                        gz: dimx * 0.5,
967                        clip: Some(Clip::Walk),
968                        face: None,
969                    });
970                    k.gx = (k.gx + 12.0).min(dimx - 2.0); // nuevo waypoint adelante
971                    act.keys.push(k);
972                    respread(act, dur);
973                    model.key_sel = act.keys.len() - 1;
974                }
975                model.gen += 1;
976            }
977        }
978        Msg::RemoveKey => {
979            let (a, k) = (model.actor_sel, model.key_sel);
980            let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0);
981            if let Some(s) = sel_scene_mut(&mut model) {
982                if let Some(act) = s.actors.get_mut(a) {
983                    if act.keys.len() > 2 && k < act.keys.len() {
984                        act.keys.remove(k);
985                        respread(act, dur);
986                    }
987                }
988                model.key_sel = model.key_sel.saturating_sub(1);
989                model.gen += 1;
990            }
991        }
992        Msg::SetKeyPos(is_gx, v) => {
993            let (a, k) = (model.actor_sel, model.key_sel);
994            let lim = world_dim(PREVIEW_DIM_XZ)[0] as f32 - 1.0;
995            if let Some(s) = sel_scene_mut(&mut model) {
996                if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) {
997                    if is_gx {
998                        key.gx = v.clamp(1.0, lim);
999                    } else {
1000                        key.gz = v.clamp(1.0, lim);
1001                    }
1002                }
1003                model.gen += 1;
1004            }
1005        }
1006        Msg::SetKeyTime(v) => {
1007            let (a, k) = (model.actor_sel, model.key_sel);
1008            let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0);
1009            if let Some(s) = sel_scene_mut(&mut model) {
1010                if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) {
1011                    key.t = v.clamp(0.0, dur);
1012                }
1013                model.gen += 1;
1014            }
1015        }
1016        Msg::CycleKeyClip => {
1017            let (a, k) = (model.actor_sel, model.key_sel);
1018            if let Some(s) = sel_scene_mut(&mut model) {
1019                if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) {
1020                    key.clip = Some(key.clip.unwrap_or(Clip::Idle).next());
1021                }
1022                model.gen += 1;
1023            }
1024        }
1025        Msg::PlaceKeyAt(gx, gz) => {
1026            let (a, k) = (model.actor_sel, model.key_sel);
1027            if let Some(s) = sel_scene_mut(&mut model) {
1028                if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) {
1029                    key.gx = gx;
1030                    key.gz = gz;
1031                }
1032                model.gen += 1;
1033            }
1034        }
1035        Msg::CycleSceneMundo => {
1036            let mids = mundo_ids(&model);
1037            if let Some(s) = sel_scene_mut(&mut model) {
1038                s.mundo = next_in(&mids, s.mundo);
1039                model.gen += 1;
1040            }
1041        }
1042        Msg::SelectDecorado(i) => model.decorado_sel = i,
1043        Msg::AddDecorado => {
1044            // Nace detrás de donde termina el primer actor (al fondo del plano),
1045            // para que no le caiga encima del recorrido. Sin actores, al centro.
1046            let dim = world_dim(PREVIEW_DIM_XZ);
1047            if let Some(s) = sel_scene_mut(&mut model) {
1048                let (gx, gz) = s
1049                    .actors
1050                    .first()
1051                    .and_then(|a| a.keys.last())
1052                    .map(|k| (k.gx, k.gz + 18.0))
1053                    .unwrap_or((dim[0] as f32 * 0.5, dim[2] as f32 * 0.5));
1054                s.decorado.push(Decorado::en(CasaTipo::Palacio, gx, gz));
1055                model.decorado_sel = s.decorado.len() - 1;
1056            }
1057        }
1058        Msg::RemoveDecorado => {
1059            let i = model.decorado_sel;
1060            if let Some(s) = sel_scene_mut(&mut model) {
1061                if i < s.decorado.len() {
1062                    s.decorado.remove(i);
1063                }
1064                model.decorado_sel = i.min(s.decorado.len().saturating_sub(1));
1065            }
1066        }
1067        Msg::CycleDecoradoCasa => {
1068            let i = model.decorado_sel;
1069            if let Some(d) = sel_scene_mut(&mut model).and_then(|s| s.decorado.get_mut(i)) {
1070                d.casa = d.casa.next();
1071            }
1072        }
1073        Msg::OtraVarianteDecorado => {
1074            let i = model.decorado_sel;
1075            if let Some(d) = sel_scene_mut(&mut model).and_then(|s| s.decorado.get_mut(i)) {
1076                d.semilla = d.semilla.wrapping_add(1);
1077            }
1078        }
1079        Msg::SetDecorado(campo, v) => {
1080            let i = model.decorado_sel;
1081            if let Some(d) = sel_scene_mut(&mut model).and_then(|s| s.decorado.get_mut(i)) {
1082                match campo {
1083                    DecoradoField::X => d.pos[0] = v.round(),
1084                    DecoradoField::Z => d.pos[1] = v.round(),
1085                }
1086            }
1087        }
1088        Msg::ToggleNoche => {
1089            if let Some(s) = sel_scene_mut(&mut model) {
1090                s.cielo = match s.cielo {
1091                    Some(_) => None,
1092                    None => Some(Cielo::noche()),
1093                };
1094            }
1095        }
1096        Msg::SetCielo(campo, v) => {
1097            if let Some(c) = sel_scene_mut(&mut model).and_then(|s| s.cielo.as_mut()) {
1098                match campo {
1099                    CieloField::Estrellas => c.estrellas = v.clamp(0.0, 1.0),
1100                    CieloField::Luna => c.luna_radio = v.clamp(0.0, 0.15),
1101                }
1102            }
1103        }
1104        Msg::SetSceneDur(v) => {
1105            if let Some(s) = sel_scene_mut(&mut model) {
1106                s.duration = v.clamp(1.0, 20.0);
1107            }
1108        }
1109        Msg::TogglePlay => model.playing = !model.playing,
1110        Msg::Scrub(v) => {
1111            model.playing = false;
1112            let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(1.0);
1113            model.time = v.clamp(0.0, dur);
1114        }
1115        Msg::ToggleSceneCam => model.script_cam = !model.script_cam,
1116        Msg::AddShot => {
1117            let t = model.time;
1118            if let Some(s) = sel_scene_mut(&mut model) {
1119                let kind = ShotKind::ALL[s.shots.len() % ShotKind::ALL.len()];
1120                s.shots.push(ShotSpec { start: t, kind });
1121                s.shots.sort_by(|a, b| a.start.total_cmp(&b.start));
1122                model.status = format!("plano: {} @ {t:.1}s", kind.label());
1123            }
1124        }
1125        Msg::RemoveShot => {
1126            if let Some(s) = sel_scene_mut(&mut model) {
1127                s.shots.pop();
1128            }
1129        }
1130        Msg::ExportVideo => {
1131            if !model.exporting {
1132                if let Some(scene) = sel_scene(&model).cloned() {
1133                    model.exporting = true;
1134                    model.status = "exportando video… (puede tardar)".into();
1135                    let project = model.project.clone();
1136                    handle.spawn(move || Msg::ExportDone(render::export_scene(&project, &scene)));
1137                }
1138            }
1139        }
1140        Msg::ExportDone(res) => {
1141            model.exporting = false;
1142            model.status = match res {
1143                Ok(p) => format!("video listo: {p}"),
1144                Err(e) => format!("export falló: {e}"),
1145            };
1146        }
1147        Msg::ToggleSim => {
1148            model.simulating = !model.simulating;
1149            model.gen += 1; // repone terreno fresco y reinicia la sim limpia
1150            model.status = match (model.level, model.simulating) {
1151                (Level::Leyes, true) => "corriendo la ley…".into(),
1152                (Level::Leyes, false) => "ley en pausa".into(),
1153                (_, true) => "simulando agua…".into(),
1154                (_, false) => "agua estática".into(),
1155            };
1156        }
1157        Msg::Orbit(dx, dy) => {
1158            model.yaw -= dx * 0.008;
1159            let lim = std::f32::consts::FRAC_PI_2 - 0.05;
1160            model.pitch = (model.pitch + dy * 0.008).clamp(-lim, lim);
1161        }
1162        Msg::Zoom(dy) => {
1163            let f = (1.0 + dy * 0.1).clamp(0.5, 1.5);
1164            let xz = PREVIEW_DIM_XZ as f32;
1165            model.dist = (model.dist * f).clamp(xz * 0.6, xz * 3.0);
1166        }
1167        Msg::AiCampo(ev) => {
1168            if matches!(ev, TextInputEvent::Press(_)) {
1169                model.ai_focused = true;
1170                model.name_focused = false;
1171                model.seed_focused = false;
1172            }
1173            model.ai_input.handle(ev, &mut model.clipboard);
1174        }
1175        Msg::AiKey(ev) => {
1176            model.ai_input.handle(TextInputEvent::Key(ev), &mut model.clipboard);
1177        }
1178        Msg::AiGenerate => {
1179            let prompt = model.ai_input.text();
1180            if prompt.trim().is_empty() || model.ai_busy {
1181                return model;
1182            }
1183            model.ai_busy = true;
1184            model.status = "generando con IA…".into();
1185            match model.level {
1186                Level::Biomas | Level::Mundos => {
1187                    let refs = MatRefs::from_project(&model.project);
1188                    handle.spawn(move || Msg::AiBioma(ai::generate_bioma(&prompt, &refs)));
1189                }
1190                Level::Seres => {
1191                    handle.spawn(move || Msg::AiSere(ai::generate_character(&prompt)));
1192                }
1193                Level::Escenas => {
1194                    let mundo = model.project.mundos.first().map(|m| m.id).unwrap_or(0);
1195                    let dim = world_dim(PREVIEW_DIM_XZ);
1196                    handle.spawn(move || Msg::AiScene(ai::generate_scene(&prompt, mundo, dim)));
1197                }
1198                _ => {
1199                    model.ai_busy = false;
1200                    model.status = "la IA genera biomas, mundos, seres o escenas".into();
1201                }
1202            }
1203        }
1204        Msg::AiBioma(mut b) => {
1205            b.id = model.project.alloc_id();
1206            let bid = b.id;
1207            let name = b.name.clone();
1208            model.project.biomas.push(b);
1209            // En modo Mundos, envolver el bioma en un mundo nuevo.
1210            if model.level == Level::Mundos {
1211                let id = model.project.alloc_id();
1212                model.project.mundos.push(Mundo { id, name: name.clone(), seed: 1337, biomas: vec![bid], mapa: None });
1213                model.set_selected(id);
1214            } else {
1215                model.level = Level::Biomas;
1216                model.set_selected(bid);
1217            }
1218            model.ai_busy = false;
1219            model.ai_input.set_text("");
1220            model.gen += 1;
1221            model.status = format!("generado: «{name}»");
1222            sync_inputs(&mut model);
1223        }
1224        Msg::AiSere(mut c) => {
1225            c.id = model.project.alloc_id();
1226            let id = c.id;
1227            let name = c.name.clone();
1228            model.project.seres.push(c);
1229            model.level = Level::Seres;
1230            model.set_selected(id);
1231            model.ai_busy = false;
1232            model.ai_input.set_text("");
1233            model.status = format!("ser generado: «{name}»");
1234            sync_inputs(&mut model);
1235        }
1236        Msg::AiScene(mut s) => {
1237            s.id = model.project.alloc_id();
1238            let id = s.id;
1239            model.project.escenas.push(s);
1240            model.level = Level::Escenas;
1241            model.set_selected(id);
1242            model.time = 0.0;
1243            model.playing = true;
1244            model.ai_busy = false;
1245            model.ai_input.set_text("");
1246            model.gen += 1;
1247            model.status = "escena generada".into();
1248            sync_inputs(&mut model);
1249        }
1250        Msg::Save => match save_project(&model.project) {
1251            Ok(_) => model.status = format!("guardado en {PROJECT_PATH}"),
1252            Err(e) => model.status = format!("error al guardar: {e}"),
1253        },
1254        Msg::Load => match load_project() {
1255            Ok(p) => {
1256                model.project = p;
1257                model.gen += 1;
1258                reselect_all(&mut model);
1259                model.status = format!("cargado de {PROJECT_PATH}");
1260                sync_inputs(&mut model);
1261            }
1262            Err(e) => model.status = format!("error al cargar: {e}"),
1263        },
1264        Msg::FileCampo(ev) => {
1265            if matches!(ev, TextInputEvent::Press(_)) {
1266                model.file_focused = true;
1267                model.name_focused = false;
1268                model.seed_focused = false;
1269                model.ai_focused = false;
1270                model.formula_focused = false;
1271                model.cond_formula_focused = false;
1272            }
1273            model.file_input.handle(ev, &mut model.clipboard);
1274        }
1275        Msg::FileKey(ev) => {
1276            model.file_input.handle(TextInputEvent::Key(ev), &mut model.clipboard);
1277        }
1278        Msg::GuardarComo => {
1279            let path = model.file_input.text();
1280            if path.trim().is_empty() {
1281                model.status = "escribe una ruta primero".into();
1282            } else {
1283                match Documento::nuevo(model.project.clone()).guardar(&path) {
1284                    Ok(_) => model.status = format!("guardado en {path}"),
1285                    Err(e) => model.status = format!("error al guardar: {e}"),
1286                }
1287            }
1288        }
1289        Msg::Abrir => {
1290            let path = model.file_input.text();
1291            match Documento::abrir(&path) {
1292                Ok(doc) => {
1293                    model.project = doc.proyecto;
1294                    model.gen += 1;
1295                    reselect_all(&mut model);
1296                    sync_inputs(&mut model);
1297                    let rotas = model.project.validar_referencias();
1298                    model.status = if rotas.is_empty() {
1299                        format!("abierto {path}")
1300                    } else {
1301                        format!("abierto {path} — ⚠ {} referencias rotas", rotas.len())
1302                    };
1303                }
1304                Err(e) => model.status = format!("error al abrir: {e}"),
1305            }
1306        }
1307        Msg::Combinar => {
1308            let path = model.file_input.text();
1309            match Documento::abrir(&path) {
1310                Ok(doc) => {
1311                    let n_esc = doc.proyecto.escenas.len();
1312                    model.project.combinar(&doc.proyecto);
1313                    model.gen += 1;
1314                    reselect_all(&mut model);
1315                    sync_inputs(&mut model);
1316                    let rotas = model.project.validar_referencias();
1317                    model.status = if rotas.is_empty() {
1318                        format!("combinado {path} (+{n_esc} escenas)")
1319                    } else {
1320                        format!("combinado {path} — ⚠ {} referencias rotas", rotas.len())
1321                    };
1322                }
1323                Err(e) => model.status = format!("error al combinar: {e}"),
1324            }
1325        }
1326        Msg::MundoDesdeMapa => {
1327            let path = model.file_input.text();
1328            // Lee la fuente (GeoJSON + .pgm hermano) → adjuntala AL MUNDO
1329            // seleccionado (se persiste con el Project) y regenerá el grid.
1330            match mapa_import::leer_fuente(&path, 300.0) {
1331                Ok(fuente) => {
1332                    model.level = Level::Mundos;
1333                    let mid = model.sel[Level::Mundos.index()];
1334                    if let Some(m) = model.project.mundos.iter_mut().find(|m| m.id == mid) {
1335                        m.mapa = Some(fuente);
1336                        model.gen += 1; // fuerza rebuild del preview desde el mapa
1337                        refrescar_mapa(&mut model);
1338                    } else {
1339                        model.status = "no hay mundo seleccionado para el mapa".into();
1340                    }
1341                }
1342                Err(e) => model.status = format!("mapa: {e}"),
1343            }
1344        }
1345        Msg::QuitarMapa => {
1346            let mid = model.sel[Level::Mundos.index()];
1347            let quitado = model
1348                .project
1349                .mundos
1350                .iter_mut()
1351                .find(|m| m.id == mid)
1352                .map(|m| m.mapa.take().is_some())
1353                .unwrap_or(false);
1354            if quitado {
1355                model.mapa = None;
1356                model.gen += 1; // vuelve al mundo procedural
1357                model.status = "mapa quitado — mundo procedural".into();
1358            }
1359        }
1360        Msg::SerDesdeFoto => {
1361            // La foto NO se voxeliza literal (eso da el valle): sólo REGULA los rasgos
1362            // (piel/ropa) de un personaje ESTILIZADO del kit, que conserva su estilo,
1363            // gestos y personalidad. El recorte de persona lo da u2net por el daemon.
1364            //
1365            // Todo esto va a un worker, y era el peor caso del barrido: no
1366            // sólo leía el archivo, además **conectaba un socket y esperaba
1367            // a que una red neuronal segmentara la persona**, con el hilo de
1368            // UI parado. Un round-trip a u2net son cientos de ms en el mejor
1369            // caso; si el daemon no está, es el timeout de la conexión.
1370            //
1371            // Sin contador de generación a propósito: cada resultado
1372            // **agrega** un personaje, así que dos clicks dan dos, que es
1373            // exactamente lo que hacía la versión sincrónica.
1374            let path = model.file_input.text();
1375            model.status = format!("foto: analizando {path}…");
1376            let para_worker = path.clone();
1377            handle.spawn(move || {
1378                let r = std::fs::read(&para_worker)
1379                    .map_err(|e| format!("foto: leer {para_worker}: {e}"))
1380                    .and_then(|bytes| {
1381                        let cliente =
1382                            ClienteBloqueante::conectar(rostro::socket_por_defecto()).ok();
1383                        let prov = cliente.as_ref().map(|c| c as &dyn Proveedor);
1384                        rostro::rasgos_desde_bytes(&bytes, prov)
1385                            .map_err(|e| format!("foto: {e}"))
1386                    });
1387                Msg::RostroListo(para_worker, r)
1388            });
1389        }
1390        Msg::RostroListo(path, r) => match r {
1391            Ok(r) => {
1392                let via = if r.recorte_real { "recorte u2net" } else { "sin recorte (centro)" };
1393                let id = model.project.alloc_id();
1394                let mut cs = CharSpec::new(nombre_desde_path(&path), Age::Adult);
1395                cs.id = id;
1396                cs.skin = r.skin;
1397                cs.shirt = r.shirt;
1398                cs.pants = r.pants;
1399                cs.hair = Some(r.hair); // pelo FIEL de la foto (no derivado)
1400                model.project.seres.push(cs);
1401                model.level = Level::Seres;
1402                model.set_selected(id);
1403                model.gen += 1;
1404                sync_inputs(&mut model);
1405                model.status =
1406                    format!("ser desde foto (rasgos, {via}): personaje estilizado");
1407            }
1408            Err(e) => model.status = e,
1409        },
1410        Msg::Versionar => {
1411            // Registra el proyecto actual como una versión nueva en la rama
1412            // actual del DAG, y persiste la historia.
1413            let etiqueta = format!("v{}", model.historia.len() + 1);
1414            match model.historia.commit(&model.project, etiqueta) {
1415                Ok(id) => {
1416                    let _ = model.historia.guardar(HISTORY_PATH);
1417                    model.status = format!(
1418                        "versión {} · rama {} · {} en total",
1419                        hex_corto(&id),
1420                        model.historia.rama_actual(),
1421                        model.historia.len()
1422                    );
1423                }
1424                Err(e) => model.status = format!("error al versionar: {e}"),
1425            }
1426        }
1427        Msg::Ramificar => {
1428            // Una alternativa nombrada desde la punta actual (nombre = ruta
1429            // escrita, o auto). Cambia a la rama nueva: lo próximo que
1430            // versiones cuelga de la alternativa.
1431            let nombre = {
1432                let t = model.file_input.text();
1433                if t.trim().is_empty() {
1434                    format!("alt-{}", model.historia.ramas().len() + 1)
1435                } else {
1436                    t
1437                }
1438            };
1439            match model.historia.ramificar(&nombre) {
1440                Ok(_) => {
1441                    model.historia.cambiar_rama(&nombre);
1442                    let _ = model.historia.guardar(HISTORY_PATH);
1443                    model.status = format!("rama nueva: {nombre} (versiona para poblarla)");
1444                }
1445                Err(_) => model.status = "versiona algo antes de ramificar".into(),
1446            }
1447        }
1448        Msg::IrAVersion(id) => {
1449            // Navegar: checkout de la versión → carga su proyecto al lienzo.
1450            if let Some(p) = model.historia.checkout(&id) {
1451                model.project = p;
1452                model.gen += 1;
1453                reselect_all(&mut model);
1454                sync_inputs(&mut model);
1455                let etq = model
1456                    .historia
1457                    .version(&id)
1458                    .map(|v| v.etiqueta.clone())
1459                    .unwrap_or_default();
1460                model.status = format!("en versión {} · {etq}", hex_corto(&id));
1461            }
1462        }
1463        Msg::EntrarSalir => {
1464            model.jugando = !model.jugando;
1465            model.input_juego = InputJuego::default();
1466            if model.jugando {
1467                model.level = Level::Escenas; // se entra a la escena
1468                *model.respawn_jugador.lock().unwrap() = true;
1469                model.status =
1470                    "JUGANDO — WASD/E-Q mover · F volar · R semilla · 1/2/3 dictar suceso · Esc salir".into();
1471            } else {
1472                model.status = "saliste de la escena".into();
1473            }
1474        }
1475        Msg::MoverJuego(eje, on) => match eje {
1476            EjeJuego::Fwd => model.input_juego.fwd = on,
1477            EjeJuego::Back => model.input_juego.back = on,
1478            EjeJuego::Left => model.input_juego.left = on,
1479            EjeJuego::Right => model.input_juego.right = on,
1480            EjeJuego::Arriba => model.input_juego.arriba = on,
1481            EjeJuego::Abajo => model.input_juego.abajo = on,
1482        },
1483        Msg::Saltar(on) => model.input_juego.salto = on,
1484        Msg::AlternarVuelo => {
1485            model.volar = !model.volar;
1486            model.status = if model.volar {
1487                "vuelo libre — E/Q suben/bajan".into()
1488            } else {
1489                "caminando (gravedad)".into()
1490            };
1491        }
1492        Msg::RerollSemilla => {
1493            // Nueva semilla (LCG u32) para el mundo de la escena → regenera
1494            // el terreno bajo tus pies.
1495            model.rng = model.rng.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1496            let nueva = model.rng;
1497            let mundo_id = sel_scene(&model).map(|s| s.mundo);
1498            if let Some(id) = mundo_id {
1499                if let Some(m) = model.project.mundos.iter_mut().find(|m| m.id == id) {
1500                    m.seed = nueva;
1501                    model.gen += 1;
1502                    model.status = format!("nueva semilla: {nueva}");
1503                }
1504            }
1505        }
1506        Msg::DictarSuceso(mut tipo) => {
1507            // Un "aparece" sin ser asignado se ata al ser seleccionado (o al
1508            // primero que haya) para que valga como referencia.
1509            if let TipoSuceso::Aparece(id) = &mut tipo {
1510                if *id == 0 {
1511                    *id = if model.project.seres.iter().any(|s| s.id == model.sel[2]) {
1512                        model.sel[2]
1513                    } else {
1514                        model.project.seres.first().map(|s| s.id).unwrap_or(0)
1515                    };
1516                }
1517            }
1518            let feet = model.jugador.lock().unwrap().pos;
1519            let t = model.time;
1520            let etiqueta = match &tipo {
1521                TipoSuceso::Aparece(0) => "marca (aún sin ser)".to_string(),
1522                TipoSuceso::Aparece(id) => format!("aparece ser {id}"),
1523                TipoSuceso::Camara => "marca de cámara".to_string(),
1524                TipoSuceso::Marca => "punto de interés".to_string(),
1525                TipoSuceso::Desmorona { radio, motas } => {
1526                    format!("se desmorona (r={radio:.0}, {motas} motas)")
1527                }
1528            };
1529            if let Some(scene) = sel_scene_mut(&mut model) {
1530                scene.sucesos.push(Suceso {
1531                    t,
1532                    pos: [feet.x, feet.y, feet.z],
1533                    tipo,
1534                    nota: String::new(),
1535                });
1536                model.status = format!("suceso dictado: {etiqueta}");
1537            } else {
1538                model.status = "entrá a una escena para dictar sucesos".into();
1539            }
1540            model.gen += 1;
1541        }
1542        Msg::Tick => {
1543            if model.level == Level::Escenas && model.playing {
1544                if let Some(dur) = sel_scene(&model).map(|s| s.duration) {
1545                    model.time += DT;
1546                    if model.time >= dur {
1547                        model.time = 0.0;
1548                    }
1549                }
1550            } else if model.level == Level::Seres {
1551                model.time += DT; // turntable + respiración
1552            } else if model.level == Level::Leyes {
1553                ensure_lab(&mut model);
1554                // En modo grafo, reconstruir si el texto cambió afuera (no por edición del grafo).
1555                if model.leyes_node_mode
1556                    && (model.eq_graph.is_none() || model.eq_graph_gen != model.lab_gen)
1557                {
1558                    rebuild_eq_graph(&mut model);
1559                }
1560                if model.simulating {
1561                    let params = sel_ley(&model)
1562                        .map(|l| l.kind.ecuacion_param_values())
1563                        .unwrap_or_default();
1564                    if let Some(lab) = &mut model.lab {
1565                        lab.step(&params);
1566                    }
1567                }
1568            }
1569        }
1570    }
1571    model
1572}