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>
This commit is contained in:
@@ -847,6 +847,14 @@ impl Shell {
|
|||||||
self.panel
|
self.panel
|
||||||
.update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
|
.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(_) => {
|
CanvasEvent::ChartRequested(_) => {
|
||||||
// Fase 7: doble click sobre un thumbnail abre la carta.
|
// Fase 7: doble click sobre un thumbnail abre la carta.
|
||||||
}
|
}
|
||||||
@@ -920,6 +928,12 @@ impl Shell {
|
|||||||
if let Some(k) = kind {
|
if let Some(k) = kind {
|
||||||
self.canvas
|
self.canvas
|
||||||
.update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx));
|
.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 {
|
} else {
|
||||||
// Filtros: actualizar module_configs + recompose.
|
// Filtros: actualizar module_configs + recompose.
|
||||||
let entry = self
|
let entry = self
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ pub enum CanvasEvent {
|
|||||||
/// El usuario togggleó una capa via hotkey — el panel debería
|
/// El usuario togggleó una capa via hotkey — el panel debería
|
||||||
/// reflejarlo si quisiera mantenerse en sync.
|
/// reflejarlo si quisiera mantenerse en sync.
|
||||||
LayerVisibilityChanged { kind: LayerKind, visible: bool },
|
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
|
/// El usuario pidió exportar el render actual como SVG. El shell
|
||||||
/// se encarga de escribir el archivo (la engine genera el string).
|
/// se encarga de escribir el archivo (la engine genera el string).
|
||||||
ExportSvgRequested,
|
ExportSvgRequested,
|
||||||
@@ -297,8 +300,19 @@ impl AstrologyCanvas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_coords(&mut self, cx: &mut Context<'_, Self>) {
|
pub fn toggle_coords(&mut self, cx: &mut Context<'_, Self>) {
|
||||||
self.state.show_coords = !self.state.show_coords;
|
let new_val = !self.state.show_coords;
|
||||||
cx.notify();
|
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
|
/// Resetea zoom y pan a sus defaults (1.0 y 0,0). No toca rotation
|
||||||
@@ -2298,14 +2312,23 @@ fn planet_glyph(
|
|||||||
.child(text)
|
.child(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formato compacto de un grado eclíptico: "DD°SS" donde SS es el
|
/// Formato compacto con precisión de minutos: "DD°MM'{signo}" donde
|
||||||
/// glyph del signo zodiacal (♈♉♊…). Ej: 14.93° → "14°♈". Los
|
/// el signo es el glyph zodiacal (♈♉♊…). Ej: 14.93° → "14°56'♈".
|
||||||
/// minutos se omiten — la pill es pequeña y los grados enteros
|
/// Los minutos se redondean al entero más cercano; si el redondeo
|
||||||
/// alcanzan para orientación visual. El tooltip muestra el detalle.
|
/// 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 {
|
fn format_coord_compact(deg: f32) -> String {
|
||||||
let normalized = deg.rem_euclid(360.0);
|
let normalized = deg.rem_euclid(360.0);
|
||||||
let sign_idx = ((normalized / 30.0).floor() as usize) % 12;
|
let total_minutes = (normalized * 60.0).round() as i64;
|
||||||
let deg_in_sign = (normalized - (sign_idx as f32) * 30.0).floor() as i32;
|
// 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 {
|
let sign_glyph = match sign_idx {
|
||||||
0 => "♈",
|
0 => "♈",
|
||||||
1 => "♉",
|
1 => "♉",
|
||||||
@@ -2320,7 +2343,41 @@ fn format_coord_compact(deg: f32) -> String {
|
|||||||
10 => "♒",
|
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
|
/// Pill pequeña con un coord ("14°♈") junto al glyph de un planeta
|
||||||
|
|||||||
@@ -222,6 +222,12 @@ pub mod natal {
|
|||||||
default: true,
|
default: true,
|
||||||
hotkey: Some("P".into()),
|
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É
|
// Filtros de aspectos: cambian QUÉ se computa, no QUÉ
|
||||||
// se pinta del render. Recompose al togglear.
|
// se pinta del render. Recompose al togglear.
|
||||||
Control::Toggle {
|
Control::Toggle {
|
||||||
|
|||||||
Reference in New Issue
Block a user