feat(tahuantinsuyu): cluster shrink, label compacto, hover destacado con z-order

Cuatro ajustes finos al esquema visual de planetas natales/topo:

1. **Discos achicados en cluster**: glyphs en cluster compartido
   (≥2 miembros) llevan un factor adicional `0.86×` sobre el
   shrink residual. Visualmente quedan apenas más pequeños — al
   estar pegados, achicar un poco evita la sensación de
   "amontonamiento" sin perder el unicode.

2. **Pill compartida más chica + libre de "espacios negros"**:
   - Cálculo del ancho ahora usa `text.chars().count()` (era
     `text.len()` en bytes — los chars unicode astronómicos
     cuentan 3 bytes c/u y inflaban el ancho).
   - Mínimo de ancho bajado de `font*2.0` a `font*1.4` y
     padding lateral reducido. Pills con 1-3 chars ya no llevan
     "espacios en negro" que sobrescriben elementos vecinos.
   - Font del label compartido normal bajado a 9.0×s (era 10);
     el hovereado sube a 10×s. Diferencial claro.
   - Label individual también bajó a 8.5×s.

3. **Hover destacado**: nuevo "hovered_idx" identifica el glyph
   bajo el cursor (de `HoverInfo::Body`). El glyph hovereado se
   pinta al FINAL del árbol DOM — queda con z-order encima del
   resto. Border al color pleno (vs 0.85), disco 1.18× y font
   1.12× para destacarlo.

4. **Label del cluster hovereado destacado**: el cluster que
   contiene al planeta bajo el cursor se renderiza con `fg_text`
   (vs `fg_muted` para los demás) y font un punto más grande.

