diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs index a18161a..8d4d5eb 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs @@ -131,6 +131,10 @@ pub struct CanvasState { pub view_pan_y: f32, /// Por-LayerKind: `true` = visible. Default = todo visible. pub layer_visibility: HashMap, + /// Indicadores de grado al lado de cada planeta y cusp de casa. + /// Default `true` — el usuario los espera ver para leer la + /// carta. Se togglean con `C` (Coords) o desde el panel. + pub show_coords: bool, /// Planeta hovered actualmente (para tooltip). `None` cuando el /// mouse no está sobre ningún cuerpo. pub hover: Option, @@ -215,6 +219,7 @@ impl Default for CanvasState { view_pan_x: 0.0, view_pan_y: 0.0, layer_visibility: HashMap::new(), + show_coords: true, hover: None, drag_jog: None, drag_pan: None, @@ -291,6 +296,11 @@ impl AstrologyCanvas { cx.notify(); } + pub fn toggle_coords(&mut self, cx: &mut Context<'_, Self>) { + self.state.show_coords = !self.state.show_coords; + cx.notify(); + } + /// Resetea zoom y pan a sus defaults (1.0 y 0,0). No toca rotation /// ni time offset — esos son ortogonales y tienen su propio reset. pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) { @@ -336,15 +346,24 @@ impl AstrologyCanvas { // ----- Internos: handlers de jog-dial ----- - /// Despacha el LMB down entre jog-dial (sobre el anillo de signos) - /// y pan (cualquier otra parte del canvas). El jog-dial es el - /// control de rectificación de hora; el pan es navegación libre. + /// Despacha el LMB down entre jog-dial y pan. El jog-dial es un + /// control "fuerte" (mueve el tiempo de la carta), así que se + /// activa SOLO con modifier Ctrl/Cmd + click sobre el anillo de + /// signos — sin modifier es siempre pan, incluso sobre el anillo, + /// para que no haya rotaciones accidentales al manipular la + /// rueda. fn on_primary_down( &mut self, position: Point, + modifiers: gpui::Modifiers, bounds: Bounds, cx: &mut Context<'_, Self>, ) { + // Sin modifier: pan, sin importar dónde caiga el click. + if !(modifiers.control || modifiers.platform) { + self.on_pan_down(position, cx); + return; + } let (cx_px, cy_px) = bounds_center(bounds); let mx: f32 = position.x.into(); let my: f32 = position.y.into(); @@ -361,6 +380,8 @@ impl AstrologyCanvas { accumulated_delta_deg: 0.0, }); } else { + // Ctrl+click fuera del anillo: pan también — el modifier + // habilita el jog-dial pero no impide la navegación. self.on_pan_down(position, cx); } } @@ -650,6 +671,10 @@ impl AstrologyCanvas { self.reset_view(cx); return; } + "c" | "C" => { + self.toggle_coords(cx); + return; + } "s" | "S" => { cx.emit(CanvasEvent::ExportSvgRequested); return; @@ -753,6 +778,7 @@ impl Render for AstrologyCanvas { self.state.view_pan_x, self.state.view_pan_y, &self.state.layer_visibility, + self.state.show_coords, self.state.hover.as_ref(), entity, ), @@ -874,6 +900,7 @@ fn render_wheel( view_pan_x: f32, view_pan_y: f32, layer_visibility: &HashMap, + show_coords: bool, hover: Option<&HoverInfo>, entity: gpui::Entity, ) -> gpui::Div { @@ -935,8 +962,9 @@ fn render_wheel( } match ev.button { MouseButton::Left => { + let mods = ev.modifiers; entity_d.update(cx, |this, cx| { - this.on_primary_down(ev.position, bounds, cx) + this.on_primary_down(ev.position, mods, bounds, cx) }); } MouseButton::Middle => { @@ -999,6 +1027,10 @@ fn render_wheel( // por view_scale para que el zoom afecte uniformemente todo el // contenido visual del wheel, no solo la geometría del canvas. let s = view_scale; + // Color del halo para los discos detrás de glyphs y pills — se + // calcula una sola vez, lo usan planetas, casas, ASC/MC y los + // coord labels. + let halo_bg = glyph_halo(theme); // Sign glyphs. if visible.get(&LayerKind::SignDial).copied().unwrap_or(true) { let sign_ring_mid = (radii.sign_outer + radii.sign_inner) / 2.0; @@ -1020,9 +1052,10 @@ fn render_wheel( } } - // House numbers. + // House numbers + (opcional) coord del cusp. if visible.get(&LayerKind::Houses).copied().unwrap_or(true) { let house_label_r = (radii.houses_outer + radii.houses_inner) / 2.0; + let house_label_color = house_ring_color(palette); for layer in &render.layers { if matches!(layer.kind, LayerKind::Houses) { for g in &layer.glyphs { @@ -1034,8 +1067,25 @@ fn render_wheel( 16.0 * s, 11.0 * s, format!("{}", h).into(), - palette.house_cusp, + house_label_color, )); + // Coord del cusp justo dentro del anillo de + // casas — los grados se imprimen en una pill + // pequeña pegada al radio del cusp. + if show_coords { + let coord = format_coord_compact(g.deg); + let label_r = radii.houses_inner - 8.0 * s; + let (lx, ly) = + polar_to_screen(g.deg, asc, rot_offset, label_r); + wheel = wheel.child(coord_label( + cx_center + lx, + cy_center + ly, + coord.into(), + theme.fg_muted, + halo_bg, + 8.5 * s, + )); + } } } } @@ -1046,7 +1096,6 @@ fn render_wheel( // solar_arc) en sus rings, ambos con disco-halo para legibilidad // contra cualquier fondo. El natal lleva un tamaño un poco mayor // que los overlays para que se lea como "el cuerpo principal". - let halo_bg = glyph_halo(theme); if visible.get(&LayerKind::Bodies).copied().unwrap_or(true) { for layer in &render.layers { if matches!(layer.kind, LayerKind::Bodies) { @@ -1075,6 +1124,26 @@ fn render_wheel( halo_bg, with_alpha(color, 0.85), )); + + // Coord label: grado dentro del signo + glyph del + // signo, pintado justo afuera del disco del + // planeta (radialmente). Sólo en natal (los + // overlays ya cargan info en su badge / tooltip). + if show_coords && is_natal { + let coord = format_coord_compact(g.deg); + let label_r = ring + disk_size * 0.7; + let (lx, ly) = polar_to_screen(g.deg, asc, rot_offset, label_r); + wheel = wheel.child( + coord_label( + cx_center + lx, + cy_center + ly, + coord.into(), + theme.fg_muted, + halo_bg, + 9.5 * s, + ), + ); + } } } } @@ -1324,7 +1393,9 @@ fn render_wheel( div() .text_size(px(10.0)) .text_color(theme.fg_disabled) - .child("[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [S]vg [R]eset"), + .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", + ), ); // Badges de overlays activos. Cada uno se pinta como pill con @@ -1633,9 +1704,13 @@ fn paint_wheel( // 2. Casas — doble anillo (inner + outer) + cusps radiales + // énfasis Asc/IC/Desc/MC. La doble línea vuelve a la zona de - // casas una "corona" claramente identificable contra el resto. + // casas una "corona" claramente identificable. Color derivado + // de `house_cusp` con un hue shift para que el sistema + // ascensional (casas) se distinga visualmente del eclíptico + // (dial zodiacal) que va en dorado. if show(LayerKind::Houses) { - let house_color = with_alpha(palette.house_cusp, 0.85); + let house_base = house_ring_color(palette); + let house_color = with_alpha(house_base, 0.85); stroke_circle_3d(window, cx, cy, radii.houses_outer, 1.1, house_color, theme); stroke_circle_3d(window, cx, cy, radii.houses_inner, 1.1, house_color, theme); @@ -1647,7 +1722,7 @@ fn paint_wheel( let color = if is_angle { palette.angle_highlight } else { - with_alpha(palette.house_cusp, 0.7) + with_alpha(house_base, 0.75) }; let width = if is_angle { 2.0 } else { 0.8 }; paint_radial_line( @@ -2223,6 +2298,62 @@ 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. +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 sign_glyph = match sign_idx { + 0 => "♈", + 1 => "♉", + 2 => "♊", + 3 => "♋", + 4 => "♌", + 5 => "♍", + 6 => "♎", + 7 => "♏", + 8 => "♐", + 9 => "♑", + 10 => "♒", + _ => "♓", + }; + format!("{}°{}", deg_in_sign, sign_glyph) +} + +/// Pill pequeña con un coord ("14°♈") junto al glyph de un planeta +/// o cusp. Fondo halo + texto fg_muted, padding mínimo para no +/// saturar la rueda con etiquetas grandes. +fn coord_label( + x: f32, + y: f32, + text: SharedString, + fg: Hsla, + halo_bg: Hsla, + font_size: f32, +) -> gpui::Div { + // Estimación gruesa del ancho (caracteres × ~5.5 px a font 9.5). + // Suficiente para no recortar; el flex centra dentro. + let w = (text.len() as f32 * (font_size * 0.58)).max(font_size * 2.0); + let h = font_size + 6.0; + div() + .absolute() + .left(px(x - w / 2.0)) + .top(px(y - h / 2.0)) + .w(px(w)) + .h(px(h)) + .flex() + .items_center() + .justify_center() + .rounded(px(h / 2.0)) + .bg(halo_bg) + .text_size(px(font_size)) + .text_color(fg) + .child(text) +} + /// Color HSL semi-opaco para los halos de los glyphs — derivado del /// theme. En dark va casi negro; en light casi blanco. Alpha alta para /// que el char quede legible contra cualquier cosa que haya detrás @@ -2246,6 +2377,29 @@ fn adjust_luma(c: Hsla, delta: f32) -> Hsla { hsla(c.h, c.s, (c.l + delta).clamp(0.0, 1.0), c.a) } +/// Devuelve `c` con el hue desplazado `delta_deg` grados sobre el +/// círculo cromático (wrap a [0,1] en la escala normalizada de gpui). +/// Usado para derivar el color del anillo de casas desde el del dial +/// zodiacal — los dos sistemas (eclíptica vs ascensional) deben +/// distinguirse a primera vista pero compartir "familia" cromática. +fn shift_hue(c: Hsla, delta_deg: f32) -> Hsla { + let new_h = (c.h + delta_deg / 360.0).rem_euclid(1.0); + hsla(new_h, c.s, c.l, c.a) +} + +/// Color para los anillos del sistema de casas (ascensional). En +/// paletas con color, lo derivamos de `house_cusp` con un hue shift +/// de ~140° para diferenciar de la eclíptica (que va con el dorado +/// de `dial_ring`). En BW devolvemos `house_cusp` tal cual — un +/// shift cromático en monocromo es ruido sin información. +fn house_ring_color(palette: &AstroPalette) -> Hsla { + if palette.is_monochrome() { + palette.house_cusp + } else { + shift_hue(palette.house_cusp, 140.0) + } +} + /// Stroke con efecto embossed: 3 trazos concéntricos. El highlight va /// 0.7 px hacia el centro con luminancia subida; el principal en `r`; /// el shadow 0.7 px hacia afuera con luminancia bajada. La dirección