feat(cosmobiologia): dial uraniano de 90° — proyección geométrica
El módulo Uranian sólo listaba las fórmulas como texto; ahora también las muestra geométricamente. - cosmobiologia-canvas: render_uranian_dial pinta un eje horizontal 0-90° con cada cuerpo natal proyectado en su longitud mod 90. Ticks en las divisiones duras (0/22½/45/67½/90°); los cuerpos que forman una fórmula uraniana van resaltados, y los clusters densos se escalonan en filas para legibilidad. La sección del footer combina el dial geométrico con la lista de pills de fórmulas. - El dial aparece siempre que el módulo Uranian está activo (antes la sección sólo salía si había grupos detectados). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use cosmobiologia_engine::{
|
use cosmobiologia_engine::{
|
||||||
Geometry, GrTrigger, Layer, LayerKind, OUTER_RING_MODULES, RenderModel,
|
Geometry, GrTrigger, Layer, LayerKind, RenderModel, UranianGroup, OUTER_RING_MODULES,
|
||||||
};
|
};
|
||||||
use cosmobiologia_model::{ChartId, ContactId, GroupId};
|
use cosmobiologia_model::{ChartId, ContactId, GroupId};
|
||||||
use cosmobiologia_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
|
use cosmobiologia_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
|
||||||
@@ -1704,12 +1704,35 @@ fn render_wheel(
|
|||||||
footer = footer.child(b);
|
footer = footer.child(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ejes uranianos detectados (cuerpos en la misma posición mod 90).
|
// Dial uraniano de 90°. Aparece cuando el módulo Uranian está
|
||||||
// Aparece sólo cuando el módulo Uranian está activo y hay
|
// activo: una proyección geométrica de los cuerpos sobre el eje
|
||||||
// grupos. Cada grupo se muestra como pill con los unicode de los
|
// 0-90° + la lista de fórmulas (cuerpos en el mismo grado dial).
|
||||||
// cuerpos + el grado dial-90.
|
if render.overlays.iter().any(|o| o.module_id == "uranian") {
|
||||||
|
let mut section = div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(4.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(10.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child("Dial 90° (uraniano)"),
|
||||||
|
)
|
||||||
|
.child(render_uranian_dial(
|
||||||
|
theme,
|
||||||
|
palette,
|
||||||
|
&render.layers,
|
||||||
|
&render.uranian_groups,
|
||||||
|
));
|
||||||
|
// Pills de fórmulas, sólo si se detectó algún eje.
|
||||||
if !render.uranian_groups.is_empty() {
|
if !render.uranian_groups.is_empty() {
|
||||||
let mut row = div().flex().flex_row().flex_wrap().gap(px(6.0));
|
let mut row = div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.flex_wrap()
|
||||||
|
.justify_center()
|
||||||
|
.gap(px(6.0));
|
||||||
for group in &render.uranian_groups {
|
for group in &render.uranian_groups {
|
||||||
let bodies_text: String = group
|
let bodies_text: String = group
|
||||||
.bodies
|
.bodies
|
||||||
@@ -1733,20 +1756,9 @@ fn render_wheel(
|
|||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
footer = footer.child(
|
section = section.child(row);
|
||||||
div()
|
}
|
||||||
.flex()
|
footer = footer.child(section);
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
|
||||||
.gap(px(3.0))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(px(10.0))
|
|
||||||
.text_color(theme.fg_muted)
|
|
||||||
.child("Ejes uranianos (90°)"),
|
|
||||||
)
|
|
||||||
.child(row),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Espectro de fuerza armónica — histograma clicable. Aparece sólo
|
// Espectro de fuerza armónica — histograma clicable. Aparece sólo
|
||||||
@@ -2002,6 +2014,122 @@ fn render_harmonic_spectrum(
|
|||||||
.child(bars)
|
.child(bars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dial uraniano de 90°: proyección geométrica de los cuerpos natales
|
||||||
|
/// sobre un eje horizontal 0-90° (longitud mod 90). Los cuerpos que
|
||||||
|
/// forman una fórmula uraniana (mismo grado dial) caen agrupados y se
|
||||||
|
/// resaltan; clusters densos se escalonan en filas para legibilidad.
|
||||||
|
fn render_uranian_dial(
|
||||||
|
theme: &Theme,
|
||||||
|
palette: &AstroPalette,
|
||||||
|
layers: &[Layer],
|
||||||
|
groups: &[UranianGroup],
|
||||||
|
) -> gpui::Div {
|
||||||
|
const DIAL_W: f32 = 560.0;
|
||||||
|
const ROW_H: f32 = 18.0;
|
||||||
|
const MAX_ROWS: usize = 4;
|
||||||
|
const AXIS_Y: f32 = ROW_H * MAX_ROWS as f32;
|
||||||
|
const MIN_GAP: f32 = 17.0;
|
||||||
|
|
||||||
|
let Some(layer) = layers
|
||||||
|
.iter()
|
||||||
|
.find(|l| l.module_id == "natal" && matches!(l.kind, LayerKind::Bodies))
|
||||||
|
else {
|
||||||
|
return div();
|
||||||
|
};
|
||||||
|
|
||||||
|
// `(símbolo, x, agrupado)` ordenados por posición en el dial.
|
||||||
|
let mut marks: Vec<(String, f32, bool)> = layer
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.map(|g| {
|
||||||
|
let x = g.deg.rem_euclid(90.0) / 90.0 * DIAL_W;
|
||||||
|
let grouped = groups
|
||||||
|
.iter()
|
||||||
|
.any(|gr| gr.bodies.iter().any(|b| b == &g.symbol));
|
||||||
|
(g.symbol.clone(), x, grouped)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
marks.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
|
||||||
|
let mut track = div().relative().w(px(DIAL_W)).h(px(AXIS_Y + 22.0));
|
||||||
|
|
||||||
|
// Eje base.
|
||||||
|
track = track.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(0.0))
|
||||||
|
.top(px(AXIS_Y))
|
||||||
|
.w(px(DIAL_W))
|
||||||
|
.h(px(1.0))
|
||||||
|
.bg(with_alpha(palette.dial_ring, 0.7)),
|
||||||
|
);
|
||||||
|
// Ticks 0 / 22½ / 45 / 67½ / 90 — las divisiones duras del dial.
|
||||||
|
for (deg, label) in [
|
||||||
|
(0.0_f32, "0°"),
|
||||||
|
(22.5, "22½°"),
|
||||||
|
(45.0, "45°"),
|
||||||
|
(67.5, "67½°"),
|
||||||
|
(90.0, "90°"),
|
||||||
|
] {
|
||||||
|
let x = deg / 90.0 * DIAL_W;
|
||||||
|
track = track
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(x))
|
||||||
|
.top(px(AXIS_Y))
|
||||||
|
.w(px(1.0))
|
||||||
|
.h(px(6.0))
|
||||||
|
.bg(with_alpha(palette.dial_ring, 0.85)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(x - 14.0))
|
||||||
|
.top(px(AXIS_Y + 8.0))
|
||||||
|
.w(px(28.0))
|
||||||
|
.flex()
|
||||||
|
.justify_center()
|
||||||
|
.text_size(px(8.0))
|
||||||
|
.text_color(theme.fg_disabled)
|
||||||
|
.child(SharedString::from(label)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Glyphs, con escalonado vertical para los clusters.
|
||||||
|
let mut last_x = f32::NEG_INFINITY;
|
||||||
|
let mut row = 0usize;
|
||||||
|
for (symbol, x, grouped) in &marks {
|
||||||
|
if x - last_x < MIN_GAP {
|
||||||
|
row = (row + 1).min(MAX_ROWS - 1);
|
||||||
|
} else {
|
||||||
|
row = 0;
|
||||||
|
}
|
||||||
|
last_x = *x;
|
||||||
|
let color = if *grouped {
|
||||||
|
palette.angle_highlight
|
||||||
|
} else {
|
||||||
|
with_alpha(planet_color(palette, symbol), 0.55)
|
||||||
|
};
|
||||||
|
track = track.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(x - 8.0))
|
||||||
|
.top(px(row as f32 * ROW_H))
|
||||||
|
.w(px(16.0))
|
||||||
|
.h(px(16.0))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.text_color(color)
|
||||||
|
.child(SharedString::from(planet_unicode(symbol).to_string())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
track
|
||||||
|
}
|
||||||
|
|
||||||
/// Color de un trigger GR según su orbe: rojo intenso (orbe cerrado,
|
/// Color de un trigger GR según su orbe: rojo intenso (orbe cerrado,
|
||||||
/// contacto fuerte) que se desatura hacia gris al ensancharse. El
|
/// contacto fuerte) que se desatura hacia gris al ensancharse. El
|
||||||
/// orbe de referencia (gris pleno) es el orbe del HUD, 2°.
|
/// orbe de referencia (gris pleno) es el orbe del HUD, 2°.
|
||||||
|
|||||||
@@ -141,9 +141,9 @@ pub enum PipelineRequest {
|
|||||||
/// `module_id = "uranian"` — calcula los "ejes" del dial uraniano
|
/// `module_id = "uranian"` — calcula los "ejes" del dial uraniano
|
||||||
/// de 90°: agrupa los cuerpos natales cuya longitud módulo 90 cae
|
/// de 90°: agrupa los cuerpos natales cuya longitud módulo 90 cae
|
||||||
/// dentro de una tolerancia (~2°). El resultado se publica en
|
/// dentro de una tolerancia (~2°). El resultado se publica en
|
||||||
/// `RenderModel.uranian_groups` para que la UI lo liste como
|
/// `RenderModel.uranian_groups`; la UI lo pinta como un dial
|
||||||
/// fórmulas analíticas. La visualización geométrica completa del
|
/// geométrico de 90° (proyección sobre el eje 0-90°) más la lista
|
||||||
/// dial de 90° queda pendiente para una fase posterior.
|
/// de fórmulas.
|
||||||
Uranian,
|
Uranian,
|
||||||
/// `module_id = "lots"` — Lots arábigos (helenísticos) calculados
|
/// `module_id = "lots"` — Lots arábigos (helenísticos) calculados
|
||||||
/// via `eternal_astrology::compute_lot`: Fortune, Spirit, Eros,
|
/// via `eternal_astrology::compute_lot`: Fortune, Spirit, Eros,
|
||||||
|
|||||||
Reference in New Issue
Block a user