tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos
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