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();
|
.to_string();
|
||||||
|
|
||||||
// Ventana ±15 min, paso 1 min — el barrido GR estándar.
|
// Ventana ±15 min, paso 1 min — el barrido GR estándar.
|
||||||
let resultado = match cosmobiologia_engine::rectificar(&chart, &eventos, 15, 1, &key_gr) {
|
match cosmobiologia_engine::rectificar(&chart, &eventos, 15, 1, &key_gr) {
|
||||||
Ok(r) => format!(
|
Ok(r) => {
|
||||||
|
let resumen = format!(
|
||||||
"{:+} min · puntaje {:.2}",
|
"{:+} min · puntaje {:.2}",
|
||||||
r.mejor_offset_minutos, r.mejor_puntaje
|
r.mejor_offset_minutos, r.mejor_puntaje
|
||||||
),
|
);
|
||||||
Err(_) => "define al menos un evento (edad > 0)".to_string(),
|
|
||||||
};
|
|
||||||
self.panel.update(cx, |p, cx| {
|
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
|
/// Snapshot del cielo en este instante anclado al lugar del
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use cosmobiologia_engine::{
|
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_model::{ChartId, ContactId, GroupId};
|
||||||
use cosmobiologia_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
|
use cosmobiologia_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
|
||||||
@@ -151,6 +152,10 @@ pub struct CanvasState {
|
|||||||
/// Planeta hovered actualmente (para tooltip). `None` cuando el
|
/// Planeta hovered actualmente (para tooltip). `None` cuando el
|
||||||
/// mouse no está sobre ningún cuerpo.
|
/// mouse no está sobre ningún cuerpo.
|
||||||
pub hover: Option<HoverInfo>,
|
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_jog: Option<JogDragState>,
|
||||||
drag_pan: Option<PanDragState>,
|
drag_pan: Option<PanDragState>,
|
||||||
}
|
}
|
||||||
@@ -234,6 +239,7 @@ impl Default for CanvasState {
|
|||||||
layer_visibility: HashMap::new(),
|
layer_visibility: HashMap::new(),
|
||||||
show_coords: true,
|
show_coords: true,
|
||||||
hover: None,
|
hover: None,
|
||||||
|
rectificacion: None,
|
||||||
drag_jog: None,
|
drag_jog: None,
|
||||||
drag_pan: 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
|
/// 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.
|
/// ni time offset — esos son ortogonales y tienen su propio reset.
|
||||||
pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) {
|
pub fn reset_view(&mut self, cx: &mut Context<'_, Self>) {
|
||||||
@@ -846,6 +863,7 @@ impl Render for AstrologyCanvas {
|
|||||||
&self.state.layer_visibility,
|
&self.state.layer_visibility,
|
||||||
self.state.show_coords,
|
self.state.show_coords,
|
||||||
self.state.hover.as_ref(),
|
self.state.hover.as_ref(),
|
||||||
|
self.state.rectificacion.as_ref(),
|
||||||
entity,
|
entity,
|
||||||
),
|
),
|
||||||
CanvasMode::Thumbnails { items, .. } => render_thumbnails(&theme, items),
|
CanvasMode::Thumbnails { items, .. } => render_thumbnails(&theme, items),
|
||||||
@@ -968,6 +986,7 @@ fn render_wheel(
|
|||||||
layer_visibility: &HashMap<LayerKind, bool>,
|
layer_visibility: &HashMap<LayerKind, bool>,
|
||||||
show_coords: bool,
|
show_coords: bool,
|
||||||
hover: Option<&HoverInfo>,
|
hover: Option<&HoverInfo>,
|
||||||
|
rectificacion: Option<&Rectificacion>,
|
||||||
entity: gpui::Entity<AstrologyCanvas>,
|
entity: gpui::Entity<AstrologyCanvas>,
|
||||||
) -> gpui::Div {
|
) -> gpui::Div {
|
||||||
let asc = render.ascendant_deg;
|
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
|
// Lista textual de aspectos (top 12 por orb). Compacta, en grid
|
||||||
// de 3 columnas, fonts pequeños. Solo aparece cuando hay aspectos
|
// de 3 columnas, fonts pequeños. Solo aparece cuando hay aspectos
|
||||||
// computados.
|
// computados.
|
||||||
@@ -2014,6 +2042,89 @@ fn render_harmonic_spectrum(
|
|||||||
.child(bars)
|
.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
|
/// Dial uraniano de 90°: proyección geométrica de los cuerpos natales
|
||||||
/// sobre un eje horizontal 0-90° (longitud mod 90). Los cuerpos que
|
/// sobre un eje horizontal 0-90° (longitud mod 90). Los cuerpos que
|
||||||
/// forman una fórmula uraniana (mismo grado dial) caen agrupados y se
|
/// forman una fórmula uraniana (mismo grado dial) caen agrupados y se
|
||||||
|
|||||||
Reference in New Issue
Block a user