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/// Canvas 3D: terreno del artefacto + (en escenas) los actores posados.
4pub(crate) fn canvas_3d(model: &Model) -> View<Msg> {
5 let (yaw, pitch, dist, gen) = (model.yaw, model.pitch, model.dist, model.gen);
6 let preview = model.preview.clone();
7 // Dirigiendo (Escenas + pestaña Reparto): el click coloca la clave seleccionada.
8 let directing = model.level == Level::Escenas && model.tool_tab == 1 && sel_scene(model).is_some();
9 let preview_click = model.preview.clone();
10 let preview_click_int = model.preview.clone();
11 let interior_sel = model.project.interiores.iter().find(|x| x.id == model.sel[6]).cloned();
12 let is_interiores = model.level == Level::Interiores;
13 let mr = model.preview_render();
14 // Detalle sub-voxel del preview 3D (Biomas/Mundos): la rugosidad y el bisel finos
15 // ya viajan **horneados POR VOXEL** en el grid (cada material del bioma con el suyo,
16 // ver `ResolvedMaterial::detalle`/`generate_window`) → dos materiales lucen distinto
17 // a la vez, lo que el look global no podía. Aquí sólo derivamos el **bisel GLOBAL** =
18 // el mayor bisel del bioma, que enciende el trazado de isosuperficie (requisito para
19 // que el bisel por voxel module la normal). La rugosidad global va en 0 (el por-voxel
20 // manda; los voxels sin detalle —agua, minerales— quedan lisos, que es lo correcto).
21 // El nivel Materiales usa su propio swatch 3D (single-material, look global).
22 let bisel_global = {
23 let p = &mr.palette;
24 let pk = p.peak.map(|m| m.detalle.1).unwrap_or(0.0);
25 p.ground.detalle.1.max(p.cliff.detalle.1).max(pk)
26 };
27 // Silueta sub-voxel POR MATERIAL: prende el contorno diferenciado (uno duro/blocky,
28 // otro liso/redondeado en el mismo frame) sólo si el bioma tiene bisel CON CONTRASTE
29 // entre terreno/acantilado/cumbre (si son todos iguales, no aporta y cuesta samplear
30 // el detalle vecino por evaluación del campo). El fauna/flora aporta su propio bisel
31 // por voxel, pero el contraste del terreno es el que domina la silueta visible.
32 let silueta_pv = {
33 let p = &mr.palette;
34 let g = p.ground.detalle.1;
35 let c = p.cliff.detalle.1;
36 let pk = p.peak.map(|m| m.detalle.1).unwrap_or(g);
37 bisel_global > 0.01 && ((g - c).abs() > 0.01 || (g - pk).abs() > 0.01 || (c - pk).abs() > 0.01)
38 };
39 // Mundo desde mapa real: sólo pisa el preview en el nivel Mundos (Escenas usa
40 // ventana móvil; Biomas/Seres/Interiores tienen su propio grid). `Arc` clonado.
41 let mapa_override = if model.level == Level::Mundos { model.mapa.clone() } else { None };
42 let simulating = model.simulating;
43 let agua = mr.palette.agua;
44 // El agua fluye SÓLO si su material tiene la ley Fluir (con sus params).
45 let fluir = model.project.water_fluir();
46 // Y las plantas (objetos) crecen si su material tiene la ley Crecer.
47 let crecer = mr.bioma.objetos.iter().find_map(|o| {
48 model
49 .project
50 .crecer_velocidad(o.material)
51 .map(|v| (model.project.resolve_material(o.material).color, v))
52 });
53 // Ley autorada (Ecuacion): el primer material de la escena que la tenga corre sobre
54 // sus celdas (recoloreándolas por el campo). `(color, tol, campos, programa, params, vis)`.
55 let ecuacion = {
56 let b = &mr.bioma;
57 let mut ids = vec![b.ground, b.cliff];
58 if let Some(p) = b.peak {
59 ids.push(p);
60 }
61 ids.extend(b.objetos.iter().map(|o| o.material));
62 ids.into_iter().find_map(|id| {
63 model.project.ecuacion_de_material(id).map(|(campos, prog, params)| {
64 let color = model.project.resolve_material(id).color;
65 let vis = campos.len().saturating_sub(1);
66 (color, campos, prog, params, vis)
67 })
68 })
69 };
70 // Pobladores del bioma: cada SereUso → unos cuantos habitantes (por probabilidad).
71 let pobladores: Vec<(CharSpec, usize)> = mr
72 .bioma
73 .seres
74 .iter()
75 .filter_map(|su| {
76 model
77 .project
78 .sere(su.sere)
79 .map(|cs| (cs.clone(), ((su.probabilidad.clamp(0.0, 1.0) * 6.0).round() as usize).max(1)))
80 })
81 .collect();
82 let absolute = Style {
83 position: Position::Absolute,
84 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
85 ..Default::default()
86 };
87
88 let mut canvas = match model.level {
89 Level::Escenas => {
90 let scene = sel_scene(model).cloned();
91 let script_cam = model.script_cam;
92 // Recorrido a dibujar en 3D mientras se dirige (pestaña Reparto): los
93 // waypoints del actor seleccionado + cuál está seleccionado.
94 let path: Option<(Vec<(f32, f32)>, usize)> = if model.tool_tab == 1 {
95 scene.as_ref().and_then(|s| s.actors.get(model.actor_sel)).map(|act| {
96 (act.keys.iter().map(|k| (k.gx, k.gz)).collect(), model.key_sel)
97 })
98 } else {
99 None
100 };
101 let script_cam = if path.is_some() { false } else { script_cam }; // órbita para dirigir
102 let scripts: Vec<ActorScript> = scene.as_ref().map(|s| s.scripts()).unwrap_or_default();
103 let puesta = scene.as_ref().map(|s| s.puesta()).unwrap_or_default();
104 let chars: Vec<CharSpec> = scene
105 .as_ref()
106 .map(|s| s.actors.iter().map(|a| model.project.character_or_default(a.character)).collect())
107 .unwrap_or_default();
108 let time = model.time;
109 let jugando = model.jugando;
110 let jugador = model.jugador.clone();
111 let input_juego = model.input_juego;
112 let volar = model.volar;
113 let respawn_jugador = model.respawn_jugador.clone();
114 View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| {
115 let dim = world_dim(PREVIEW_DIM_XZ);
116 let mut guard = preview.lock().unwrap();
117 let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, None));
118 // Al dirigir, ventana fija en el origen → la columna de grilla coincide
119 // con la de mundo (el picking del click queda directo).
120 let origin = if directing { [0, 0] } else { window_origin_for_cast(&scripts, time, dim) };
121 p.ensure_window_con(device, queue, &mr, gen, origin, &puesta);
122 let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5;
123 let mut poses = Vec::with_capacity(scripts.len());
124 let mut centroid = Vec3::ZERO;
125 for (script, ch) in scripts.iter().zip(&chars) {
126 let at = script.quantize(time);
127 let s = script.sample(at);
128 let pos = p.pie_en_mundo(s.gx, s.gz) - half;
129 centroid += pos;
130 poses.push((pos, s, ch, at));
131 }
132 let look = if poses.is_empty() {
133 orbit_center(dim) - half
134 } else {
135 centroid / poses.len() as f32 + Vec3::new(0.0, 1.0, 0.0)
136 };
137 let cast_d = 6.0 + poses.len() as f32 * 1.2;
138 let scene_dist = (dist * 0.18).clamp(10.0, 70.0);
139 let camera = if jugando {
140 // Primera persona: reposar al entrar, luego avanzar la física
141 // (caminar o volar) desde las teclas + el rumbo del drag.
142 if std::mem::take(&mut *respawn_jugador.lock().unwrap()) {
143 *jugador.lock().unwrap() = p.spawn_jugador();
144 }
145 let mut wish = Vec3::ZERO;
146 if input_juego.fwd {
147 wish += forward_h(yaw);
148 }
149 if input_juego.back {
150 wish -= forward_h(yaw);
151 }
152 if input_juego.right {
153 wish += right_h(yaw);
154 }
155 if input_juego.left {
156 wish -= right_h(yaw);
157 }
158 if volar {
159 if input_juego.arriba {
160 wish += Vec3::Y;
161 }
162 if input_juego.abajo {
163 wish -= Vec3::Y;
164 }
165 }
166 let eye = {
167 let mut j = jugador.lock().unwrap();
168 p.step_jugador(&mut j, wish, input_juego.salto, 1.0 / 30.0, volar)
169 };
170 Camera3d::fly(eye, yaw, pitch)
171 } else {
172 match (script_cam, &scene) {
173 (true, Some(sc)) => sc.camera_at(look, cast_d, time),
174 _ => Camera3d::orbit(look, yaw, pitch, scene_dist),
175 }
176 };
177 // Guardar la cámara para resolver clicks → suelo (plano a la altura de pies).
178 if directing && rect.h > 0.0 {
179 let inv_vp = camera.view_proj(rect.w / rect.h).inverse();
180 p.set_pick(inv_vp, camera.eye, look.y - 1.0);
181 }
182 let mut metas = Vec::with_capacity(poses.len());
183 let mut sers = Vec::new();
184 for (pos, s, ch, at) in &poses {
185 if ch.es_sdf() {
186 // Humanoide o bicho → SER SDF analítico (toon + cara por decals), posado
187 // en coords de GRILLA (el `pos` de la escena está centrado).
188 let g = *pos + half;
189 sers.push(ch.to_sdf(s.clip, *at).posado([g.x, g.y, g.z], s.facing));
190 } else {
191 // Rig (cuadrúpedo/ave/serpiente) → malla de su andar.
192 metas.push(ch.to_meta(*pos, s.facing, s.clip, *at, Some(camera.eye)));
193 }
194 }
195 // La utilería de la escena (estrella, flor…), en el mundo o en la mano
196 // de su actor: la misma que filma `ende`.
197 if let Some(sc) = &scene {
198 let en_mano: Vec<(Vec3, f32, Vec3)> =
199 poses.iter().map(|(pos, s, ch, at)| (*pos + half, s.facing, ch.mano(s.clip, *at))).collect();
200 sers.extend(sc.utileria_en(time, &en_mano));
201 p.set_luces(sc.luces_en(time, &en_mano));
202 } else {
203 p.set_luces(Vec::new());
204 }
205 // Recorrido: un marcador (cubo) por waypoint del actor dirigido; el
206 // seleccionado, más grande y dorado.
207 if let Some((keys, sel)) = &path {
208 for (i, (gx, gz)) in keys.iter().enumerate() {
209 let pos = p.ground_at_world(*gx as i32, *gz as i32) - half + Vec3::new(0.0, 1.4, 0.0);
210 let (color, sz) = if i == *sel { ([0.96, 0.84, 0.20], 0.8) } else { ([0.55, 0.58, 0.66], 0.5) };
211 let (mut mv, mut mi) = (Vec::new(), Vec::new());
212 push_cube(&mut mv, &mut mi, Mat4::from_translation(pos) * Mat4::from_scale(Vec3::splat(sz)), color);
213 metas.push((Mat4::IDENTITY, mv, mi));
214 }
215 }
216 // Atmósfera de apertura (bruma + fundido desde negro): cada cuadro,
217 // después de `ensure_window` (que resetea la niebla) y antes de pintar.
218 // Marcadores de los SUCESOS dictados (siempre visibles: se dirige
219 // viéndolos). Verde = aparece un ser, azul = cámara, amarillo = marca.
220 if let Some(sc) = &scene {
221 for s in &sc.sucesos {
222 let pos = Vec3::new(s.pos[0], s.pos[1], s.pos[2]) - half
223 + Vec3::new(0.0, 1.2, 0.0);
224 let color = match s.tipo {
225 TipoSuceso::Aparece(_) => [0.35, 0.85, 0.45],
226 TipoSuceso::Camara => [0.40, 0.65, 0.95],
227 TipoSuceso::Marca => [0.95, 0.82, 0.30],
228 // Naranja ceniza: acá el mundo se cae a pedazos.
229 TipoSuceso::Desmorona { .. } => [0.92, 0.45, 0.20],
230 };
231 let (mut mv, mut mi) = (Vec::new(), Vec::new());
232 push_cube(
233 &mut mv,
234 &mut mi,
235 Mat4::from_translation(pos) * Mat4::from_scale(Vec3::splat(0.6)),
236 color,
237 );
238 metas.push((Mat4::IDENTITY, mv, mi));
239 }
240 }
241 // El cielo de noche (o la vuelta al día): antes de la apertura, que gana.
242 p.set_cielo(scene.as_ref().and_then(|s| s.cielo.as_ref()));
243 if !jugando {
244 if let Some(atm) = scene.as_ref().and_then(|s| s.atmosfera) {
245 p.set_atmosfera_escena(&atm, time);
246 }
247 }
248 // Las flores de los canteros, como seres: lo mismo que filma `ende`.
249 let mira = (camera.target - camera.eye).normalize_or_zero();
250 llimphi_voxel::sumar_macizos(&mut sers, p.macizos(), (camera.eye + half).into(), mira.into());
251 p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers);
252 })
253 }
254 Level::Seres => {
255 let sere = sel_sere(model).cloned();
256 let time = model.time;
257 let manada = model.manada;
258 let _andar_estado = model.andar_estado;
259 // Previsualizar el estado de andar que se está editando.
260 let estado_clip = match model.andar_estado {
261 0 => Clip::Idle,
262 1 => Clip::Walk,
263 _ => Clip::Run,
264 };
265 View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| {
266 let dim = world_dim(PREVIEW_DIM_XZ);
267 let mut guard = preview.lock().unwrap();
268 let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, None));
269 p.rebuild_if(device, queue, &mr, dim, gen, None);
270 let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5;
271 match (&sere, manada) {
272 // Manada viva: una bandada del ser deambula/se junta por su conducta.
273 (Some(cs), true) => {
274 p.ensure_manada(&[(cs.clone(), 9)]);
275 let look = p.ground_at(dim[0] / 2, dim[2] / 2) - half + Vec3::new(0.0, 1.0, 0.0);
276 let camera = Camera3d::orbit(look, yaw, pitch, 32.0);
277 // La bandada mira a cámara (ojo en coords de GRILLA: los seres
278 // SDF viven en [0,dim], la cámara en centrado).
279 let (metas, sers) = p.manada_metas(1.0 / 30.0, None, Some(camera.eye + half));
280 p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers);
281 }
282 // Turntable de un solo ser.
283 other => {
284 p.clear_manada();
285 // Humanoide → **SER SDF analítico** (toon + cara por decals): el
286 // mismo cuerpo que rinde una escena, parado sobre el terreno del
287 // preview, girando en turntable. Reemplaza al volumen voxel
288 // redondeado (`to_voxel` + show_grid), que queda de LOD lejano.
289 let pos = p.ground_at(dim[0] / 2, dim[2] / 2); // grilla (ser SDF)
290 let pos_c = pos - half; // centrado (mallas + camara)
291 let look = pos_c + Vec3::new(0.0, 1.0, 0.0);
292 // Primer plano para el humanoide (~1.8 de alto): editar la cara
293 // exige verla. El rig conserva el rango amplio clásico.
294 let es_humanoide = matches!(other.0, Some(cs) if cs.cuerpo.is_none() && cs.bicho.is_none());
295 let cam_dist = if es_humanoide {
296 (dist * 0.06).clamp(2.4, 5.0)
297 } else {
298 (dist * 0.06).clamp(3.5, 14.0)
299 };
300 let camera = Camera3d::orbit(look, yaw, pitch, cam_dist);
301 let (metas, sers) = match other.0 {
302 Some(cs) if cs.es_sdf() => (
303 Vec::new(),
304 // Vivo y MIRANDO A CÁMARA: parpadea, la boca sigue al
305 // clip y la cabeza te busca mientras el turntable gira
306 // (editar una cara exige que la cara te mire).
307 vec![cs.to_sdf_posado(
308 estado_clip,
309 time,
310 pos,
311 time * 0.6,
312 Some(camera.eye + half),
313 )],
314 ),
315 // Rig (cuadrúpedo/ave/serpiente) → malla propia (clásico).
316 Some(cs) => (
317 vec![cs.to_meta(pos_c, time * 0.6, estado_clip, time, None)],
318 Vec::new(),
319 ),
320 None => (Vec::new(), Vec::new()),
321 };
322 p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers);
323 }
324 }
325 })
326 }
327 Level::Interiores => {
328 let interior = interior_sel;
329 View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| {
330 let dim = world_dim(PREVIEW_DIM_XZ);
331 let mut guard = preview.lock().unwrap();
332 let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, None));
333 let (ancho, fondo) = interior.as_ref().map(|i| (i.ancho, i.fondo)).unwrap_or((24, 20));
334 // Recorte casa de muñecas: se ocultan los muros/techo del lado de la
335 // cámara para ver adentro. Cambiar el rumbo regenera el grid.
336 let cut = Cutaway::from_yaw(yaw);
337 let key = gen.wrapping_mul(16).wrapping_add(cut.code());
338 if let Some(it) = &interior {
339 p.show_grid(device, queue, it.build_grid(dim, cut), dim, key);
340 }
341 let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5;
342 let floor_top = Interior::FLOOR_H as f32;
343 let look = Vec3::new(ancho as f32 * 0.5, floor_top + 2.0, fondo as f32 * 0.5) - half;
344 let cam_dist = (dist * 0.18).clamp(14.0, 90.0);
345 let camera = Camera3d::orbit(look, yaw, pitch, cam_dist);
346 if rect.h > 0.0 {
347 let inv_vp = camera.view_proj(rect.w / rect.h).inverse();
348 p.set_pick(inv_vp, camera.eye, floor_top - half.y);
349 }
350 // Iluminación de interior: ambiente atenuado (cálido) + las lámparas
351 // encendidas como luces puntuales, para que sus charcos de luz se lean.
352 let luces = interior.as_ref().map(|i| i.luces()).unwrap_or_default();
353 p.set_interior_lighting(
354 [0.55, 0.5, 0.42],
355 Atmosphere {
356 sky_zenith: [40, 46, 62],
357 sky_horizon: [82, 86, 100],
358 fog_density: 0.0,
359 god_rays: 0.0,
360 },
361 luces,
362 );
363 p.render(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera);
364 })
365 }
366 // Mundos / Biomas: sólo el terreno, en órbita. Con «simular», corre la ley
367 // Fluir: el agua cae/esparce/cae por cornisas, paso por cuadro.
368 _ => View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| {
369 let dim = world_dim(PREVIEW_DIM_XZ);
370 let ov = mapa_override.as_deref();
371 let mut guard = preview.lock().unwrap();
372 let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, ov));
373 p.rebuild_if(device, queue, &mr, dim, gen, ov);
374 p.set_detalle(0.0, 1.6, bisel_global);
375 p.set_silueta_por_voxel(silueta_pv);
376 if simulating {
377 if let Some((g, h)) = fluir {
378 p.ensure_sim(agua, g, h);
379 p.sim_step(queue, agua);
380 }
381 if let Some((col, vel)) = crecer {
382 p.ensure_growth(queue, col, vel);
383 p.growth_step(queue);
384 }
385 if let Some((color, campos, prog, params, vis)) = &ecuacion {
386 p.ensure_ecuacion(*color, ECUACION_TOL, campos, *vis);
387 p.ecuacion_step(queue, prog, params);
388 }
389 if !pobladores.is_empty() {
390 p.ensure_manada(&pobladores);
391 }
392 } else {
393 p.clear_sim();
394 p.clear_growth();
395 p.clear_ecuacion();
396 p.clear_manada();
397 }
398 let camera = Camera3d::orbit(orbit_center(dim), yaw, pitch, dist);
399 // Con bandada, el mundo se ve vivo: se componen los habitantes caminando.
400 if simulating && p.tiene_manada() {
401 let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5;
402 let (metas, sers) = p.manada_metas(1.0 / 30.0, None, Some(camera.eye + half));
403 p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers);
404 } else {
405 p.render(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera);
406 }
407 }),
408 }
409 .draggable(|phase, dx, dy| match phase {
410 DragPhase::Move => Some(Msg::Orbit(dx, dy)),
411 DragPhase::End => None,
412 });
413
414 // Al dirigir, un click (sin arrastrar) coloca la clave seleccionada sobre el suelo.
415 if directing {
416 canvas = canvas.on_click_at(move |lx, ly, rw, rh| {
417 if rw <= 0.0 || rh <= 0.0 {
418 return None;
419 }
420 let ndc_x = lx / rw * 2.0 - 1.0;
421 let ndc_y = 1.0 - ly / rh * 2.0;
422 let guard = preview_click.lock().ok()?;
423 let (gx, gz) = guard.as_ref()?.pick_world(ndc_x, ndc_y)?;
424 Some(Msg::PlaceKeyAt(gx, gz))
425 });
426 }
427 // En el editor de interiores, un click coloca el mueble-pincel sobre el piso.
428 if is_interiores {
429 canvas = canvas.on_click_at(move |lx, ly, rw, rh| {
430 if rw <= 0.0 || rh <= 0.0 {
431 return None;
432 }
433 let ndc_x = lx / rw * 2.0 - 1.0;
434 let ndc_y = 1.0 - ly / rh * 2.0;
435 let guard = preview_click_int.lock().ok()?;
436 let (gx, gz) = guard.as_ref()?.pick_world(ndc_x, ndc_y)?;
437 Some(Msg::PlaceMuebleAt(gx, gz))
438 });
439 }
440 canvas
441}
442
443/// Centro de órbita: el medio del mundo, algo por debajo del tope.
444pub(crate) fn orbit_center(dim: [u32; 3]) -> Vec3 {
445 Vec3::new(dim[0] as f32 * 0.5, dim[1] as f32 * 0.32, dim[2] as f32 * 0.5)
446}
447
448// =============================================================================
449// Editores por nivel (rail derecho)
450// =============================================================================
451
452/// Editor de interiores: parámetros de la sala (tab 0) y el mueble-pincel (tab 1).
453/// Las piezas se agregan haciendo click sobre el piso en el preview 3D.
454pub(crate) fn interior_editor(model: &Model, tab: usize) -> Vec<View<Msg>> {
455 let theme = &model.theme;
456 let sp = SliderPalette::from_theme(theme);
457 let btn = ButtonPalette::from_theme(theme);
458 let Some(it) = model.project.interiores.iter().find(|x| x.id == model.sel[6]) else {
459 return vec![section_title("sin interior — crea uno", theme)];
460 };
461 if tab == 0 {
462 let ancho = it.ancho as f32;
463 let fondo = it.fondo as f32;
464 vec![
465 section_title("SALA", theme),
466 slider_view("ancho", ancho, 6.0, 60.0, &sp, move |_p, dv| Some(Msg::SetInteriorAncho(ancho + dv))),
467 slider_view("fondo", fondo, 6.0, 60.0, &sp, move |_p, dv| Some(Msg::SetInteriorFondo(fondo + dv))),
468 spacer(4.0),
469 button_view(format!("piso: {}", it.piso.label()), &btn, Msg::CycleInteriorPiso),
470 spacer(3.0),
471 button_view(format!("muro: {}", it.muro.label()), &btn, Msg::CycleInteriorMuro),
472 spacer(3.0),
473 button_view(if it.muros { "muros: sí" } else { "muros: no" }, &btn, Msg::ToggleInteriorMuros),
474 button_view(if it.ventanas { "ventanas: sí" } else { "ventanas: no" }, &btn, Msg::ToggleInteriorVentanas),
475 spacer(3.0),
476 button_view(if it.con_techo { "techo: sí" } else { "techo: no" }, &btn, Msg::ToggleInteriorTecho),
477 spacer(3.0),
478 button_view(format!("tipo techo: {}", it.techo.label()), &btn, Msg::CycleInteriorTecho),
479 spacer(3.0),
480 spacer(8.0),
481 section_title("MUEBLES", theme),
482 body_text(format!("{} colocados", it.piezas.len()), theme.fg_muted, theme),
483 spacer(4.0),
484 button_view("- quitar último", &btn, Msg::RemoveLastPieza),
485 spacer(3.0),
486 button_view("vaciar", &btn, Msg::ClearPiezas),
487 ]
488 } else {
489 let rumbo = ["norte", "este", "sur", "oeste"][(model.mueble_facing & 3) as usize];
490 vec![
491 section_title("MUEBLE (PINCEL)", theme),
492 button_view(format!("mueble: {}", model.mueble_brush.label()), &btn, Msg::CycleMuebleBrush),
493 spacer(3.0),
494 button_view(format!("orientación: {rumbo}"), &btn, Msg::CycleMuebleFacing),
495 spacer(8.0),
496 body_text("click en el piso para colocar el mueble".into(), theme.fg_placeholder, theme),
497 ]
498 }
499}
500
501pub(crate) fn editor(model: &Model, tab: usize) -> Vec<View<Msg>> {
502 match model.level {
503 Level::Leyes => ley_editor(model),
504 Level::Materiales => material_editor(model, tab),
505 Level::Seres => sere_editor(model, tab),
506 Level::Biomas => bioma_editor(model, tab),
507 Level::Mundos => mundo_editor(model, tab),
508 Level::Escenas => scene_editor(model, tab),
509 Level::Interiores => interior_editor(model, tab),
510 }
511}
512