feat(cosmobiologia): rectificador automático — curva del perfil del barrido
Tercer y último incremento: la visualización. El rectificador ya muestra POR QUÉ una hora gana, no sólo cuál. - cosmobiologia-canvas: CanvasState gana `rectificacion` + `set_rectificacion`. render_rectify_profile dibuja el barrido como un histograma en el footer — cada barra es una hora candidata, su altura crece cuanto menor el puntaje; la barra más alta (el valle del puntaje) es la hora rectificada, resaltada. Etiqueta los hitos (mejor, 0, extremos). - shell: run_rectificacion publica el Rectificacion al canvas además del resumen textual al panel. Con esto el rectificador automático (#67) queda completo: motor de escaneo GR + UI de entrada + visualización del perfil. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1411,16 +1411,31 @@ impl Shell {
|
||||
.to_string();
|
||||
|
||||
// Ventana ±15 min, paso 1 min — el barrido GR estándar.
|
||||
let resultado = match cosmobiologia_engine::rectificar(&chart, &eventos, 15, 1, &key_gr) {
|
||||
Ok(r) => format!(
|
||||
match cosmobiologia_engine::rectificar(&chart, &eventos, 15, 1, &key_gr) {
|
||||
Ok(r) => {
|
||||
let resumen = format!(
|
||||
"{:+} min · puntaje {:.2}",
|
||||
r.mejor_offset_minutos, r.mejor_puntaje
|
||||
),
|
||||
Err(_) => "define al menos un evento (edad > 0)".to_string(),
|
||||
};
|
||||
);
|
||||
self.panel.update(cx, |p, cx| {
|
||||
p.set_string("primary_directions", "resultado", Some(resultado), cx)
|
||||
p.set_string("primary_directions", "resultado", Some(resumen), cx)
|
||||
});
|
||||
// Publicar el perfil al canvas: dibuja la curva del
|
||||
// barrido, cuyo valle marca la hora rectificada.
|
||||
self.canvas
|
||||
.update(cx, |c, cx| c.set_rectificacion(Some(r), cx));
|
||||
}
|
||||
Err(_) => {
|
||||
self.panel.update(cx, |p, cx| {
|
||||
p.set_string(
|
||||
"primary_directions",
|
||||
"resultado",
|
||||
Some("define al menos un evento (edad > 0)".to_string()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot del cielo en este instante anclado al lugar del
|
||||
|
||||
@@ -41,7 +41,8 @@ use gpui::{
|
||||
};
|
||||
|
||||
use cosmobiologia_engine::{
|
||||
Geometry, GrTrigger, Layer, LayerKind, RenderModel, UranianGroup, OUTER_RING_MODULES,
|
||||
Geometry, GrTrigger, Layer, LayerKind, Rectificacion, RenderModel, UranianGroup,
|
||||
OUTER_RING_MODULES,
|
||||
};
|
||||
use cosmobiologia_model::{ChartId, ContactId, GroupId};
|
||||
use cosmobiologia_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
|
||||
@@ -151,6 +152,10 @@ pub struct CanvasState {
|
||||
/// Planeta hovered actualmente (para tooltip). `None` cuando el
|
||||
/// mouse no está sobre ningún cuerpo.
|
||||
pub hover: Option<HoverInfo>,
|
||||
/// Último resultado del rectificador automático, si se corrió uno.
|
||||
/// El canvas dibuja su perfil como una curva en el footer; el valle
|
||||
/// marca la hora de nacimiento que mejor explica los eventos.
|
||||
pub rectificacion: Option<Rectificacion>,
|
||||
drag_jog: Option<JogDragState>,
|
||||
drag_pan: Option<PanDragState>,
|
||||
}
|
||||
@@ -234,6 +239,7 @@ impl Default for CanvasState {
|
||||
layer_visibility: HashMap::new(),
|
||||
show_coords: true,
|
||||
hover: None,
|
||||
rectificacion: None,
|
||||
drag_jog: None,
|
||||
drag_pan: None,
|
||||
}
|
||||
@@ -340,6 +346,17 @@ impl AstrologyCanvas {
|
||||
}
|
||||
}
|
||||
|
||||
/// Publica el resultado de un barrido de rectificación: el canvas
|
||||
/// dibuja su perfil como una curva en el footer. `None` lo borra.
|
||||
pub fn set_rectificacion(
|
||||
&mut self,
|
||||
rectificacion: Option<Rectificacion>,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) {
|
||||
self.state.rectificacion = rectificacion;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Resetea zoom y pan a sus defaults (1.0 y 0,0). No toca rotation
|
||||
/// ni time offset — esos son ortogonales y tienen su propio reset.
|
||||
pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) {
|
||||
@@ -846,6 +863,7 @@ impl Render for AstrologyCanvas {
|
||||
&self.state.layer_visibility,
|
||||
self.state.show_coords,
|
||||
self.state.hover.as_ref(),
|
||||
self.state.rectificacion.as_ref(),
|
||||
entity,
|
||||
),
|
||||
CanvasMode::Thumbnails { items, .. } => render_thumbnails(&theme, items),
|
||||
@@ -968,6 +986,7 @@ fn render_wheel(
|
||||
layer_visibility: &HashMap<LayerKind, bool>,
|
||||
show_coords: bool,
|
||||
hover: Option<&HoverInfo>,
|
||||
rectificacion: Option<&Rectificacion>,
|
||||
entity: gpui::Entity<AstrologyCanvas>,
|
||||
) -> gpui::Div {
|
||||
let asc = render.ascendant_deg;
|
||||
@@ -1773,6 +1792,15 @@ fn render_wheel(
|
||||
));
|
||||
}
|
||||
|
||||
// Perfil del rectificador automático — la curva del barrido de horas
|
||||
// candidatas. Aparece tras correr una rectificación; su valle marca
|
||||
// la hora de nacimiento que mejor explica los eventos conocidos.
|
||||
if let Some(r) = rectificacion {
|
||||
if !r.perfil.is_empty() {
|
||||
footer = footer.child(render_rectify_profile(theme, palette, r));
|
||||
}
|
||||
}
|
||||
|
||||
// Lista textual de aspectos (top 12 por orb). Compacta, en grid
|
||||
// de 3 columnas, fonts pequeños. Solo aparece cuando hay aspectos
|
||||
// computados.
|
||||
@@ -2014,6 +2042,89 @@ fn render_harmonic_spectrum(
|
||||
.child(bars)
|
||||
}
|
||||
|
||||
/// Curva del barrido del rectificador automático. Cada barra es una hora
|
||||
/// de nacimiento candidata; su altura crece cuanto MEJOR explica los
|
||||
/// eventos conocidos (menor puntaje de convergencia). La barra más alta
|
||||
/// —el valle del puntaje— es la hora rectificada, y va resaltada.
|
||||
fn render_rectify_profile(
|
||||
theme: &Theme,
|
||||
palette: &AstroPalette,
|
||||
r: &Rectificacion,
|
||||
) -> gpui::Div {
|
||||
const BAR_AREA_H: f32 = 46.0;
|
||||
|
||||
let (min_p, max_p) = r.perfil.iter().fold(
|
||||
(f32::INFINITY, f32::NEG_INFINITY),
|
||||
|(lo, hi), &(_, p)| (lo.min(p), hi.max(p)),
|
||||
);
|
||||
let rango = (max_p - min_p).max(1e-3);
|
||||
let primero = r.perfil.first().map(|&(o, _)| o).unwrap_or(0);
|
||||
let ultimo = r.perfil.last().map(|&(o, _)| o).unwrap_or(0);
|
||||
|
||||
let mut bars = div().flex().flex_row().items_end().gap(px(2.0));
|
||||
for &(offset, puntaje) in &r.perfil {
|
||||
// Fitness: el mejor candidato (puntaje mínimo) → barra más alta.
|
||||
let fitness = ((max_p - puntaje) / rango).clamp(0.0, 1.0);
|
||||
let bar_h = (fitness * BAR_AREA_H).max(2.0);
|
||||
let es_mejor = offset == r.mejor_offset_minutos;
|
||||
let color = if es_mejor {
|
||||
palette.angle_highlight
|
||||
} else {
|
||||
with_alpha(palette.angle_highlight, 0.25 + fitness * 0.45)
|
||||
};
|
||||
// Etiquetar sólo los hitos: el mejor, el 0 y los dos extremos.
|
||||
let label = if es_mejor || offset == 0 || offset == primero || offset == ultimo {
|
||||
if offset == 0 {
|
||||
"0".to_string()
|
||||
} else {
|
||||
format!("{offset:+}")
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let column = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap(px(2.0))
|
||||
.child(
|
||||
div()
|
||||
.h(px(BAR_AREA_H))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_end()
|
||||
.child(div().w(px(9.0)).h(px(bar_h)).rounded(px(1.5)).bg(color)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(7.0))
|
||||
.text_color(if es_mejor {
|
||||
palette.angle_highlight
|
||||
} else {
|
||||
theme.fg_disabled
|
||||
})
|
||||
.child(SharedString::from(label)),
|
||||
);
|
||||
bars = bars.child(column);
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap(px(3.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_muted)
|
||||
.child(SharedString::from(format!(
|
||||
"Rectificación · hora {:+} min · puntaje {:.2} · el valle es la hora",
|
||||
r.mejor_offset_minutos, r.mejor_puntaje
|
||||
))),
|
||||
)
|
||||
.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
|
||||
|
||||
Reference in New Issue
Block a user