tawasuyu

tawasuyu/brahman

Rust 321TOML 122JSON 14Markdown 114332 Definitions

feat(cosmobiologia): GR — scrubbing live de la edad con el jog-dial

Tercer y último incremento del Sistema GR: en modo GR (direcciones
primarias activas) el jog-dial deja de rotar el wheel y pasa a
scrubear la edad en vivo.

- canvas: CanvasState::gr_active() detecta el modo; on_jog_move emite
  CanvasEvent::GrAgeDelta (años por grado de jog, sensibilidad 0.1)
  en vez de rotar; on_jog_up no aplica snap de tiempo.
- shell: scrub_gr_age acumula el delta sobre target_age_years del
  módulo primary_directions, clampa a [0,120], sincroniza el slider
  del panel y recompone — los glifos dirigidos y el HUD se mueven en
  vivo bajo el cursor.

Con esto el Sistema GR queda completo: cómputo de triggers, resaltado
de convergencias, HUD de rectificación y scrubbing live.

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

ec111a2e2711ad6bd597a9a90112b5da990339df · by sergio · (5 months ago) · parent 363f401b7 · browse the tree at this commit

Semantic delta · 8

crates/apps/cosmobiologia/src/shell.rs

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

2 files · +67 −4

diff --git a/crates/apps/cosmobiologia/src/shell.rs b/crates/apps/cosmobiologia/src/shell.rs
index 96f31ec..52ce17f 100644
--- a/crates/apps/cosmobiologia/src/shell.rs
+++ b/crates/apps/cosmobiologia/src/shell.rs
@@ -1172,7 +1172,36 @@ impl Shell {
             CanvasEvent::ExportSvgRequested => {
                 self.export_current_to_svg();
             }
+            CanvasEvent::GrAgeDelta(delta) => {
+                self.scrub_gr_age(*delta, cx);
+            }
+        }
+    }
+
+    /// Scrubbing en vivo de la edad GR vía jog-dial. Acumula `delta`
+    /// sobre `target_age_years` del módulo `primary_directions`,
+    /// clampa a [0,120], sincroniza el slider del panel y recompone.
+    fn scrub_gr_age(&mut self, delta_years: f64, cx: &mut Context<Self>) {
+        if !module_enabled(&self.module_configs, "primary_directions") {
+            return;
         }
+        let current = self.module_age_or_current("primary_directions");
+        let next = (current + delta_years).clamp(0.0, 120.0);
+        if (next - current).abs() < 1e-6 {
+            return;
+        }
+        let entry = self
+            .module_configs
+            .entry("primary_directions".into())
+            .or_insert_with(|| serde_json::json!({}));
+        if let serde_json::Value::Object(map) = entry {
+            map.insert("target_age_years".into(), serde_json::json!(next));
+        }
+        self.panel.update(cx, |p, cx| {
+            p.set_slider("primary_directions", "target_age_years", next, cx)
+        });
+        self.persist_module("primary_directions");
+        self.render_current(cx);
     }
 
     /// Recompone la carta actual + escribe el SVG a un archivo en
diff --git a/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs b/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs
index 11a7d72..e9d6f18 100644
--- a/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs
+++ b/crates/modules/cosmobiologia/cosmobiologia-canvas/src/lib.rs
@@ -67,6 +67,10 @@ pub enum CanvasEvent {
     /// El usuario pidió exportar el render actual como SVG. El shell
     /// se encarga de escribir el archivo (la engine genera el string).
     ExportSvgRequested,
+    /// En modo GR (direcciones primarias activas) el jog-dial scrubea
+    /// la edad en vez del tiempo. Lleva el delta de edad en años; el
+    /// host lo acumula sobre `target_age_years` y recompone en vivo.
+    GrAgeDelta(f64),
 }
 
 // =====================================================================
@@ -236,8 +240,23 @@ impl CanvasState {
     pub fn is_layer_visible(&self, kind: LayerKind) -> bool {
         self.layer_visibility.get(&kind).copied().unwrap_or(true)
     }
+
+    /// `true` cuando hay un overlay de direcciones primarias activo.
+    /// En ese modo el jog-dial scrubea la edad GR en vez del tiempo.
+    fn gr_active(&self) -> bool {
+        matches!(
+            &self.mode,
+            CanvasMode::Wheel { render }
+                if render.layers.iter().any(|l| l.module_id == "pd_direct")
+        )
+    }
 }
 
+/// Sensibilidad del scrubbing GR: años de edad por grado de jog. A
+/// 0.1, una vuelta completa del dial barre 36 años — fino para
+/// explorar contactos sin perder rango.
+const GR_AGE_PER_DEG: f32 = 0.1;
+
 // =====================================================================
 // Widget
 // =====================================================================
@@ -408,6 +427,7 @@ impl AstrologyCanvas {
         bounds: Bounds<Pixels>,
         cx: &mut Context<'_, Self>,
     ) {
+        let gr = self.state.gr_active();
         let Some(jog) = self.state.drag_jog.as_mut() else {
             return;
         };
@@ -426,9 +446,17 @@ impl AstrologyCanvas {
         }
         jog.accumulated_delta_deg += delta;
         jog.last_screen_angle_deg = angle;
-        // Reflejo visual durante el drag (sin recomputar).
-        self.state.view_rotation_deg = jog.accumulated_delta_deg;
-        cx.notify();
+        let accumulated = jog.accumulated_delta_deg;
+        if gr {
+            // Modo GR: el jog scrubea la edad. No rota el wheel — el
+            // feedback es el movimiento de los glifos dirigidos cuando
+            // el shell recompone con la edad nueva.
+            cx.emit(CanvasEvent::GrAgeDelta((-delta * GR_AGE_PER_DEG) as f64));
+        } else {
+            // Reflejo visual durante el drag (sin recomputar).
+            self.state.view_rotation_deg = accumulated;
+            cx.notify();
+        }
     }
 
     /// Hit-test sobre body glyphs + house cusps. Para bodies: distancia
@@ -662,9 +690,15 @@ impl AstrologyCanvas {
     }
 
     fn on_jog_up(&mut self, cx: &mut Context<'_, Self>) {
+        let gr = self.state.gr_active();
         let Some(jog) = self.state.drag_jog.take() else {
             return;
         };
+        if gr {
+            // El scrub GR se aplicó en vivo durante el drag; al soltar
+            // no queda nada que confirmar.
+            return;
+        }
         // 1° de arco ≈ 4 minutos de tiempo sideral (15°/hora).
         // CW visual (delta negativa en nuestra convención) → tiempo
         // hacia adelante.
@@ -1641,7 +1675,7 @@ fn render_wheel(
                 .text_size(px(10.0))
                 .text_color(theme.fg_disabled)
                 .child(
-                    "[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [C]oords  ·  Ctrl+drag = tiempo  ·  [0] reset zoom  ·  [R] reset tiempo  ·  [S]vg",
+                    "[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [C]oords  ·  Ctrl+drag = tiempo/edad GR  ·  [0] reset zoom  ·  [R] reset tiempo  ·  [S]vg",
                 ),
         );