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>
diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
index 334396e..54ab5be 100644
--- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
+++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs
@@ -1212,11 +1212,36 @@ fn render_wheel(
}
}
- let shrink = (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);
+ let shrink_residual = (1.0 - residual * 0.30).clamp(0.60, 1.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() {
+ 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 (x, y) = polar_to_screen(display_deg, asc, rot_offset, ring);
let color = with_alpha(planet_color(palette, &g.symbol), alpha);
@@ -1240,7 +1265,6 @@ fn render_wheel(
// Coord label individual: solo cuando el glyph
// 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 {
let coord = format_coord_compact(g.deg);
let label_r = ring - disk_size * 1.3;
@@ -1252,20 +1276,21 @@ fn render_wheel(
coord.into(),
theme.fg_muted,
halo_bg,
- 9.5 * s,
+ 8.5 * s,
));
}
}
// Label compartido para CADA cluster con ≥2 miembros.
- // Texto = símbolos concatenados + coord del centroide
- // real. Posicionado sobre el centroide DISPLAY (donde
- // se ven los discos tras el shift).
+ // El del cluster hovereado se destaca: color fg_text
+ // (vs fg_muted) y font un punto más grande.
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() {
if c.len() < 2 {
continue;
}
+ let highlighted = Some(ci) == hovered_cluster;
let center_display_deg = display_centroids[ci];
let center_real_deg = cluster_centroids[ci];
let symbols: String = c
@@ -1275,17 +1300,68 @@ fn render_wheel(
.join(" ");
let coord = format_coord_compact(center_real_deg);
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(
center_display_deg,
asc,
rot_offset,
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(
cx_center + lx,
cy_center + ly,
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,
halo_bg,
10.0 * s,
@@ -2862,10 +2938,14 @@ fn coord_label(
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;
+ // Estimación del ancho basada en `chars().count()` (NO `text.len()`
+ // — los chars unicode astronómicos cuentan 3 bytes pero ocupan
+ // ~1 columna de fuente). Padding lateral muy pequeño en lugar de
+ // 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()
.absolute()
.left(px(x - w / 2.0))