refactor(tahuantinsuyu): fase 6 — Modules pluggables vía compose + PipelineRequest
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>
This commit is contained in:
Generated
+1
@@ -10906,6 +10906,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"gpui",
|
||||
"serde_json",
|
||||
"tahuantinsuyu-canvas",
|
||||
"tahuantinsuyu-card",
|
||||
"tahuantinsuyu-engine",
|
||||
|
||||
@@ -20,6 +20,7 @@ yahweh-bus = { workspace = true }
|
||||
yahweh-theme = { workspace = true }
|
||||
gpui = { workspace = true }
|
||||
directories = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "tahuantinsuyu"
|
||||
|
||||
@@ -6,13 +6,22 @@
|
||||
//! Flujo:
|
||||
//!
|
||||
//! ```text
|
||||
//! Tree.Selected(Chart) → Shell → load chart + compute + set_mode(Wheel)
|
||||
//! Tree.Selected(Chart) → Shell → load chart + compose + 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_*)
|
||||
//! Canvas.TimeOffsetChanged → Shell → compose(current_chart, off, requests)
|
||||
//! Canvas.LayerVisibility[T] → Shell → flip module_configs[transit][enabled]
|
||||
//! Panel.ControlChanged → Shell → update module_configs OR canvas visibility
|
||||
//! ```
|
||||
//!
|
||||
//! ## module_configs
|
||||
//!
|
||||
//! Mapa `module_id → JSON` con la configuración persistente de cada
|
||||
//! módulo (transit, progression, …). De ahí derivamos los
|
||||
//! `PipelineRequest` que la engine consume. Los toggles "visuales"
|
||||
//! del NatalModule (`show_sign_dial`, `show_houses`, …) NO viven acá
|
||||
//! — afectan solo el render del canvas, no la composición.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use gpui::{
|
||||
Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, Window, div,
|
||||
@@ -22,7 +31,7 @@ use gpui::{
|
||||
use tahuantinsuyu_canvas::{
|
||||
AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
|
||||
};
|
||||
use tahuantinsuyu_engine::{LayerKind, compute_at_offset, compute_with_transits_at_now};
|
||||
use tahuantinsuyu_engine::{LayerKind, PipelineRequest, compose};
|
||||
use tahuantinsuyu_model::{Chart, TreeSelection};
|
||||
use tahuantinsuyu_panel::{ControlPanel, PanelEvent};
|
||||
use tahuantinsuyu_store::Store;
|
||||
@@ -40,13 +49,12 @@ 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,
|
||||
/// Overlay de tránsitos al instante actual sobre la natal. Disparado
|
||||
/// por el toggle `show_transits` del panel o la hotkey `[T]`.
|
||||
show_transits: bool,
|
||||
/// Estado de los módulos overlay (transit, progression, …) por
|
||||
/// `module_id`. Las claves dentro del JSON dependen del módulo (la
|
||||
/// convención es `"enabled": bool` para el toggle principal).
|
||||
module_configs: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
@@ -81,7 +89,7 @@ impl Shell {
|
||||
panel,
|
||||
current_chart: None,
|
||||
current_offset_minutes: 0,
|
||||
show_transits: false,
|
||||
module_configs: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,23 +172,28 @@ impl Shell {
|
||||
}
|
||||
}
|
||||
|
||||
/// Deriva los `PipelineRequest` activos a partir del `module_configs`.
|
||||
fn build_requests(&self) -> Vec<PipelineRequest> {
|
||||
let mut requests = Vec::new();
|
||||
if module_enabled(&self.module_configs, "transit") {
|
||||
requests.push(PipelineRequest::Transit);
|
||||
}
|
||||
requests
|
||||
}
|
||||
|
||||
fn render_current(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(chart) = self.current_chart.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let result = if self.show_transits {
|
||||
compute_with_transits_at_now(chart, self.current_offset_minutes)
|
||||
} else {
|
||||
compute_at_offset(chart, self.current_offset_minutes)
|
||||
};
|
||||
let render = match result {
|
||||
let requests = self.build_requests();
|
||||
let render = match compose(chart, self.current_offset_minutes, &requests) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[shell] compute {}{} (+{}min): {}",
|
||||
"[shell] compose {} (+{}min, {} reqs): {}",
|
||||
chart.id,
|
||||
if self.show_transits { " +transits" } else { "" },
|
||||
self.current_offset_minutes,
|
||||
requests.len(),
|
||||
e
|
||||
);
|
||||
return;
|
||||
@@ -205,17 +218,19 @@ impl Shell {
|
||||
}
|
||||
}
|
||||
CanvasEvent::LayerVisibilityChanged { kind, visible } => {
|
||||
// El toggle de Outer (hotkey [T]) significa "transit
|
||||
// overlay" — no es solo un layer hide, dispara un
|
||||
// recompute distinto. El resto son visibility puros.
|
||||
// El toggle de Outer ([T]) no es visibility puro: dispara
|
||||
// un pipeline distinto. Lo traducimos a un cambio en
|
||||
// module_configs["transit"]["enabled"] + re-render.
|
||||
if matches!(kind, LayerKind::Outer) {
|
||||
self.show_transits = *visible;
|
||||
set_module_enabled(&mut self.module_configs, "transit", *visible);
|
||||
self.panel.update(cx, |p, cx| {
|
||||
p.set_toggle("natal", "show_transits", *visible, cx)
|
||||
p.set_toggle("transit", "enabled", *visible, cx)
|
||||
});
|
||||
self.render_current(cx);
|
||||
return;
|
||||
}
|
||||
// El resto son visibility puros sobre el canvas. Sync el
|
||||
// panel para que el toggle visual coincida con la hotkey.
|
||||
let key = match kind {
|
||||
LayerKind::SignDial => "show_sign_dial",
|
||||
LayerKind::Houses => "show_houses",
|
||||
@@ -227,7 +242,7 @@ impl Shell {
|
||||
.update(cx, |p, cx| p.set_toggle("natal", key, *visible, cx));
|
||||
}
|
||||
CanvasEvent::ChartRequested(_) => {
|
||||
// Fase 5: doble click sobre un thumbnail abre la carta.
|
||||
// Fase 7: doble click sobre un thumbnail abre la carta.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,16 +252,10 @@ impl Shell {
|
||||
PanelEvent::ControlChanged {
|
||||
module_id, key, value,
|
||||
} => {
|
||||
let visible = value.as_bool().unwrap_or(true);
|
||||
let bool_val = value.as_bool().unwrap_or(true);
|
||||
if module_id == "natal" {
|
||||
if key == "show_transits" {
|
||||
self.show_transits = visible;
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_layer_visible(LayerKind::Outer, visible, cx)
|
||||
});
|
||||
self.render_current(cx);
|
||||
return;
|
||||
}
|
||||
// Toggles puramente visuales — solo afectan visibility
|
||||
// del render actual, sin recomponer.
|
||||
let kind = match key.as_str() {
|
||||
"show_sign_dial" => Some(LayerKind::SignDial),
|
||||
"show_houses" => Some(LayerKind::Houses),
|
||||
@@ -256,19 +265,62 @@ impl Shell {
|
||||
};
|
||||
if let Some(k) = kind {
|
||||
self.canvas
|
||||
.update(cx, |c, cx| c.set_layer_visible(k, visible, cx));
|
||||
.update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx));
|
||||
}
|
||||
} else {
|
||||
// Cualquier otro módulo: actualizamos su config y
|
||||
// recompomemos. La engine vuelve a llamarse con el
|
||||
// PipelineRequest derivado del nuevo estado.
|
||||
let entry = self
|
||||
.module_configs
|
||||
.entry(module_id.clone())
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
if let serde_json::Value::Object(map) = entry {
|
||||
map.insert(key.clone(), value.clone());
|
||||
}
|
||||
// Sincronizar visualmente el toggle [T] del canvas
|
||||
// cuando el cambio fue al "enabled" del transit.
|
||||
if module_id == "transit" && key == "enabled" {
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_layer_visible(LayerKind::Outer, bool_val, cx)
|
||||
});
|
||||
}
|
||||
self.render_current(cx);
|
||||
}
|
||||
}
|
||||
PanelEvent::ModuleToggled { .. } => {
|
||||
// Fase 6: encender/apagar módulos enteros (Progression,
|
||||
// Synastry, Uranian).
|
||||
// Fase 7: encender/apagar módulos enteros desde un
|
||||
// header con switch (vs. el toggle por-control de hoy).
|
||||
}
|
||||
}
|
||||
let _ = (&self.store, &self.tree, &self.bus);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Helpers de module_configs
|
||||
// =====================================================================
|
||||
|
||||
fn module_enabled(cfgs: &HashMap<String, serde_json::Value>, id: &str) -> bool {
|
||||
cfgs.get(id)
|
||||
.and_then(|c| c.get("enabled"))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn set_module_enabled(
|
||||
cfgs: &mut HashMap<String, serde_json::Value>,
|
||||
id: &str,
|
||||
enabled: bool,
|
||||
) {
|
||||
let entry = cfgs
|
||||
.entry(id.to_string())
|
||||
.or_insert_with(|| serde_json::json!({}));
|
||||
if let serde_json::Value::Object(map) = entry {
|
||||
map.insert("enabled".into(), serde_json::Value::Bool(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Shell {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = Theme::global(cx).clone();
|
||||
|
||||
@@ -237,42 +237,45 @@ fn compute_natal_chart(
|
||||
Ok((natal, config_e, observer))
|
||||
}
|
||||
|
||||
pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result<RenderModel, EngineError> {
|
||||
let t0 = Instant::now();
|
||||
let (natal, _, _) = compute_natal_chart(chart, offset_minutes)?;
|
||||
let aspects = find_aspects(&natal, &OrbTable::modern_western());
|
||||
Ok(build_render_model(chart, &natal, &aspects, t0))
|
||||
}
|
||||
|
||||
/// Pipeline natal + overlay de tránsitos. Computa la carta natal
|
||||
/// (eventualmente con un `offset_minutes` aplicado) **y además** una
|
||||
/// segunda `NatalChart` con el mismo observer pero al instante
|
||||
/// `transit_at` (usualmente `Instant::now()`). Devuelve un `RenderModel`
|
||||
/// con dos capas extra:
|
||||
///
|
||||
/// - `LayerKind::Outer` con `module_id = "transit"` — glifos
|
||||
/// planetarios del cielo actual, pintados en un anillo externo.
|
||||
/// - `LayerKind::Aspects` con `module_id = "transit"` — aspectos cross
|
||||
/// natal × transit (sólo mayores). Convención: `LineSeg.from_deg` =
|
||||
/// longitud natal, `LineSeg.to_deg` = longitud transit.
|
||||
pub fn compute_with_transits(
|
||||
/// Composición principal: natal + overlays pedidos. Es la función que
|
||||
/// `lib::compose` delega cuando el feature `eternal-bridge` está activo.
|
||||
pub fn compose(
|
||||
chart: &Chart,
|
||||
offset_minutes: i64,
|
||||
transit_at: ESInstant,
|
||||
requests: &[crate::PipelineRequest],
|
||||
) -> Result<RenderModel, EngineError> {
|
||||
let t0 = Instant::now();
|
||||
let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?;
|
||||
let aspects = find_aspects(&natal, &OrbTable::modern_western());
|
||||
let mut render = build_render_model(chart, &natal, &aspects, t0);
|
||||
|
||||
// Carta de tránsito: mismo observer, mismo config, instante "ahora".
|
||||
for req in requests {
|
||||
match req {
|
||||
crate::PipelineRequest::Transit => {
|
||||
build_transit_overlay(&natal, &config_e, observer, ESInstant::now(), &mut render)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render.compute_ms = t0.elapsed().as_millis() as u64;
|
||||
Ok(render)
|
||||
}
|
||||
|
||||
/// Helper: agrega al `RenderModel` las dos capas del overlay de
|
||||
/// tránsitos (Outer + cross Aspects).
|
||||
fn build_transit_overlay(
|
||||
natal: &NatalChart,
|
||||
config_e: &ChartConfig,
|
||||
observer: Observer,
|
||||
transit_at: ESInstant,
|
||||
render: &mut RenderModel,
|
||||
) -> Result<(), EngineError> {
|
||||
let transit_birth = BirthData::new(transit_at, observer);
|
||||
let session = session()?;
|
||||
let transit = NatalChart::compute(&transit_birth, &config_e, session).map_err(|e| {
|
||||
let transit = NatalChart::compute(&transit_birth, config_e, session).map_err(|e| {
|
||||
EngineError::Eternal(format!("NatalChart::compute (transit): {:?}", e))
|
||||
})?;
|
||||
|
||||
// Outer ring de glifos: planetas del cielo actual.
|
||||
let outer_glyphs: Vec<Glyph> = transit
|
||||
.placements
|
||||
.iter()
|
||||
@@ -293,10 +296,8 @@ pub fn compute_with_transits(
|
||||
glyphs: outer_glyphs,
|
||||
});
|
||||
|
||||
// Cross aspects natal × transit. find_synastry_aspects toma una lista
|
||||
// de `AspectKind`s — usamos solo mayores para no saturar.
|
||||
let cross = find_synastry_aspects(
|
||||
&natal,
|
||||
natal,
|
||||
&transit,
|
||||
&OrbTable::modern_western(),
|
||||
EAspectKind::MAJORS,
|
||||
@@ -311,8 +312,6 @@ pub fn compute_with_transits(
|
||||
from_deg: natal_p.longitude.longitude_deg() as f32,
|
||||
to_deg: transit_p.longitude.longitude_deg() as f32,
|
||||
kind: aspect_kind_id(a.kind).into(),
|
||||
// Apagamos un poco más los cross para distinguirlos del
|
||||
// tejido natal-natal.
|
||||
opacity: opacity * 0.75,
|
||||
})
|
||||
})
|
||||
@@ -325,17 +324,7 @@ pub fn compute_with_transits(
|
||||
geometry: Geometry::Lines(cross_lines),
|
||||
glyphs: Vec::new(),
|
||||
});
|
||||
|
||||
render.compute_ms = t0.elapsed().as_millis() as u64;
|
||||
Ok(render)
|
||||
}
|
||||
|
||||
/// Atajo: tránsitos al instante actual del reloj.
|
||||
pub fn compute_with_transits_at_now(
|
||||
chart: &Chart,
|
||||
offset_minutes: i64,
|
||||
) -> Result<RenderModel, EngineError> {
|
||||
compute_with_transits(chart, offset_minutes, ESInstant::now())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
|
||||
@@ -151,53 +151,60 @@ pub enum EngineError {
|
||||
// API pública
|
||||
// =====================================================================
|
||||
|
||||
/// Computa el RenderModel real contra eternal-astrology si el feature
|
||||
/// está prendido; sino cae al mock.
|
||||
pub fn compute(chart: &Chart) -> Result<RenderModel, EngineError> {
|
||||
compute_at_offset(chart, 0)
|
||||
/// Pedidos que el host (Shell) eleva a la engine para componer un
|
||||
/// `RenderModel`. La capa natal **siempre** se computa; estos requests
|
||||
/// son **overlays adicionales**.
|
||||
///
|
||||
/// Cada variante mapea 1-a-1 con un Module declarado en
|
||||
/// `tahuantinsuyu-modules` por id string. Esto deja la engine como
|
||||
/// dueña única del cómputo (no depende del trait Module — los módulos
|
||||
/// son sólo metadata + UI controls).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PipelineRequest {
|
||||
/// `module_id = "transit"` — anillo externo con planetas al
|
||||
/// instante actual (reloj de pared) + cross aspects natal × transit.
|
||||
Transit,
|
||||
// ── Fase 7 ──────────────────────────────────────────────────────
|
||||
// SecondaryProgression { target_year: i32 },
|
||||
// SolarArc { target_year: i32 },
|
||||
// Synastry { partner: tahuantinsuyu_model::ChartId },
|
||||
}
|
||||
|
||||
/// Variante con offset temporal en minutos sobre el instante del chart.
|
||||
/// Útil para time-scrubbing: el jog-dial del canvas pasa el offset
|
||||
/// acumulado y la engine recompone toda la pipeline (Asc, casas,
|
||||
/// posiciones planetarias, aspectos) para ese instante desplazado.
|
||||
pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result<RenderModel, EngineError> {
|
||||
/// Composición canónica: carta natal + todos los overlays pedidos.
|
||||
/// Es la única función que el Shell necesita llamar — `compute_at_offset`
|
||||
/// y `compute_with_transits_at_now` quedan como atajos retrocompatibles.
|
||||
pub fn compose(
|
||||
chart: &Chart,
|
||||
offset_minutes: i64,
|
||||
requests: &[PipelineRequest],
|
||||
) -> Result<RenderModel, EngineError> {
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
{
|
||||
bridge::compute_at_offset(chart, offset_minutes)
|
||||
bridge::compose(chart, offset_minutes, requests)
|
||||
}
|
||||
#[cfg(not(feature = "eternal-bridge"))]
|
||||
{
|
||||
let _ = offset_minutes;
|
||||
let _ = (offset_minutes, requests);
|
||||
Ok(compute_mock(chart))
|
||||
}
|
||||
}
|
||||
|
||||
/// Variante con overlay de tránsitos al **instante actual** (reloj de
|
||||
/// pared). Computa la carta natal igual que [`compute_at_offset`] y le
|
||||
/// suma dos capas extras:
|
||||
///
|
||||
/// - `LayerKind::Outer` con `module_id = "transit"` — glifos
|
||||
/// planetarios del cielo del momento, sobre un anillo externo.
|
||||
/// - `LayerKind::Aspects` con `module_id = "transit"` — líneas natal ↔
|
||||
/// transit (sólo aspectos mayores). Por convención, en cada
|
||||
/// `LineSeg` el `from_deg` es la longitud natal y el `to_deg` la
|
||||
/// longitud del planeta de tránsito.
|
||||
///
|
||||
/// Sin el feature `eternal-bridge` cae al mock (sin overlay).
|
||||
/// Atajo: natal sin overlays. Equivalente a `compose(chart, 0, &[])`.
|
||||
pub fn compute(chart: &Chart) -> Result<RenderModel, EngineError> {
|
||||
compose(chart, 0, &[])
|
||||
}
|
||||
|
||||
/// Atajo: natal con time-scrubbing pero sin overlays.
|
||||
pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result<RenderModel, EngineError> {
|
||||
compose(chart, offset_minutes, &[])
|
||||
}
|
||||
|
||||
/// Atajo: natal + overlay de tránsitos al instante actual.
|
||||
pub fn compute_with_transits_at_now(
|
||||
chart: &Chart,
|
||||
offset_minutes: i64,
|
||||
) -> Result<RenderModel, EngineError> {
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
{
|
||||
bridge::compute_with_transits_at_now(chart, offset_minutes)
|
||||
}
|
||||
#[cfg(not(feature = "eternal-bridge"))]
|
||||
{
|
||||
let _ = offset_minutes;
|
||||
Ok(compute_mock(chart))
|
||||
}
|
||||
compose(chart, offset_minutes, &[PipelineRequest::Transit])
|
||||
}
|
||||
|
||||
/// Stub determinista — útil para tests + para la UI sin eternal.
|
||||
|
||||
@@ -123,6 +123,7 @@ impl Registry {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -203,12 +204,6 @@ pub mod natal {
|
||||
default: true,
|
||||
hotkey: Some("P".into()),
|
||||
},
|
||||
Control::Toggle {
|
||||
key: "show_transits".into(),
|
||||
label: "Tránsitos (ahora)".into(),
|
||||
default: false,
|
||||
hotkey: Some("T".into()),
|
||||
},
|
||||
Control::Slider {
|
||||
key: "harmonic".into(),
|
||||
label: "Armónico".into(),
|
||||
@@ -229,15 +224,68 @@ pub mod natal {
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 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_natal() {
|
||||
fn registry_finds_builtins() {
|
||||
let r = Registry::with_builtins();
|
||||
assert!(r.find("natal").is_some());
|
||||
assert_eq!(r.for_kind(ChartKind::Natal).len(), 1);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user