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:
sergio
2026-05-17 10:31:11 +00:00
parent 4d14a4495f
commit d4761bf238
6 changed files with 215 additions and 117 deletions
@@ -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(())
}
// =====================================================================