feat(tahuantinsuyu): fase 27 — Lots helenísticos + 9 fixed stars

Dos módulos astrológicos pluggables más:

- LotsModule: 7 Arabic Parts vía `all_lots(natal)` (Fortune,
  Spirit, Eros, Necessity, Courage, Victory, Nemesis). Glifos
  `lot:Fo` en ring 0.54, hover muestra el nombre completo.
- FixedStarsModule: 9 estrellas notables (Aldebaran, Regulus,
  Antares, Fomalhaut, Spica, Sirius, Algol, Vega, Pollux) con
  longitudes tropicales J2000 + precesión general de 50.29″/año
  proyectada al año natal. Marcadores `✦Xxx` en ring 1.04.

Registry pasa de 9 a 11 módulos; test actualizado. Sin cambios
de esquema en RenderModel — los `LayerKind::Lots` y
`LayerKind::FixedStars` ya existían.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-18 00:21:00 +00:00
parent e2da24239e
commit a4d1e0dc17
4 changed files with 206 additions and 3 deletions
@@ -9,7 +9,7 @@ use std::sync::OnceLock;
use std::time::Instant;
use eternal_astrology::{
composite, find_aspects, find_synastry_aspects, next_return, secondary_progression,
all_lots, composite, find_aspects, find_synastry_aspects, next_return, secondary_progression,
solar_arc_true, Aspect, AspectKind as EAspectKind, BirthData, BodySet, ChartConfig,
HouseSystem as EHouseSystem, NatalChart, OrbTable, Zodiac as EZodiac,
};
@@ -356,6 +356,18 @@ pub fn compose(
},
);
}
crate::PipelineRequest::Lots => {
let count = build_lots_overlay(&natal, &mut render)?;
push_overlay_meta(&mut render, "lots", format!("Lots · {}", count));
}
crate::PipelineRequest::FixedStars => {
let count = build_fixed_stars_overlay(chart, &mut render);
push_overlay_meta(
&mut render,
"fixed_stars",
format!("Estrellas fijas · {}", count),
);
}
}
}
@@ -1104,6 +1116,91 @@ fn push_overlay_meta(render: &mut RenderModel, module_id: &str, label: String) {
});
}
/// Helper: agrega al `RenderModel` los Lots arábigos clásicos
/// (helenísticos) — Fortune, Spirit, Eros, Necessity, Courage, Victory,
/// Nemesis. Cada uno se renderea como un glifo `lot:Fo` en el anillo
/// `0.54` (entre midpoints y cuerpos progresados). Retorna la cantidad
/// de lots renderizados.
fn build_lots_overlay(
natal: &NatalChart,
render: &mut RenderModel,
) -> Result<usize, EngineError> {
let lots = all_lots(natal)
.map_err(|e| EngineError::Eternal(format!("all_lots: {:?}", e)))?;
let glyphs: Vec<Glyph> = lots
.iter()
.map(|l| {
let name = l.name.map(|n| n.label()).unwrap_or("Lot");
// Tres-letras compactas para no recargar la rueda.
let abbrev: String = name.chars().take(2).collect();
Glyph {
deg: l.longitude.longitude_deg() as f32,
symbol: format!("lot:{}", abbrev),
annotation: Some(name.to_string()),
retrograde: false,
house: Some(l.house_number),
dignity_marker: None,
}
})
.collect();
let count = glyphs.len();
render.layers.push(Layer {
module_id: "lots".into(),
kind: LayerKind::Lots,
ring: 0.54,
z: 13,
geometry: Geometry::GlyphsOnly,
glyphs,
});
Ok(count)
}
/// Helper: agrega al `RenderModel` 9 estrellas fijas notables. Las
/// longitudes están en J2000 ecliptica tropical; aplicamos precesión
/// general de 50.29″/año hacia adelante hasta el año natal — basta
/// para el orbe de conjunción de ±1.5° con que se interpretan.
fn build_fixed_stars_overlay(chart: &Chart, render: &mut RenderModel) -> usize {
// (símbolo, nombre, longitud tropical J2000 en grados)
const STARS: &[(&str, &str, f64)] = &[
("✦Ald", "Aldebaran", 69.79), // 09°47 Gem
("✦Reg", "Regulus", 149.83), // 29°50 Leo
("✦Ant", "Antares", 249.77), // 09°46 Sag
("✦Fom", "Fomalhaut", 333.87), // 03°52 Pis
("✦Spi", "Spica", 203.84), // 23°50 Lib
("✦Sir", "Sirius", 104.10), // 14°06 Can
("✦Alg", "Algol", 56.18), // 26°10 Tau
("✦Veg", "Vega", 285.31), // 15°19 Cap
("✦Pol", "Pollux", 113.27), // 23°16 Can
];
let years_from_j2000 = (chart.birth_data.year - 2000) as f64;
// 50.29″/año ≈ 0.01397°/año de precesión en longitud eclíptica.
let precession_deg = years_from_j2000 * (50.29 / 3600.0);
let glyphs: Vec<Glyph> = STARS
.iter()
.map(|(sym, name, j2000_deg)| {
let lon = (j2000_deg + precession_deg).rem_euclid(360.0) as f32;
Glyph {
deg: lon,
symbol: (*sym).to_string(),
annotation: Some((*name).to_string()),
retrograde: false,
house: None,
dignity_marker: None,
}
})
.collect();
let count = glyphs.len();
render.layers.push(Layer {
module_id: "fixed_stars".into(),
kind: LayerKind::FixedStars,
ring: 1.04,
z: 16,
geometry: Geometry::GlyphsOnly,
glyphs,
});
count
}
/// Decora cada Glyph de Bodies (module_id="natal") con su dignity
/// marker en `glyph.dignity_marker`. Usa `essential_dignity(body, sign)`
/// — los cuerpos modernos quedan sin marker.
@@ -313,6 +313,17 @@ pub enum PipelineRequest {
/// fórmulas analíticas. La visualización geométrica completa del
/// dial de 90° queda pendiente para una fase posterior.
Uranian,
/// `module_id = "lots"` — Lots arábigos (helenísticos) calculados
/// via `eternal_astrology::compute_lot`: Fortune, Spirit, Eros,
/// Necessity, Courage, Victory, Nemesis. Renderea cada lot como
/// un texto pequeño en el ring de bodies natales.
Lots,
/// `module_id = "fixed_stars"` — overlay con ~9 estrellas fijas
/// notables (Aldebaran, Regulus, Antares, Fomalhaut, Spica,
/// Sirius, Algol, Vega, Pollux). Posiciones tropicales J2000
/// aproximadas + precesión simple (~50.29″/año). Renderea como
/// marcadores chicos justo afuera del sign dial.
FixedStars,
}
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué