fix(tahuantinsuyu): spread no propaga + label lejos del disco + glyph legible
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 <noreply@anthropic.com>
This commit is contained in:
@@ -1155,33 +1155,79 @@ fn render_wheel(
|
|||||||
22.0
|
22.0
|
||||||
}) * s;
|
}) * s;
|
||||||
|
|
||||||
// Anti-solapamiento: aplicamos `spread_angles` con el
|
// Anti-solapamiento en dos pasos para evitar que el
|
||||||
// threshold derivado del ancho del LABEL (pill más
|
// empuje se propague en cadena (un cluster denso no
|
||||||
// grande que el disco). Si el spread no puede
|
// debe mover planetas que estaban lejos):
|
||||||
// converger, `residual` se acerca a 1 y achicamos
|
//
|
||||||
// los discos. Los planetas siguen en su grado real
|
// 1) `find_clusters` agrupa los que están dentro de
|
||||||
// — solo el display angular se ajusta.
|
// `cluster_thresh` (~4° real); dentro del cluster
|
||||||
let label_w_est = 38.0 * s;
|
// los glyphs se mantienen en sus posiciones REALES
|
||||||
let min_sep_deg = (label_w_est / (std::f32::consts::TAU * ring)) * 360.0;
|
// — los discos pueden tocarse, eso refleja la
|
||||||
let raw_degs: Vec<f32> =
|
// verdadera geometría astrológica.
|
||||||
layer.glyphs.iter().map(|g| g.deg).collect();
|
// 2) `spread_angles` se aplica SOLO a los centroides
|
||||||
let (display_degs, residual) = spread_angles(&raw_degs, min_sep_deg);
|
// de los clusters, con threshold = ancho angular
|
||||||
let shrink = (1.0 - residual * 0.45).clamp(0.55, 1.0);
|
// del disco. El empuje queda contenido a la
|
||||||
let disk_size = disk_size_base * shrink;
|
// vecindad inmediata; planetas lejos no se mueven.
|
||||||
let font_size_eff = font_size * shrink;
|
let raw_degs: Vec<f32> = 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).
|
// Centroide circular de cada cluster (en grados reales).
|
||||||
// Clusters de ≥3 planetas en menos de 2.5° comparten
|
let cluster_centroids: Vec<f32> = clusters
|
||||||
// un solo label en su centroide; los individuales y
|
.iter()
|
||||||
// los duos llevan su label propio.
|
.map(|c| {
|
||||||
let clusters = find_clusters(&raw_degs, 2.5);
|
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<f32> = 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 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 (ci, c) in clusters.iter().enumerate() {
|
||||||
for &gi in c {
|
for &idx in c {
|
||||||
cluster_of[gi] = ci;
|
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() {
|
for (i, g) in layer.glyphs.iter().enumerate() {
|
||||||
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);
|
||||||
@@ -1204,14 +1250,12 @@ fn render_wheel(
|
|||||||
with_alpha(color, 0.85),
|
with_alpha(color, 0.85),
|
||||||
));
|
));
|
||||||
|
|
||||||
// Coord label individual: solo cuando el cluster
|
// Coord label individual: solo cuando el glyph
|
||||||
// del glyph es chico (<3 miembros). Para clusters
|
// está SOLO en su cluster (≥2 ⇒ label compartido).
|
||||||
// grandes el label se pinta una sola vez abajo,
|
|
||||||
// compartido por todos los miembros.
|
|
||||||
let cluster_size = clusters[cluster_of[i]].len();
|
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 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) =
|
let (lx, ly) =
|
||||||
polar_to_screen(display_deg, asc, rot_offset, label_r);
|
polar_to_screen(display_deg, asc, rot_offset, label_r);
|
||||||
wheel = wheel.child(coord_label(
|
wheel = wheel.child(coord_label(
|
||||||
@@ -1220,38 +1264,22 @@ fn render_wheel(
|
|||||||
coord.into(),
|
coord.into(),
|
||||||
theme.fg_muted,
|
theme.fg_muted,
|
||||||
halo_bg,
|
halo_bg,
|
||||||
9.5 * s * shrink,
|
9.5 * s,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label compartido para clusters densos (≥3 glyphs en
|
// Label compartido para CADA cluster con ≥2 miembros.
|
||||||
// <2.5° reales). Texto = symbols concatenados +
|
// Texto = símbolos concatenados + coord del centroide
|
||||||
// coord del centroide. Posicionado sobre el centroide
|
// real. Posicionado sobre el centroide DISPLAY (donde
|
||||||
// de los display_degs.
|
// se ven los discos tras el shift).
|
||||||
if show_coords && (is_natal || is_topo) {
|
if show_coords && (is_natal || is_topo) {
|
||||||
for c in &clusters {
|
for (ci, c) in clusters.iter().enumerate() {
|
||||||
if c.len() < 3 {
|
if c.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Centroide del display (promedio circular).
|
let center_display_deg = display_centroids[ci];
|
||||||
let mut sx = 0.0_f32;
|
let center_real_deg = cluster_centroids[ci];
|
||||||
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 symbols: String = c
|
let symbols: String = c
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&idx| planet_unicode(&layer.glyphs[idx].symbol))
|
.map(|&idx| planet_unicode(&layer.glyphs[idx].symbol))
|
||||||
@@ -1259,7 +1287,7 @@ 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.3;
|
let label_r = ring - disk_size * 1.5;
|
||||||
let (lx, ly) = polar_to_screen(
|
let (lx, ly) = polar_to_screen(
|
||||||
center_display_deg,
|
center_display_deg,
|
||||||
asc,
|
asc,
|
||||||
@@ -1289,11 +1317,47 @@ fn render_wheel(
|
|||||||
&& (OUTER_RING_MODULES.contains(&layer.module_id.as_str()))
|
&& (OUTER_RING_MODULES.contains(&layer.module_id.as_str()))
|
||||||
{
|
{
|
||||||
let disk_base = 20.0 * s;
|
let disk_base = 20.0 * s;
|
||||||
let min_sep_deg =
|
|
||||||
(disk_base / (std::f32::consts::TAU * radii.transits)) * 360.0;
|
|
||||||
let raw_degs: Vec<f32> = layer.glyphs.iter().map(|g| g.deg).collect();
|
let raw_degs: Vec<f32> = layer.glyphs.iter().map(|g| g.deg).collect();
|
||||||
let (display_degs, residual) = spread_angles(&raw_degs, min_sep_deg);
|
let disk_angular =
|
||||||
let shrink = (1.0 - residual * 0.45).clamp(0.55, 1.0);
|
(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<f32> = 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<f32> = 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() {
|
for (i, g) in layer.glyphs.iter().enumerate() {
|
||||||
let display_deg = display_degs[i];
|
let display_deg = display_degs[i];
|
||||||
let (x, y) = polar_to_screen(display_deg, asc, rot_offset, radii.transits);
|
let (x, y) = polar_to_screen(display_deg, asc, rot_offset, radii.transits);
|
||||||
|
|||||||
Reference in New Issue
Block a user