feat(tahuantinsuyu): fase 20 — accordion + lunar shift + CompositeModule + 90 ciudades
Cuatro features que cierran el set inicial de funcionalidades de
fase 1:
## D — Acordeón colapsable en el panel
Cuando hay 8 módulos en el panel se llenaba de cards. Ahora cada card
es expandible/colapsable por click en el header. Defaults:
- Natal siempre expanded
- Módulos con toggle "enabled" = true → expanded
- Resto → collapsed
El usuario puede forzar cualquiera vía override (collapse_overrides
HashMap). Chevron ▾/▸ a la izquierda del header. Hover sobre el
header lo resalta para invitar al click.
## B — Lunar return shift (navegación mensual)
PipelineRequest::PlanetaryReturn gana campo `shift_days: i64` (range
±180). El bridge lo suma a after_seconds del search anchor antes de
next_return. Para Solar return típicamente 0 (mantiene comportamiento).
Para Moon return, mover el slider ±28 días salta al retorno lunar
anterior o siguiente, permitiendo navegar mes a mes la lunación que
le toca al sujeto cumplido N años. PlanetaryReturnModule.controls()
agrega un slider "Shift días (lunar nav)". El badge del overlay
muestra "Moon return 38a +14d" cuando shift_days != 0. Helper
`planetary_return_request(body, age)` para callers que no necesitan
shift (zero default).
## C — CompositeModule
Carta compuesta (midpoint Davison) entre la natal del sujeto y otra
carta partner. Cada placement compuesto es el angular midpoint entre
los dos correspondientes. Engine: `PipelineRequest::Composite {
partner_chart: Box<Chart> }` + build_composite_overlay que llama
`eternal_astrology::composite()`. Renderiza placements en
`radii.composite = r * 0.32` (entre solar_arc 0.40 y aspects 0.24,
re-balanced). Módulo `composite::CompositeModule` con toggle +
ChartPicker (mismo patrón que synastry).
Shell: resolve_composite_partner reusa el fallback al primer hermano
del contacto, igual que synastry.
## A — 90 ciudades expandidas + dropdown scrollable
CITY_PRESETS pasa de 25 a 90 ciudades cubriendo:
- Latinoamérica (35): todas las capitales + grandes ciudades de AR/
VE/CO/PE/CL/EC/UY/PY/BO/MX/CU/PR/CR/PA/SV/GT/HN/NI/DO/BR
- España (5) + Europa (20): Madrid/Barcelona/Sevilla/Valencia/Bilbao
+ London/Paris/Berlin/München/Roma/Milano/Amsterdam/Bruxelles/Wien/
Zürich/Lisboa/Dublin/Stockholm/Oslo/København/Helsinki/Warszawa/
Praha/Budapest/Athina/İstanbul/Moskva
- USA + Canadá (12): NY/LA/Chicago/Miami/Houston/SF/Seattle/Boston/
DC + Toronto/Montreal/Vancouver
- Asia (16): Tokyo/Beijing/Shanghai/HK/Singapore/Seoul/Bangkok/
Jakarta/Manila/Mumbai/Delhi/Bangalore/Karachi/Tehran/Dubai/Tel Aviv
- África (6): Cairo/Lagos/Nairobi/Johannesburg/Cape Town/Casablanca
- Oceanía (3): Sydney/Melbourne/Auckland
El popup del dropdown ahora es scrollable (h=360px, overflow_y_scroll)
con id estable para no perder scroll position entre re-renders.
cargo check verde, 8 tests engine + 1 test modules (8 módulos
aplicables a ChartKind::Natal) verdes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,9 @@ use std::sync::OnceLock;
|
||||
use std::time::Instant;
|
||||
|
||||
use eternal_astrology::{
|
||||
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,
|
||||
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,
|
||||
};
|
||||
use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig};
|
||||
|
||||
@@ -306,6 +306,7 @@ pub fn compose(
|
||||
crate::PipelineRequest::PlanetaryReturn {
|
||||
body,
|
||||
target_age_years,
|
||||
shift_days,
|
||||
} => {
|
||||
let body_e = map_body(body).ok_or_else(|| {
|
||||
EngineError::Eternal(format!(
|
||||
@@ -319,12 +320,27 @@ pub fn compose(
|
||||
observer,
|
||||
body_e,
|
||||
*target_age_years,
|
||||
*shift_days,
|
||||
&mut render,
|
||||
)?;
|
||||
let shift_label = if *shift_days == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {:+}d", shift_days)
|
||||
};
|
||||
push_overlay_meta(
|
||||
&mut render,
|
||||
"planetary_return",
|
||||
format!("{} return {:.0}a", body_e.name(), target_age_years),
|
||||
format!("{} return {:.0}a{}", body_e.name(), target_age_years, shift_label),
|
||||
);
|
||||
}
|
||||
crate::PipelineRequest::Composite { partner_chart } => {
|
||||
let partner_label = partner_chart.label.clone();
|
||||
build_composite_overlay(&natal, partner_chart, &mut render)?;
|
||||
push_overlay_meta(
|
||||
&mut render,
|
||||
"composite",
|
||||
format!("Composite · {}", partner_label),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -471,6 +487,43 @@ fn build_progression_overlay(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper: agrega al `RenderModel` la carta compuesta (midpoint
|
||||
/// composite, Davison 1958) entre la natal del sujeto y la carta del
|
||||
/// partner. Cada planeta compuesto es el angular midpoint entre los
|
||||
/// dos correspondientes. Se renderea en `radii.composite` (ring 0.36).
|
||||
fn build_composite_overlay(
|
||||
natal: &NatalChart,
|
||||
partner_chart: &Chart,
|
||||
render: &mut RenderModel,
|
||||
) -> Result<(), EngineError> {
|
||||
let (partner_natal, _config, _observer) = compute_natal_chart(partner_chart, 0)?;
|
||||
let comp = composite(natal, &partner_natal).map_err(|e| {
|
||||
EngineError::Eternal(format!("composite: {:?}", e))
|
||||
})?;
|
||||
|
||||
let glyphs: Vec<Glyph> = comp
|
||||
.placements
|
||||
.iter()
|
||||
.map(|p| Glyph {
|
||||
deg: p.longitude.longitude_deg() as f32,
|
||||
symbol: body_symbol(p.body).into(),
|
||||
annotation: Some(format!("composite {}", p.body.name())),
|
||||
retrograde: false,
|
||||
house: Some(p.house_number),
|
||||
dignity_marker: None,
|
||||
})
|
||||
.collect();
|
||||
render.layers.push(Layer {
|
||||
module_id: "composite".into(),
|
||||
kind: LayerKind::Bodies,
|
||||
ring: 0.36,
|
||||
z: 15,
|
||||
geometry: Geometry::GlyphsOnly,
|
||||
glyphs,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper: agrega al `RenderModel` los midpoints entre pares de
|
||||
/// cuerpos natales. Filtra para mostrar solo los que involucran al
|
||||
/// Sol o a la Luna (~10 puntos) — son los más significativos
|
||||
@@ -666,6 +719,7 @@ fn build_planetary_return_overlay(
|
||||
observer: Observer,
|
||||
body: Body,
|
||||
target_age_years: f64,
|
||||
shift_days: i64,
|
||||
render: &mut RenderModel,
|
||||
) -> Result<(), EngineError> {
|
||||
let session = session()?;
|
||||
@@ -682,7 +736,10 @@ fn build_planetary_return_overlay(
|
||||
// el return; para Moon, ~15 días. Tomamos un margen amplio que
|
||||
// sirve para todos.
|
||||
const TROPICAL_YEAR_SECS: f64 = 365.242190 * 86400.0;
|
||||
let after_seconds = (target_age_years * 365.242190 - 30.0) * 86400.0;
|
||||
// shift_days permite saltar de un retorno mensual al siguiente
|
||||
// cuando body=Moon, o ajustar finamente el año en Solar return.
|
||||
let after_seconds =
|
||||
(target_age_years * 365.242190 - 30.0 + shift_days as f64) * 86400.0;
|
||||
let after_utc = natal
|
||||
.birth
|
||||
.instant
|
||||
|
||||
@@ -257,11 +257,23 @@ pub enum PipelineRequest {
|
||||
/// "jupiter", …). El bridge lo mapea a `eternal_sky::Body`.
|
||||
body: String,
|
||||
target_age_years: f64,
|
||||
/// Días extra que se suman al anchor de búsqueda (birth +
|
||||
/// age*año). Para Solar return suele ser 0 (el return cae cerca
|
||||
/// del cumpleaños); para Lunar return permite saltar de un
|
||||
/// retorno mensual al siguiente (~28 días por click).
|
||||
shift_days: i64,
|
||||
},
|
||||
/// `module_id = "midpoints"` — anillo de puntos medios entre pares
|
||||
/// de cuerpos natales. Por simplicidad filtramos a los que
|
||||
/// involucran al Sol o a la Luna (~10 puntos).
|
||||
Midpoints,
|
||||
/// `module_id = "composite"` — carta compuesta (midpoint composite,
|
||||
/// método Davison) entre dos sujetos. Renderea los planetas
|
||||
/// compuestos en un anillo interno propio (radio 0.36, entre solar
|
||||
/// arc 0.40 y aspects). Útil para análisis de relaciones.
|
||||
Composite {
|
||||
partner_chart: Box<Chart>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
||||
@@ -339,6 +351,17 @@ pub fn compute_with_transits_at_now(
|
||||
compose(chart, offset_minutes, &[PipelineRequest::Transit])
|
||||
}
|
||||
|
||||
/// Helper retrocompatible: construye un `PlanetaryReturn` con
|
||||
/// `shift_days = 0`. Útil para llamadores que no necesitan ajuste
|
||||
/// fino (todos los Solar return y muchos casos básicos).
|
||||
pub fn planetary_return_request(body: String, target_age_years: f64) -> PipelineRequest {
|
||||
PipelineRequest::PlanetaryReturn {
|
||||
body,
|
||||
target_age_years,
|
||||
shift_days: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub determinista — útil para tests + para la UI sin eternal.
|
||||
pub fn compute_mock(chart: &Chart) -> RenderModel {
|
||||
use std::time::Instant;
|
||||
|
||||
Reference in New Issue
Block a user