feat(tahuantinsuyu): scaffolding del estudio astrológico (10 crates + ventana 3-panes)
Módulo nuevo `modules/tahuantinsuyu/` con 9 crates reusables + app `apps/tahuantinsuyu` ejecutable que abre la ventana del explorador y coordina los widgets: - tahuantinsuyu-card: Card Brahman + spawn_sidecar (flows chart-request/chart-result). - tahuantinsuyu-model: tipos agnósticos (Group/Contact/Chart, StoredBirthData, StoredChartConfig, ChartKind, TreeSelection). - tahuantinsuyu-store: persistencia SQLite (rusqlite) con migración v1, CRUD por entidad y descenso recursivo `charts_under_group`. - tahuantinsuyu-engine: bridge agnóstico al canvas vía `RenderModel` (Layer/Glyph/Geometry). Feature `eternal-bridge` (off por default) reservada para enchufar eternal-astrology desde ~/eternal. - tahuantinsuyu-modules: registry de módulos pluggables (Module trait + Control schema) con `NatalModule` placeholder. - tahuantinsuyu-theme: AstroPalette (elementos / modos / planetas / aspectos) con variantes dark + light sobre yahweh-theme. - tahuantinsuyu-canvas: widget GPUI con CanvasState (Empty / Wheel / Thumbnails). Render placeholder hasta cablear la rueda real. - tahuantinsuyu-tree: explorador izquierdo sobre yahweh-widget-tree, prefijos g:/c:/h: para Group/Contact/Chart. - tahuantinsuyu-panel: control panel inferior que lee Controls de los módulos del registry y los pinta. - apps/tahuantinsuyu: binario `tahuantinsuyu` (launch_app-style) con Shell coordinador (tree↔canvas↔panel), DB en $XDG_DATA_HOME. Workspace Cargo.toml actualizado con los 10 miembros. `cargo check` verde, tests unitarios verdes (model/store/engine/modules/theme/card). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
//! `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_ecliptic".into(),
|
||||
label: "Eclíptica".into(),
|
||||
default: true,
|
||||
hotkey: Some("E".into()),
|
||||
},
|
||||
Control::Toggle {
|
||||
key: "show_ascensional".into(),
|
||||
label: "Ascensional".into(),
|
||||
default: false,
|
||||
hotkey: Some("A".into()),
|
||||
},
|
||||
Control::Toggle {
|
||||
key: "show_aspects".into(),
|
||||
label: "Aspectos".into(),
|
||||
default: true,
|
||||
hotkey: None,
|
||||
},
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user