tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1use super::*;
2
3/// 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(¶_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(¶ms);
1566 }
1567 }
1568 }
1569 }
1570 }
1571 model
1572}