feat(tahuantinsuyu): fase 19 — theme switcher + house tooltips + midpoints + city presets

Fase completa con 4 mejoras independientes que aprovechan toda la
infraestructura previa. La aplicación ahora cubre lecturas profundas
(midpoints uranianos), accesibilidad visual (tooltips de cusps),
personalización (6 themes vía yahweh-widget-theme-switcher) y
usabilidad pragmática (city presets en el form).

## C — Theme switcher en header

- apps/tahuantinsuyu: nueva dep yahweh-widget-theme-switcher.
- shell render(): theme_switcher(cx) en el extremo derecho del header
  (con flex_grow del divider del medio). Click cicla entre los 6
  presets de yahweh-theme (Nebula, Aurora, Sunset, FlatDark,
  SolarizedLight, HighContrast). AstroPalette::for_theme(theme) lee
  is_dark, así toda la rueda se re-tinta automáticamente.

## B — Tooltips sobre house cusps

- canvas: HoverInfo deja de ser struct para ser enum con variantes
  Body { ... } y HouseCusp { house_number, deg, local_x, local_y }.
  Helpers .local() y .key() unifican el acceso.
- on_hover_check: primero hit-test bodies (threshold 14px); si no hubo
  match Y el mouse está dentro del anillo de casas
  (houses_inner..houses_outer ± 6px), calcula la longitud zodiacal
  desde el ángulo de pantalla (inversa de polar_to_screen) y busca el
  cusp más cercano (proximidad angular < 2.5°). HoverInfo::HouseCusp.
- Tooltip render: "Cusp Casa N · Signo XX.X°".

## D — MidpointsModule (Uranian-lite)

- engine: PipelineRequest::Midpoints (sin parámetros, default empty).
- bridge: build_midpoints_overlay computa midpoints entre todos los
  pares de placements donde involucran Sol o Luna (~10 puntos según
  body set). Fórmula: si |a-b|>180, mid=((a+b)/2+180) mod 360, sino
  (a+b)/2 mod 360. Emite como Layer { kind: Midpoints, module_id:
  "midpoints", ring: 0.62 } con Glyph.symbol="sun/jupiter" y
  annotation="Sun/Jupiter".
- modules: midpoints::MidpointsModule con toggle "Activar". Registry
  pasa a 7 módulos. Test actualizado.
- shell: build_requests detecta midpoints.enabled, pushea
  PipelineRequest::Midpoints (no toma age ni body — es derivado puro).
- canvas: Radii agrega midpoints: r * 0.62 (entre houses_inner y
  bodies natales). body_ring("midpoints") y aspect_endpoints retornan
  ese radio. paint_wheel agrega un loop para LayerKind::Midpoints
  pintando dots pequeños (r=0.012, alpha 0.7 sobre house_cusp color)
  — los midpoints no llevan unicode symbol propio (no existe en
  Unicode astrológico estándar). El detalle del par viene en hover.
- Hover sobre un midpoint: tooltip muestra "☉/♄ Tauro 14.3° ·
  Sun/Jupiter" (display_symbol parsea "a/b" en dos unicodes;
  annotation incluye nombres completos eternal).

## A — City presets en el ChartForm

- tree: nueva const CITY_PRESETS con 25 ciudades (Latinoamérica
  capitales + 5 europeas + 5 anglosajonas + Tokyo/Sydney/Mumbai/Cairo)
  con (name, lat, lon, tz_offset_minutes) sin DST. CityPreset struct.
- tree: TahuantinsuyuTree gana city_picker_open: bool. close_modal
  lo resetea. toggle_city_picker + apply_city_preset(preset) helpers.
  apply_city_preset lee el Modal activo (CreateChart o EditChart),
  llama TextInput::set_text en place/lat/lon/tz del ChartForm,
  cierra el picker.
- render_chart_form: title_row ahora tiene "📍 Ciudad rápida ▾"
  button a la derecha del title. Click → toggle. Cuando picker_open,
  popup absoluto debajo con la lista de presets. Click en preset →
  autocompleta + cierra. El usuario sigue pudiendo editar manualmente
  cualquier campo después; el preset es solo un punto de partida
  rápido para evitar tipear coordenadas a mano.

cargo check verde, 8 tests engine + 1 test modules (7 módulos)
verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-17 23:12:17 +00:00
parent 2cd34c82da
commit 32ab22f954
8 changed files with 436 additions and 63 deletions
@@ -299,6 +299,10 @@ pub fn compose(
format!("Sinastría · {}", partner_label),
);
}
crate::PipelineRequest::Midpoints => {
build_midpoints_overlay(&natal, &mut render);
push_overlay_meta(&mut render, "midpoints", "Midpoints ☉/☽".into());
}
crate::PipelineRequest::PlanetaryReturn {
body,
target_age_years,
@@ -467,6 +471,57 @@ fn build_progression_overlay(
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
/// astrológicamente y mantiene la rueda legible.
///
/// El midpoint de dos longitudes es la menor distancia angular entre
/// ellas. Si `|a - b| > 180`, hay que sumar 180 al promedio para
/// obtener el midpoint "corto".
fn build_midpoints_overlay(natal: &NatalChart, render: &mut RenderModel) {
let mut glyphs: Vec<Glyph> = Vec::new();
let placements = &natal.placements;
for i in 0..placements.len() {
for j in (i + 1)..placements.len() {
let pa = &placements[i];
let pb = &placements[j];
// Solo midpoints que involucren Sol o Luna.
let involves_luminary = matches!(pa.body, Body::Sun | Body::Moon)
|| matches!(pb.body, Body::Sun | Body::Moon);
if !involves_luminary {
continue;
}
let a = pa.longitude.longitude_deg() as f32;
let b = pb.longitude.longitude_deg() as f32;
let diff = (a - b).abs();
let mid = if diff > 180.0 {
((a + b) / 2.0 + 180.0).rem_euclid(360.0)
} else {
((a + b) / 2.0).rem_euclid(360.0)
};
glyphs.push(Glyph {
deg: mid,
symbol: format!("{}/{}", body_symbol(pa.body), body_symbol(pb.body)),
annotation: Some(format!("{}/{}", pa.body.name(), pb.body.name())),
retrograde: false,
house: None,
dignity_marker: None,
});
}
}
render.layers.push(Layer {
module_id: "midpoints".into(),
kind: LayerKind::Midpoints,
ring: 0.62,
z: 14,
geometry: Geometry::GlyphsOnly,
glyphs,
});
}
/// Helper: agrega al `RenderModel` las capas del overlay de Solar Arc
/// (método true-progressed-Sun por default). Cada cuerpo natal se
/// desplaza por el mismo arco — preserva las relaciones angulares y