tawasuyu

tawasuyu/brahman

Rust 230TOML 69JSON 14HTML 12901 Sutichaykuna

feat(tahuantinsuyu): fase 15 — badges de overlays activos en el footer

Cuando hay overlays activos, debajo del info_row aparecen pills con
los nombres de cada uno (Natal, Tránsito ahora, Progresión 38.2a,
Sinastría · Ana, Saturn return 29a) — el usuario ve de un vistazo qué
está mirando sin tener que mapear los anillos manualmente.

El border de cada pill toma color según a qué slot del wheel
pertenece: outer ring (transit/synastry/planetary_return) →
palette.angle_highlight (dorado), inner overlays (progression/
solar_arc) → palette.house_cusp (tono apagado), natal → neutro.
Permite leer la pila de izquierda a derecha y ubicar visualmente cada
glyph del wheel.

- engine: nuevo OverlayMeta { module_id, label } + campo overlays:
  Vec<OverlayMeta> en RenderModel. build_render_model lo inicializa
  vacío; bridge::compose pushea un OverlayMeta por cada
  PipelineRequest después de su build_*_overlay correspondiente. Helper
  push_overlay_meta(render, id, label). Labels: "Tránsito ahora",
  "Progresión {age:.1}a", "Solar Arc {age:.1}a", "Sinastría · {name}"
  (lee partner_chart.label antes de mover el Box al builder),
  "{Body} return {age:.0}a" (usa eternal_sky body.name()).
- canvas: render_wheel separa el viejo footer en info_row (Asc/MC/ms +
  offset + hotkeys) y un badges_row opcional. badges_row aparece solo
  cuando render.overlays != empty. Pill helper centralizado: bg
  panel_alt, border 1px, text size 10, rounded 10. Border color
  decidido por module_id para correlacionar con el ring visual.

Compatible con compute_mock (que setea overlays = vec![] — ningún
mock badge). Persiste sin cambios — los configs siguen guardando su
estado, los OverlayMeta se reconstruyen en cada compose desde los
requests activos.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

1232e3939739e6bbf8885d687561796666611eae · ruwaq sergio · (5 killa ñawpaq) · tayta 6d572c81c · kay commitpi sach'ata qhaway

Semántico sayk'uy · 8

crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs

crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs

3 qillqakuna · +96 −2

diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
index c3ebef2..b0b6d4e 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
@@ -626,7 +626,7 @@ fn render_wheel(
     } else {
         palette.angle_highlight
     };
-    let footer = div()
+    let info_row = div()
         .flex()
         .flex_row()
         .gap(px(10.0))
@@ -652,6 +652,27 @@ fn render_wheel(
                 .child("[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [R]eset"),
         );
 
+    // Badges de overlays activos. Cada uno se pinta como pill con
+    // background sutil y border tenue. Solo aparecen cuando hay
+    // overlays — la carta natal pura ve solo el info_row.
+    let badges_row = if render.overlays.is_empty() {
+        None
+    } else {
+        let mut row = div().flex().flex_row().flex_wrap().gap(px(6.0));
+        // Badge "natal" base, siempre presente cuando hay overlays —
+        // ayuda al usuario a leer la pila de izquierda a derecha.
+        row = row.child(badge(theme, palette, "natal", "Natal", true));
+        for ov in &render.overlays {
+            row = row.child(badge(theme, palette, &ov.module_id, &ov.label, false));
+        }
+        Some(row)
+    };
+
+    let mut footer = div().flex().flex_col().items_center().gap(px(4.0)).child(info_row);
+    if let Some(b) = badges_row {
+        footer = footer.child(b);
+    }
+
     div()
         .flex()
         .flex_col()
@@ -662,6 +683,33 @@ fn render_wheel(
         .child(footer)
 }
 
