d4761bf238
El shell ya no carga el flag `show_transits: bool` ni hardcodea qué pipeline corre. La engine expone una sola API `compose(chart, offset, &[PipelineRequest])` que la shell alimenta a partir de un map `module_configs: HashMap<String, serde_json::Value>`. Los toggles de overlay (transit hoy, progression/synastry/solar_arc en fase 7) viven como módulos propios en el panel. - engine: PipelineRequest enum (variante Transit por ahora; comentarios con el roadmap de SecondaryProgression/SolarArc/Synastry). compose() es la nueva entrada canónica; compute / compute_at_offset / compute_with_transits_at_now quedan como atajos retrocompatibles que delegan en compose. bridge.rs refactor: extraído build_transit_overlay como helper que muta &mut RenderModel, listo para que más pipelines apilen capas encima. - modules: nuevo módulo `transit::TransitModule` (id "transit", toggle "enabled" con hotkey [T], applies_to Natal). Sacado el toggle show_transits de NatalModule — ahora cada módulo declara lo suyo. Registry::with_builtins() registra ambos. Test asegura los dos aplican a Natal. - panel: sin cambios — ya itera Registry::for_kind(kind) y renderea cada módulo aplicable con sus controls. La adición del TransitModule aparece automática como segunda card en el panel. - shell: replace show_transits por module_configs map. build_requests() deriva PipelineRequest::Transit cuando module_configs["transit"] ["enabled"] == true. on_panel_event: toggles del NatalModule afectan solo visibility del canvas; toggles de otros módulos van al module_configs y disparan render_current. on_canvas_event: [T] hotkey → flip transit.enabled + sync panel + recompose. apps Cargo agrega serde_json como dep directa. Todos los tests verdes. Fase 7 puede sumar overlays adicionales (progression, solar_arc) solo agregando variantes a PipelineRequest + helpers en bridge + módulos declarativos — sin tocar el shell. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
292 lines
9.3 KiB
Rust
292 lines
9.3 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.register(Box::new(transit::TransitModule));
|
||
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
|
||
}
|
||
}
|
||
}
|
||
|
||
// =====================================================================
|
||
// TransitModule — overlay del cielo del momento sobre la carta natal
|
||
// =====================================================================
|
||
|
||
pub mod transit {
|
||
use super::*;
|
||
|
||
/// Anillo externo con las posiciones planetarias del **instante
|
||
/// actual** (reloj de pared) sobre el sujeto natal, más las
|
||
/// cross-aspects natal × transit. La engine despacha al pipeline
|
||
/// `PipelineRequest::Transit` cuando este módulo está activo en el
|
||
/// `module_configs` del shell.
|
||
pub struct TransitModule;
|
||
|
||
impl Module for TransitModule {
|
||
fn id(&self) -> &'static str {
|
||
"transit"
|
||
}
|
||
fn label(&self) -> &'static str {
|
||
"Tránsitos"
|
||
}
|
||
fn description(&self) -> &'static str {
|
||
"Cielo del momento sobre la natal + cross aspects."
|
||
}
|
||
fn applies_to(&self, kind: ChartKind) -> bool {
|
||
// Por ahora solo overlay sobre cartas natales — más adelante
|
||
// podríamos overlayar tránsitos sobre Progresiones, etc.
|
||
matches!(kind, ChartKind::Natal)
|
||
}
|
||
fn enabled_by_default(&self) -> bool {
|
||
false
|
||
}
|
||
fn controls(&self) -> Vec<Control> {
|
||
vec![Control::Toggle {
|
||
key: "enabled".into(),
|
||
label: "Activar".into(),
|
||
default: false,
|
||
hotkey: Some("T".into()),
|
||
}]
|
||
}
|
||
fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
|
||
// Las capas de tránsito se construyen en la engine vía
|
||
// `PipelineRequest::Transit` porque necesitan acceso a la
|
||
// NatalChart cruda + EphemerisSession. Este método queda
|
||
// como no-op — el módulo es puramente declarativo.
|
||
Vec::new()
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn registry_finds_builtins() {
|
||
let r = Registry::with_builtins();
|
||
assert!(r.find("natal").is_some());
|
||
assert!(r.find("transit").is_some());
|
||
// Natal kind tiene 2 módulos aplicables: el propio + transit overlay.
|
||
assert_eq!(r.for_kind(ChartKind::Natal).len(), 2);
|
||
// Synastry kind no tiene módulos hoy.
|
||
assert!(r.for_kind(ChartKind::Synastry).is_empty());
|
||
}
|
||
}
|