tawasuyu

tawasuyu/brahman

Rust 459TOML 177Markdown 49JSON 176207 Definitions

feat(tahuantinsuyu): coord labels con minutos + control en panel

- Precisión a minutos: `format_coord_compact` ahora emite
  "DD°MM'{signo}" (ej. "14°56'♈"). Trabaja en minutos enteros para
  evitar drift de floats acumulado, hace rollover correcto a través
  de bordes de signo (29°60' → 0° del siguiente) y wrap-around de
  ángulos negativos. 5 tests verdes:
  * 0° → "0°00'♈"
  * 14.9333° → "14°56'♈"
  * 29.9995° → "0°00'♉" (carry-over)
  * 270° → "0°00'♑"
  * -10° → "20°00'♓" (wrap)

- Toggle en panel: nuevo `Control::Toggle` "Coordenadas (grado°min')"
  en NatalModule, default ON, hotkey C. Sincronización bidireccional:
  panel → canvas via `set_show_coords` (idempotente, no emite),
  canvas → panel via nuevo evento `CanvasEvent::ShowCoordsChanged`
  que el shell traduce a `panel.set_toggle("natal","show_coords",…)`.
  Sin loop porque el setter no emite.

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

86c5fd8653dcff24b8c98a1822d15f444179831a · by sergio · (5 months ago) · parent 8ede06f8c · browse the tree at this commit

Semantic delta · 13

crates/apps/tahuantinsuyu/src/shell.rs

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

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

3 files · +86 −9

diff --git a/crates/apps/tahuantinsuyu/src/shell.rs b/crates/apps/tahuantinsuyu/src/shell.rs
index b20010e..d989afa 100644
--- a/crates/apps/tahuantinsuyu/src/shell.rs
+++ b/crates/apps/tahuantinsuyu/src/shell.rs
@@ -847,6 +847,14 @@ impl Shell {
                 self.panel
                     .update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
             }
+            CanvasEvent::ShowCoordsChanged(visible) => {
+                // Sync el toggle del panel para que coincida con la
+                // hotkey C. No persist — los coord labels son una
+                // preferencia visual, no parte del module_state.
+                self.panel.update(cx, |p, cx| {
+                    p.set_toggle("natal", "show_coords", *visible, cx)
+                });
+            }
             CanvasEvent::ChartRequested(_) => {
                 // Fase 7: doble click sobre un thumbnail abre la carta.
             }
@@ -920,6 +928,12 @@ impl Shell {
                     if let Some(k) = kind {
                         self.canvas
                             .update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx));
+                    } else if key == "show_coords" {
+                        // Coord labels viven en el canvas (no son una
+                        // capa pintada como otros show_*). Sync sin
+                        // recompose ni persist en module_state.
+                        self.canvas
+                            .update(cx, |c, cx| c.set_show_coords(bool_val, cx));
                     } else {
                         // Filtros: actualizar module_configs + recompose.
                         let entry = self
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
index 8d4d5eb..3d1cb0e 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
@@ -59,6 +59,9 @@ pub enum CanvasEvent {
     /// El usuario togggleó una capa via hotkey — el panel debería
     /// reflejarlo si quisiera mantenerse en sync.
     LayerVisibilityChanged { kind: LayerKind, visible: bool },
+    /// El usuario togggleó los coord labels via hotkey C. El panel
+    /// debe sincronizar el toggle "show_coords" del NatalModule.
+    ShowCoordsChanged(bool),
     /// El usuario pidió exportar el render actual como SVG. El shell
     /// se encarga de escribir el archivo (la engine genera el string).
     ExportSvgRequested,
@@ -297,8 +300,19 @@ impl AstrologyCanvas {
     }
 
     pub fn toggle_coords(&mut self, cx: &mut Context<'_, Self>) {
-        self.state.show_coords = !self.state.show_coords;
-        cx.notify();
+        let new_val = !self.state.show_coords;
+        self.set_show_coords(new_val, cx);
+        cx.emit(CanvasEvent::ShowCoordsChanged(new_val));
+    }
+
+    /// Setter idempotente — el shell lo usa para reflejar cambios del
+    /// panel sin disparar el `ShowCoordsChanged` (que iría en el otro
+    /// sentido y crearía un loop).
+    pub fn set_show_coords(&mut self, value: bool, cx: &mut Context<'_, Self>) {
+        if self.state.show_coords != value {
+            self.state.show_coords = value;
+            cx.notify();
+        }
     }
 
     /// Resetea zoom y pan a sus defaults (1.0 y 0,0). No toca rotation
@@ -2298,14 +2312,23 @@ fn planet_glyph(
         .child(text)
 }
 
-/// Formato compacto de un grado eclíptico: "DD°SS" donde SS es el
-/// glyph del signo zodiacal (♈♉♊…). Ej: 14.93° → "14°♈". Los
-/// minutos se omiten — la pill es pequeña y los grados enteros
-/// alcanzan para orientación visual. El tooltip muestra el detalle.
+/// Formato compacto con precisión de minutos: "DD°MM'{signo}" donde
+/// el signo es el glyph zodiacal (♈♉♊…). Ej: 14.93° → "14°56'♈".
+/// Los minutos se redondean al entero más cercano; si el redondeo
+/// excede 60, bumpea el grado y mantiene la representación canónica
+/// (29°60' → 30°00', que a su vez = 0° del signo siguiente — lo
+/// recalculamos para evitar mostrar "30°00'♈" en vez de "0°00'♉").
 fn format_coord_compact(deg: f32) -> String {
     let normalized = deg.rem_euclid(360.0);
-    let sign_idx = ((normalized / 30.0).floor() as usize) % 12;
-    let deg_in_sign = (normalized - (sign_idx as f32) * 30.0).floor() as i32;
+    let total_minutes = (normalized * 60.0).round() as i64;
+    // Carry-overs: 60' → siguiente grado; 30° → siguiente signo (eso
+    // ya está cubierto porque el total_minutes refleja la posición
+    // ABSOLUTA y volvemos a derivar sign + minutos del entero limpio).
+    let total_minutes = total_minutes.rem_euclid(360 * 60);
+    let sign_idx = (total_minutes / (30 * 60)) as usize % 12;
+    let within_sign = total_minutes - (sign_idx as i64) * 30 * 60;
+    let deg_int = (within_sign / 60) as i32;
+    let minutes = (within_sign % 60) as i32;
     let sign_glyph = match sign_idx {
         0 => "♈",
         1 => "♉",
@@ -2320,7 +2343,41 @@ fn format_coord_compact(deg: f32) -> String {
         10 => "♒",
         _ => "♓",
     };
-    format!("{}°{}", deg_in_sign, sign_glyph)
+    format!("{}°{:02}'{}", deg_int, minutes, sign_glyph)
+}
+
+#[cfg(test)]
+mod coord_tests {
+    use super::format_coord_compact;
+
+    #[test]
+    fn zero_aries() {
+        assert_eq!(format_coord_compact(0.0), "0°00'♈");
+    }
+
+    #[test]
+    fn fourteen_fiftysix_aries() {
+        // 14.9333° = 14° 56'
+        assert_eq!(format_coord_compact(14.933_3), "14°56'♈");
+    }
+
+    #[test]
+    fn rollover_to_taurus() {
+        // 29.9995° debería redondear a 30° y caer en 0°00'♉.
+        assert_eq!(format_coord_compact(29.9995), "0°00'♉");
+    }
+
+    #[test]
+    fn capricorn_anchor() {
+        // 270° = inicio de Capricornio.
+        assert_eq!(format_coord_compact(270.0), "0°00'♑");
+    }
+
+    #[test]
+    fn negative_wraps() {
+        // -10° = 350° = 20°00'♓.
+        assert_eq!(format_coord_compact(-10.0), "20°00'♓");
+    }
 }
 
 /// Pill pequeña con un coord ("14°♈") junto al glyph de un planeta
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
index a0948f9..295ad39 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-modules/src/lib.rs
@@ -222,6 +222,12 @@ pub mod natal {
                     default: true,
                     hotkey: Some("P".into()),
                 },
+                Control::Toggle {
+                    key: "show_coords".into(),
+                    label: "Coordenadas (grado°min')".into(),
+                    default: true,
+                    hotkey: Some("C".into()),
+                },
                 // Filtros de aspectos: cambian QUÉ se computa, no QUÉ
                 // se pinta del render. Recompose al togglear.
                 Control::Toggle {