From a0f67fd86f4375a1d4e1825c2c869805175c9a16 Mon Sep 17 00:00:00 2001 From: sergio Date: Mon, 18 May 2026 18:51:00 +0000 Subject: [PATCH] fix(tahuantinsuyu): spread no propaga + label lejos del disco + glyph legible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tres bugs en la pasada anterior de anti-solapamiento: 1. **Empuje propagado en cadena**: el spread greedy aplicado a cada glyph movía planetas lejanos cuando un cluster denso los empujaba. Ejemplo reportado: planetas a 9° y 10° (conjunción real) terminaban moviéndose hacia el 26° por la propagación simétrica del empuje. Solución: spread en dos pasos. * `find_clusters` con threshold `min(4°, disk_angular*0.5)` agrupa solo los que realmente están en conjunción cerrada. Dentro del cluster los glyphs SE QUEDAN en sus pos reales — dos planetas a 1° se ven a 1° (sus discos se rozan, refleja la geometría astrológica). * `spread_angles` se aplica SOLO a los **centroides** de los clusters, con threshold = ancho angular del disco. El empuje queda contenido a la vecindad inmediata; planetas lejos del cluster no se mueven. * Cada glyph hereda el shift de su cluster (centroide displayed − centroide real, wrap a ±180°). 2. **Label pisaba al planeta**: `label_r = ring - disk*0.7` dejaba solo ~2 px entre el borde del disco y la pill. Movido a `ring - disk*1.3` para individuales y `ring - disk*1.5` para clusters compartidos. Gap visual ~12 px. 3. **Símbolo se perdía en clusters densos**: shrink agresivo (0.45 sobre residual) achicaba el font por debajo del umbral legible del unicode astronómico. Bajado a 0.30, piso del shrink subido a 0.60×, y piso absoluto del font a 11 px. 4. Threshold de label compartido bajado a ≥2 miembros (era ≥3). En astrología, dos planetas en conjunción ya cuentan como un stellium funcional y se beneficiarían del label combinado. Tests: 10 verdes (5 spread + 5 coord). Co-Authored-By: Claude Opus 4.7 --- .../tahuantinsuyu-canvas/src/lib.rs | 178 ++++++++++++------ 1 file changed, 121 insertions(+), 57 deletions(-) diff --git a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs index cf6b685..e7a4905 100644 --- a/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs +++ b/crates/modules/tahuantinsuyu/tahuantinsuyu-canvas/src/lib.rs @@ -1155,33 +1155,79 @@ fn render_wheel( 22.0 }) * s; - // Anti-solapamiento: aplicamos `spread_angles` con el - // threshold derivado del ancho del LABEL (pill más - // grande que el disco). Si el spread no puede - // converger, `residual` se acerca a 1 y achicamos - // los discos. Los planetas siguen en su grado real - // — solo el display angular se ajusta. - let label_w_est = 38.0 * s; - let min_sep_deg = (label_w_est / (std::f32::consts::TAU * ring)) * 360.0; - let raw_degs: Vec = - layer.glyphs.iter().map(|g| g.deg).collect(); - let (display_degs, residual) = spread_angles(&raw_degs, min_sep_deg); - let shrink = (1.0 - residual * 0.45).clamp(0.55, 1.0); - let disk_size = disk_size_base * shrink; - let font_size_eff = font_size * shrink; + // Anti-solapamiento en dos pasos para evitar que el + // empuje se propague en cadena (un cluster denso no + // debe mover planetas que estaban lejos): + // + // 1) `find_clusters` agrupa los que están dentro de + // `cluster_thresh` (~4° real); dentro del cluster + // los glyphs se mantienen en sus posiciones REALES + // — los discos pueden tocarse, eso refleja la + // verdadera geometría astrológica. + // 2) `spread_angles` se aplica SOLO a los centroides + // de los clusters, con threshold = ancho angular + // del disco. El empuje queda contenido a la + // vecindad inmediata; planetas lejos no se mueven. + let raw_degs: Vec = layer.glyphs.iter().map(|g| g.deg).collect(); + let disk_angular_deg = + (disk_size_base / (std::f32::consts::TAU * ring)) * 360.0; + let cluster_thresh = 4.0_f32.min(disk_angular_deg * 0.5); + let clusters = find_clusters(&raw_degs, cluster_thresh); - // Cluster detection sobre grados REALES (no display). - // Clusters de ≥3 planetas en menos de 2.5° comparten - // un solo label en su centroide; los individuales y - // los duos llevan su label propio. - let clusters = find_clusters(&raw_degs, 2.5); + // Centroide circular de cada cluster (en grados reales). + let cluster_centroids: Vec = clusters + .iter() + .map(|c| { + let mut sx = 0.0_f32; + let mut sy = 0.0_f32; + for &idx in c { + let a = raw_degs[idx].to_radians(); + sx += a.cos(); + sy += a.sin(); + } + sy.atan2(sx).to_degrees().rem_euclid(360.0) + }) + .collect(); + + // Spread entre clusters: separación mínima ≈ ancho + // angular del disco (sin holgura, los discos se rozan). + let between_min_sep = disk_angular_deg; + let (display_centroids, residual) = + spread_angles(&cluster_centroids, between_min_sep); + + // Shift por cluster = displayed - original (wrap a ±180°). + let cluster_shifts: Vec = clusters + .iter() + .enumerate() + .map(|(ci, _)| { + let mut d = display_centroids[ci] - cluster_centroids[ci]; + if d > 180.0 { + d -= 360.0; + } + if d < -180.0 { + d += 360.0; + } + d + }) + .collect(); + + // Asignar el shift a cada glyph según su cluster. let mut cluster_of = vec![0usize; layer.glyphs.len()]; + let mut display_degs = vec![0.0_f32; layer.glyphs.len()]; for (ci, c) in clusters.iter().enumerate() { - for &gi in c { - cluster_of[gi] = ci; + for &idx in c { + cluster_of[idx] = ci; + display_degs[idx] = + (raw_degs[idx] + cluster_shifts[ci]).rem_euclid(360.0); } } + // Shrink suave + piso de font para mantener legibilidad + // del unicode dentro del disco. + 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); + for (i, g) in layer.glyphs.iter().enumerate() { let display_deg = display_degs[i]; let (x, y) = polar_to_screen(display_deg, asc, rot_offset, ring); @@ -1204,14 +1250,12 @@ fn render_wheel( with_alpha(color, 0.85), )); - // Coord label individual: solo cuando el cluster - // del glyph es chico (<3 miembros). Para clusters - // grandes el label se pinta una sola vez abajo, - // compartido por todos los miembros. + // 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 < 3 { + if show_coords && (is_natal || is_topo) && cluster_size == 1 { let coord = format_coord_compact(g.deg); - let label_r = ring - disk_size * 0.7; + 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( @@ -1220,38 +1264,22 @@ fn render_wheel( coord.into(), theme.fg_muted, halo_bg, - 9.5 * s * shrink, + 9.5 * s, )); } } - // Label compartido para clusters densos (≥3 glyphs en - // <2.5° reales). Texto = symbols concatenados + - // coord del centroide. Posicionado sobre el centroide - // de los display_degs. + // 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). if show_coords && (is_natal || is_topo) { - for c in &clusters { - if c.len() < 3 { + for (ci, c) in clusters.iter().enumerate() { + if c.len() < 2 { continue; } - // Centroide del display (promedio circular). - let mut sx = 0.0_f32; - let mut sy = 0.0_f32; - for &idx in c { - let a = display_degs[idx].to_radians(); - sx += a.cos(); - sy += a.sin(); - } - let center_display_deg = sy.atan2(sx).to_degrees().rem_euclid(360.0); - // Texto: ☉ ☿ ♀ + coord del centroide real - let mut sx_r = 0.0_f32; - let mut sy_r = 0.0_f32; - for &idx in c { - let a = layer.glyphs[idx].deg.to_radians(); - sx_r += a.cos(); - sy_r += a.sin(); - } - let center_real_deg = sy_r.atan2(sx_r).to_degrees().rem_euclid(360.0); + let center_display_deg = display_centroids[ci]; + let center_real_deg = cluster_centroids[ci]; let symbols: String = c .iter() .map(|&idx| planet_unicode(&layer.glyphs[idx].symbol)) @@ -1259,7 +1287,7 @@ fn render_wheel( .join(" "); let coord = format_coord_compact(center_real_deg); let text = format!("{} {}", symbols, coord); - let label_r = ring - disk_size * 1.3; + let label_r = ring - disk_size * 1.5; let (lx, ly) = polar_to_screen( center_display_deg, asc, @@ -1289,11 +1317,47 @@ fn render_wheel( && (OUTER_RING_MODULES.contains(&layer.module_id.as_str())) { let disk_base = 20.0 * s; - let min_sep_deg = - (disk_base / (std::f32::consts::TAU * radii.transits)) * 360.0; let raw_degs: Vec = layer.glyphs.iter().map(|g| g.deg).collect(); - let (display_degs, residual) = spread_angles(&raw_degs, min_sep_deg); - let shrink = (1.0 - residual * 0.45).clamp(0.55, 1.0); + let disk_angular = + (disk_base / (std::f32::consts::TAU * radii.transits)) * 360.0; + let cluster_thresh = 4.0_f32.min(disk_angular * 0.5); + let clusters = find_clusters(&raw_degs, cluster_thresh); + let centroids: Vec = clusters + .iter() + .map(|c| { + let mut sx = 0.0_f32; + let mut sy = 0.0_f32; + for &idx in c { + let a = raw_degs[idx].to_radians(); + sx += a.cos(); + sy += a.sin(); + } + sy.atan2(sx).to_degrees().rem_euclid(360.0) + }) + .collect(); + let (disp_centroids, residual) = spread_angles(¢roids, disk_angular); + let shifts: Vec = clusters + .iter() + .enumerate() + .map(|(ci, _)| { + let mut d = disp_centroids[ci] - centroids[ci]; + if d > 180.0 { + d -= 360.0; + } + if d < -180.0 { + d += 360.0; + } + d + }) + .collect(); + let mut display_degs = vec![0.0_f32; layer.glyphs.len()]; + for (ci, c) in clusters.iter().enumerate() { + for &idx in c { + display_degs[idx] = + (raw_degs[idx] + shifts[ci]).rem_euclid(360.0); + } + } + let shrink = (1.0 - residual * 0.30).clamp(0.60, 1.0); for (i, g) in layer.glyphs.iter().enumerate() { let display_deg = display_degs[i]; let (x, y) = polar_to_screen(display_deg, asc, rot_offset, radii.transits);