feat(tahuantinsuyu): capa "ascensional" topocéntrica completa
5 fases que cierran el sistema topocéntrico end-to-end, conviviendo
con el cómputo geocéntrico tradicional sin reemplazarlo:
T3 — Pipeline en tahuantinsuyu-engine:
- Nuevo `PipelineRequest::Topocentric`.
- `build_topocentric_overlay(natal, render)`: para cada placement
natal aplica `topocentric_ecliptic` (paralaje horizontal con
`distance_km/AU` + observer.lat_rad + LST + obliquidad), emite
Layer Bodies en ring=0.50 con `module_id="topocentric"`.
Recalcula cusps con `Houses::compute(PolichPage, ...)` y emite
Layer Houses asociado. Si la latitud cae en el círculo polar y
Polich-Page diverge, sigue con planetas topocéntricos solos.
T4 — Render overlay en canvas:
- Nuevo `Radii.topocentric = 0.555·r` (justo bajo el carril natal
bodies=0.60). `body_ring("topocentric")` lo mapea.
- Glyphs topocéntricos con disco más chico (22→22*s) y alpha 0.75
(vs 1.0 natal) — se distinguen como "el sutil debajo del
fuerte". En Luna el shift natal↔topo es visible; en Saturno los
dos glyphs casi se superponen.
- Cusps Polich-Page pintadas como línea punteada (dash 3/2.5px)
en un anillo interior al de casas geocéntricas, color
`house_cusp` α=0.55 — claramente sistema secundario sin
esconderse.
T5 — Módulo TopocentricModule:
- Nuevo módulo en tahuantinsuyu-modules con id="topocentric",
label "Topocéntrico (ascensional)". Toggle "Activar" default
OFF (es overlay opcional). Registrado en `Registry::with_builtins`.
- Shell traduce `module_configs["topocentric"]["enabled"] = true`
→ `PipelineRequest::Topocentric` en `build_requests`. Persiste
por carta vía el mismo mecanismo de `persist_module`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -547,6 +547,9 @@ impl Shell {
|
|||||||
if module_enabled(&self.module_configs, "fixed_stars") {
|
if module_enabled(&self.module_configs, "fixed_stars") {
|
||||||
requests.push(PipelineRequest::FixedStars);
|
requests.push(PipelineRequest::FixedStars);
|
||||||
}
|
}
|
||||||
|
if module_enabled(&self.module_configs, "topocentric") {
|
||||||
|
requests.push(PipelineRequest::Topocentric);
|
||||||
|
}
|
||||||
if module_enabled(&self.module_configs, "composite") {
|
if module_enabled(&self.module_configs, "composite") {
|
||||||
if let Some(partner) = self.resolve_composite_partner() {
|
if let Some(partner) = self.resolve_composite_partner() {
|
||||||
requests.push(PipelineRequest::Composite {
|
requests.push(PipelineRequest::Composite {
|
||||||
|
|||||||
@@ -1114,10 +1114,33 @@ fn render_wheel(
|
|||||||
for layer in &render.layers {
|
for layer in &render.layers {
|
||||||
if matches!(layer.kind, LayerKind::Bodies) {
|
if matches!(layer.kind, LayerKind::Bodies) {
|
||||||
let is_natal = layer.module_id == "natal";
|
let is_natal = layer.module_id == "natal";
|
||||||
|
let is_topo = layer.module_id == "topocentric";
|
||||||
let ring = radii.body_ring(&layer.module_id);
|
let ring = radii.body_ring(&layer.module_id);
|
||||||
let alpha = if is_natal { 1.0 } else { 0.88 };
|
let alpha = if is_natal {
|
||||||
let font_size = (if is_natal { 18.0 } else { 14.0 }) * s;
|
1.0
|
||||||
let disk_size = (if is_natal { 26.0 } else { 22.0 }) * s;
|
} else if is_topo {
|
||||||
|
0.75
|
||||||
|
} else {
|
||||||
|
0.88
|
||||||
|
};
|
||||||
|
// Topocéntrico va con disco un poco más chico que el
|
||||||
|
// natal, y con desaturación implícita en `alpha`. El
|
||||||
|
// shift respecto al natal es lo que el ojo lee, no el
|
||||||
|
// tamaño individual.
|
||||||
|
let font_size = (if is_natal {
|
||||||
|
18.0
|
||||||
|
} else if is_topo {
|
||||||
|
15.0
|
||||||
|
} else {
|
||||||
|
14.0
|
||||||
|
}) * s;
|
||||||
|
let disk_size = (if is_natal {
|
||||||
|
26.0
|
||||||
|
} else if is_topo {
|
||||||
|
22.0
|
||||||
|
} else {
|
||||||
|
22.0
|
||||||
|
}) * s;
|
||||||
for g in &layer.glyphs {
|
for g in &layer.glyphs {
|
||||||
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, ring);
|
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, ring);
|
||||||
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
|
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
|
||||||
@@ -1596,6 +1619,12 @@ struct Radii {
|
|||||||
/// Borde interior del cinturón de planetas. Marca dónde "termina"
|
/// Borde interior del cinturón de planetas. Marca dónde "termina"
|
||||||
/// la zona de cuerpos y empieza la zona de aspectos.
|
/// la zona de cuerpos y empieza la zona de aspectos.
|
||||||
bodies_inner: f32,
|
bodies_inner: f32,
|
||||||
|
/// Cuerpos topocéntricos (capa "ascensional") — un poco hacia
|
||||||
|
/// adentro de `bodies` para que un mismo planeta se vea como
|
||||||
|
/// "doble glyph": natal afuera, topocéntrico justo dentro. En
|
||||||
|
/// Luna la separación angular es visible (~1°); en exteriores
|
||||||
|
/// los dos glyphs se superponen casi exactamente.
|
||||||
|
topocentric: f32,
|
||||||
/// Anillo interno con cuerpos progresados (overlay opcional).
|
/// Anillo interno con cuerpos progresados (overlay opcional).
|
||||||
progression: f32,
|
progression: f32,
|
||||||
/// Anillo más interno con cuerpos dirigidos por Solar Arc.
|
/// Anillo más interno con cuerpos dirigidos por Solar Arc.
|
||||||
@@ -1623,6 +1652,11 @@ impl Radii {
|
|||||||
// forman un "carril" estrecho que delimita la franja de
|
// forman un "carril" estrecho que delimita la franja de
|
||||||
// planetas, no dos líneas separadas que confunden.
|
// planetas, no dos líneas separadas que confunden.
|
||||||
bodies_inner: r * 0.57,
|
bodies_inner: r * 0.57,
|
||||||
|
// Topocéntrico justo bajo el carril natal: los dos
|
||||||
|
// glyphs comparten ancho visual, el shift relativo
|
||||||
|
// (Luna en particular) se lee como "el natal apunta a
|
||||||
|
// este grado, el topo a este otro".
|
||||||
|
topocentric: r * 0.555,
|
||||||
// aspects justo bajo el carril de cuerpos. Las líneas
|
// aspects justo bajo el carril de cuerpos. Las líneas
|
||||||
// de aspecto entran a este radio, pero el círculo en sí
|
// de aspecto entran a este radio, pero el círculo en sí
|
||||||
// no se pinta — son las líneas las que importan, no
|
// no se pinta — son las líneas las que importan, no
|
||||||
@@ -1641,6 +1675,7 @@ impl Radii {
|
|||||||
"solar_arc" => self.solar_arc,
|
"solar_arc" => self.solar_arc,
|
||||||
"composite" => self.composite,
|
"composite" => self.composite,
|
||||||
"midpoints" => self.midpoints,
|
"midpoints" => self.midpoints,
|
||||||
|
"topocentric" => self.topocentric,
|
||||||
_ => self.bodies,
|
_ => self.bodies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1730,27 +1765,59 @@ fn paint_wheel(
|
|||||||
|
|
||||||
for layer in layers {
|
for layer in layers {
|
||||||
if matches!(layer.kind, LayerKind::Houses) {
|
if matches!(layer.kind, LayerKind::Houses) {
|
||||||
|
let is_topo = layer.module_id == "topocentric";
|
||||||
if let Geometry::Ring { cusps_deg } = &layer.geometry {
|
if let Geometry::Ring { cusps_deg } = &layer.geometry {
|
||||||
for (i, c) in cusps_deg.iter().enumerate() {
|
for (i, c) in cusps_deg.iter().enumerate() {
|
||||||
let is_angle = i == 0 || i == 3 || i == 6 || i == 9;
|
let is_angle = i == 0 || i == 3 || i == 6 || i == 9;
|
||||||
let color = if is_angle {
|
let color = if is_topo {
|
||||||
|
with_alpha(house_base, 0.55)
|
||||||
|
} else if is_angle {
|
||||||
palette.angle_highlight
|
palette.angle_highlight
|
||||||
} else {
|
} else {
|
||||||
with_alpha(house_base, 0.75)
|
with_alpha(house_base, 0.75)
|
||||||
};
|
};
|
||||||
let width = if is_angle { 2.0 } else { 0.8 };
|
let width = if is_angle && !is_topo { 2.0 } else { 0.8 };
|
||||||
paint_radial_line(
|
if is_topo {
|
||||||
window,
|
// Topocéntrico: cusp como línea punteada
|
||||||
cx,
|
// en su propio anillo (un poco más
|
||||||
cy,
|
// adentro que las casas geocéntricas) →
|
||||||
*c,
|
// se distingue como sistema alternativo.
|
||||||
ascendant_deg,
|
let (xi, yi) = polar_to_screen(
|
||||||
rot_offset_deg,
|
*c,
|
||||||
radii.houses_inner,
|
ascendant_deg,
|
||||||
radii.houses_outer,
|
rot_offset_deg,
|
||||||
color,
|
radii.houses_inner - 4.0,
|
||||||
width,
|
);
|
||||||
);
|
let (xo, yo) = polar_to_screen(
|
||||||
|
*c,
|
||||||
|
ascendant_deg,
|
||||||
|
rot_offset_deg,
|
||||||
|
radii.houses_inner - 28.0,
|
||||||
|
);
|
||||||
|
paint_segment(
|
||||||
|
window,
|
||||||
|
cx + xi,
|
||||||
|
cy + yi,
|
||||||
|
cx + xo,
|
||||||
|
cy + yo,
|
||||||
|
color,
|
||||||
|
Some((3.0, 2.5)),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
paint_radial_line(
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
*c,
|
||||||
|
ascendant_deg,
|
||||||
|
rot_offset_deg,
|
||||||
|
radii.houses_inner,
|
||||||
|
radii.houses_outer,
|
||||||
|
color,
|
||||||
|
width,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use eternal_astrology::{
|
use eternal_astrology::{
|
||||||
all_lots, 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,
|
solar_arc_true, topocentric_ecliptic, Aspect, AspectKind as EAspectKind, BirthData, BodySet,
|
||||||
HouseSystem as EHouseSystem, NatalChart, OrbTable, Zodiac as EZodiac,
|
ChartConfig, HouseSystem as EHouseSystem, Houses as EHouses, NatalChart, OrbTable,
|
||||||
|
Zodiac as EZodiac,
|
||||||
};
|
};
|
||||||
use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig};
|
use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig};
|
||||||
|
|
||||||
@@ -380,6 +381,14 @@ pub fn compose(
|
|||||||
format!("Estrellas fijas · {}", count),
|
format!("Estrellas fijas · {}", count),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
crate::PipelineRequest::Topocentric => {
|
||||||
|
build_topocentric_overlay(&natal, &mut render)?;
|
||||||
|
push_overlay_meta(
|
||||||
|
&mut render,
|
||||||
|
"topocentric",
|
||||||
|
"Topocéntrico (Polich-Page)".into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +471,93 @@ fn build_transit_overlay(
|
|||||||
/// secundaria. La carta progresada se computa con el mismo observer y
|
/// secundaria. La carta progresada se computa con el mismo observer y
|
||||||
/// config que la natal pero al instante natal+(age_years/period_years)
|
/// config que la natal pero al instante natal+(age_years/period_years)
|
||||||
/// días.
|
/// días.
|
||||||
|
/// Overlay topocéntrico: re-proyecta cada placement natal a longitud
|
||||||
|
/// topocéntrica (con paralaje horizontal) y recalcula las casas con
|
||||||
|
/// Polich-Page. Los dos quedan emparentados al mismo `module_id =
|
||||||
|
/// "topocentric"` para que el canvas los pinte con un visual
|
||||||
|
/// consistente. La capa convive con la natal geocéntrica — ambas se
|
||||||
|
/// ven simultáneamente.
|
||||||
|
fn build_topocentric_overlay(
|
||||||
|
natal: &NatalChart,
|
||||||
|
render: &mut RenderModel,
|
||||||
|
) -> Result<(), EngineError> {
|
||||||
|
const KM_PER_AU: f64 = 149_597_870.7;
|
||||||
|
let lst = natal.local_apparent_sidereal_time_rad;
|
||||||
|
let eps = natal.obliquity_rad;
|
||||||
|
let obs_lat = natal.birth.observer.lat_rad;
|
||||||
|
|
||||||
|
// 1) Planetas topocéntricos. Para puntos sin distancia (nodos,
|
||||||
|
// Lilith calculada) `topocentric_ecliptic` retorna la entrada sin
|
||||||
|
// cambios — geocéntrico y topocéntrico coinciden ahí.
|
||||||
|
let body_glyphs: Vec<Glyph> = natal
|
||||||
|
.placements
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
let dist_au = p.distance_km / KM_PER_AU;
|
||||||
|
let (lon_topo, _) = topocentric_ecliptic(
|
||||||
|
p.longitude.longitude_rad(),
|
||||||
|
p.latitude_rad,
|
||||||
|
dist_au,
|
||||||
|
obs_lat,
|
||||||
|
lst,
|
||||||
|
eps,
|
||||||
|
);
|
||||||
|
let lon_topo_deg = lon_topo.to_degrees() as f32;
|
||||||
|
Glyph {
|
||||||
|
deg: lon_topo_deg,
|
||||||
|
symbol: body_symbol(p.body).into(),
|
||||||
|
annotation: Some(format!("{:.2}° topo", lon_topo_deg)),
|
||||||
|
retrograde: p.longitude_rate_rad_per_day < 0.0,
|
||||||
|
house: None,
|
||||||
|
dignity_marker: None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
render.layers.push(Layer {
|
||||||
|
module_id: "topocentric".into(),
|
||||||
|
kind: LayerKind::Bodies,
|
||||||
|
ring: 0.50,
|
||||||
|
z: 8,
|
||||||
|
geometry: Geometry::GlyphsOnly,
|
||||||
|
glyphs: body_glyphs,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2) Casas Polich-Page. Si la latitud cae en el círculo polar el
|
||||||
|
// sistema diverge — devolvemos un error parcial pero conservamos
|
||||||
|
// la capa de planetas topocéntricos (que sí es válida).
|
||||||
|
match EHouses::compute(EHouseSystem::PolichPage, lst, obs_lat, eps) {
|
||||||
|
Ok(houses_pp) => {
|
||||||
|
let cusps_deg: Vec<f32> =
|
||||||
|
houses_pp.cusps.iter().map(|c| c.to_degrees() as f32).collect();
|
||||||
|
let house_glyphs: Vec<Glyph> = (0..12)
|
||||||
|
.map(|i| Glyph {
|
||||||
|
deg: cusps_deg[i] + 4.0,
|
||||||
|
symbol: format!("h{}", i + 1),
|
||||||
|
annotation: None,
|
||||||
|
retrograde: false,
|
||||||
|
house: Some((i as u8) + 1),
|
||||||
|
dignity_marker: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
render.layers.push(Layer {
|
||||||
|
module_id: "topocentric".into(),
|
||||||
|
kind: LayerKind::Houses,
|
||||||
|
ring: 0.78,
|
||||||
|
z: 9,
|
||||||
|
geometry: Geometry::Ring { cusps_deg },
|
||||||
|
glyphs: house_glyphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Polo: el visual se queda solo con planetas topocéntricos.
|
||||||
|
eprintln!("[bridge] PolichPage no disponible en lat polar: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn build_progression_overlay(
|
fn build_progression_overlay(
|
||||||
natal: &NatalChart,
|
natal: &NatalChart,
|
||||||
target_age_years: f64,
|
target_age_years: f64,
|
||||||
|
|||||||
@@ -326,6 +326,13 @@ pub enum PipelineRequest {
|
|||||||
/// aproximadas + precesión simple (~50.29″/año). Renderea como
|
/// aproximadas + precesión simple (~50.29″/año). Renderea como
|
||||||
/// marcadores chicos justo afuera del sign dial.
|
/// marcadores chicos justo afuera del sign dial.
|
||||||
FixedStars,
|
FixedStars,
|
||||||
|
/// `module_id = "topocentric"` — capa "ascensional": planetas
|
||||||
|
/// re-proyectados a longitud eclíptica topocéntrica (con paralaje
|
||||||
|
/// horizontal aplicada por cuerpo) + casas Polich-Page (sistema
|
||||||
|
/// topocéntrico de domificación). Visible sobre todo en la Luna
|
||||||
|
/// (~1° de shift); imperceptible en planetas exteriores. La capa
|
||||||
|
/// convive con la natal geocéntrica como overlay comparativo.
|
||||||
|
Topocentric,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ impl Registry {
|
|||||||
r.register(Box::new(uranian::UranianModule));
|
r.register(Box::new(uranian::UranianModule));
|
||||||
r.register(Box::new(lots::LotsModule));
|
r.register(Box::new(lots::LotsModule));
|
||||||
r.register(Box::new(fixed_stars::FixedStarsModule));
|
r.register(Box::new(fixed_stars::FixedStarsModule));
|
||||||
|
r.register(Box::new(topocentric::TopocentricModule));
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,3 +809,49 @@ pub mod uranian {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// TopocentricModule — capa "ascensional" (paralaje + Polich-Page)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
pub mod topocentric {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Capa topocéntrica que convive con la natal geocéntrica: cada
|
||||||
|
/// planeta se re-proyecta a longitud eclíptica topocéntrica (con
|
||||||
|
/// paralaje horizontal por cuerpo) y las casas se calculan con el
|
||||||
|
/// sistema Polich-Page. El shift es visible en la Luna (~1°),
|
||||||
|
/// modesto en interiores cerca de oposición, e imperceptible en
|
||||||
|
/// exteriores. La engine despacha al pipeline
|
||||||
|
/// `PipelineRequest::Topocentric` cuando este módulo está activo.
|
||||||
|
pub struct TopocentricModule;
|
||||||
|
|
||||||
|
impl Module for TopocentricModule {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"topocentric"
|
||||||
|
}
|
||||||
|
fn label(&self) -> &'static str {
|
||||||
|
"Topocéntrico (ascensional)"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Paralaje horizontal por cuerpo + casas Polich-Page."
|
||||||
|
}
|
||||||
|
fn applies_to(&self, kind: ChartKind) -> bool {
|
||||||
|
matches!(kind, ChartKind::Natal)
|
||||||
|
}
|
||||||
|
fn enabled_by_default(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn controls(&self) -> Vec<Control> {
|
||||||
|
vec![Control::Toggle {
|
||||||
|
key: "enabled".into(),
|
||||||
|
label: "Activar".into(),
|
||||||
|
default: false,
|
||||||
|
hotkey: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user