gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-voxel-studio/src/items.rs Rust · 442 lines · 16.5 KB History Who touched what raw

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1use super::*;
2
3/// Crea un item nuevo en el nivel activo y devuelve su id.
4pub(crate) fn new_item(model: &mut Model) -> u64 {
5    let id = model.project.alloc_id();
6    match model.level {
7        Level::Leyes => model.project.leyes.push(llimphi_voxel::Ley {
8            id,
9            name: format!("ley {id}"),
10            kind: LeyKind::Fluir { gravedad: 1.0, horizontal: 0.6 },
11        }),
12        Level::Materiales => model.project.materiales.push(MaterialDef {
13            id,
14            name: format!("material {id}"),
15            parent: None,
16            role: MatRole::Terreno,
17            color: Some([0.6, 0.6, 0.6]),
18            grain: Some(0.4),
19            brillo: None,
20            rugosidad: None,
21            bisel: None,
22            leyes: vec![],
23            builtin: None,
24        }),
25        Level::Seres => {
26            let mut c = CharSpec::new(format!("ser {id}"), Age::Adult);
27            c.id = id;
28            model.project.seres.push(c);
29        }
30        Level::Biomas => model.project.biomas.push(default_bioma(model, id)),
31        Level::Mundos => {
32            let bioma = model.project.biomas.first().map(|b| b.id).unwrap_or(0);
33            model.project.mundos.push(Mundo { id, name: format!("mundo {id}"), seed: 1, biomas: vec![bioma], mapa: None });
34        }
35        Level::Escenas => {
36            let mundo = model.project.mundos.first().map(|m| m.id).unwrap_or(0);
37            let dim = world_dim(PREVIEW_DIM_XZ);
38            let mut s = SceneSpec::walk_and_emote(format!("escena {id}"), mundo, 3, Clip::Wave, dim);
39            s.id = id;
40            model.project.escenas.push(s);
41        }
42        Level::Interiores => model.project.interiores.push(Interior::nuevo(id, format!("sala {id}"))),
43    }
44    id
45}
46
47/// Un bioma por defecto que referencia los materiales del proyecto (o 0).
48pub(crate) fn default_bioma(model: &Model, id: u64) -> Bioma {
49    let ground = model.project.material_id_for(Material::Grass).unwrap_or(0);
50    let cliff = model.project.material_id_for(Material::Rock).unwrap_or(0);
51    Bioma {
52        id,
53        name: format!("bioma {id}"),
54        base: 0.22,
55        dune: 0.10,
56        relief: 0.6,
57        mountains: 0.4,
58        water_level: 0.30,
59        rivers: 0.2,
60        peak_at: 0.8,
61        ground,
62        cliff,
63        peak: None,
64        objetos: vec![],
65        minerales: vec![],
66        densidad_urbana: 0.4,
67        casas: vec![],
68        fauna: vec![],
69        seres: vec![],
70    }
71}
72
73/// Duplica el item seleccionado (copia con id nuevo) y devuelve su id.
74pub(crate) fn duplicate_item(model: &mut Model) -> Option<u64> {
75    let sel = model.selected();
76    let id = model.project.alloc_id();
77    match model.level {
78        Level::Leyes => {
79            let mut x = model.project.ley(sel)?.clone();
80            x.id = id;
81            x.name = format!("{} copia", x.name);
82            model.project.leyes.push(x);
83        }
84        Level::Materiales => {
85            let mut x = model.project.material(sel)?.clone();
86            x.id = id;
87            x.name = format!("{} copia", x.name);
88            x.builtin = None; // una copia editable, no la de fábrica
89            model.project.materiales.push(x);
90        }
91        Level::Seres => {
92            let mut x = sel_sere(model)?.clone();
93            x.id = id;
94            x.name = format!("{} copia", x.name);
95            model.project.seres.push(x);
96        }
97        Level::Biomas => {
98            let mut x = model.project.bioma(sel)?.clone();
99            x.id = id;
100            x.name = format!("{} copia", x.name);
101            model.project.biomas.push(x);
102        }
103        Level::Mundos => {
104            let mut x = model.project.mundo(sel)?.clone();
105            x.id = id;
106            x.name = format!("{} copia", x.name);
107            model.project.mundos.push(x);
108        }
109        Level::Escenas => {
110            let mut x = sel_scene(model)?.clone();
111            x.id = id;
112            x.name = format!("{} copia", x.name);
113            model.project.escenas.push(x);
114        }
115        Level::Interiores => {
116            let mut x = model.project.interiores.iter().find(|i| i.id == model.selected())?.clone();
117            x.id = id;
118            x.name = format!("{} copia", x.name);
119            model.project.interiores.push(x);
120        }
121    }
122    Some(id)
123}
124
125/// Borra el item seleccionado del nivel activo.
126pub(crate) fn delete_selected(model: &mut Model) {
127    let sel = model.selected();
128    match model.level {
129        Level::Leyes => model.project.leyes.retain(|x| x.id != sel),
130        Level::Materiales => model.project.materiales.retain(|x| x.id != sel),
131        Level::Seres => model.project.seres.retain(|x| x.id != sel),
132        Level::Biomas => model.project.biomas.retain(|x| x.id != sel),
133        Level::Mundos => model.project.mundos.retain(|x| x.id != sel),
134        Level::Escenas => model.project.escenas.retain(|x| x.id != sel),
135        Level::Interiores => model.project.interiores.retain(|x| x.id != sel),
136    }
137}
138
139/// Fija el nombre del item seleccionado.
140pub(crate) fn set_selected_name(model: &mut Model, name: String) {
141    let sel = model.selected();
142    match model.level {
143        Level::Leyes => {
144            if let Some(x) = model.project.leyes.iter_mut().find(|x| x.id == sel) {
145                x.name = name;
146            }
147        }
148        Level::Materiales => {
149            if let Some(x) = model.project.materiales.iter_mut().find(|x| x.id == sel) {
150                x.name = name;
151            }
152        }
153        Level::Seres => {
154            if let Some(x) = model.project.seres.iter_mut().find(|x| x.id == sel) {
155                x.name = name;
156            }
157        }
158        Level::Biomas => {
159            if let Some(x) = model.project.biomas.iter_mut().find(|x| x.id == sel) {
160                x.name = name;
161            }
162        }
163        Level::Mundos => {
164            if let Some(x) = model.project.mundos.iter_mut().find(|x| x.id == sel) {
165                x.name = name;
166            }
167        }
168        Level::Escenas => {
169            if let Some(x) = model.project.escenas.iter_mut().find(|x| x.id == sel) {
170                x.name = name;
171            }
172        }
173        Level::Interiores => {
174            if let Some(x) = model.project.interiores.iter_mut().find(|x| x.id == sel) {
175                x.name = name;
176            }
177        }
178    }
179}
180
181/// Recarga `name_input`/`seed_input` desde el item seleccionado.
182pub(crate) fn sync_inputs(model: &mut Model) {
183    model.name_input.set_text(&model.selected_name());
184    if model.level == Level::Mundos {
185        let s = model.project.mundo(model.selected()).map(|m| m.seed).unwrap_or(0);
186        model.seed_input.set_text(&s.to_string());
187    }
188}
189
190/// Reselecciona el primer item de cada nivel (tras cargar un proyecto).
191pub(crate) fn reselect_all(model: &mut Model) {
192    model.sel[0] = model.project.leyes.first().map(|x| x.id).unwrap_or(0);
193    model.sel[1] = model.project.materiales.first().map(|x| x.id).unwrap_or(0);
194    model.sel[2] = model.project.seres.first().map(|x| x.id).unwrap_or(0);
195    model.sel[3] = model.project.biomas.first().map(|x| x.id).unwrap_or(0);
196    model.sel[4] = model.project.mundos.first().map(|x| x.id).unwrap_or(0);
197    model.sel[5] = model.project.escenas.first().map(|x| x.id).unwrap_or(0);
198    model.sel[6] = model.project.interiores.first().map(|x| x.id).unwrap_or(0);
199    refrescar_mapa(model); // el mundo recién seleccionado puede venir de un mapa
200}
201
202/// Regenera el **grid cache** (`model.mapa`) desde la [`MapaFuente`] persistida en el
203/// mundo seleccionado, o lo limpia si ese mundo es procedural. Barato cuando no hay
204/// mapa (sólo asigna `None`). Se llama al abrir un proyecto (vía [`reselect_all`]),
205/// al cambiar de mundo y al importar/quitar un mapa.
206pub(crate) fn refrescar_mapa(model: &mut Model) {
207    let mid = model.sel[Level::Mundos.index()];
208    let fuente = model.project.mundos.iter().find(|m| m.id == mid).and_then(|m| m.mapa.clone());
209    match fuente {
210        Some(f) => {
211            let dim = world_dim(PREVIEW_DIM_XZ);
212            match mapa_import::generar(&f, dim) {
213                Ok((grid, resumen)) => {
214                    model.mapa = Some(Arc::new(grid));
215                    model.status = resumen;
216                }
217                Err(e) => {
218                    model.mapa = None;
219                    model.status = format!("mapa: {e}");
220                }
221            }
222        }
223        None => model.mapa = None,
224    }
225}
226
227/// Nombre legible de un ser a partir de la ruta de su foto (el nombre de archivo sin
228/// extensión), con un respaldo si la ruta viene vacía.
229pub(crate) fn nombre_desde_path(path: &str) -> String {
230    std::path::Path::new(path)
231        .file_stem()
232        .and_then(|s| s.to_str())
233        .filter(|s| !s.is_empty())
234        .unwrap_or("retrato")
235        .to_string()
236}
237
238// --- Accesores al item seleccionado (mut e inmut) ---
239pub(crate) fn sel_material_mut(model: &mut Model) -> Option<&mut MaterialDef> {
240    let id = model.sel[1];
241    model.project.materiales.iter_mut().find(|x| x.id == id)
242}
243pub(crate) fn sel_ley(model: &Model) -> Option<&llimphi_voxel::Ley> {
244    model.project.ley(model.sel[0])
245}
246pub(crate) fn sel_ley_mut(model: &mut Model) -> Option<&mut llimphi_voxel::Ley> {
247    let id = model.sel[0];
248    model.project.leyes.iter_mut().find(|x| x.id == id)
249}
250
251/// Catálogo de **leyes de fábrica** por ecuación (autoradas, editables). Ciclar
252/// reemplaza la `Ecuacion` actual por la siguiente del catálogo. Todas verificadas
253/// como comportamientos vivos por los tests de `llimphi-voxel`.
254pub(crate) fn law_presets() -> Vec<(&'static str, LeyKind)> {
255    vec![
256        ("difusión", LeyKind::ecuacion_default()),
257        (
258            "reacción-difusión",
259            LeyKind::Ecuacion {
260                campos: vec![
261                    FieldDef::new("u", 1.0, 0.0, 1.0),
262                    FieldDef::new("v", 0.0, 0.0, 1.0),
263                ],
264                params: vec![
265                    ParamDef::new("Du", 0.16, 0.0, 1.0),
266                    ParamDef::new("Dv", 0.08, 0.0, 1.0),
267                    ParamDef::new("F", 0.06, 0.0, 0.1),
268                    ParamDef::new("k", 0.062, 0.0, 0.1),
269                ],
270                fuentes: vec![
271                    "Du * lap(u) - u * v * v + F * (1 - u)".into(),
272                    "Dv * lap(v) + u * v * v - (F + k) * v".into(),
273                ],
274            },
275        ),
276        (
277            "calor",
278            LeyKind::Ecuacion {
279                campos: vec![FieldDef::new("t", 0.0, 0.0, 1.0)],
280                params: vec![ParamDef::new("difus", 0.2, 0.0, 0.25)],
281                fuentes: vec!["difus * lap(t)".into()],
282            },
283        ),
284        (
285            "crecer",
286            LeyKind::Ecuacion {
287                campos: vec![FieldDef::new("h", 0.0, 0.0, 1.0)],
288                params: vec![ParamDef::new("vel", 0.05, 0.0, 0.3), ParamDef::new("tope", 0.8, 0.0, 1.0)],
289                fuentes: vec!["vel * (h < tope)".into()],
290            },
291        ),
292    ]
293}
294
295/// La siguiente ley del catálogo tras la actual (por label del preset actual).
296pub(crate) fn next_preset(actual: &LeyKind) -> LeyKind {
297    let presets = law_presets();
298    // Encuentra el preset cuya estructura de campos coincide con la actual; si no,
299    // arranca del primero. Ciclo simple por índice.
300    let idx = presets
301        .iter()
302        .position(|(_, k)| leyes_misma_forma(k, actual))
303        .map(|i| (i + 1) % presets.len())
304        .unwrap_or(0);
305    presets[idx].1.clone()
306}
307
308/// Dos leyes `Ecuacion` "tienen la misma forma" si coinciden los nombres de campos
309/// (heurística para ubicar el preset actual al ciclar).
310pub(crate) fn leyes_misma_forma(a: &LeyKind, b: &LeyKind) -> bool {
311    match (a, b) {
312        (LeyKind::Ecuacion { campos: ca, .. }, LeyKind::Ecuacion { campos: cb, .. }) => {
313            ca.len() == cb.len() && ca.iter().zip(cb).all(|(x, y)| x.name == y.name)
314        }
315        _ => false,
316    }
317}
318pub(crate) fn sel_sere<'a>(model: &'a Model) -> Option<&'a CharSpec> {
319    let id = model.sel[2];
320    model.project.seres.iter().find(|x| x.id == id)
321}
322pub(crate) fn sel_sere_mut(model: &mut Model) -> Option<&mut CharSpec> {
323    let id = model.sel[2];
324    model.project.seres.iter_mut().find(|x| x.id == id)
325}
326pub(crate) fn sel_bioma_mut(model: &mut Model) -> Option<&mut Bioma> {
327    let id = model.sel[3];
328    model.project.biomas.iter_mut().find(|x| x.id == id)
329}
330
331pub(crate) fn sel_interior_mut(model: &mut Model) -> Option<&mut Interior> {
332    let id = model.sel[6];
333    model.project.interiores.iter_mut().find(|x| x.id == id)
334}
335pub(crate) fn sel_mundo_mut(model: &mut Model) -> Option<&mut Mundo> {
336    let id = model.sel[4];
337    model.project.mundos.iter_mut().find(|x| x.id == id)
338}
339/// Mapea el teclado a mensajes de JUEGO (movimiento en primera persona).
340/// WASD mueve, E/Q suben/bajan en vuelo, Espacio salta (a pie), F alterna vuelo,
341/// R nueva semilla, Esc sale.
342pub(crate) fn juego_on_key(ev: &KeyEvent) -> Option<Msg> {
343    let pressed = matches!(ev.state, KeyState::Pressed);
344    match &ev.key {
345        Key::Named(NamedKey::Escape) if pressed && !ev.repeat => Some(Msg::EntrarSalir),
346        Key::Named(NamedKey::Space) => Some(Msg::Saltar(pressed)),
347        Key::Character(c) => match c.to_ascii_lowercase().as_str() {
348            "w" => Some(Msg::MoverJuego(EjeJuego::Fwd, pressed)),
349            "s" => Some(Msg::MoverJuego(EjeJuego::Back, pressed)),
350            "a" => Some(Msg::MoverJuego(EjeJuego::Left, pressed)),
351            "d" => Some(Msg::MoverJuego(EjeJuego::Right, pressed)),
352            "e" => Some(Msg::MoverJuego(EjeJuego::Arriba, pressed)),
353            "q" => Some(Msg::MoverJuego(EjeJuego::Abajo, pressed)),
354            "f" if pressed && !ev.repeat => Some(Msg::AlternarVuelo),
355            "r" if pressed && !ev.repeat => Some(Msg::RerollSemilla),
356            // Dictar sucesos donde estás parado: 1 aparece un ser, 2 marca de
357            // cámara, 3 punto de interés, 4 acá se desmorona el mundo.
358            "1" if pressed && !ev.repeat => Some(Msg::DictarSuceso(TipoSuceso::Aparece(0))),
359            "2" if pressed && !ev.repeat => Some(Msg::DictarSuceso(TipoSuceso::Camara)),
360            "3" if pressed && !ev.repeat => Some(Msg::DictarSuceso(TipoSuceso::Marca)),
361            "4" if pressed && !ev.repeat => {
362                Some(Msg::DictarSuceso(TipoSuceso::Desmorona { radio: 4.0, motas: 32 }))
363            }
364            _ => None,
365        },
366        _ => None,
367    }
368}
369
370pub(crate) fn sel_scene<'a>(model: &'a Model) -> Option<&'a SceneSpec> {
371    let id = model.sel[5];
372    model.project.escenas.iter().find(|x| x.id == id)
373}
374pub(crate) fn sel_scene_mut(model: &mut Model) -> Option<&mut SceneSpec> {
375    let id = model.sel[5];
376    model.project.escenas.iter_mut().find(|x| x.id == id)
377}
378
379// --- Listas de ids para los botones de ciclo ---
380pub(crate) fn terreno_ids(model: &Model) -> Vec<u64> {
381    model.project.materiales.iter().map(|m| m.id).collect()
382}
383pub(crate) fn objeto_ids(model: &Model) -> Vec<u64> {
384    let objs: Vec<u64> = model
385        .project
386        .materiales
387        .iter()
388        .filter(|m| matches!(m.role, MatRole::Objeto(_)))
389        .map(|m| m.id)
390        .collect();
391    if objs.is_empty() {
392        terreno_ids(model)
393    } else {
394        objs
395    }
396}
397pub(crate) fn bioma_ids(model: &Model) -> Vec<u64> {
398    model.project.biomas.iter().map(|b| b.id).collect()
399}
400pub(crate) fn sere_ids(model: &Model) -> Vec<u64> {
401    model.project.seres.iter().map(|s| s.id).collect()
402}
403
404/// Reparte los tiempos de las claves de un actor uniformemente en `[0, dur]`, así el
405/// recorrido se recorre a lo largo de toda la escena (el director interpola entre ellas).
406pub(crate) fn respread(actor: &mut ActorSpec, dur: f32) {
407    let n = actor.keys.len();
408    for (i, k) in actor.keys.iter_mut().enumerate() {
409        k.t = if n <= 1 { 0.0 } else { i as f32 / (n - 1) as f32 * dur };
410    }
411}
412pub(crate) fn mundo_ids(model: &Model) -> Vec<u64> {
413    model.project.mundos.iter().map(|m| m.id).collect()
414}
415
416/// El siguiente id de `ids` después de `cur` (cicla); `cur` si la lista está vacía.
417pub(crate) fn next_in(ids: &[u64], cur: u64) -> u64 {
418    if ids.is_empty() {
419        return cur;
420    }
421    let i = ids.iter().position(|&x| x == cur).unwrap_or(usize::MAX);
422    ids[(i.wrapping_add(1)) % ids.len()]
423}
424
425/// Cicla un `Option<u64>` por `None → ids[0] → … → None`.
426pub(crate) fn next_option(cur: Option<u64>, ids: &[u64]) -> Option<u64> {
427    match cur {
428        None => ids.first().copied(),
429        Some(c) => {
430            let i = ids.iter().position(|&x| x == c);
431            match i {
432                Some(i) if i + 1 < ids.len() => Some(ids[i + 1]),
433                _ => None,
434            }
435        }
436    }
437}
438
439// =============================================================================
440//  Helpers de layout
441// =============================================================================
442