360797132e
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>
238 lines
7.2 KiB
Rust
238 lines
7.2 KiB
Rust
//! `tahuantinsuyu-modules` — registry de módulos astrológicos.
|
|
//!
|
|
//! Cada tipo de astrología (natal, tránsito, progresión, sinastría,
|
|
//! Uraniano, …) es un **módulo** que declara:
|
|
//!
|
|
//! - Qué `Layer`s aporta al `RenderModel`.
|
|
//! - Qué `Control`s expone al panel inferior (toggles, sliders, selects).
|
|
//! - Hotkeys opcionales.
|
|
//! - Si su cómputo es lazy (sólo cuando se activa) o eager.
|
|
//!
|
|
//! El registry es un `Vec<&dyn Module>` estático: el canvas consulta
|
|
//! "para esta `ChartKind`, ¿qué módulos están disponibles?" y el panel
|
|
//! pinta sus controles. Activar / desactivar persiste en
|
|
//! `ModuleState` (en la store).
|
|
//!
|
|
//! Esta fase 1 trae el trait + un módulo `NatalModule` de placeholder.
|
|
//! En fases posteriores agregamos Transit, Progression, Synastry,
|
|
//! Composite, SolarArc, Uranian, FixedStars, Dignities, Lots…
|
|
|
|
#![forbid(unsafe_code)]
|
|
#![warn(rust_2018_idioms)]
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use tahuantinsuyu_engine::Layer;
|
|
use tahuantinsuyu_model::{Chart, ChartKind};
|
|
|
|
// =====================================================================
|
|
// Trait Module
|
|
// =====================================================================
|
|
|
|
/// Una capa de astrología enchufable.
|
|
///
|
|
/// `Send + Sync` para que el registry sea estático y se pueda consultar
|
|
/// desde cualquier thread (el cómputo pesado va a un background executor).
|
|
pub trait Module: Send + Sync {
|
|
/// Identidad estable del módulo. Coincide con `ModuleState.module_id`
|
|
/// en la store.
|
|
fn id(&self) -> &'static str;
|
|
|
|
/// Etiqueta amigable para el panel.
|
|
fn label(&self) -> &'static str;
|
|
|
|
/// Breve descripción para tooltip.
|
|
fn description(&self) -> &'static str;
|
|
|
|
/// Para qué tipos de carta tiene sentido este módulo. El panel filtra
|
|
/// con esto al armar la lista de toggles disponibles.
|
|
fn applies_to(&self, kind: ChartKind) -> bool;
|
|
|
|
/// Si el módulo está activado por default al crear una carta.
|
|
fn enabled_by_default(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
/// Controles que aporta al panel inferior.
|
|
fn controls(&self) -> Vec<Control> {
|
|
Vec::new()
|
|
}
|
|
|
|
/// Computa las capas que este módulo aporta al RenderModel de
|
|
/// `chart`. La engine la llama solo si el módulo está activado
|
|
/// para esa carta.
|
|
///
|
|
/// Devuelve `Vec` (no Option) — un módulo puede no aportar capas
|
|
/// si su config interna lo apaga (ej. "Uranian: mostrar simetría
|
|
/// = false"); en ese caso retorna `Vec::new()`.
|
|
fn compute_layers(&self, chart: &Chart, config: &serde_json::Value) -> Vec<Layer>;
|
|
}
|
|
|
|
// =====================================================================
|
|
// Controls expuestos al panel
|
|
// =====================================================================
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum Control {
|
|
Toggle {
|
|
key: String,
|
|
label: String,
|
|
default: bool,
|
|
hotkey: Option<String>,
|
|
},
|
|
Slider {
|
|
key: String,
|
|
label: String,
|
|
min: f64,
|
|
max: f64,
|
|
step: f64,
|
|
default: f64,
|
|
},
|
|
Select {
|
|
key: String,
|
|
label: String,
|
|
options: Vec<SelectOption>,
|
|
default: String,
|
|
},
|
|
/// Texto libre — útil para etiquetas, comentarios.
|
|
TextInput {
|
|
key: String,
|
|
label: String,
|
|
default: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SelectOption {
|
|
pub value: String,
|
|
pub label: String,
|
|
}
|
|
|
|
// =====================================================================
|
|
// Registry
|
|
// =====================================================================
|
|
|
|
/// Lista estática de módulos disponibles. La app los registra al boot.
|
|
pub struct Registry {
|
|
modules: Vec<Box<dyn Module>>,
|
|
}
|
|
|
|
impl Registry {
|
|
/// Registry con todos los módulos built-in. La app llama esto al
|
|
/// boot y luego usa `find()` / `for_kind()` para consultar.
|
|
pub fn with_builtins() -> Self {
|
|
let mut r = Self { modules: Vec::new() };
|
|
r.register(Box::new(natal::NatalModule));
|
|
r
|
|
}
|
|
|
|
pub fn register(&mut self, m: Box<dyn Module>) {
|
|
self.modules.push(m);
|
|
}
|
|
|
|
pub fn all(&self) -> &[Box<dyn Module>] {
|
|
&self.modules
|
|
}
|
|
|
|
pub fn find(&self, id: &str) -> Option<&dyn Module> {
|
|
self.modules
|
|
.iter()
|
|
.find(|m| m.id() == id)
|
|
.map(|m| m.as_ref())
|
|
}
|
|
|
|
pub fn for_kind(&self, kind: ChartKind) -> Vec<&dyn Module> {
|
|
self.modules
|
|
.iter()
|
|
.filter(|m| m.applies_to(kind))
|
|
.map(|m| m.as_ref())
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
// =====================================================================
|
|
// NatalModule — placeholder fase 1
|
|
// =====================================================================
|
|
|
|
pub mod natal {
|
|
use super::*;
|
|
use tahuantinsuyu_engine::compute_mock;
|
|
|
|
pub struct NatalModule;
|
|
|
|
impl Module for NatalModule {
|
|
fn id(&self) -> &'static str {
|
|
"natal"
|
|
}
|
|
fn label(&self) -> &'static str {
|
|
"Carta natal"
|
|
}
|
|
fn description(&self) -> &'static str {
|
|
"Posiciones natales, casas y aspectos."
|
|
}
|
|
fn applies_to(&self, kind: ChartKind) -> bool {
|
|
matches!(kind, ChartKind::Natal)
|
|
}
|
|
fn enabled_by_default(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn controls(&self) -> Vec<Control> {
|
|
vec![
|
|
Control::Toggle {
|
|
key: "show_sign_dial".into(),
|
|
label: "Dial zodiacal".into(),
|
|
default: true,
|
|
hotkey: Some("D".into()),
|
|
},
|
|
Control::Toggle {
|
|
key: "show_houses".into(),
|
|
label: "Casas".into(),
|
|
default: true,
|
|
hotkey: Some("H".into()),
|
|
},
|
|
Control::Toggle {
|
|
key: "show_aspects".into(),
|
|
label: "Aspectos".into(),
|
|
default: true,
|
|
hotkey: Some("X".into()),
|
|
},
|
|
Control::Toggle {
|
|
key: "show_bodies".into(),
|
|
label: "Cuerpos".into(),
|
|
default: true,
|
|
hotkey: Some("P".into()),
|
|
},
|
|
Control::Slider {
|
|
key: "harmonic".into(),
|
|
label: "Armónico".into(),
|
|
min: 1.0,
|
|
max: 20.0,
|
|
step: 1.0,
|
|
default: 1.0,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn compute_layers(&self, chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
|
|
// Fase 1: delega al mock de la engine para que la UI tenga
|
|
// algo que pintar. Fase 3 reemplaza con `engine::compute`
|
|
// contra `eternal-astrology`.
|
|
compute_mock(chart).layers
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn registry_finds_natal() {
|
|
let r = Registry::with_builtins();
|
|
assert!(r.find("natal").is_some());
|
|
assert_eq!(r.for_kind(ChartKind::Natal).len(), 1);
|
|
assert!(r.for_kind(ChartKind::Synastry).is_empty());
|
|
}
|
|
}
|