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,
|
||||
/// Por-LayerKind: `true` = visible. Default = todo visible.
|
||||
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
|
||||
/// mouse no está sobre ningún cuerpo.
|
||||
pub hover: Option<HoverInfo>,
|
||||
@@ -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<Pixels>,
|
||||
modifiers: gpui::Modifiers,
|
||||
bounds: Bounds<Pixels>,
|
||||
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<LayerKind, bool>,
|
||||
show_coords: bool,
|
||||
hover: Option<&HoverInfo>,
|
||||
entity: gpui::Entity<AstrologyCanvas>,
|
||||
) -> 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
|
||||
|
||||
Reference in New Issue
Block a user