feat(tahuantinsuyu): fase 4 — jog-dial perimetral, hotkeys y panel interactivo
Time scrubbing por drag en el aro exterior del wheel: rota visualmente mientras dura el drag, al soltar traduce el delta angular a minutos (1° = 4 min sideral, CW = forward) y emite CanvasEvent::TimeOffsetChanged. La Shell recomputa con engine::compute_at_offset y el ascendant rotado queda en la nueva posición. Snap visual a 0° tras commit. - engine: nueva variante compute_at_offset(chart, minutes) que suma segundos al UTC base via add_seconds + Instant::from_utc y corre la pipeline normal. compute() es ahora wrapper con offset=0. - canvas: estado nuevo layer_visibility + drag_jog. Mouse handlers registrados desde el paint callback (mismo patrón que splitter/tiled). Hotkeys D/H/X/P toggle SignDial/Houses/Aspects/Bodies, R resetea offset. FocusHandle + click-to-focus para recibir teclas. Indicador ⏱ ±Xd HH:MM en el footer con color highlight cuando el offset != 0. paint_wheel + glyph overlays respetan layer_visibility (skip capas ocultas). - modules: NatalModule.controls() ahora expone show_sign_dial / show_houses / show_aspects / show_bodies con hotkeys [D/H/X/P], más el slider de armónico. - panel: ControlPanel mantiene toggle_state cache (module_id, key) → bool, inicializa desde defaults al cambiar de ChartKind. Click invierte el toggle visualmente y emite ControlChanged. Nuevo set_toggle(module, key, value) para que la Shell mantenga sync cuando el canvas se autotogglea por hotkey. - shell: nuevo current_chart + current_offset_minutes. render_current() delega a compute_at_offset. Suscripción a CanvasEvent traduce TimeOffsetChanged → re-render, LayerVisibilityChanged → panel sync. Suscripción a PanelEvent::ControlChanged traduce show_* keys a set_layer_visible sobre el canvas. Todos los tests verdes. La fase 5 sumará módulos extra (transit, progression, synastry, uranian) + extracción de eternal de lo que falte. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,22 +3,16 @@
|
||||
//! Es el "director de orquesta": dueño del tree, del canvas y del panel,
|
||||
//! reenvía eventos entre ellos y aplica las mutaciones en la store.
|
||||
//!
|
||||
//! Flujo típico:
|
||||
//! Flujo:
|
||||
//!
|
||||
//! ```text
|
||||
//! Tree.Selected(Chart) → Shell → Canvas.set_mode(Wheel)
|
||||
//! → Panel.set_active_kind(chart.kind)
|
||||
//!
|
||||
//! Tree.Selected(Group) → Shell → Canvas.set_mode(Thumbnails{…})
|
||||
//! → Panel.set_active_kind(None)
|
||||
//!
|
||||
//! Panel.ModuleToggled → Shell → Store.upsert_module_state
|
||||
//! → Canvas.toggle_module
|
||||
//! Tree.Selected(Chart) → Shell → load chart + compute + set_mode(Wheel)
|
||||
//! Tree.Selected(Group/Contact)→ Shell → charts_under_* + set_mode(Thumbnails)
|
||||
//! Canvas.TimeOffsetChanged → Shell → compute_at_offset(current_chart, off)
|
||||
//! → set_mode(Wheel) con la rueda re-pintada
|
||||
//! Canvas.LayerVisibility... → Shell → Panel.set_toggle (mantener sync visual)
|
||||
//! Panel.ControlChanged → Shell → Canvas.set_layer_visible (show_*)
|
||||
//! ```
|
||||
//!
|
||||
//! Fase 1: las suscripciones están cableadas pero los handlers son
|
||||
//! mínimos (logging + transición de modo). La pipeline real de cómputo
|
||||
//! viene con la fase 3.
|
||||
|
||||
use gpui::{
|
||||
Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div,
|
||||
@@ -26,10 +20,10 @@ use gpui::{
|
||||
};
|
||||
|
||||
use tahuantinsuyu_canvas::{
|
||||
AstrologyCanvas, CanvasMode, ThumbnailItem, ThumbnailScope,
|
||||
AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
|
||||
};
|
||||
use tahuantinsuyu_engine::compute;
|
||||
use tahuantinsuyu_model::TreeSelection;
|
||||
use tahuantinsuyu_engine::{LayerKind, compute_at_offset};
|
||||
use tahuantinsuyu_model::{Chart, TreeSelection};
|
||||
use tahuantinsuyu_panel::{ControlPanel, PanelEvent};
|
||||
use tahuantinsuyu_store::Store;
|
||||
use tahuantinsuyu_tree::{TahuantinsuyuTree, TreeEvent};
|
||||
@@ -46,6 +40,10 @@ pub struct Shell {
|
||||
tree: Entity<TahuantinsuyuTree>,
|
||||
canvas: Entity<AstrologyCanvas>,
|
||||
panel: Entity<ControlPanel>,
|
||||
/// Carta abierta actualmente en el canvas. La cacheamos para poder
|
||||
/// recomputarla con time-offsets sin re-leer la DB cada vez.
|
||||
current_chart: Option<Chart>,
|
||||
current_offset_minutes: i64,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
@@ -57,24 +55,29 @@ impl Shell {
|
||||
let canvas = cx.new(AstrologyCanvas::new);
|
||||
let panel = cx.new(ControlPanel::new);
|
||||
|
||||
// Tree → Shell: aplicar selección al canvas/panel.
|
||||
cx.subscribe(&tree, |this: &mut Self, _, ev: &TreeEvent, cx| {
|
||||
this.on_tree_event(ev, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Panel → Shell: persistir y propagar al canvas.
|
||||
cx.subscribe(&panel, |this: &mut Self, _, ev: &PanelEvent, cx| {
|
||||
this.on_panel_event(ev, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&canvas, |this: &mut Self, _, ev: &CanvasEvent, cx| {
|
||||
this.on_canvas_event(ev, cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
store,
|
||||
bus,
|
||||
tree,
|
||||
canvas,
|
||||
panel,
|
||||
current_chart: None,
|
||||
current_offset_minutes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +86,6 @@ impl Shell {
|
||||
TreeEvent::Selected(s) => s,
|
||||
TreeEvent::Opened(s) => s,
|
||||
TreeEvent::HierarchyChanged => {
|
||||
// El tree ya hizo refresh internamente; el canvas/panel
|
||||
// se enteran cuando llegue una nueva Selección. Fase 3
|
||||
// podría re-disparar la última selección para que el
|
||||
// thumbnail grid se actualice si era una vista de grupo.
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -104,26 +103,15 @@ impl Shell {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let kind = chart.kind;
|
||||
let render = match compute(&chart) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("[shell] compute {}: {}", id, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_mode(
|
||||
CanvasMode::Wheel {
|
||||
render: Box::new(render),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
self.current_chart = Some(chart.clone());
|
||||
self.current_offset_minutes = 0;
|
||||
self.render_current(cx);
|
||||
self.panel
|
||||
.update(cx, |p, cx| p.set_active_kind(Some(kind), cx));
|
||||
.update(cx, |p, cx| p.set_active_kind(Some(chart.kind), cx));
|
||||
}
|
||||
TreeSelection::Contact(id) => {
|
||||
self.current_chart = None;
|
||||
self.current_offset_minutes = 0;
|
||||
let charts = self.store.list_charts(id).unwrap_or_default();
|
||||
let items: Vec<ThumbnailItem> = charts
|
||||
.into_iter()
|
||||
@@ -146,6 +134,8 @@ impl Shell {
|
||||
self.panel.update(cx, |p, cx| p.set_active_kind(None, cx));
|
||||
}
|
||||
TreeSelection::Group(id) => {
|
||||
self.current_chart = None;
|
||||
self.current_offset_minutes = 0;
|
||||
let charts = self.store.charts_under_group(id).unwrap_or_default();
|
||||
let items: Vec<ThumbnailItem> = charts
|
||||
.into_iter()
|
||||
@@ -170,18 +160,80 @@ impl Shell {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_panel_event(&mut self, ev: &PanelEvent, cx: &mut Context<Self>) {
|
||||
match ev {
|
||||
PanelEvent::ModuleToggled { module_id, .. } => {
|
||||
self.canvas
|
||||
.update(cx, |c, cx| c.toggle_module(module_id, cx));
|
||||
fn render_current(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(chart) = self.current_chart.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let render = match compute_at_offset(chart, self.current_offset_minutes) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[shell] compute_at_offset {} (+{}min): {}",
|
||||
chart.id, self.current_offset_minutes, e
|
||||
);
|
||||
return;
|
||||
}
|
||||
PanelEvent::ControlChanged { .. } => {
|
||||
// Fase 4: aplicar config al canvas + persistir en store.
|
||||
};
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_mode(
|
||||
CanvasMode::Wheel {
|
||||
render: Box::new(render),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_canvas_event(&mut self, ev: &CanvasEvent, cx: &mut Context<Self>) {
|
||||
match ev {
|
||||
CanvasEvent::TimeOffsetChanged(off) => {
|
||||
self.current_offset_minutes = *off;
|
||||
if self.current_chart.is_some() {
|
||||
self.render_current(cx);
|
||||
}
|
||||
}
|
||||
CanvasEvent::LayerVisibilityChanged { kind, visible } => {
|
||||
// Sync el panel para que el toggle visual coincida con
|
||||
// lo que disparó el hotkey en el canvas.
|
||||
let key = match kind {
|
||||
LayerKind::SignDial => "show_sign_dial",
|
||||
LayerKind::Houses => "show_houses",
|
||||
LayerKind::Aspects => "show_aspects",
|
||||
LayerKind::Bodies => "show_bodies",
|
||||
_ => return,
|
||||
};
|
||||
self.panel
|
||||
.update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
|
||||
}
|
||||
CanvasEvent::ChartRequested(_) => {
|
||||
// Fase 5: doble click sobre un thumbnail abre la carta.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_panel_event(&mut self, ev: &PanelEvent, cx: &mut Context<Self>) {
|
||||
match ev {
|
||||
PanelEvent::ControlChanged { module_id, key, value } => {
|
||||
let visible = value.as_bool().unwrap_or(true);
|
||||
if module_id == "natal" {
|
||||
let kind = match key.as_str() {
|
||||
"show_sign_dial" => Some(LayerKind::SignDial),
|
||||
"show_houses" => Some(LayerKind::Houses),
|
||||
"show_aspects" => Some(LayerKind::Aspects),
|
||||
"show_bodies" => Some(LayerKind::Bodies),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(k) = kind {
|
||||
self.canvas
|
||||
.update(cx, |c, cx| c.set_layer_visible(k, visible, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
PanelEvent::ModuleToggled { .. } => {
|
||||
// Fase 5: encender/apagar módulos enteros (Transit,
|
||||
// Synastry, Uranian).
|
||||
}
|
||||
}
|
||||
// Silenciar warnings de campos no leídos hasta que la fase 2
|
||||
// cablee CRUD desde el tree.
|
||||
let _ = (&self.store, &self.tree, &self.bus);
|
||||
}
|
||||
}
|
||||
@@ -220,10 +272,7 @@ impl Render for Shell {
|
||||
.border_color(theme.border)
|
||||
.child(self.tree.clone());
|
||||
|
||||
let canvas_panel = div()
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.child(self.canvas.clone());
|
||||
let canvas_panel = div().flex_grow().h_full().child(self.canvas.clone());
|
||||
|
||||
let main_row = div()
|
||||
.flex_grow()
|
||||
|
||||
Reference in New Issue
Block a user