11 tests verdes (sin cambios — los affectados son del path de
render, no del cómputo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-18 19:07:47 +00:00
parent 121f19b915
commit 074d8bcbc8
@@ -1212,11 +1212,36 @@ fn render_wheel(
} }
} }
let shrink = (1.0 - residual * 0.30).clamp(0.60, 1.0); let shrink_residual = (1.0 - residual * 0.30).clamp(0.60, 1.0);
let disk_size = disk_size_base * shrink;
let font_size_eff = (font_size * shrink).max(11.0); // El hovered glyph y su cluster reciben tratamiento
// especial: lo postponemos para pintarlo al FINAL del
// árbol (queda por encima del resto = z-order), y le
// damos un border más fuerte. Su label cluster también
// se destaca (color fg_text en lugar de fg_muted, font
// un punto más grande).
let hovered_sym: Option<&str> = match hover {
Some(HoverInfo::Body { symbol, .. }) => Some(symbol.as_str()),
_ => None,
};
let hovered_idx: Option<usize> = hovered_sym.and_then(|sym| {
layer.glyphs.iter().position(|g| g.symbol == sym)
});
let hovered_cluster: Option<usize> = hovered_idx.map(|i| cluster_of[i]);
for (i, g) in layer.glyphs.iter().enumerate() { for (i, g) in layer.glyphs.iter().enumerate() {
if Some(i) == hovered_idx {
continue; // se pinta al final
}
// Achicar discos cuando el glyph está en cluster
// (≥2 miembros) — al estar pegados se ven mejor
// un poco más pequeños.
let cluster_size = clusters[cluster_of[i]].len();
let in_cluster_shrink = if cluster_size >= 2 { 0.86 } else { 1.0 };
let effective_shrink = shrink_residual * in_cluster_shrink;
let disk_size = disk_size_base * effective_shrink;
let font_size_eff = (font_size * effective_shrink).max(11.0);
let display_deg = display_degs[i]; let display_deg = display_degs[i];
let (x, y) = polar_to_screen(display_deg, asc, rot_offset, ring); let (x, y) = polar_to_screen(display_deg, asc, rot_offset, ring);
let color = with_alpha(planet_color(palette, &g.symbol), alpha); let color = with_alpha(planet_color(palette, &g.symbol), alpha);
@@ -1240,7 +1265,6 @@ fn render_wheel(
// Coord label individual: solo cuando el glyph // Coord label individual: solo cuando el glyph
// está SOLO en su cluster (≥2 ⇒ label compartido). // está SOLO en su cluster (≥2 ⇒ label compartido).
let cluster_size = clusters[cluster_of[i]].len();
if show_coords && (is_natal || is_topo) && cluster_size == 1 { if show_coords && (is_natal || is_topo) && cluster_size == 1 {
let coord = format_coord_compact(g.deg); let coord = format_coord_compact(g.deg);
let label_r = ring - disk_size * 1.3; let label_r = ring - disk_size * 1.3;
@@ -1252,20 +1276,21 @@ fn render_wheel(
coord.into(), coord.into(),
theme.fg_muted, theme.fg_muted,
halo_bg, halo_bg,
9.5 * s, 8.5 * s,
)); ));
} }
} }
// Label compartido para CADA cluster con ≥2 miembros. // Label compartido para CADA cluster con ≥2 miembros.
// Texto = símbolos concatenados + coord del centroide // El del cluster hovereado se destaca: color fg_text
// real. Posicionado sobre el centroide DISPLAY (donde // (vs fg_muted) y font un punto más grande.
// se ven los discos tras el shift).
if show_coords && (is_natal || is_topo) { if show_coords && (is_natal || is_topo) {
let disk_size_typical = disk_size_base * shrink_residual * 0.86;
for (ci, c) in clusters.iter().enumerate() { for (ci, c) in clusters.iter().enumerate() {
if c.len() < 2 { if c.len() < 2 {
continue; continue;
} }
let highlighted = Some(ci) == hovered_cluster;
let center_display_deg = display_centroids[ci]; let center_display_deg = display_centroids[ci];
let center_real_deg = cluster_centroids[ci]; let center_real_deg = cluster_centroids[ci];
let symbols: String = c let symbols: String = c
@@ -1275,17 +1300,68 @@ fn render_wheel(
.join(" "); .join(" ");
let coord = format_coord_compact(center_real_deg); let coord = format_coord_compact(center_real_deg);
let text = format!("{} {}", symbols, coord); let text = format!("{} {}", symbols, coord);
let label_r = ring - disk_size * 1.5; let label_r = ring - disk_size_typical * 1.5;
let (lx, ly) = polar_to_screen( let (lx, ly) = polar_to_screen(
center_display_deg, center_display_deg,
asc, asc,
rot_offset, rot_offset,
label_r, label_r,
); );
let (fg, font_sz) = if highlighted {
(theme.fg_text, 10.0 * s)
} else {
(theme.fg_muted, 9.0 * s)
};
wheel = wheel.child(coord_label( wheel = wheel.child(coord_label(
cx_center + lx, cx_center + lx,
cy_center + ly, cy_center + ly,
text.into(), text.into(),
fg,
halo_bg,
font_sz,
));
}
}
// Render del glyph hovered al FINAL: queda encima del
// resto en z-order. Disco un poco más grande y border
// más prominente para destacar.
if let Some(hi) = hovered_idx {
let g = &layer.glyphs[hi];
let display_deg = display_degs[hi];
let (x, y) = polar_to_screen(display_deg, asc, rot_offset, ring);
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
let mut glyph_text = planet_unicode(&g.symbol).to_string();
if g.retrograde {
glyph_text.push('ᴿ');
}
if let Some(marker) = &g.dignity_marker {
glyph_text.push_str(marker);
}
let disk_size = disk_size_base * shrink_residual * 1.18;
let font_size_eff = font_size * shrink_residual * 1.12;
wheel = wheel.child(planet_glyph(
cx_center + x,
cy_center + y,
disk_size,
font_size_eff,
glyph_text.into(),
color,
halo_bg,
color, // border al color pleno (no .85) — destaca
));
// Si el hovered no está en cluster compartido,
// pintamos su coord individual destacada acá.
let cluster_size = clusters[cluster_of[hi]].len();
if show_coords && (is_natal || is_topo) && cluster_size == 1 {
let coord = format_coord_compact(g.deg);
let label_r = ring - disk_size * 1.3;
let (lx, ly) =
polar_to_screen(display_deg, asc, rot_offset, label_r);
wheel = wheel.child(coord_label(
cx_center + lx,
cy_center + ly,
coord.into(),
theme.fg_text, theme.fg_text,
halo_bg, halo_bg,
10.0 * s, 10.0 * s,
@@ -2862,10 +2938,14 @@ fn coord_label(
halo_bg: Hsla, halo_bg: Hsla,
font_size: f32, font_size: f32,
) -> gpui::Div { ) -> gpui::Div {
// Estimación gruesa del ancho (caracteres × ~5.5 px a font 9.5). // Estimación del ancho basada en `chars().count()` (NO `text.len()`
// Suficiente para no recortar; el flex centra dentro. // — los chars unicode astronómicos cuentan 3 bytes pero ocupan
let w = (text.len() as f32 * (font_size * 0.58)).max(font_size * 2.0); // ~1 columna de fuente). Padding lateral muy pequeño en lugar de
let h = font_size + 6.0; // un mínimo grande: pills con 1-3 chars no llevan "espacios en
// negro" que sobrescriben elementos vecinos.
let char_count = text.chars().count() as f32;
let w = (char_count * font_size * 0.62 + font_size * 0.5).max(font_size * 1.4);
let h = font_size + 5.0;
div() div()
.absolute() .absolute()
.left(px(x - w / 2.0)) .left(px(x - w / 2.0))