feat(cosmobiologia): esfera 3D — switch de constelaciones + luz de la Vía Láctea

- Switch de constelaciones: botón flotante «● Constelaciones» (o tecla
  B) que las enciende y apaga en la esfera 3D.
- La luminosidad se reparte: el brillo especular fijo a la pantalla se
  bajó mucho (no giraba, se sentía despegado), y en su lugar la Vía
  Láctea aporta un resplandor difuso a lo largo del plano galáctico —
  que SÍ gira con la esfera. Más intenso hacia el centro galáctico
  (Sagitario, como en el cielo real) y atenuado bajo el horizonte
  local: la franja como se ve desde la Tierra esa noche.

42 tests verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 19:40:11 +00:00
parent cfb37af0cf
commit 6e30dc2d72
2 changed files with 98 additions and 5 deletions
@@ -171,6 +171,8 @@ pub struct CanvasState {
pub sphere_3d: bool, pub sphere_3d: bool,
/// Orientación de la esfera 3D — la muta el drag. /// Orientación de la esfera 3D — la muta el drag.
pub sphere_view: SphereView, pub sphere_view: SphereView,
/// Si se dibujan las figuras de constelaciones en la esfera 3D.
pub show_constellations: bool,
drag_jog: Option<JogDragState>, drag_jog: Option<JogDragState>,
drag_pan: Option<PanDragState>, drag_pan: Option<PanDragState>,
drag_sphere: Option<SphereDragState>, drag_sphere: Option<SphereDragState>,
@@ -258,6 +260,7 @@ impl Default for CanvasState {
rectificacion: None, rectificacion: None,
sphere_3d: false, sphere_3d: false,
sphere_view: SphereView::default(), sphere_view: SphereView::default(),
show_constellations: true,
drag_jog: None, drag_jog: None,
drag_pan: None, drag_pan: None,
drag_sphere: None, drag_sphere: None,
@@ -383,6 +386,12 @@ impl AstrologyCanvas {
cx.notify(); cx.notify();
} }
/// Enciende o apaga las figuras de constelaciones en la esfera 3D.
pub fn toggle_constellations(&mut self, cx: &mut Context<'_, Self>) {
self.state.show_constellations = !self.state.show_constellations;
cx.notify();
}
/// Resetea zoom y pan a sus defaults (1.0 y 0,0). No toca rotation /// Resetea zoom y pan a sus defaults (1.0 y 0,0). No toca rotation
/// ni time offset — esos son ortogonales y tienen su propio reset. /// ni time offset — esos son ortogonales y tienen su propio reset.
pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) { pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) {
@@ -821,6 +830,10 @@ impl AstrologyCanvas {
self.toggle_sphere(cx); self.toggle_sphere(cx);
return; return;
} }
"b" | "B" => {
self.toggle_constellations(cx);
return;
}
_ => return, _ => return,
}; };
self.toggle_layer(kind, cx); self.toggle_layer(kind, cx);
@@ -914,6 +927,7 @@ impl Render for AstrologyCanvas {
&theme, &theme,
render, render,
self.state.sphere_view, self.state.sphere_view,
self.state.show_constellations,
self.state.view_scale, self.state.view_scale,
self.state.view_pan_x, self.state.view_pan_x,
self.state.view_pan_y, self.state.view_pan_y,
@@ -965,6 +979,36 @@ impl Render for AstrologyCanvas {
) )
}); });
// Switch de constelaciones — solo en modo esfera 3D.
let constellations_toggle = (matches!(self.state.mode, CanvasMode::Wheel { .. })
&& self.state.sphere_3d)
.then(|| {
let on = self.state.show_constellations;
let label = if on {
"● Constelaciones"
} else {
"○ Constelaciones"
};
div()
.absolute()
.top(px(44.0))
.right(px(12.0))
.px(px(11.0))
.py(px(5.0))
.rounded(px(6.0))
.bg(theme.bg_panel_alt.clone())
.border_1()
.border_color(theme.border)
.text_size(px(11.0))
.text_color(if on { theme.fg_text } else { theme.fg_muted })
.cursor_pointer()
.child(label)
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, _w, cx| this.toggle_constellations(cx)),
)
});
// Depth field: capa absoluta detrás del body, ocupa todo el // Depth field: capa absoluta detrás del body, ocupa todo el
// canvas. Vignette radial — el centro queda claro y los // canvas. Vignette radial — el centro queda claro y los
// bordes se oscurecen, dando profundidad sin "ruido" de // bordes se oscurecen, dando profundidad sin "ruido" de
@@ -1005,6 +1049,7 @@ impl Render for AstrologyCanvas {
.child(body), .child(body),
) )
.children(sphere_toggle) .children(sphere_toggle)
.children(constellations_toggle)
} }
} }
@@ -1080,6 +1125,7 @@ fn render_sphere(
theme: &Theme, theme: &Theme,
render: &RenderModel, render: &RenderModel,
view: SphereView, view: SphereView,
show_constellations: bool,
view_scale: f32, view_scale: f32,
view_pan_x: f32, view_pan_x: f32,
view_pan_y: f32, view_pan_y: f32,
@@ -1093,6 +1139,7 @@ fn render_sphere(
} else { } else {
Palette::light() Palette::light()
}, },
show_constellations,
..Default::default() ..Default::default()
}; };
let commands = compose_sphere(render, &view, &opts); let commands = compose_sphere(render, &view, &opts);
@@ -313,15 +313,16 @@ fn add_sphere_shading(
cy: center, cy: center,
r: rad * (0.95 - 0.95 * t), r: rad * (0.95 - 0.95 * t),
stroke: None, stroke: None,
fill: Some(glow.with_alpha(0.04)), fill: Some(glow.with_alpha(0.028)),
stroke_w: 0.0, stroke_w: 0.0,
}, },
)); ));
} }
// Brillo especular desplazado hacia la luz. // Brillo especular desplazado hacia la luz — tenue: la luminosidad
// viva la reparte la Vía Láctea, que sí gira con la esfera.
let hx = center - rad * 0.34; let hx = center - rad * 0.34;
let hy = center - rad * 0.34; let hy = center - rad * 0.34;
const HALO: usize = 7; const HALO: usize = 6;
for i in 0..HALO { for i in 0..HALO {
let t = i as f32 / (HALO - 1) as f32; let t = i as f32 / (HALO - 1) as f32;
items.push(( items.push((
@@ -331,7 +332,7 @@ fn add_sphere_shading(
cy: hy, cy: hy,
r: rad * 0.5 * (1.0 - t), r: rad * 0.5 * (1.0 - t),
stroke: None, stroke: None,
fill: Some(highlight.with_alpha(0.05)), fill: Some(highlight.with_alpha(0.018)),
stroke_w: 0.0, stroke_w: 0.0,
}, },
)); ));
@@ -562,6 +563,10 @@ fn add_point_marker(
/// estándar IAU que fija el plano de la Vía Láctea. /// estándar IAU que fija el plano de la Vía Láctea.
const GAL_POLE_RA: f32 = 192.859; const GAL_POLE_RA: f32 = 192.859;
const GAL_POLE_DEC: f32 = 27.128; const GAL_POLE_DEC: f32 = 27.128;
/// Centro galáctico (Sgr A*, J2000): AR 266.405°, Dec 28.936°. Hacia
/// ahí la Vía Láctea es más brillante.
const GAL_CENTER_RA: f32 = 266.405;
const GAL_CENTER_DEC: f32 = -28.936;
/// Hash entero → f32 en [0,1). Determinista (variante de splitmix32): /// Hash entero → f32 en [0,1). Determinista (variante de splitmix32):
/// la misma entrada da siempre el mismo valor, así el campo de /// la misma entrada da siempre el mismo valor, así el campo de
@@ -659,6 +664,46 @@ fn add_starfield(items: &mut Vec<(f32, DrawCommand)>, proj: &Projector, size: f3
} }
} }
/// El resplandor difuso de la Vía Láctea — una luminosidad repartida a
/// lo largo del plano galáctico, no un brillo fijo a la pantalla. Gira
/// con la esfera. Es más intensa hacia el centro galáctico (en
/// Sagitario, como en el cielo real) y, si hay horizonte, se atenúa en
/// la parte que queda bajo tierra esa noche — la franja como se ve
/// desde la Tierra ese día.
fn add_milky_way_glow(
items: &mut Vec<(f32, DrawCommand)>,
proj: &Projector,
eps: f32,
size: f32,
zenith: Option<Vec3>,
) {
let gpole = rot_x(equatorial_dir(GAL_POLE_RA, GAL_POLE_DEC), eps);
let gcenter = rot_x(equatorial_dir(GAL_CENTER_RA, GAL_CENTER_DEC), eps);
let band = Rgba::opaque(0.78, 0.82, 0.96);
for p3 in great_circle_perp(gpole, 54) {
// Más brillo hacia el centro galáctico.
let toward = (p3.dot(gcenter) * 0.5 + 0.5).clamp(0.0, 1.0);
let bright = 0.28 + 0.72 * toward * toward;
// Atenuada bajo el horizonte local (no se ve esa noche).
let vis = match zenith {
Some(z) if p3.dot(z) < 0.0 => 0.40,
_ => 1.0,
};
let p = proj.project(p3);
items.push((
p.depth - 4.0,
DrawCommand::Circle {
cx: p.x,
cy: p.y,
r: size * 0.045,
stroke: None,
fill: Some(band.with_alpha(0.030 * bright * vis * depth_alpha(p.depth))),
stroke_w: 0.0,
},
));
}
}
// --- Estrellas fijas notables ---------------------------------------- // --- Estrellas fijas notables ----------------------------------------
/// Latitud eclíptica (grados, J2000) de las estrellas fijas notables /// Latitud eclíptica (grados, J2000) de las estrellas fijas notables
@@ -982,8 +1027,9 @@ pub fn compose_sphere(
// --- Cuerpo de la esfera: sombreado con volumen --- // --- Cuerpo de la esfera: sombreado con volumen ---
add_sphere_shading(&mut items, pal, center, rad); add_sphere_shading(&mut items, pal, center, rad);
// --- Cielo de fondo: estrellas + Vía Láctea (solo tema oscuro) --- // --- Cielo de fondo: Vía Láctea + estrellas (solo tema oscuro) ---
if opts.show_sky && pal.is_dark { if opts.show_sky && pal.is_dark {
add_milky_way_glow(&mut items, &proj, eps, size, zenith);
add_starfield(&mut items, &proj, size, eps); add_starfield(&mut items, &proj, size, eps);
} }