tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definiciones

feat(cosmobiologia): GR — resaltado de eventos + HUD lateral de triggers

Segundo incremento del Sistema GR: el canvas ahora hace visible la
rectificación.

- Resaltado de convergencias: por cada punto natal donde un directo y
  un converso coinciden dentro del micro-orbe, un eje brillante cruza
  la zona del dual-ring hasta el cinturón natal, con marcador glow.
- HUD lateral: columna a la derecha del wheel cuando GR está activo,
  con los triggers ordenados por orbe. Color rojo→gris según orbe; las
  convergencias llevan ✦ y fondo resaltado.

paint_wheel recibe los gr_triggers; render_wheel monta el body como
fila wheel+HUD sólo en modo GR.

Falta: scrubbing live del jog-dial mapeado a la edad.

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

363f401b758493074bcf3a371b79886881f5d269 · por sergio · (hace 5 meses) · padre 15e45ace9 · ver el árbol en este commit

Delta semántico · 7

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

1 archivos · +176 −2

diff --git a/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs b/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs
index 3040402..11a7d72 100644
--- a/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs
+++ b/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs
@@ -40,7 +40,9 @@ use gpui::{
     Window, canvas, div, hsla, point, prelude::*, px,
 };
 
-use cosmobiologia_engine::{Geometry, Layer, LayerKind, OUTER_RING_MODULES, RenderModel};
+use cosmobiologia_engine::{
+    Geometry, GrTrigger, Layer, LayerKind, OUTER_RING_MODULES, RenderModel,
+};
 use cosmobiologia_model::{ChartId, ContactId, GroupId};
 use cosmobiologia_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
 use nahual_theme::Theme;
@@ -949,6 +951,7 @@ fn render_wheel(
     let palette_paint = palette.clone();
     let theme_paint = theme.clone();
     let layers_paint: Vec<Layer> = render.layers.clone();
+    let gr_triggers_paint: Vec<GrTrigger> = render.gr_triggers.clone();
     let asc_for_paint = asc;
     let mc_for_paint = render.midheaven_deg;
     let visibility_for_paint = visible.clone();
@@ -975,6 +978,7 @@ fn render_wheel(
                 radii,
                 &visibility_for_paint,
                 hover_focus_paint.as_deref(),
+                &gr_triggers_paint,
             );
 
             // Handlers de mouse — se registran cada frame contra el
@@ -1749,16 +1753,150 @@ fn render_wheel(
         footer = footer.child(grid);
     }
 
+    // El wheel va solo, salvo en modo GR: ahí lo acompaña el HUD
+    // lateral de triggers de rectificación, anclado a su derecha.
+    let body = if render.gr_triggers.is_empty() {
+        div().child(wheel)
+    } else {
+        div()
+            .flex()
+            .flex_row()
+            .items_start()
+            .gap(px(14.0))
+            .child(wheel)
+            .child(render_gr_hud(theme, &render.gr_triggers))
+    };
+
     div()
         .flex()
         .flex_col()
         .items_center()
         .gap(px(8.0))
         .child(header)
-        .child(wheel)
+        .child(body)
         .child(footer)
 }
 
+/// HUD lateral de rectificación GR: lista los triggers de direcciones
+/// primarias ordenados por orbe (los más cerrados arriba). El color va
+/// de rojo (orbe apretado) a gris (orbe ancho); las convergencias
+/// directo+converso llevan un marcador ✦ y un fondo resaltado.
+fn render_gr_hud(theme: &Theme, triggers: &[GrTrigger]) -> gpui::Div {
+    const SHOWN: usize = 20;
+    let event_count = triggers.iter().filter(|t| t.event).count();
+
+    let mut col = div()
+        .flex()
+        .flex_col()
+        .gap(px(2.0))
+        .w(px(238.0))
+        .p(px(10.0))
+        .rounded(px(8.0))
+        .bg(theme.bg_panel_alt.clone())
+        .border_1()
+        .border_color(theme.border);
+
+    col = col.child(
+        div()
+            .flex()
+            .flex_row()
+            .justify_between()
+            .items_center()
+            .child(
+                div()
+                    .text_size(px(12.0))
+                    .text_color(theme.fg_text)
+                    .child("Triggers GR"),
+            )
+            .child(
+                div()
+                    .text_size(px(10.0))
+                    .text_color(theme.fg_muted)
+                    .child(SharedString::from(format!(
+                        "{} · {} conv.",
+                        triggers.len(),
+                        event_count
+                    ))),
+            ),
+    );
+    col = col.child(
+        div()
+            .text_size(px(9.0))
+            .text_color(theme.fg_disabled)
+            .mb(px(4.0))
+            .child("rectificación · orbe ascendente"),
+    );
+
+    for t in triggers.iter().take(SHOWN) {
+        let color = if t.event {
+            hsla(0.0, 0.88, 0.64, 1.0)
+        } else {
+            gr_orb_color(t.orb_deg)
+        };
+        let marker = if t.event { "✦" } else { "·" };
+        let line = format!(
+            "{} {}{} → {}  {}",
+            marker,
+            planet_unicode(&t.promissor),
+            t.direction.short(),
+            gr_target_glyph(&t.natal_target),
+            format_orb(t.orb_deg),
+        );
+        let mut row = div()
+            .px(px(5.0))
+            .py(px(2.0))
+            .rounded(px(3.0))
+            .text_size(px(11.0))
+            .text_color(color)
+            .child(SharedString::from(line));
+        if t.event {
+            row = row.bg(with_alpha(hsla(0.0, 0.80, 0.50, 1.0), 0.16));
+        }
+        col = col.child(row);
+    }
+    if triggers.len() > SHOWN {
+        col = col.child(
+            div()
+                .text_size(px(9.0))
+                .text_color(theme.fg_disabled)
+                .mt(px(3.0))
+                .child(SharedString::from(format!(
+                    "+{} más",
+                    triggers.len() - SHOWN
+                ))),
+        );
+    }
+    col
+}
+
+/// Color de un trigger GR según su orbe: rojo intenso (orbe cerrado,
+/// contacto fuerte) que se desatura hacia gris al ensancharse. El
+/// orbe de referencia (gris pleno) es el orbe del HUD, 2°.
+fn gr_orb_color(orb_deg: f32) -> Hsla {
+    let t = (orb_deg / 2.0).clamp(0.0, 1.0);
+    let s = 0.82 + (0.10 - 0.82) * t;
+    let l = 0.62 + (0.52 - 0.62) * t;
+    hsla(0.0, s, l, 1.0)
+}
+
+/// Orbe en grados → texto compacto `D°MM'`.
+fn format_orb(orb_deg: f32) -> String {
+    let total_min = (orb_deg.abs() * 60.0).round() as i64;
+    format!("{}°{:02}'", total_min / 60, total_min % 60)
+}
+
+/// Glyph corto de un punto natal objetivo: ángulos como texto,
+/// cuerpos vía [`planet_unicode`].
+fn gr_target_glyph(name: &str) -> String {
+    match name {
+        "asc" => "Asc".to_string(),
+        "mc" => "MC".to_string(),
+        "desc" => "Dsc".to_string(),
+        "ic" => "IC".to_string(),
+        other => planet_unicode(other).to_string(),
+    }
+}
+
 /// 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/
@@ -1831,6 +1969,7 @@ fn paint_wheel(
     radii: Radii,
     visibility: &HashMap<LayerKind, bool>,
     hover_focus: Option<&str>,
+    gr_triggers: &[GrTrigger],
 ) {
     let (cx, cy) = bounds_center(bounds);
     let show = |k: LayerKind| visibility.get(&k).copied().unwrap_or(true);
@@ -2035,6 +2174,41 @@ fn paint_wheel(
                 }
             }
         }
+
+        // Resaltado de convergencias GR: por cada punto natal donde un
+        // trigger directo y otro converso coinciden dentro del
+        // micro-orbe, un eje brillante atraviesa la zona del dual-ring
+        // hasta el cinturón natal. Es la señal de rectificación — si la
+        // hora natal es correcta, el evento real cae sobre este eje.
+        let mut event_degs: Vec<f32> = gr_triggers
+            .iter()
+            .filter(|t| t.event)
+            .map(|t| t.natal_deg)
+            .collect();
+        event_degs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+        event_degs.dedup_by(|a, b| (*a - *b).abs() < 0.02);
+        if !event_degs.is_empty() {
+            let hot = hsla(0.0, 0.86, 0.60, 1.0);
+            let marker_r = (radii.sign_outer * 0.014).max(2.5);
+            for deg in event_degs {
+                paint_radial_line(
+                    window,
+                    cx,
+                    cy,
+                    deg,
+                    ascendant_deg,
+                    rot_offset_deg,
+                    radii.pd_converse,
+                    radii.houses_inner,
+                    with_alpha(hot, 0.92),
+                    2.6,
+                );
+                let (mx, my) =
+                    polar_to_screen(deg, ascendant_deg, rot_offset_deg, radii.bodies);
+                paint_glow(window, cx + mx, cy + my, marker_r * 2.0, hot);
+                fill_circle(window, cx + mx, cy + my, marker_r, hot);
+            }
+        }
     }
 
     // 3. Aspectos. Cada module_id usa su par de radios — natal-natal