From 86c5fd8653dcff24b8c98a1822d15f444179831a Mon Sep 17 00:00:00 2001 From: sergio Date: Mon, 18 May 2026 16:49:34 +0000 Subject: [PATCH] feat(tahuantinsuyu): coord labels con minutos + control en panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- crates/apps/tahuantinsuyu/src/shell.rs | 14 ++++ .../tahuantinsuyu-canvas/src/lib.rs | 75 ++++++++++++++++--- .../tahuantinsuyu-modules/src/lib.rs | 6 ++ 3 files changed, 86 insertions(+), 9 deletions(-) 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 {