use super::*; /// Canvas 3D: terreno del artefacto + (en escenas) los actores posados. pub(crate) fn canvas_3d(model: &Model) -> View { let (yaw, pitch, dist, gen) = (model.yaw, model.pitch, model.dist, model.gen); let preview = model.preview.clone(); // Dirigiendo (Escenas + pestaña Reparto): el click coloca la clave seleccionada. let directing = model.level == Level::Escenas && model.tool_tab == 1 && sel_scene(model).is_some(); let preview_click = model.preview.clone(); let preview_click_int = model.preview.clone(); let interior_sel = model.project.interiores.iter().find(|x| x.id == model.sel[6]).cloned(); let is_interiores = model.level == Level::Interiores; let mr = model.preview_render(); // Detalle sub-voxel del preview 3D (Biomas/Mundos): la rugosidad y el bisel finos // ya viajan **horneados POR VOXEL** en el grid (cada material del bioma con el suyo, // ver `ResolvedMaterial::detalle`/`generate_window`) → dos materiales lucen distinto // a la vez, lo que el look global no podía. Aquí sólo derivamos el **bisel GLOBAL** = // el mayor bisel del bioma, que enciende el trazado de isosuperficie (requisito para // que el bisel por voxel module la normal). La rugosidad global va en 0 (el por-voxel // manda; los voxels sin detalle —agua, minerales— quedan lisos, que es lo correcto). // El nivel Materiales usa su propio swatch 3D (single-material, look global). let bisel_global = { let p = &mr.palette; let pk = p.peak.map(|m| m.detalle.1).unwrap_or(0.0); p.ground.detalle.1.max(p.cliff.detalle.1).max(pk) }; // Silueta sub-voxel POR MATERIAL: prende el contorno diferenciado (uno duro/blocky, // otro liso/redondeado en el mismo frame) sólo si el bioma tiene bisel CON CONTRASTE // entre terreno/acantilado/cumbre (si son todos iguales, no aporta y cuesta samplear // el detalle vecino por evaluación del campo). El fauna/flora aporta su propio bisel // por voxel, pero el contraste del terreno es el que domina la silueta visible. let silueta_pv = { let p = &mr.palette; let g = p.ground.detalle.1; let c = p.cliff.detalle.1; let pk = p.peak.map(|m| m.detalle.1).unwrap_or(g); bisel_global > 0.01 && ((g - c).abs() > 0.01 || (g - pk).abs() > 0.01 || (c - pk).abs() > 0.01) }; // Mundo desde mapa real: sólo pisa el preview en el nivel Mundos (Escenas usa // ventana móvil; Biomas/Seres/Interiores tienen su propio grid). `Arc` clonado. let mapa_override = if model.level == Level::Mundos { model.mapa.clone() } else { None }; let simulating = model.simulating; let agua = mr.palette.agua; // El agua fluye SÓLO si su material tiene la ley Fluir (con sus params). let fluir = model.project.water_fluir(); // Y las plantas (objetos) crecen si su material tiene la ley Crecer. let crecer = mr.bioma.objetos.iter().find_map(|o| { model .project .crecer_velocidad(o.material) .map(|v| (model.project.resolve_material(o.material).color, v)) }); // Ley autorada (Ecuacion): el primer material de la escena que la tenga corre sobre // sus celdas (recoloreándolas por el campo). `(color, tol, campos, programa, params, vis)`. let ecuacion = { let b = &mr.bioma; let mut ids = vec![b.ground, b.cliff]; if let Some(p) = b.peak { ids.push(p); } ids.extend(b.objetos.iter().map(|o| o.material)); ids.into_iter().find_map(|id| { model.project.ecuacion_de_material(id).map(|(campos, prog, params)| { let color = model.project.resolve_material(id).color; let vis = campos.len().saturating_sub(1); (color, campos, prog, params, vis) }) }) }; // Pobladores del bioma: cada SereUso → unos cuantos habitantes (por probabilidad). let pobladores: Vec<(CharSpec, usize)> = mr .bioma .seres .iter() .filter_map(|su| { model .project .sere(su.sere) .map(|cs| (cs.clone(), ((su.probabilidad.clamp(0.0, 1.0) * 6.0).round() as usize).max(1))) }) .collect(); let absolute = Style { position: Position::Absolute, size: Size { width: percent(1.0_f32), height: percent(1.0_f32) }, ..Default::default() }; let mut canvas = match model.level { Level::Escenas => { let scene = sel_scene(model).cloned(); let script_cam = model.script_cam; // Recorrido a dibujar en 3D mientras se dirige (pestaña Reparto): los // waypoints del actor seleccionado + cuál está seleccionado. let path: Option<(Vec<(f32, f32)>, usize)> = if model.tool_tab == 1 { scene.as_ref().and_then(|s| s.actors.get(model.actor_sel)).map(|act| { (act.keys.iter().map(|k| (k.gx, k.gz)).collect(), model.key_sel) }) } else { None }; let script_cam = if path.is_some() { false } else { script_cam }; // órbita para dirigir let scripts: Vec = scene.as_ref().map(|s| s.scripts()).unwrap_or_default(); let puesta = scene.as_ref().map(|s| s.puesta()).unwrap_or_default(); let chars: Vec = scene .as_ref() .map(|s| s.actors.iter().map(|a| model.project.character_or_default(a.character)).collect()) .unwrap_or_default(); let time = model.time; let jugando = model.jugando; let jugador = model.jugador.clone(); let input_juego = model.input_juego; let volar = model.volar; let respawn_jugador = model.respawn_jugador.clone(); View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| { let dim = world_dim(PREVIEW_DIM_XZ); let mut guard = preview.lock().unwrap(); let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, None)); // Al dirigir, ventana fija en el origen → la columna de grilla coincide // con la de mundo (el picking del click queda directo). let origin = if directing { [0, 0] } else { window_origin_for_cast(&scripts, time, dim) }; p.ensure_window_con(device, queue, &mr, gen, origin, &puesta); let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5; let mut poses = Vec::with_capacity(scripts.len()); let mut centroid = Vec3::ZERO; for (script, ch) in scripts.iter().zip(&chars) { let at = script.quantize(time); let s = script.sample(at); let pos = p.pie_en_mundo(s.gx, s.gz) - half; centroid += pos; poses.push((pos, s, ch, at)); } let look = if poses.is_empty() { orbit_center(dim) - half } else { centroid / poses.len() as f32 + Vec3::new(0.0, 1.0, 0.0) }; let cast_d = 6.0 + poses.len() as f32 * 1.2; let scene_dist = (dist * 0.18).clamp(10.0, 70.0); let camera = if jugando { // Primera persona: reposar al entrar, luego avanzar la física // (caminar o volar) desde las teclas + el rumbo del drag. if std::mem::take(&mut *respawn_jugador.lock().unwrap()) { *jugador.lock().unwrap() = p.spawn_jugador(); } let mut wish = Vec3::ZERO; if input_juego.fwd { wish += forward_h(yaw); } if input_juego.back { wish -= forward_h(yaw); } if input_juego.right { wish += right_h(yaw); } if input_juego.left { wish -= right_h(yaw); } if volar { if input_juego.arriba { wish += Vec3::Y; } if input_juego.abajo { wish -= Vec3::Y; } } let eye = { let mut j = jugador.lock().unwrap(); p.step_jugador(&mut j, wish, input_juego.salto, 1.0 / 30.0, volar) }; Camera3d::fly(eye, yaw, pitch) } else { match (script_cam, &scene) { (true, Some(sc)) => sc.camera_at(look, cast_d, time), _ => Camera3d::orbit(look, yaw, pitch, scene_dist), } }; // Guardar la cámara para resolver clicks → suelo (plano a la altura de pies). if directing && rect.h > 0.0 { let inv_vp = camera.view_proj(rect.w / rect.h).inverse(); p.set_pick(inv_vp, camera.eye, look.y - 1.0); } let mut metas = Vec::with_capacity(poses.len()); let mut sers = Vec::new(); for (pos, s, ch, at) in &poses { if ch.es_sdf() { // Humanoide o bicho → SER SDF analítico (toon + cara por decals), posado // en coords de GRILLA (el `pos` de la escena está centrado). let g = *pos + half; sers.push(ch.to_sdf(s.clip, *at).posado([g.x, g.y, g.z], s.facing)); } else { // Rig (cuadrúpedo/ave/serpiente) → malla de su andar. metas.push(ch.to_meta(*pos, s.facing, s.clip, *at, Some(camera.eye))); } } // La utilería de la escena (estrella, flor…), en el mundo o en la mano // de su actor: la misma que filma `ende`. if let Some(sc) = &scene { let en_mano: Vec<(Vec3, f32, Vec3)> = poses.iter().map(|(pos, s, ch, at)| (*pos + half, s.facing, ch.mano(s.clip, *at))).collect(); sers.extend(sc.utileria_en(time, &en_mano)); p.set_luces(sc.luces_en(time, &en_mano)); } else { p.set_luces(Vec::new()); } // Recorrido: un marcador (cubo) por waypoint del actor dirigido; el // seleccionado, más grande y dorado. if let Some((keys, sel)) = &path { for (i, (gx, gz)) in keys.iter().enumerate() { let pos = p.ground_at_world(*gx as i32, *gz as i32) - half + Vec3::new(0.0, 1.4, 0.0); let (color, sz) = if i == *sel { ([0.96, 0.84, 0.20], 0.8) } else { ([0.55, 0.58, 0.66], 0.5) }; let (mut mv, mut mi) = (Vec::new(), Vec::new()); push_cube(&mut mv, &mut mi, Mat4::from_translation(pos) * Mat4::from_scale(Vec3::splat(sz)), color); metas.push((Mat4::IDENTITY, mv, mi)); } } // Atmósfera de apertura (bruma + fundido desde negro): cada cuadro, // después de `ensure_window` (que resetea la niebla) y antes de pintar. // Marcadores de los SUCESOS dictados (siempre visibles: se dirige // viéndolos). Verde = aparece un ser, azul = cámara, amarillo = marca. if let Some(sc) = &scene { for s in &sc.sucesos { let pos = Vec3::new(s.pos[0], s.pos[1], s.pos[2]) - half + Vec3::new(0.0, 1.2, 0.0); let color = match s.tipo { TipoSuceso::Aparece(_) => [0.35, 0.85, 0.45], TipoSuceso::Camara => [0.40, 0.65, 0.95], TipoSuceso::Marca => [0.95, 0.82, 0.30], // Naranja ceniza: acá el mundo se cae a pedazos. TipoSuceso::Desmorona { .. } => [0.92, 0.45, 0.20], }; let (mut mv, mut mi) = (Vec::new(), Vec::new()); push_cube( &mut mv, &mut mi, Mat4::from_translation(pos) * Mat4::from_scale(Vec3::splat(0.6)), color, ); metas.push((Mat4::IDENTITY, mv, mi)); } } // El cielo de noche (o la vuelta al día): antes de la apertura, que gana. p.set_cielo(scene.as_ref().and_then(|s| s.cielo.as_ref())); if !jugando { if let Some(atm) = scene.as_ref().and_then(|s| s.atmosfera) { p.set_atmosfera_escena(&atm, time); } } // Las flores de los canteros, como seres: lo mismo que filma `ende`. let mira = (camera.target - camera.eye).normalize_or_zero(); llimphi_voxel::sumar_macizos(&mut sers, p.macizos(), (camera.eye + half).into(), mira.into()); p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers); }) } Level::Seres => { let sere = sel_sere(model).cloned(); let time = model.time; let manada = model.manada; let _andar_estado = model.andar_estado; // Previsualizar el estado de andar que se está editando. let estado_clip = match model.andar_estado { 0 => Clip::Idle, 1 => Clip::Walk, _ => Clip::Run, }; View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| { let dim = world_dim(PREVIEW_DIM_XZ); let mut guard = preview.lock().unwrap(); let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, None)); p.rebuild_if(device, queue, &mr, dim, gen, None); let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5; match (&sere, manada) { // Manada viva: una bandada del ser deambula/se junta por su conducta. (Some(cs), true) => { p.ensure_manada(&[(cs.clone(), 9)]); let look = p.ground_at(dim[0] / 2, dim[2] / 2) - half + Vec3::new(0.0, 1.0, 0.0); let camera = Camera3d::orbit(look, yaw, pitch, 32.0); // La bandada mira a cámara (ojo en coords de GRILLA: los seres // SDF viven en [0,dim], la cámara en centrado). let (metas, sers) = p.manada_metas(1.0 / 30.0, None, Some(camera.eye + half)); p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers); } // Turntable de un solo ser. other => { p.clear_manada(); // Humanoide → **SER SDF analítico** (toon + cara por decals): el // mismo cuerpo que rinde una escena, parado sobre el terreno del // preview, girando en turntable. Reemplaza al volumen voxel // redondeado (`to_voxel` + show_grid), que queda de LOD lejano. let pos = p.ground_at(dim[0] / 2, dim[2] / 2); // grilla (ser SDF) let pos_c = pos - half; // centrado (mallas + camara) let look = pos_c + Vec3::new(0.0, 1.0, 0.0); // Primer plano para el humanoide (~1.8 de alto): editar la cara // exige verla. El rig conserva el rango amplio clásico. let es_humanoide = matches!(other.0, Some(cs) if cs.cuerpo.is_none() && cs.bicho.is_none()); let cam_dist = if es_humanoide { (dist * 0.06).clamp(2.4, 5.0) } else { (dist * 0.06).clamp(3.5, 14.0) }; let camera = Camera3d::orbit(look, yaw, pitch, cam_dist); let (metas, sers) = match other.0 { Some(cs) if cs.es_sdf() => ( Vec::new(), // Vivo y MIRANDO A CÁMARA: parpadea, la boca sigue al // clip y la cabeza te busca mientras el turntable gira // (editar una cara exige que la cara te mire). vec![cs.to_sdf_posado( estado_clip, time, pos, time * 0.6, Some(camera.eye + half), )], ), // Rig (cuadrúpedo/ave/serpiente) → malla propia (clásico). Some(cs) => ( vec![cs.to_meta(pos_c, time * 0.6, estado_clip, time, None)], Vec::new(), ), None => (Vec::new(), Vec::new()), }; p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers); } } }) } Level::Interiores => { let interior = interior_sel; View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| { let dim = world_dim(PREVIEW_DIM_XZ); let mut guard = preview.lock().unwrap(); let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, None)); let (ancho, fondo) = interior.as_ref().map(|i| (i.ancho, i.fondo)).unwrap_or((24, 20)); // Recorte casa de muñecas: se ocultan los muros/techo del lado de la // cámara para ver adentro. Cambiar el rumbo regenera el grid. let cut = Cutaway::from_yaw(yaw); let key = gen.wrapping_mul(16).wrapping_add(cut.code()); if let Some(it) = &interior { p.show_grid(device, queue, it.build_grid(dim, cut), dim, key); } let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5; let floor_top = Interior::FLOOR_H as f32; let look = Vec3::new(ancho as f32 * 0.5, floor_top + 2.0, fondo as f32 * 0.5) - half; let cam_dist = (dist * 0.18).clamp(14.0, 90.0); let camera = Camera3d::orbit(look, yaw, pitch, cam_dist); if rect.h > 0.0 { let inv_vp = camera.view_proj(rect.w / rect.h).inverse(); p.set_pick(inv_vp, camera.eye, floor_top - half.y); } // Iluminación de interior: ambiente atenuado (cálido) + las lámparas // encendidas como luces puntuales, para que sus charcos de luz se lean. let luces = interior.as_ref().map(|i| i.luces()).unwrap_or_default(); p.set_interior_lighting( [0.55, 0.5, 0.42], Atmosphere { sky_zenith: [40, 46, 62], sky_horizon: [82, 86, 100], fog_density: 0.0, god_rays: 0.0, }, luces, ); p.render(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera); }) } // Mundos / Biomas: sólo el terreno, en órbita. Con «simular», corre la ley // Fluir: el agua cae/esparce/cae por cornisas, paso por cuadro. _ => View::new(absolute).gpu_paint_with(move |device, queue, encoder, target, rect, vp| { let dim = world_dim(PREVIEW_DIM_XZ); let ov = mapa_override.as_deref(); let mut guard = preview.lock().unwrap(); let p = guard.get_or_insert_with(|| WorldPreview::build(device, queue, &mr, dim, gen, ov)); p.rebuild_if(device, queue, &mr, dim, gen, ov); p.set_detalle(0.0, 1.6, bisel_global); p.set_silueta_por_voxel(silueta_pv); if simulating { if let Some((g, h)) = fluir { p.ensure_sim(agua, g, h); p.sim_step(queue, agua); } if let Some((col, vel)) = crecer { p.ensure_growth(queue, col, vel); p.growth_step(queue); } if let Some((color, campos, prog, params, vis)) = &ecuacion { p.ensure_ecuacion(*color, ECUACION_TOL, campos, *vis); p.ecuacion_step(queue, prog, params); } if !pobladores.is_empty() { p.ensure_manada(&pobladores); } } else { p.clear_sim(); p.clear_growth(); p.clear_ecuacion(); p.clear_manada(); } let camera = Camera3d::orbit(orbit_center(dim), yaw, pitch, dist); // Con bandada, el mundo se ve vivo: se componen los habitantes caminando. if simulating && p.tiene_manada() { let half = Vec3::new(dim[0] as f32, dim[1] as f32, dim[2] as f32) * 0.5; let (metas, sers) = p.manada_metas(1.0 / 30.0, None, Some(camera.eye + half)); p.render_scene(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera, &metas, sers); } else { p.render(device, queue, encoder, target, vp, (rect.x, rect.y, rect.w, rect.h), &camera); } }), } .draggable(|phase, dx, dy| match phase { DragPhase::Move => Some(Msg::Orbit(dx, dy)), DragPhase::End => None, }); // Al dirigir, un click (sin arrastrar) coloca la clave seleccionada sobre el suelo. if directing { canvas = canvas.on_click_at(move |lx, ly, rw, rh| { if rw <= 0.0 || rh <= 0.0 { return None; } let ndc_x = lx / rw * 2.0 - 1.0; let ndc_y = 1.0 - ly / rh * 2.0; let guard = preview_click.lock().ok()?; let (gx, gz) = guard.as_ref()?.pick_world(ndc_x, ndc_y)?; Some(Msg::PlaceKeyAt(gx, gz)) }); } // En el editor de interiores, un click coloca el mueble-pincel sobre el piso. if is_interiores { canvas = canvas.on_click_at(move |lx, ly, rw, rh| { if rw <= 0.0 || rh <= 0.0 { return None; } let ndc_x = lx / rw * 2.0 - 1.0; let ndc_y = 1.0 - ly / rh * 2.0; let guard = preview_click_int.lock().ok()?; let (gx, gz) = guard.as_ref()?.pick_world(ndc_x, ndc_y)?; Some(Msg::PlaceMuebleAt(gx, gz)) }); } canvas } /// Centro de órbita: el medio del mundo, algo por debajo del tope. pub(crate) fn orbit_center(dim: [u32; 3]) -> Vec3 { Vec3::new(dim[0] as f32 * 0.5, dim[1] as f32 * 0.32, dim[2] as f32 * 0.5) } // ============================================================================= // Editores por nivel (rail derecho) // ============================================================================= /// Editor de interiores: parámetros de la sala (tab 0) y el mueble-pincel (tab 1). /// Las piezas se agregan haciendo click sobre el piso en el preview 3D. pub(crate) fn interior_editor(model: &Model, tab: usize) -> Vec> { let theme = &model.theme; let sp = SliderPalette::from_theme(theme); let btn = ButtonPalette::from_theme(theme); let Some(it) = model.project.interiores.iter().find(|x| x.id == model.sel[6]) else { return vec![section_title("sin interior — crea uno", theme)]; }; if tab == 0 { let ancho = it.ancho as f32; let fondo = it.fondo as f32; vec![ section_title("SALA", theme), slider_view("ancho", ancho, 6.0, 60.0, &sp, move |_p, dv| Some(Msg::SetInteriorAncho(ancho + dv))), slider_view("fondo", fondo, 6.0, 60.0, &sp, move |_p, dv| Some(Msg::SetInteriorFondo(fondo + dv))), spacer(4.0), button_view(format!("piso: {}", it.piso.label()), &btn, Msg::CycleInteriorPiso), spacer(3.0), button_view(format!("muro: {}", it.muro.label()), &btn, Msg::CycleInteriorMuro), spacer(3.0), button_view(if it.muros { "muros: sí" } else { "muros: no" }, &btn, Msg::ToggleInteriorMuros), button_view(if it.ventanas { "ventanas: sí" } else { "ventanas: no" }, &btn, Msg::ToggleInteriorVentanas), spacer(3.0), button_view(if it.con_techo { "techo: sí" } else { "techo: no" }, &btn, Msg::ToggleInteriorTecho), spacer(3.0), button_view(format!("tipo techo: {}", it.techo.label()), &btn, Msg::CycleInteriorTecho), spacer(3.0), spacer(8.0), section_title("MUEBLES", theme), body_text(format!("{} colocados", it.piezas.len()), theme.fg_muted, theme), spacer(4.0), button_view("- quitar último", &btn, Msg::RemoveLastPieza), spacer(3.0), button_view("vaciar", &btn, Msg::ClearPiezas), ] } else { let rumbo = ["norte", "este", "sur", "oeste"][(model.mueble_facing & 3) as usize]; vec![ section_title("MUEBLE (PINCEL)", theme), button_view(format!("mueble: {}", model.mueble_brush.label()), &btn, Msg::CycleMuebleBrush), spacer(3.0), button_view(format!("orientación: {rumbo}"), &btn, Msg::CycleMuebleFacing), spacer(8.0), body_text("click en el piso para colocar el mueble".into(), theme.fg_placeholder, theme), ] } } pub(crate) fn editor(model: &Model, tab: usize) -> Vec> { match model.level { Level::Leyes => ley_editor(model), Level::Materiales => material_editor(model, tab), Level::Seres => sere_editor(model, tab), Level::Biomas => bioma_editor(model, tab), Level::Mundos => mundo_editor(model, tab), Level::Escenas => scene_editor(model, tab), Level::Interiores => interior_editor(model, tab), } }