+/// Pequeña pill con la etiqueta de un overlay activo. El borde toma
+/// color según el "tipo" del módulo para ayudar a mapear a su anillo
+/// en el wheel: natal = neutro, outer ring share (transit/synastry/
+/// planetary_return) = palette.angle_highlight, inner overlays
+/// (progression/solar_arc) = palette.house_cusp.
+fn badge(theme: &Theme, palette: &AstroPalette, module_id: &str, label: &str, is_natal: bool) -> gpui::Div {
+    let border = if is_natal {
+        theme.border
+    } else {
+        match module_id {
+            "transit" | "synastry" | "planetary_return" => palette.angle_highlight,
+            "progression" | "solar_arc" => palette.house_cusp,
+            _ => theme.border,
+        }
+    };
+    div()
+        .px(px(8.0))
+        .py(px(2.0))
+        .rounded(px(10.0))
+        .bg(theme.bg_panel_alt.clone())
+        .border_1()
+        .border_color(border)
+        .text_size(px(10.0))
+        .text_color(theme.fg_text)
+        .child(SharedString::from(label.to_string()))
+}
+
 fn format_offset(minutes: i64) -> String {
     if minutes == 0 {
         return "⏱ ahora".to_string();
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
index aaf25fc..29870e5 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/bridge.rs
@@ -17,7 +17,7 @@ use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Obser
 
 use tahuantinsuyu_model::{Chart, HouseSystem, StoredChartConfig, Zodiac};
 
-use crate::{EngineError, Geometry, Glyph, Layer, LayerKind, LineSeg, RenderModel};
+use crate::{EngineError, Geometry, Glyph, Layer, LayerKind, LineSeg, OverlayMeta, RenderModel};
 
 // =====================================================================
 // Sesión global cacheada
@@ -254,15 +254,32 @@ pub fn compose(
         match req {
             crate::PipelineRequest::Transit => {
                 build_transit_overlay(&natal, &config_e, observer, ESInstant::now(), &mut render)?;
+                push_overlay_meta(&mut render, "transit", "Tránsito ahora".into());
             }
             crate::PipelineRequest::SecondaryProgression { target_age_years } => {
                 build_progression_overlay(&natal, *target_age_years, &mut render)?;
+                push_overlay_meta(
+                    &mut render,
+                    "progression",
+                    format!("Progresión {:.1}a", target_age_years),
+                );
             }
             crate::PipelineRequest::SolarArc { target_age_years } => {
                 build_solar_arc_overlay(&natal, *target_age_years, &mut render)?;
+                push_overlay_meta(
+                    &mut render,
+                    "solar_arc",
+                    format!("Solar Arc {:.1}a", target_age_years),
+                );
             }
             crate::PipelineRequest::Synastry { partner_chart } => {
+                let partner_label = partner_chart.label.clone();
                 build_synastry_overlay(&natal, partner_chart, &mut render)?;
+                push_overlay_meta(
+                    &mut render,
+                    "synastry",
+                    format!("Sinastría · {}", partner_label),
+                );
             }
             crate::PipelineRequest::PlanetaryReturn {
                 body,
@@ -282,6 +299,11 @@ pub fn compose(
                     *target_age_years,
                     &mut render,
                 )?;
+                push_overlay_meta(
+                    &mut render,
+                    "planetary_return",
+                    format!("{} return {:.0}a", body_e.name(), target_age_years),
+                );
             }
         }
     }
@@ -808,9 +830,17 @@ fn build_render_model(
         descendant_deg,
         imum_coeli_deg,
         layers: vec![sign_dial, houses, bodies, aspects_layer],
+        overlays: Vec::new(),
     }
 }
 
+fn push_overlay_meta(render: &mut RenderModel, module_id: &str, label: String) {
+    render.overlays.push(OverlayMeta {
+        module_id: module_id.to_string(),
+        label,
+    });
+}
+
 /// Mapea el orb absoluto a una opacidad — los aspectos más exactos se
 /// pintan más fuerte, los flojos casi se desvanecen.
 fn orb_to_opacity(orb_deg: f64, kind: EAspectKind) -> f32 {
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
index c04c9e0..533cfca 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-engine/src/lib.rs
@@ -61,6 +61,21 @@ pub struct RenderModel {
 
     /// Capas a pintar. Orden = z-order ascendente.
     pub layers: Vec<Layer>,
+    /// Metadata humana por overlay activo (transit, progresión,
+    /// sinastría, retorno...). Vacío para una carta natal pura. La UI
+    /// la pinta como badges en el footer.
+    #[serde(default)]
+    pub overlays: Vec<OverlayMeta>,
+}
+
+/// Etiqueta legible de un overlay para el footer del canvas. La engine
+/// la pushea desde cada `build_*_overlay`; el canvas solo lee y pinta.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct OverlayMeta {
+    pub module_id: String,
+    /// Etiqueta corta — ej. "Tránsito ahora", "Progresión 38.2a",
+    /// "Sinastría · Ana", "Saturn return 29a".
+    pub label: String,
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -277,6 +292,7 @@ pub fn compute_mock(chart: &Chart) -> RenderModel {
         descendant_deg: 180.0,
         imum_coeli_deg: 90.0,
         layers: vec![sign_dial],
+        overlays: Vec::new(),
     }
 }