feat(cosmobiologia): esfera 3D — figuras de las 88 constelaciones
Las constelaciones de un catálogo REAL, no inventadas de memoria: d3-celestial (dominio público), 89 figuras / 743 segmentos, en coordenadas ecuatoriales J2000. El dataset se convirtió a un módulo Rust generado (`constellations_data.rs`) — datos en el repo, auditables. Cada figura: sus polilíneas unen estrellas reales del catálogo (un punto por vértice) y el nombre va en el centroide. Capa tenue, atenuada por profundidad — referencia, no protagonista. Se convierten al marco eclíptico con la misma rotación por oblicuidad que el resto. 42 tests verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,8 @@ pub struct SphereOpts {
|
||||
/// La Tierra interior — un globo pequeño, transparente, con los
|
||||
/// continentes esquemáticos y el observador marcado en su lugar.
|
||||
pub show_earth: bool,
|
||||
/// Las figuras de las 88 constelaciones (catálogo d3-celestial).
|
||||
pub show_constellations: bool,
|
||||
}
|
||||
|
||||
impl Default for SphereOpts {
|
||||
@@ -107,6 +109,7 @@ impl Default for SphereOpts {
|
||||
show_horizon: true,
|
||||
show_sky: true,
|
||||
show_earth: true,
|
||||
show_constellations: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,6 +379,90 @@ fn add_loop(
|
||||
}
|
||||
}
|
||||
|
||||
/// Proyecta una polilínea ABIERTA y empuja un `Line` por segmento.
|
||||
fn add_path(
|
||||
items: &mut Vec<(f32, DrawCommand)>,
|
||||
proj: &Projector,
|
||||
pts: &[Vec3],
|
||||
color: Rgba,
|
||||
width: f32,
|
||||
) {
|
||||
for i in 0..pts.len().saturating_sub(1) {
|
||||
let a = proj.project(pts[i]);
|
||||
let b = proj.project(pts[i + 1]);
|
||||
let d = (a.depth + b.depth) * 0.5;
|
||||
items.push((
|
||||
d,
|
||||
DrawCommand::Line {
|
||||
x1: a.x,
|
||||
y1: a.y,
|
||||
x2: b.x,
|
||||
y2: b.y,
|
||||
color: dim(color, d),
|
||||
width,
|
||||
dash: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Dibuja las figuras de las 88 constelaciones: cada trazo une estrellas
|
||||
/// reales del catálogo (un punto por vértice), y el nombre va en el
|
||||
/// centroide. Capa tenue — referencia, no protagonista.
|
||||
fn add_constellations(
|
||||
items: &mut Vec<(f32, DrawCommand)>,
|
||||
proj: &Projector,
|
||||
eps: f32,
|
||||
size: f32,
|
||||
pal: &Palette,
|
||||
) {
|
||||
let line_col = pal.fg_muted.with_alpha(0.42);
|
||||
let star = Rgba::opaque(0.92, 0.95, 1.0);
|
||||
for fig in crate::constellations_data::FIGURAS {
|
||||
let (mut sx, mut sy, mut sz, mut n) = (0.0_f32, 0.0_f32, 0.0_f32, 0.0_f32);
|
||||
for path in fig.paths {
|
||||
let pts: Vec<Vec3> = path
|
||||
.iter()
|
||||
.map(|&(ra, dec)| rot_x(equatorial_dir(ra, dec), eps))
|
||||
.collect();
|
||||
add_path(items, proj, &pts, line_col, 0.7);
|
||||
for v in &pts {
|
||||
sx += v.x;
|
||||
sy += v.y;
|
||||
sz += v.z;
|
||||
n += 1.0;
|
||||
let p = proj.project(*v);
|
||||
items.push((
|
||||
p.depth - 0.01,
|
||||
DrawCommand::Circle {
|
||||
cx: p.x,
|
||||
cy: p.y,
|
||||
r: size * 0.0017,
|
||||
stroke: None,
|
||||
fill: Some(star.with_alpha(0.70 * depth_alpha(p.depth))),
|
||||
stroke_w: 0.0,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
if n > 0.0 {
|
||||
let c = Vec3::new(sx / n, sy / n, sz / n).normalized();
|
||||
let lp = proj.project(c);
|
||||
items.push((
|
||||
lp.depth + 0.001,
|
||||
DrawCommand::Text {
|
||||
x: lp.x,
|
||||
y: lp.y,
|
||||
content: fig.nombre.into(),
|
||||
color: pal.fg_muted.with_alpha(0.42 * depth_alpha(lp.depth)),
|
||||
size: size * 0.0135,
|
||||
anchor: TextAnchor::Middle,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Los `n` puntos de un círculo máximo perpendicular a `normal`.
|
||||
fn great_circle_perp(normal: Vec3, n: usize) -> Vec<Vec3> {
|
||||
let z = normal.normalized();
|
||||
@@ -813,6 +900,11 @@ pub fn compose_sphere(
|
||||
add_starfield(&mut items, &proj, size, eps);
|
||||
}
|
||||
|
||||
// --- Figuras de las constelaciones ---
|
||||
if opts.show_constellations {
|
||||
add_constellations(&mut items, &proj, eps, size, pal);
|
||||
}
|
||||
|
||||
// --- Rejilla: meridianos + paralelos de la eclíptica ---
|
||||
if opts.show_grid {
|
||||
let grid = pal.fg_muted.with_alpha(0.16);
|
||||
@@ -1231,7 +1323,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn compose_sphere_emite_esqueleto_y_cuerpos() {
|
||||
let cmds = compose_sphere(&modelo_demo(), &SphereView::default(), &SphereOpts::default());
|
||||
// Sin constelaciones, para contar solo el esqueleto base.
|
||||
let cmds = compose_sphere(
|
||||
&modelo_demo(),
|
||||
&SphereView::default(),
|
||||
&SphereOpts { show_constellations: false, ..Default::default() },
|
||||
);
|
||||
assert!(!cmds.is_empty(), "la esfera produce comandos");
|
||||
let lineas = cmds.iter().filter(|c| matches!(c, DrawCommand::Line { .. })).count();
|
||||
let textos = cmds.iter().filter(|c| matches!(c, DrawCommand::Text { .. })).count();
|
||||
@@ -1241,6 +1338,30 @@ mod tests {
|
||||
assert_eq!(textos, 22, "glifos de signos, ángulos, polos y cuerpos: {textos}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn las_constelaciones_dibujan_sus_figuras() {
|
||||
assert!(
|
||||
crate::constellations_data::FIGURAS.len() > 80,
|
||||
"el catálogo trae las 88 constelaciones"
|
||||
);
|
||||
let modelo = modelo_demo();
|
||||
let lineas = |c: &[DrawCommand]| {
|
||||
c.iter().filter(|d| matches!(d, DrawCommand::Line { .. })).count()
|
||||
};
|
||||
let con = compose_sphere(&modelo, &SphereView::default(), &SphereOpts::default());
|
||||
let sin = compose_sphere(
|
||||
&modelo,
|
||||
&SphereView::default(),
|
||||
&SphereOpts { show_constellations: false, ..Default::default() },
|
||||
);
|
||||
assert!(
|
||||
lineas(&con) > lineas(&sin) + 500,
|
||||
"las figuras agregan cientos de trazos: {} vs {}",
|
||||
lineas(&con),
|
||||
lineas(&sin),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el_cenit_esta_a_la_colatitud_del_polo_celeste() {
|
||||
let eps = OBLICUIDAD_DEG.to_radians();
|
||||
|
||||
Reference in New Issue
Block a user