use super::*; /// El `update` de la app. Vive afuera del bloque `impl App` porque un trait /// admite UN solo bloque por tipo: para partir un método de mil cuatrocientas /// líneas hay que sacarle el cuerpo y dejar el impl delegando. Se llama /// distinto que el método para que el delegador no se llame a sí mismo. pub(crate) fn actualizar(mut model: Model, msg: Msg, handle: &Handle) -> Model { match msg { Msg::SelectLevel(l) => { model.level = l; model.tool_tab = 0; model.name_focused = false; model.seed_focused = false; model.formula_focused = false; model.cond_formula_focused = false; model.gen += 1; // Selector fijo: el diente del nivel activo queda abierto; el de // herramientas vuelve al primer tool. Se limpia el filtro de ambos. pin_sidebars(&mut model); model.sidebar_left.search.clear(); model.sidebar_right.search.clear(); sync_inputs(&mut model); if model.level == Level::Mundos { refrescar_mapa(&mut model); // al entrar a Mundos, refleja su mapa (o no) } } Msg::SelectItem(id) => { model.set_selected(id); model.time = 0.0; model.cond_formula_focused = false; model.gen += 1; sync_inputs(&mut model); if model.level == Level::Mundos { refrescar_mapa(&mut model); // el mundo elegido puede venir de un mapa } } Msg::SelectTool(i) => { model.tool_tab = i; model.cond_formula_focused = false; pin_sidebars(&mut model); model.sidebar_right.search.clear(); } Msg::SidebarLeft(sm) => { // Selector fijo de niveles: tras reducir ejes/buscador/resize, el // diente del nivel activo sigue abierto (nunca colapsa). llimphi_widget_rag_sidebar::persistencia::update_persistente(&mut model.sidebar_left, sm, "voxel-studio", "izq"); pin_sidebars(&mut model); } Msg::SidebarRight(sm) => { llimphi_widget_rag_sidebar::persistencia::update_persistente(&mut model.sidebar_right, sm, "voxel-studio", "der"); pin_sidebars(&mut model); } Msg::NewItem => { let id = new_item(&mut model); model.set_selected(id); model.gen += 1; model.status = format!("nuevo en {}", model.level.label()); sync_inputs(&mut model); } Msg::DupItem => { if let Some(id) = duplicate_item(&mut model) { model.set_selected(id); model.gen += 1; model.status = "duplicado".into(); sync_inputs(&mut model); } } Msg::DelItem => { delete_selected(&mut model); // Reseleccionar el primero que quede. let first = model.items().first().map(|(i, _)| *i).unwrap_or(0); model.set_selected(first); model.gen += 1; model.status = "borrado".into(); sync_inputs(&mut model); } Msg::RenameCampo(ev) => { // El press/click enfoca este campo (y desenfoca los demás); el // arrastre extiende la selección. if matches!(ev, TextInputEvent::Press(_)) { model.name_focused = true; model.seed_focused = false; model.ai_focused = false; } model.name_input.handle(ev, &mut model.clipboard); let txt = model.name_input.text(); set_selected_name(&mut model, txt); } Msg::RenameKey(ev) => { // `handle` cubre escribir, copiar/cortar/pegar y acentos. model.name_input.handle(TextInputEvent::Key(ev), &mut model.clipboard); let txt = model.name_input.text(); set_selected_name(&mut model, txt); } Msg::CycleLeyKind => { if let Some(l) = model.project.leyes.iter_mut().find(|x| x.id == model.sel[0]) { l.kind = l.kind.next(); } } Msg::SetLeyParam(i, v) => { if let Some(l) = model.project.leyes.iter_mut().find(|x| x.id == model.sel[0]) { l.kind.set_param(i, v); } } Msg::LeyPreset => { // Cicla la ley `Ecuacion` seleccionada por un catálogo de leyes de // fábrica (autoradas, editables). Cambio estructural → reconstruye el lab. if let Some(l) = sel_ley_mut(&mut model) { if matches!(l.kind, LeyKind::Ecuacion { .. }) { l.kind = next_preset(&l.kind); model.formula_focused = false; model.lab_gen += 1; } } } Msg::SetEcuParam(i, v) => { if let Some(l) = sel_ley_mut(&mut model) { l.kind.set_ecuacion_param(i, v); } } Msg::FormulaFocus(i) => { let src = sel_ley(&model) .and_then(|l| match &l.kind { LeyKind::Ecuacion { fuentes, .. } => fuentes.get(i).cloned(), _ => None, }) .unwrap_or_default(); model.formula_input.set_text(src); model.formula_focused = true; model.formula_target = i; model.name_focused = false; model.seed_focused = false; model.ai_focused = false; } Msg::FormulaCampo(ev) => { // El campo sólo se dibuja mientras se edita esa fórmula, así que // el mouse (click→caret, arrastre→selección) va directo al buffer. model.formula_input.handle(ev, &mut model.clipboard); } Msg::FormulaKey(ev) => { // Enter confirma (desenfoca); el resto edita el buffer y se escribe en vivo. if ev.state == KeyState::Pressed && matches!(&ev.key, Key::Named(NamedKey::Enter)) { model.formula_focused = false; } else { // `handle` cubre copiar/cortar/pegar y acentos. model.formula_input.handle(TextInputEvent::Key(ev), &mut model.clipboard); let txt = model.formula_input.text(); let target = model.formula_target; if let Some(l) = sel_ley_mut(&mut model) { if let LeyKind::Ecuacion { fuentes, .. } = &mut l.kind { if let Some(f) = fuentes.get_mut(target) { *f = txt; } } } model.lab_gen += 1; } } Msg::AddCampo => { if let Some(l) = sel_ley_mut(&mut model) { if let LeyKind::Ecuacion { campos, fuentes, .. } = &mut l.kind { let name = format!("c{}", campos.len()); campos.push(FieldDef::new(name, 0.0, 0.0, 1.0)); fuentes.push("0".to_string()); model.lab_gen += 1; } } } Msg::RemoveCampo => { if let Some(l) = sel_ley_mut(&mut model) { if let LeyKind::Ecuacion { campos, fuentes, .. } = &mut l.kind { if campos.len() > 1 { campos.pop(); fuentes.pop(); model.formula_focused = false; model.lab_gen += 1; } } } } Msg::AddEcuParam => { if let Some(l) = sel_ley_mut(&mut model) { if let LeyKind::Ecuacion { params, .. } = &mut l.kind { let name = format!("p{}", params.len()); params.push(ParamDef::new(name, 0.1, 0.0, 1.0)); model.lab_gen += 1; } } } Msg::RemoveEcuParam => { if let Some(l) = sel_ley_mut(&mut model) { if let LeyKind::Ecuacion { params, .. } = &mut l.kind { params.pop(); model.lab_gen += 1; } } } Msg::CycleLabField => { if let Some(lab) = &mut model.lab { let n = lab.engine.fields().len().max(1); lab.vis = (lab.vis + 1) % n; } } Msg::ReseedLab => { if let Some(lab) = &mut model.lab { lab.reseed(); } } Msg::ToggleLeyesView => { model.leyes_node_mode = !model.leyes_node_mode; model.formula_focused = false; if model.leyes_node_mode { rebuild_eq_graph(&mut model); } else { model.eq_graph = None; } } Msg::GraphDrag(id, dx, dy) => { if let Some(g) = &mut model.eq_graph { g.drag(id, dx, dy); // layout puro: no recompila } } Msg::GraphConnect(from, _fp, to, tp) => { let ok = model.eq_graph.as_mut().map(|g| g.connect(from, to, tp)).unwrap_or(false); if ok { sync_graph_to_fuentes(&mut model); } } Msg::GraphAddNode(op) => { if let Some(g) = &mut model.eq_graph { g.add(op, 40.0, 40.0); // suelto; entra en la fórmula al conectarlo } } Msg::GraphDeleteNode(id) => { let had = model.eq_graph.is_some(); if let Some(g) = &mut model.eq_graph { g.delete(id); } if had { sync_graph_to_fuentes(&mut model); } } Msg::CycleMatRole => { if let Some(m) = sel_material_mut(&mut model) { m.role = match m.role { MatRole::Terreno => MatRole::Objeto(Forma::Columnar), MatRole::Objeto(_) => MatRole::Terreno, }; model.gen += 1; } } Msg::SetMatColor(ch, v) => { if let Some(m) = sel_material_mut(&mut model) { let mut c = m.color.unwrap_or([0.6, 0.6, 0.6]); if ch < 3 { c[ch] = v.clamp(0.0, 1.0); } m.color = Some(c); model.gen += 1; } } Msg::SetMatGrain(v) => { if let Some(m) = sel_material_mut(&mut model) { m.grain = Some(v.clamp(0.0, 1.0)); model.gen += 1; } } Msg::SetMatBrillo(v) => { if let Some(m) = sel_material_mut(&mut model) { m.brillo = Some(v.clamp(0.0, 1.0)); model.gen += 1; } } Msg::SetMatRugosidad(v) => { if let Some(m) = sel_material_mut(&mut model) { m.rugosidad = Some(v.clamp(0.0, 1.5)); // No cambia el grid (look de shader) → sin bump de gen. } } Msg::SetMatBisel(v) => { if let Some(m) = sel_material_mut(&mut model) { m.bisel = Some(v.clamp(0.0, 1.0)); } } Msg::CycleMatParent => { let id = model.sel[1]; // Lista de candidatos a padre (todos menos sí mismo). let cands: Vec = model.project.materiales.iter().map(|m| m.id).filter(|&i| i != id).collect(); if let Some(m) = sel_material_mut(&mut model) { m.parent = next_option(m.parent, &cands); model.gen += 1; } } Msg::AddMatLey => { let first_ley = model.project.leyes.first().map(|l| l.id); if let (Some(ley), Some(m)) = (first_ley, sel_material_mut(&mut model)) { m.leyes.push(LeyUso { ley, params: vec![] }); } } Msg::RemoveMatLey => { if let Some(m) = sel_material_mut(&mut model) { m.leyes.pop(); } } Msg::CycleMatLey(i) => { // Cicla a qué ley del proyecto apunta la ley `i` del material (para // poder señalar una ley Ecuacion y que corra sobre el material). let ids: Vec = model.project.leyes.iter().map(|l| l.id).collect(); if !ids.is_empty() { if let Some(m) = sel_material_mut(&mut model) { if let Some(u) = m.leyes.get_mut(i) { let pos = ids.iter().position(|&id| id == u.ley).unwrap_or(0); u.ley = ids[(pos + 1) % ids.len()]; u.params.clear(); } } model.gen += 1; } } Msg::CycleSereAge => { if let Some(c) = sel_sere_mut(&mut model) { c.age = c.age.next(); } } Msg::CycleSereCuerpo => { if let Some(c) = sel_sere_mut(&mut model) { c.cycle_cuerpo(); model.gen += 1; } } Msg::CycleSereBicho(atras) => { if let Some(c) = sel_sere_mut(&mut model) { c.cycle_bicho(atras); model.gen += 1; } } Msg::SetSereColor(part, ch, v) => { if let Some(c) = sel_sere_mut(&mut model) { // Una prenda sin color propio arranca del que se le ve (el de fábrica), // no de negro: si no, el primer toque del slider la apaga entera. let remera = c.shirt; let a = &mut c.atuendo; let rgb = match part { Part::Skin => &mut c.skin, Part::Shirt => &mut c.shirt, Part::Pants => &mut c.pants, Part::Sombrero => { let de_fabrica = a.color_sombrero_efectivo(remera); a.color_sombrero.get_or_insert(de_fabrica) } Part::Falda => { let de_fabrica = a.color_falda_efectivo(remera); a.color_falda.get_or_insert(de_fabrica) } Part::Manto => { let de_fabrica = a.color_manto_efectivo(remera); a.color_manto.get_or_insert(de_fabrica) } }; if ch < 3 { rgb[ch] = v.clamp(0.0, 1.0); } } } Msg::CycleSerePeinado => { if let Some(c) = sel_sere_mut(&mut model) { c.atuendo.peinado = c.atuendo.peinado.next(); } } Msg::CycleSereSombrero => { if let Some(c) = sel_sere_mut(&mut model) { c.atuendo.sombrero = c.atuendo.sombrero.next(); } } Msg::ToggleSereAnteojos => { if let Some(c) = sel_sere_mut(&mut model) { c.atuendo.anteojos = !c.atuendo.anteojos; } } Msg::SetSerePrenda(campo, v) => { if let Some(c) = sel_sere_mut(&mut model) { let v = v.clamp(0.0, 1.0); match campo { PrendaField::Barba => c.atuendo.barba = v, PrendaField::Falda => c.atuendo.falda = v, PrendaField::Manto => c.atuendo.manto = v, } } } Msg::CycleAndarEstado => { model.andar_estado = (model.andar_estado + 1) % 3; } Msg::SetAndarCadencia(v) => { let est = model.andar_estado; if let Some(c) = sel_sere_mut(&mut model) { if let Some(m) = &mut c.cuerpo { m.andares.estado_mut(est).cadencia = v.clamp(0.0, 18.0); } } } Msg::SetAndarAmplitud(seg, v) => { let est = model.andar_estado; if let Some(c) = sel_sere_mut(&mut model) { if let Some(m) = &mut c.cuerpo { if let Some(o) = m.andares.estado_mut(est).osc.get_mut(seg) { o.amplitud = v.clamp(0.0, 1.5); } } } } Msg::SetConducta(i, v) => { if let Some(c) = sel_sere_mut(&mut model) { c.conducta.set(i, v); } } Msg::CondAddImpulso => { if let Some(c) = sel_sere_mut(&mut model) { materializar_impulsos(c); c.conducta.impulsos.push(Impulso::new(Primitiva::Deambular, "1")); } } Msg::CondRemoveImpulso => { if let Some(c) = sel_sere_mut(&mut model) { materializar_impulsos(c); if c.conducta.impulsos.len() > 1 { c.conducta.impulsos.pop(); } model.cond_formula_focused = false; } } Msg::CondCycleImpulso(i) => { if let Some(c) = sel_sere_mut(&mut model) { materializar_impulsos(c); if let Some(imp) = c.conducta.impulsos.get_mut(i) { imp.primitiva = imp.primitiva.next(); } } } Msg::CondFormulaFocus(i) => { let src = sel_sere(&model) .map(|c| c.conducta.impulsos_efectivos()) .and_then(|imps| imps.get(i).map(|imp| imp.peso.clone())) .unwrap_or_default(); model.cond_formula_input.set_text(src); model.cond_formula_focused = true; model.cond_formula_target = i; model.formula_focused = false; model.name_focused = false; model.seed_focused = false; model.ai_focused = false; } Msg::CondFormulaCampo(ev) => { // El campo sólo se dibuja mientras se edita ese peso: el mouse va // directo al buffer (click→caret, arrastre→selección). model.cond_formula_input.handle(ev, &mut model.clipboard); } Msg::CondFormulaKey(ev) => { if ev.state == KeyState::Pressed && matches!(&ev.key, Key::Named(NamedKey::Enter)) { model.cond_formula_focused = false; } else { model.cond_formula_input.handle(TextInputEvent::Key(ev), &mut model.clipboard); let txt = model.cond_formula_input.text(); let target = model.cond_formula_target; if let Some(c) = sel_sere_mut(&mut model) { materializar_impulsos(c); if let Some(imp) = c.conducta.impulsos.get_mut(target) { imp.peso = txt; } } } } Msg::ToggleManada => { model.manada = !model.manada; model.gen += 1; // repone el terreno y (re)inicia/limpia la manada model.status = if model.manada { "manada viva".into() } else { "manada quieta".into() }; } Msg::SetBiomaField(f, v) => { if let Some(b) = sel_bioma_mut(&mut model) { match f { BiomaField::Base => b.base = v.clamp(0.0, 0.9), BiomaField::Dune => b.dune = v.clamp(0.0, 0.4), BiomaField::Relief => b.relief = v.clamp(0.0, 1.0), BiomaField::Mountains => b.mountains = v.clamp(0.0, 1.0), BiomaField::Water => b.water_level = v.clamp(0.0, 0.9), BiomaField::Rivers => b.rivers = v.clamp(0.0, 1.0), BiomaField::PeakAt => b.peak_at = v.clamp(0.0, 1.0), } model.gen += 1; } } Msg::CycleBiomaGround => { let mats = terreno_ids(&model); if let Some(b) = sel_bioma_mut(&mut model) { b.ground = next_in(&mats, b.ground); model.gen += 1; } } Msg::CycleBiomaCliff => { let mats = terreno_ids(&model); if let Some(b) = sel_bioma_mut(&mut model) { b.cliff = next_in(&mats, b.cliff); model.gen += 1; } } Msg::CycleBiomaPeak => { let mats = terreno_ids(&model); if let Some(b) = sel_bioma_mut(&mut model) { b.peak = next_option(b.peak, &mats); model.gen += 1; } } Msg::AddBiomaObjeto => { let obj = objeto_ids(&model); if let (Some(&mat), Some(b)) = (obj.first(), sel_bioma_mut(&mut model)) { b.objetos.push(llimphi_voxel::ObjetoUso { material: mat, densidad: 0.01, forma: Forma::Columnar }); model.gen += 1; } } Msg::RemoveBiomaObjeto => { if let Some(b) = sel_bioma_mut(&mut model) { b.objetos.pop(); model.gen += 1; } } Msg::SetObjetoDensidad(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(o) = b.objetos.get_mut(i) { o.densidad = v.clamp(0.0, 0.05); } model.gen += 1; } } Msg::CycleObjetoForma(i) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(o) = b.objetos.get_mut(i) { o.forma = o.forma.next(); } model.gen += 1; } } Msg::CycleObjetoMaterial(i) => { let ids = objeto_ids(&model); if let Some(b) = sel_bioma_mut(&mut model) { if let Some(o) = b.objetos.get_mut(i) { if let Some(pos) = ids.iter().position(|&m| m == o.material) { o.material = ids[(pos + 1) % ids.len().max(1)]; } else if let Some(&first) = ids.first() { o.material = first; } } model.gen += 1; } } Msg::AddBiomaMineral => { if let Some(b) = sel_bioma_mut(&mut model) { b.minerales.push(MineralUso::of(Mineral::Hierro)); model.gen += 1; } } Msg::RemoveBiomaMineral => { if let Some(b) = sel_bioma_mut(&mut model) { b.minerales.pop(); model.gen += 1; } } Msg::CycleMineralTipo(i) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { let next = mu.mineral.next(); mu.reset_to(next); } model.gen += 1; } } Msg::CycleMineralAspecto(i) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { mu.aspecto = mu.aspecto.next(); } model.gen += 1; } } Msg::SetMineralAbundancia(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { mu.abundancia = v.clamp(0.0, 1.0); } model.gen += 1; } } Msg::SetMineralProfMin(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { mu.prof_min = v.clamp(0.0, mu.prof_max); } model.gen += 1; } } Msg::SetMineralProfMax(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { mu.prof_max = v.clamp(mu.prof_min, 1.0); } model.gen += 1; } } Msg::SetMineralColor(i, ch, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { if let Some(c) = mu.color.get_mut(ch) { *c = (v.clamp(0.0, 1.0) * 255.0) as u8; } } model.gen += 1; } } Msg::SetMineralDureza(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { mu.dureza = v.clamp(1.0, 10.0); } model.gen += 1; } } Msg::SetMineralBrillo(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(mu) = b.minerales.get_mut(i) { mu.brillo = v.clamp(0.0, 1.0); } model.gen += 1; } } Msg::AddBiomaCasa => { if let Some(b) = sel_bioma_mut(&mut model) { b.casas.push(CasaUso { tipo: CasaTipo::Cabana, densidad: 0.25 }); model.gen += 1; } } Msg::RemoveBiomaCasa => { if let Some(b) = sel_bioma_mut(&mut model) { b.casas.pop(); model.gen += 1; } } Msg::CycleCasaTipo(i) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(cu) = b.casas.get_mut(i) { cu.tipo = cu.tipo.next(); } model.gen += 1; } } Msg::SetCasaDensidad(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(cu) = b.casas.get_mut(i) { cu.densidad = v.clamp(0.0, 1.0); } model.gen += 1; } } Msg::SetDensidadUrbana(v) => { if let Some(b) = sel_bioma_mut(&mut model) { b.densidad_urbana = v.clamp(0.05, 0.95); model.gen += 1; } } Msg::CyclePuebloTipo => { model.pueblo_brush = model.pueblo_brush.next(); model.status = format!("preset de pueblo: {}", model.pueblo_brush.label()); } Msg::AplicarPuebloTipo => { let pt = model.pueblo_brush; if let Some(b) = sel_bioma_mut(&mut model) { b.casas = pt.casas(); b.densidad_urbana = pt.densidad(); model.gen += 1; model.status = format!("aplicado preset {}", pt.label()); } } Msg::AddBiomaFauna => { if let Some(b) = sel_bioma_mut(&mut model) { b.fauna.push(BichoUso { bicho: Bicho::Vaca, densidad: 0.3 }); model.gen += 1; } } Msg::RemoveBiomaFauna => { if let Some(b) = sel_bioma_mut(&mut model) { b.fauna.pop(); model.gen += 1; } } Msg::CycleFaunaBicho(i) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(fu) = b.fauna.get_mut(i) { fu.bicho = fu.bicho.next(); } model.gen += 1; } } Msg::SetFaunaDensidad(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(fu) = b.fauna.get_mut(i) { fu.densidad = v.clamp(0.0, 1.0); } model.gen += 1; } } Msg::CycleMuebleBrush => { model.mueble_brush = model.mueble_brush.next(); model.status = format!("mueble: {}", model.mueble_brush.label()); } Msg::CycleMuebleFacing => { model.mueble_facing = (model.mueble_facing + 1) % 4; } Msg::PlaceMuebleAt(gx, gz) => { let (mb, fc) = (model.mueble_brush, model.mueble_facing); if let Some(it) = sel_interior_mut(&mut model) { it.colocar(mb, gx as u32, gz as u32, fc); model.gen += 1; } } Msg::RemoveLastPieza => { if let Some(it) = sel_interior_mut(&mut model) { it.piezas.pop(); model.gen += 1; } } Msg::ClearPiezas => { if let Some(it) = sel_interior_mut(&mut model) { it.piezas.clear(); model.gen += 1; } } Msg::SetInteriorAncho(v) => { if let Some(it) = sel_interior_mut(&mut model) { it.ancho = v.clamp(6.0, 60.0) as u32; model.gen += 1; } } Msg::SetInteriorFondo(v) => { if let Some(it) = sel_interior_mut(&mut model) { it.fondo = v.clamp(6.0, 60.0) as u32; model.gen += 1; } } Msg::CycleInteriorPiso => { if let Some(it) = sel_interior_mut(&mut model) { it.piso = it.piso.next(); model.gen += 1; } } Msg::CycleInteriorMuro => { if let Some(it) = sel_interior_mut(&mut model) { it.muro = it.muro.next(); model.gen += 1; } } Msg::ToggleInteriorMuros => { if let Some(it) = sel_interior_mut(&mut model) { it.muros = !it.muros; model.gen += 1; } } Msg::ToggleInteriorVentanas => { if let Some(it) = sel_interior_mut(&mut model) { it.ventanas = !it.ventanas; model.gen += 1; } } Msg::CycleInteriorTecho => { if let Some(it) = sel_interior_mut(&mut model) { it.techo = it.techo.next(); model.gen += 1; } } Msg::ToggleInteriorTecho => { if let Some(it) = sel_interior_mut(&mut model) { it.con_techo = !it.con_techo; model.gen += 1; } } Msg::AddBiomaSere => { let sids = sere_ids(&model); if let (Some(&sere), Some(b)) = (sids.first(), sel_bioma_mut(&mut model)) { b.seres.push(llimphi_voxel::SereUso { sere, probabilidad: 0.5 }); model.gen += 1; } } Msg::RemoveBiomaSere => { if let Some(b) = sel_bioma_mut(&mut model) { b.seres.pop(); model.gen += 1; } } Msg::CycleBiomaSere(i) => { let sids = sere_ids(&model); if let Some(b) = sel_bioma_mut(&mut model) { if let Some(u) = b.seres.get_mut(i) { u.sere = next_in(&sids, u.sere); } model.gen += 1; } } Msg::SetBiomaSereProb(i, v) => { if let Some(b) = sel_bioma_mut(&mut model) { if let Some(u) = b.seres.get_mut(i) { u.probabilidad = v.clamp(0.0, 1.0); } model.gen += 1; } } Msg::SeedCampo(ev) => { if matches!(ev, TextInputEvent::Press(_)) { model.seed_focused = true; model.name_focused = false; model.ai_focused = false; } model.seed_input.handle(ev, &mut model.clipboard); if let Ok(s) = model.seed_input.text().trim().parse::() { if let Some(m) = sel_mundo_mut(&mut model) { m.seed = s; model.gen += 1; } } } Msg::SeedKey(ev) => { model.seed_input.handle(TextInputEvent::Key(ev), &mut model.clipboard); if let Ok(s) = model.seed_input.text().trim().parse::() { if let Some(m) = sel_mundo_mut(&mut model) { m.seed = s; model.gen += 1; } } } Msg::SeedRandom => { model.rng = model.rng.wrapping_mul(1664525).wrapping_add(1013904223); let s = model.rng % 100_000; if let Some(m) = sel_mundo_mut(&mut model) { m.seed = s; } model.seed_input.set_text(&s.to_string()); model.gen += 1; } Msg::CycleMundoBioma => { let bids = bioma_ids(&model); if let Some(m) = sel_mundo_mut(&mut model) { let cur = m.biomas.first().copied().unwrap_or(0); m.biomas = vec![next_in(&bids, cur)]; model.gen += 1; } } Msg::SelectUtileria(i) => model.utileria_sel = i, Msg::AddUtileria => { // Nace donde más se usa: en la mano del primer actor, desde el instante // que se está mirando. Sin actores, flotando en el centro del mundo. let dim = world_dim(PREVIEW_DIM_XZ); let t = model.time; if let Some(s) = sel_scene_mut(&mut model) { let centro = [dim[0] as f32 * 0.5, dim[1] as f32 * 0.6, dim[2] as f32 * 0.5]; let mut u = UtileriaSpec::en(Utileria::Estrella, centro); u.desde = t.min(s.duration); u.lleva = (!s.actors.is_empty()).then_some(0); s.utileria.push(u); model.utileria_sel = s.utileria.len() - 1; } } Msg::RemoveUtileria => { let i = model.utileria_sel; if let Some(s) = sel_scene_mut(&mut model) { if i < s.utileria.len() { s.utileria.remove(i); } model.utileria_sel = i.min(s.utileria.len().saturating_sub(1)); } } Msg::CycleUtileriaCosa => { let i = model.utileria_sel; if let Some(u) = sel_scene_mut(&mut model).and_then(|s| s.utileria.get_mut(i)) { let k = Utileria::ALL.iter().position(|&x| x == u.cosa).unwrap_or(0); u.cosa = Utileria::ALL[(k + 1) % Utileria::ALL.len()]; } } Msg::CycleUtileriaLleva => { let i = model.utileria_sel; if let Some(s) = sel_scene_mut(&mut model) { let n = s.actors.len(); if let Some(u) = s.utileria.get_mut(i) { u.lleva = match u.lleva { None if n > 0 => Some(0), Some(a) if a + 1 < n => Some(a + 1), _ => None, }; } } } Msg::SetUtileria(campo, v) => { let i = model.utileria_sel; let dim = world_dim(PREVIEW_DIM_XZ); if let Some(s) = sel_scene_mut(&mut model) { let dur = s.duration; if let Some(u) = s.utileria.get_mut(i) { match campo { UtileriaField::X => u.pos[0] = v.clamp(0.0, dim[0] as f32), UtileriaField::Y => u.pos[1] = v.clamp(0.0, dim[1] as f32), UtileriaField::Z => u.pos[2] = v.clamp(0.0, dim[2] as f32), UtileriaField::Alto => u.alto = v.clamp(0.02, 4.0), UtileriaField::Desde => u.desde = v.clamp(0.0, dur), UtileriaField::Giro => u.giro = v.clamp(0.0, 1.0), } } } } Msg::SelectActor(i) => { model.actor_sel = i; model.key_sel = 0; } Msg::AddActor => { let dim = world_dim(PREVIEW_DIM_XZ); let (cz, gx0, gx1) = (dim[2] as f32 * 0.5, dim[0] as f32 * 0.35, dim[0] as f32 * 0.65); let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0); if let Some(s) = sel_scene_mut(&mut model) { s.actors.push(ActorSpec { character: 0, keys: vec![ ActorKeySpec { t: 0.0, gx: gx0, gz: cz, clip: Some(Clip::Walk), face: None }, ActorKeySpec { t: dur, gx: gx1, gz: cz, clip: Some(Clip::Walk), face: None }, ], frame_rate: None, }); model.actor_sel = s.actors.len() - 1; model.key_sel = 0; model.gen += 1; } } Msg::RemoveActor => { let a = model.actor_sel; if let Some(s) = sel_scene_mut(&mut model) { if s.actors.len() > 1 && a < s.actors.len() { s.actors.remove(a); } } model.actor_sel = model.actor_sel.saturating_sub(1); model.key_sel = 0; model.gen += 1; } Msg::CycleActorSere => { let a = model.actor_sel; let n = model.project.seres.len().max(1); if let Some(s) = sel_scene_mut(&mut model) { if let Some(act) = s.actors.get_mut(a) { act.character = (act.character + 1) % n; } model.gen += 1; } } Msg::SelectKey(i) => model.key_sel = i, Msg::AddKey => { let a = model.actor_sel; let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0); let dimx = world_dim(PREVIEW_DIM_XZ)[0] as f32; if let Some(s) = sel_scene_mut(&mut model) { if let Some(act) = s.actors.get_mut(a) { let mut k = act.keys.last().cloned().unwrap_or(ActorKeySpec { t: 0.0, gx: dimx * 0.5, gz: dimx * 0.5, clip: Some(Clip::Walk), face: None, }); k.gx = (k.gx + 12.0).min(dimx - 2.0); // nuevo waypoint adelante act.keys.push(k); respread(act, dur); model.key_sel = act.keys.len() - 1; } model.gen += 1; } } Msg::RemoveKey => { let (a, k) = (model.actor_sel, model.key_sel); let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0); if let Some(s) = sel_scene_mut(&mut model) { if let Some(act) = s.actors.get_mut(a) { if act.keys.len() > 2 && k < act.keys.len() { act.keys.remove(k); respread(act, dur); } } model.key_sel = model.key_sel.saturating_sub(1); model.gen += 1; } } Msg::SetKeyPos(is_gx, v) => { let (a, k) = (model.actor_sel, model.key_sel); let lim = world_dim(PREVIEW_DIM_XZ)[0] as f32 - 1.0; if let Some(s) = sel_scene_mut(&mut model) { if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) { if is_gx { key.gx = v.clamp(1.0, lim); } else { key.gz = v.clamp(1.0, lim); } } model.gen += 1; } } Msg::SetKeyTime(v) => { let (a, k) = (model.actor_sel, model.key_sel); let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(5.0); if let Some(s) = sel_scene_mut(&mut model) { if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) { key.t = v.clamp(0.0, dur); } model.gen += 1; } } Msg::CycleKeyClip => { let (a, k) = (model.actor_sel, model.key_sel); if let Some(s) = sel_scene_mut(&mut model) { if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) { key.clip = Some(key.clip.unwrap_or(Clip::Idle).next()); } model.gen += 1; } } Msg::PlaceKeyAt(gx, gz) => { let (a, k) = (model.actor_sel, model.key_sel); if let Some(s) = sel_scene_mut(&mut model) { if let Some(key) = s.actors.get_mut(a).and_then(|act| act.keys.get_mut(k)) { key.gx = gx; key.gz = gz; } model.gen += 1; } } Msg::CycleSceneMundo => { let mids = mundo_ids(&model); if let Some(s) = sel_scene_mut(&mut model) { s.mundo = next_in(&mids, s.mundo); model.gen += 1; } } Msg::SelectDecorado(i) => model.decorado_sel = i, Msg::AddDecorado => { // Nace detrás de donde termina el primer actor (al fondo del plano), // para que no le caiga encima del recorrido. Sin actores, al centro. let dim = world_dim(PREVIEW_DIM_XZ); if let Some(s) = sel_scene_mut(&mut model) { let (gx, gz) = s .actors .first() .and_then(|a| a.keys.last()) .map(|k| (k.gx, k.gz + 18.0)) .unwrap_or((dim[0] as f32 * 0.5, dim[2] as f32 * 0.5)); s.decorado.push(Decorado::en(CasaTipo::Palacio, gx, gz)); model.decorado_sel = s.decorado.len() - 1; } } Msg::RemoveDecorado => { let i = model.decorado_sel; if let Some(s) = sel_scene_mut(&mut model) { if i < s.decorado.len() { s.decorado.remove(i); } model.decorado_sel = i.min(s.decorado.len().saturating_sub(1)); } } Msg::CycleDecoradoCasa => { let i = model.decorado_sel; if let Some(d) = sel_scene_mut(&mut model).and_then(|s| s.decorado.get_mut(i)) { d.casa = d.casa.next(); } } Msg::OtraVarianteDecorado => { let i = model.decorado_sel; if let Some(d) = sel_scene_mut(&mut model).and_then(|s| s.decorado.get_mut(i)) { d.semilla = d.semilla.wrapping_add(1); } } Msg::SetDecorado(campo, v) => { let i = model.decorado_sel; if let Some(d) = sel_scene_mut(&mut model).and_then(|s| s.decorado.get_mut(i)) { match campo { DecoradoField::X => d.pos[0] = v.round(), DecoradoField::Z => d.pos[1] = v.round(), } } } Msg::ToggleNoche => { if let Some(s) = sel_scene_mut(&mut model) { s.cielo = match s.cielo { Some(_) => None, None => Some(Cielo::noche()), }; } } Msg::SetCielo(campo, v) => { if let Some(c) = sel_scene_mut(&mut model).and_then(|s| s.cielo.as_mut()) { match campo { CieloField::Estrellas => c.estrellas = v.clamp(0.0, 1.0), CieloField::Luna => c.luna_radio = v.clamp(0.0, 0.15), } } } Msg::SetSceneDur(v) => { if let Some(s) = sel_scene_mut(&mut model) { s.duration = v.clamp(1.0, 20.0); } } Msg::TogglePlay => model.playing = !model.playing, Msg::Scrub(v) => { model.playing = false; let dur = sel_scene(&model).map(|s| s.duration).unwrap_or(1.0); model.time = v.clamp(0.0, dur); } Msg::ToggleSceneCam => model.script_cam = !model.script_cam, Msg::AddShot => { let t = model.time; if let Some(s) = sel_scene_mut(&mut model) { let kind = ShotKind::ALL[s.shots.len() % ShotKind::ALL.len()]; s.shots.push(ShotSpec { start: t, kind }); s.shots.sort_by(|a, b| a.start.total_cmp(&b.start)); model.status = format!("plano: {} @ {t:.1}s", kind.label()); } } Msg::RemoveShot => { if let Some(s) = sel_scene_mut(&mut model) { s.shots.pop(); } } Msg::ExportVideo => { if !model.exporting { if let Some(scene) = sel_scene(&model).cloned() { model.exporting = true; model.status = "exportando video… (puede tardar)".into(); let project = model.project.clone(); handle.spawn(move || Msg::ExportDone(render::export_scene(&project, &scene))); } } } Msg::ExportDone(res) => { model.exporting = false; model.status = match res { Ok(p) => format!("video listo: {p}"), Err(e) => format!("export falló: {e}"), }; } Msg::ToggleSim => { model.simulating = !model.simulating; model.gen += 1; // repone terreno fresco y reinicia la sim limpia model.status = match (model.level, model.simulating) { (Level::Leyes, true) => "corriendo la ley…".into(), (Level::Leyes, false) => "ley en pausa".into(), (_, true) => "simulando agua…".into(), (_, false) => "agua estática".into(), }; } Msg::Orbit(dx, dy) => { model.yaw -= dx * 0.008; let lim = std::f32::consts::FRAC_PI_2 - 0.05; model.pitch = (model.pitch + dy * 0.008).clamp(-lim, lim); } Msg::Zoom(dy) => { let f = (1.0 + dy * 0.1).clamp(0.5, 1.5); let xz = PREVIEW_DIM_XZ as f32; model.dist = (model.dist * f).clamp(xz * 0.6, xz * 3.0); } Msg::AiCampo(ev) => { if matches!(ev, TextInputEvent::Press(_)) { model.ai_focused = true; model.name_focused = false; model.seed_focused = false; } model.ai_input.handle(ev, &mut model.clipboard); } Msg::AiKey(ev) => { model.ai_input.handle(TextInputEvent::Key(ev), &mut model.clipboard); } Msg::AiGenerate => { let prompt = model.ai_input.text(); if prompt.trim().is_empty() || model.ai_busy { return model; } model.ai_busy = true; model.status = "generando con IA…".into(); match model.level { Level::Biomas | Level::Mundos => { let refs = MatRefs::from_project(&model.project); handle.spawn(move || Msg::AiBioma(ai::generate_bioma(&prompt, &refs))); } Level::Seres => { handle.spawn(move || Msg::AiSere(ai::generate_character(&prompt))); } Level::Escenas => { let mundo = model.project.mundos.first().map(|m| m.id).unwrap_or(0); let dim = world_dim(PREVIEW_DIM_XZ); handle.spawn(move || Msg::AiScene(ai::generate_scene(&prompt, mundo, dim))); } _ => { model.ai_busy = false; model.status = "la IA genera biomas, mundos, seres o escenas".into(); } } } Msg::AiBioma(mut b) => { b.id = model.project.alloc_id(); let bid = b.id; let name = b.name.clone(); model.project.biomas.push(b); // En modo Mundos, envolver el bioma en un mundo nuevo. if model.level == Level::Mundos { let id = model.project.alloc_id(); model.project.mundos.push(Mundo { id, name: name.clone(), seed: 1337, biomas: vec![bid], mapa: None }); model.set_selected(id); } else { model.level = Level::Biomas; model.set_selected(bid); } model.ai_busy = false; model.ai_input.set_text(""); model.gen += 1; model.status = format!("generado: «{name}»"); sync_inputs(&mut model); } Msg::AiSere(mut c) => { c.id = model.project.alloc_id(); let id = c.id; let name = c.name.clone(); model.project.seres.push(c); model.level = Level::Seres; model.set_selected(id); model.ai_busy = false; model.ai_input.set_text(""); model.status = format!("ser generado: «{name}»"); sync_inputs(&mut model); } Msg::AiScene(mut s) => { s.id = model.project.alloc_id(); let id = s.id; model.project.escenas.push(s); model.level = Level::Escenas; model.set_selected(id); model.time = 0.0; model.playing = true; model.ai_busy = false; model.ai_input.set_text(""); model.gen += 1; model.status = "escena generada".into(); sync_inputs(&mut model); } Msg::Save => match save_project(&model.project) { Ok(_) => model.status = format!("guardado en {PROJECT_PATH}"), Err(e) => model.status = format!("error al guardar: {e}"), }, Msg::Load => match load_project() { Ok(p) => { model.project = p; model.gen += 1; reselect_all(&mut model); model.status = format!("cargado de {PROJECT_PATH}"); sync_inputs(&mut model); } Err(e) => model.status = format!("error al cargar: {e}"), }, Msg::FileCampo(ev) => { if matches!(ev, TextInputEvent::Press(_)) { model.file_focused = true; model.name_focused = false; model.seed_focused = false; model.ai_focused = false; model.formula_focused = false; model.cond_formula_focused = false; } model.file_input.handle(ev, &mut model.clipboard); } Msg::FileKey(ev) => { model.file_input.handle(TextInputEvent::Key(ev), &mut model.clipboard); } Msg::GuardarComo => { let path = model.file_input.text(); if path.trim().is_empty() { model.status = "escribe una ruta primero".into(); } else { match Documento::nuevo(model.project.clone()).guardar(&path) { Ok(_) => model.status = format!("guardado en {path}"), Err(e) => model.status = format!("error al guardar: {e}"), } } } Msg::Abrir => { let path = model.file_input.text(); match Documento::abrir(&path) { Ok(doc) => { model.project = doc.proyecto; model.gen += 1; reselect_all(&mut model); sync_inputs(&mut model); let rotas = model.project.validar_referencias(); model.status = if rotas.is_empty() { format!("abierto {path}") } else { format!("abierto {path} — ⚠ {} referencias rotas", rotas.len()) }; } Err(e) => model.status = format!("error al abrir: {e}"), } } Msg::Combinar => { let path = model.file_input.text(); match Documento::abrir(&path) { Ok(doc) => { let n_esc = doc.proyecto.escenas.len(); model.project.combinar(&doc.proyecto); model.gen += 1; reselect_all(&mut model); sync_inputs(&mut model); let rotas = model.project.validar_referencias(); model.status = if rotas.is_empty() { format!("combinado {path} (+{n_esc} escenas)") } else { format!("combinado {path} — ⚠ {} referencias rotas", rotas.len()) }; } Err(e) => model.status = format!("error al combinar: {e}"), } } Msg::MundoDesdeMapa => { let path = model.file_input.text(); // Lee la fuente (GeoJSON + .pgm hermano) → adjuntala AL MUNDO // seleccionado (se persiste con el Project) y regenerá el grid. match mapa_import::leer_fuente(&path, 300.0) { Ok(fuente) => { model.level = Level::Mundos; let mid = model.sel[Level::Mundos.index()]; if let Some(m) = model.project.mundos.iter_mut().find(|m| m.id == mid) { m.mapa = Some(fuente); model.gen += 1; // fuerza rebuild del preview desde el mapa refrescar_mapa(&mut model); } else { model.status = "no hay mundo seleccionado para el mapa".into(); } } Err(e) => model.status = format!("mapa: {e}"), } } Msg::QuitarMapa => { let mid = model.sel[Level::Mundos.index()]; let quitado = model .project .mundos .iter_mut() .find(|m| m.id == mid) .map(|m| m.mapa.take().is_some()) .unwrap_or(false); if quitado { model.mapa = None; model.gen += 1; // vuelve al mundo procedural model.status = "mapa quitado — mundo procedural".into(); } } Msg::SerDesdeFoto => { // La foto NO se voxeliza literal (eso da el valle): sólo REGULA los rasgos // (piel/ropa) de un personaje ESTILIZADO del kit, que conserva su estilo, // gestos y personalidad. El recorte de persona lo da u2net por el daemon. // // Todo esto va a un worker, y era el peor caso del barrido: no // sólo leía el archivo, además **conectaba un socket y esperaba // a que una red neuronal segmentara la persona**, con el hilo de // UI parado. Un round-trip a u2net son cientos de ms en el mejor // caso; si el daemon no está, es el timeout de la conexión. // // Sin contador de generación a propósito: cada resultado // **agrega** un personaje, así que dos clicks dan dos, que es // exactamente lo que hacía la versión sincrónica. let path = model.file_input.text(); model.status = format!("foto: analizando {path}…"); let para_worker = path.clone(); handle.spawn(move || { let r = std::fs::read(¶_worker) .map_err(|e| format!("foto: leer {para_worker}: {e}")) .and_then(|bytes| { let cliente = ClienteBloqueante::conectar(rostro::socket_por_defecto()).ok(); let prov = cliente.as_ref().map(|c| c as &dyn Proveedor); rostro::rasgos_desde_bytes(&bytes, prov) .map_err(|e| format!("foto: {e}")) }); Msg::RostroListo(para_worker, r) }); } Msg::RostroListo(path, r) => match r { Ok(r) => { let via = if r.recorte_real { "recorte u2net" } else { "sin recorte (centro)" }; let id = model.project.alloc_id(); let mut cs = CharSpec::new(nombre_desde_path(&path), Age::Adult); cs.id = id; cs.skin = r.skin; cs.shirt = r.shirt; cs.pants = r.pants; cs.hair = Some(r.hair); // pelo FIEL de la foto (no derivado) model.project.seres.push(cs); model.level = Level::Seres; model.set_selected(id); model.gen += 1; sync_inputs(&mut model); model.status = format!("ser desde foto (rasgos, {via}): personaje estilizado"); } Err(e) => model.status = e, }, Msg::Versionar => { // Registra el proyecto actual como una versión nueva en la rama // actual del DAG, y persiste la historia. let etiqueta = format!("v{}", model.historia.len() + 1); match model.historia.commit(&model.project, etiqueta) { Ok(id) => { let _ = model.historia.guardar(HISTORY_PATH); model.status = format!( "versión {} · rama {} · {} en total", hex_corto(&id), model.historia.rama_actual(), model.historia.len() ); } Err(e) => model.status = format!("error al versionar: {e}"), } } Msg::Ramificar => { // Una alternativa nombrada desde la punta actual (nombre = ruta // escrita, o auto). Cambia a la rama nueva: lo próximo que // versiones cuelga de la alternativa. let nombre = { let t = model.file_input.text(); if t.trim().is_empty() { format!("alt-{}", model.historia.ramas().len() + 1) } else { t } }; match model.historia.ramificar(&nombre) { Ok(_) => { model.historia.cambiar_rama(&nombre); let _ = model.historia.guardar(HISTORY_PATH); model.status = format!("rama nueva: {nombre} (versiona para poblarla)"); } Err(_) => model.status = "versiona algo antes de ramificar".into(), } } Msg::IrAVersion(id) => { // Navegar: checkout de la versión → carga su proyecto al lienzo. if let Some(p) = model.historia.checkout(&id) { model.project = p; model.gen += 1; reselect_all(&mut model); sync_inputs(&mut model); let etq = model .historia .version(&id) .map(|v| v.etiqueta.clone()) .unwrap_or_default(); model.status = format!("en versión {} · {etq}", hex_corto(&id)); } } Msg::EntrarSalir => { model.jugando = !model.jugando; model.input_juego = InputJuego::default(); if model.jugando { model.level = Level::Escenas; // se entra a la escena *model.respawn_jugador.lock().unwrap() = true; model.status = "JUGANDO — WASD/E-Q mover · F volar · R semilla · 1/2/3 dictar suceso · Esc salir".into(); } else { model.status = "saliste de la escena".into(); } } Msg::MoverJuego(eje, on) => match eje { EjeJuego::Fwd => model.input_juego.fwd = on, EjeJuego::Back => model.input_juego.back = on, EjeJuego::Left => model.input_juego.left = on, EjeJuego::Right => model.input_juego.right = on, EjeJuego::Arriba => model.input_juego.arriba = on, EjeJuego::Abajo => model.input_juego.abajo = on, }, Msg::Saltar(on) => model.input_juego.salto = on, Msg::AlternarVuelo => { model.volar = !model.volar; model.status = if model.volar { "vuelo libre — E/Q suben/bajan".into() } else { "caminando (gravedad)".into() }; } Msg::RerollSemilla => { // Nueva semilla (LCG u32) para el mundo de la escena → regenera // el terreno bajo tus pies. model.rng = model.rng.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); let nueva = model.rng; let mundo_id = sel_scene(&model).map(|s| s.mundo); if let Some(id) = mundo_id { if let Some(m) = model.project.mundos.iter_mut().find(|m| m.id == id) { m.seed = nueva; model.gen += 1; model.status = format!("nueva semilla: {nueva}"); } } } Msg::DictarSuceso(mut tipo) => { // Un "aparece" sin ser asignado se ata al ser seleccionado (o al // primero que haya) para que valga como referencia. if let TipoSuceso::Aparece(id) = &mut tipo { if *id == 0 { *id = if model.project.seres.iter().any(|s| s.id == model.sel[2]) { model.sel[2] } else { model.project.seres.first().map(|s| s.id).unwrap_or(0) }; } } let feet = model.jugador.lock().unwrap().pos; let t = model.time; let etiqueta = match &tipo { TipoSuceso::Aparece(0) => "marca (aún sin ser)".to_string(), TipoSuceso::Aparece(id) => format!("aparece ser {id}"), TipoSuceso::Camara => "marca de cámara".to_string(), TipoSuceso::Marca => "punto de interés".to_string(), TipoSuceso::Desmorona { radio, motas } => { format!("se desmorona (r={radio:.0}, {motas} motas)") } }; if let Some(scene) = sel_scene_mut(&mut model) { scene.sucesos.push(Suceso { t, pos: [feet.x, feet.y, feet.z], tipo, nota: String::new(), }); model.status = format!("suceso dictado: {etiqueta}"); } else { model.status = "entrá a una escena para dictar sucesos".into(); } model.gen += 1; } Msg::Tick => { if model.level == Level::Escenas && model.playing { if let Some(dur) = sel_scene(&model).map(|s| s.duration) { model.time += DT; if model.time >= dur { model.time = 0.0; } } } else if model.level == Level::Seres { model.time += DT; // turntable + respiración } else if model.level == Level::Leyes { ensure_lab(&mut model); // En modo grafo, reconstruir si el texto cambió afuera (no por edición del grafo). if model.leyes_node_mode && (model.eq_graph.is_none() || model.eq_graph_gen != model.lab_gen) { rebuild_eq_graph(&mut model); } if model.simulating { let params = sel_ley(&model) .map(|l| l.kind.ecuacion_param_values()) .unwrap_or_default(); if let Some(lab) = &mut model.lab { lab.step(¶ms); } } } } } model }