feat(tahuantinsuyu): distinción eclíptica/casas, coord labels, jog-dial con Ctrl
- Anillo de casas claramente distinto del dial zodiacal: nuevo
`house_ring_color(palette)` que toma `house_cusp` y le aplica
un hue shift de 140° en paletas con color (en BW devuelve el
color original — un shift cromático en monocromo es ruido sin
información). El sistema ascensional (casas) ya no se confunde
con el eclíptico (signos): dorado vs verde/teal.
- Coordenadas permanentes en planetas y cusps de casa: por
default visibles, togglean con hotkey `C` o desde el panel
vía `state.show_coords`. Cada planeta natal lleva una pill
pequeña afuera del disco con "DD°{signo}" (ej. "14°♈"); cada
cusp de casa lleva la misma pill por dentro del anillo
interior. Helpers nuevos: `format_coord_compact`,
`coord_label`.
- Jog-dial requiere Ctrl/Cmd para activar. Sin modifier, LMB
drag es siempre pan — sobre o fuera del anillo. Con modifier
+ LMB sobre el anillo se activa la rotación de tiempo (el
control de rectificación). Evita rotaciones accidentales al
navegar la rueda.
- Hint del info_row actualizado: incluye [C]oords y la
convención "Ctrl+drag = tiempo".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,10 @@ pub struct CanvasState {
|
|||||||
pub view_pan_y: f32,
|
pub view_pan_y: f32,
|
||||||
/// Por-LayerKind: `true` = visible. Default = todo visible.
|
/// Por-LayerKind: `true` = visible. Default = todo visible.
|
||||||
pub layer_visibility: HashMap<LayerKind, bool>,
|
pub layer_visibility: HashMap<LayerKind, bool>,
|
||||||
|
/// 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
|
/// Planeta hovered actualmente (para tooltip). `None` cuando el
|
||||||
/// mouse no está sobre ningún cuerpo.
|
/// mouse no está sobre ningún cuerpo.
|
||||||
pub hover: Option<HoverInfo>,
|
pub hover: Option<HoverInfo>,
|
||||||
@@ -215,6 +219,7 @@ impl Default for CanvasState {
|
|||||||
view_pan_x: 0.0,
|
view_pan_x: 0.0,
|
||||||
view_pan_y: 0.0,
|
view_pan_y: 0.0,
|
||||||
layer_visibility: HashMap::new(),
|
layer_visibility: HashMap::new(),
|
||||||
|
show_coords: true,
|
||||||
hover: None,
|
hover: None,
|
||||||
drag_jog: None,
|
drag_jog: None,
|
||||||
drag_pan: None,
|
drag_pan: None,
|
||||||
@@ -291,6 +296,11 @@ impl AstrologyCanvas {
|
|||||||
cx.notify();
|
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
|
/// 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.
|
/// ni time offset — esos son ortogonales y tienen su propio reset.
|
||||||
pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) {
|
pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) {
|
||||||
@@ -336,15 +346,24 @@ impl AstrologyCanvas {
|
|||||||
|
|
||||||
// ----- Internos: handlers de jog-dial -----
|
// ----- Internos: handlers de jog-dial -----
|
||||||
|
|
||||||
/// Despacha el LMB down entre jog-dial (sobre el anillo de signos)
|
/// Despacha el LMB down entre jog-dial y pan. El jog-dial es un
|
||||||
/// y pan (cualquier otra parte del canvas). El jog-dial es el
|
/// control "fuerte" (mueve el tiempo de la carta), así que se
|
||||||
/// control de rectificación de hora; el pan es navegación libre.
|
/// 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(
|
fn on_primary_down(
|
||||||
&mut self,
|
&mut self,
|
||||||
position: Point<Pixels>,
|
position: Point<Pixels>,
|
||||||
|
modifiers: gpui::Modifiers,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
cx: &mut Context<'_, Self>,
|
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 (cx_px, cy_px) = bounds_center(bounds);
|
||||||
let mx: f32 = position.x.into();
|
let mx: f32 = position.x.into();
|
||||||
let my: f32 = position.y.into();
|
let my: f32 = position.y.into();
|
||||||
@@ -361,6 +380,8 @@ impl AstrologyCanvas {
|
|||||||
accumulated_delta_deg: 0.0,
|
accumulated_delta_deg: 0.0,
|
||||||
});
|
});
|
||||||
} else {
|
} 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);
|
self.on_pan_down(position, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -650,6 +671,10 @@ impl AstrologyCanvas {
|
|||||||
self.reset_view(cx);
|
self.reset_view(cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
"c" | "C" => {
|
||||||
|
self.toggle_coords(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
"s" | "S" => {
|
"s" | "S" => {
|
||||||
cx.emit(CanvasEvent::ExportSvgRequested);
|
cx.emit(CanvasEvent::ExportSvgRequested);
|
||||||
return;
|
return;
|
||||||
@@ -753,6 +778,7 @@ impl Render for AstrologyCanvas {
|
|||||||
self.state.view_pan_x,
|
self.state.view_pan_x,
|
||||||
self.state.view_pan_y,
|
self.state.view_pan_y,
|
||||||
&self.state.layer_visibility,
|
&self.state.layer_visibility,
|
||||||
|
self.state.show_coords,
|
||||||
self.state.hover.as_ref(),
|
self.state.hover.as_ref(),
|
||||||
entity,
|
entity,
|
||||||
),
|
),
|
||||||
@@ -874,6 +900,7 @@ fn render_wheel(
|
|||||||
view_pan_x: f32,
|
view_pan_x: f32,
|
||||||
view_pan_y: f32,
|
view_pan_y: f32,
|
||||||
layer_visibility: &HashMap<LayerKind, bool>,
|
layer_visibility: &HashMap<LayerKind, bool>,
|
||||||
|
show_coords: bool,
|
||||||
hover: Option<&HoverInfo>,
|
hover: Option<&HoverInfo>,
|
||||||
entity: gpui::Entity<AstrologyCanvas>,
|
entity: gpui::Entity<AstrologyCanvas>,
|
||||||
) -> gpui::Div {
|
) -> gpui::Div {
|
||||||
@@ -935,8 +962,9 @@ fn render_wheel(
|
|||||||
}
|
}
|
||||||
match ev.button {
|
match ev.button {
|
||||||
MouseButton::Left => {
|
MouseButton::Left => {
|
||||||
|
let mods = ev.modifiers;
|
||||||
entity_d.update(cx, |this, cx| {
|
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 => {
|
MouseButton::Middle => {
|
||||||
@@ -999,6 +1027,10 @@ fn render_wheel(
|
|||||||
// por view_scale para que el zoom afecte uniformemente todo el
|
// por view_scale para que el zoom afecte uniformemente todo el
|
||||||
// contenido visual del wheel, no solo la geometría del canvas.
|
// contenido visual del wheel, no solo la geometría del canvas.
|
||||||
let s = view_scale;
|
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.
|
// Sign glyphs.
|
||||||
if visible.get(&LayerKind::SignDial).copied().unwrap_or(true) {
|
if visible.get(&LayerKind::SignDial).copied().unwrap_or(true) {
|
||||||
let sign_ring_mid = (radii.sign_outer + radii.sign_inner) / 2.0;
|
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) {
|
if visible.get(&LayerKind::Houses).copied().unwrap_or(true) {
|
||||||
let house_label_r = (radii.houses_outer + radii.houses_inner) / 2.0;
|
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 {
|
for layer in &render.layers {
|
||||||
if matches!(layer.kind, LayerKind::Houses) {
|
if matches!(layer.kind, LayerKind::Houses) {
|
||||||
for g in &layer.glyphs {
|
for g in &layer.glyphs {
|
||||||
@@ -1034,8 +1067,25 @@ fn render_wheel(
|
|||||||
16.0 * s,
|
16.0 * s,
|
||||||
11.0 * s,
|
11.0 * s,
|
||||||
format!("{}", h).into(),
|
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
|
// solar_arc) en sus rings, ambos con disco-halo para legibilidad
|
||||||
// contra cualquier fondo. El natal lleva un tamaño un poco mayor
|
// contra cualquier fondo. El natal lleva un tamaño un poco mayor
|
||||||
// que los overlays para que se lea como "el cuerpo principal".
|
// 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) {
|
if visible.get(&LayerKind::Bodies).copied().unwrap_or(true) {
|
||||||
for layer in &render.layers {
|
for layer in &render.layers {
|
||||||
if matches!(layer.kind, LayerKind::Bodies) {
|
if matches!(layer.kind, LayerKind::Bodies) {
|
||||||
@@ -1075,6 +1124,26 @@ fn render_wheel(
|
|||||||
halo_bg,
|
halo_bg,
|
||||||
with_alpha(color, 0.85),
|
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()
|
div()
|
||||||
.text_size(px(10.0))
|
.text_size(px(10.0))
|
||||||
.text_color(theme.fg_disabled)
|
.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
|
// 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 +
|
// 2. Casas — doble anillo (inner + outer) + cusps radiales +
|
||||||
// énfasis Asc/IC/Desc/MC. La doble línea vuelve a la zona de
|
// é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) {
|
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_outer, 1.1, house_color, theme);
|
||||||
stroke_circle_3d(window, cx, cy, radii.houses_inner, 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 {
|
let color = if is_angle {
|
||||||
palette.angle_highlight
|
palette.angle_highlight
|
||||||
} else {
|
} else {
|
||||||
with_alpha(palette.house_cusp, 0.7)
|
with_alpha(house_base, 0.75)
|
||||||
};
|
};
|
||||||
let width = if is_angle { 2.0 } else { 0.8 };
|
let width = if is_angle { 2.0 } else { 0.8 };
|
||||||
paint_radial_line(
|
paint_radial_line(
|
||||||
@@ -2223,6 +2298,62 @@ fn planet_glyph(
|
|||||||
.child(text)
|
.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
|
/// 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
|
/// 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
|
/// 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)
|
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
|
/// Stroke con efecto embossed: 3 trazos concéntricos. El highlight va
|
||||||
/// 0.7 px hacia el centro con luminancia subida; el principal en `r`;
|
/// 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
|
/// el shadow 0.7 px hacia afuera con luminancia bajada. La dirección
|
||||||
|
|||||||
Reference in New Issue
Block a user