refactor(tahuantinsuyu): extrae tahuantinsuyu-render — preparación para WASM
Fase 1 de "módulo web": extracción del modelo y la matemática
agnóstica de surface a un crate separado, sin dependencia de
gpui ni de eternal. Es la base sobre la que el cliente WASM y
el canvas nativo van a converger.
Crate nuevo `tahuantinsuyu-render`:
- Tipos del RenderModel migrados desde `tahuantinsuyu-engine`:
`RenderModel`, `Layer`, `LayerKind`, `Geometry`, `LineSeg`,
`PointMark`, `Glyph`, `OverlayMeta`, `UranianGroup`,
`AspectSummary`, `OUTER_RING_MODULES`. El engine los
reexporta — ningún call site del shell/canvas/modules/tree/
panel cambia su `use`.
- Módulo `math` con la geometría canónica del wheel migrada
desde `tahuantinsuyu-canvas`:
* `Radii` con los aros A/B/C/D/E + helpers `body_ring` y
`aspect_endpoints`
* `polar_to_screen` (Asc a las 9 del reloj)
* `spread_angles` (anti-solapamiento con damping + clamp por
glyph)
* `find_clusters` (con wrap-around)
* `format_coord_compact` ("DD°MM'{signo}")
- 10 tests del math (5 spread + 4 coord + 1 polar) viajaron con
las implementaciones. El canvas se queda solo con los tests
de UI.
Por qué un crate aparte:
- `tahuantinsuyu-engine` arrastra `eternal-sky` (VSOP2013 +
I/O de tablas) que NO compila a WASM sin empaquetar 30+ MB
de efemérides. Los tipos del modelo son serde puro y sí
compilan a WASM — extraerlos libera al cliente web futuro
de la dependencia transitiva.
- Cuando llegue la fase 2 (`tahuantinsuyu-server` axum) y la
fase 3 (`tahuantinsuyu-web` cdylib WASM), ambos consumen
`tahuantinsuyu-render` con la misma fuente de verdad sobre
el layout, evitando duplicar la lógica entre desktop y web.
Pendiente: `tahuantinsuyu-model` arrastra `uuid → getrandom`
que falla a WASM sin `wasm_js` feature flag. Lo resuelvo en la
fase del cliente WASM (necesita su propio Cargo.toml con la
config getrandom + .cargo/config con RUSTFLAGS).
Tests: 20 verdes (10 shell + 10 render math). Compilación
nativa OK; canvas sin cambios visuales (mismo código,
diferente origen).
This commit is contained in:
@@ -7,6 +7,7 @@ description = "Tahuantinsuyu — bridge entre el modelo agnóstico y eternal-ast
|
||||
|
||||
[dependencies]
|
||||
tahuantinsuyu-model = { path = "../tahuantinsuyu-model" }
|
||||
tahuantinsuyu-render = { path = "../tahuantinsuyu-render" }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -25,11 +25,21 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub use tahuantinsuyu_model::{Chart, ChartId, ChartKind};
|
||||
|
||||
// Los tipos del RenderModel viven en `tahuantinsuyu-render` (crate
|
||||
// agnóstico de surface — compila a WASM, lo consumen tanto el canvas
|
||||
// gpui como el cliente web). El engine los reexporta para mantener
|
||||
// compatibilidad con todos los call sites históricos
|
||||
// (`tahuantinsuyu_engine::Layer`, etc.) sin tener que cambiar
|
||||
// imports en el shell, canvas, modules, tree, panel...
|
||||
pub use tahuantinsuyu_render::{
|
||||
AspectSummary, Geometry, Glyph, Layer, LayerKind, LineSeg, OverlayMeta, PointMark,
|
||||
RenderModel, UranianGroup, OUTER_RING_MODULES,
|
||||
};
|
||||
|
||||
// `Chart` reexportado arriba es lo que `PipelineRequest::Synastry`
|
||||
// transporta — el caller (shell) lee del Store y pasa el Chart entero
|
||||
// para que el bridge construya su NatalChart en eternal.
|
||||
@@ -43,181 +53,6 @@ mod natal_cache;
|
||||
#[cfg(feature = "eternal-bridge")]
|
||||
pub mod svg_export;
|
||||
|
||||
// =====================================================================
|
||||
// RenderModel — lo que el canvas necesita pintar
|
||||
// =====================================================================
|
||||
|
||||
/// Resultado agnóstico de un cómputo astrológico, listo para renderizar.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RenderModel {
|
||||
pub chart_id: ChartId,
|
||||
pub chart_kind: ChartKind,
|
||||
pub title: String,
|
||||
#[serde(default)]
|
||||
pub subtitle: Option<String>,
|
||||
pub compute_ms: u64,
|
||||
|
||||
// ─── Ángulos del chart (grados eclípticos, 0..360) ───────────────
|
||||
/// Ascendente — punto fijo de rotación del lienzo. La rueda se gira
|
||||
/// de modo que el Asc cae a las 9 (lado izquierdo).
|
||||
pub ascendant_deg: f32,
|
||||
pub midheaven_deg: f32,
|
||||
pub descendant_deg: f32,
|
||||
pub imum_coeli_deg: f32,
|
||||
|
||||
/// Capas a pintar. Orden = z-order ascendente.
|
||||
pub layers: Vec<Layer>,
|
||||
/// Metadata humana por overlay activo (transit, progresión,
|
||||
/// sinastría, retorno...). Vacío para una carta natal pura. La UI
|
||||
/// la pinta como badges en el footer.
|
||||
#[serde(default)]
|
||||
pub overlays: Vec<OverlayMeta>,
|
||||
/// Lista paralela a las LineSeg de aspectos — uno por aspecto
|
||||
/// natal o cross. Ordenado por `orb_deg` ascendente (los más
|
||||
/// cerrados primero). La UI lo usa para la lista textual.
|
||||
#[serde(default)]
|
||||
pub aspect_summary: Vec<AspectSummary>,
|
||||
/// Grupos uranianos detectados (cuerpos en la misma posición mod 90).
|
||||
/// Vacío sino se activó el módulo Uranian.
|
||||
#[serde(default)]
|
||||
pub uranian_groups: Vec<UranianGroup>,
|
||||
}
|
||||
|
||||
/// Etiqueta legible de un overlay para el footer del canvas. La engine
|
||||
/// la pushea desde cada `build_*_overlay`; el canvas solo lee y pinta.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OverlayMeta {
|
||||
pub module_id: String,
|
||||
/// Etiqueta corta — ej. "Tránsito ahora", "Progresión 38.2a",
|
||||
/// "Sinastría · Ana", "Saturn return 29a".
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
/// Grupo de cuerpos natales que caen en la misma posición del
|
||||
/// dial uraniano de 90° (su longitud zodiacal módulo 90 es igual o
|
||||
/// muy cercana). En la astrología uraniana esto es una "fórmula" o
|
||||
/// "axis" — los cuerpos están en correspondencia simbólica directa
|
||||
/// porque comparten un cuadrante simétrico.
|
||||
///
|
||||
/// Solo se emiten grupos con 2+ miembros (los singletons no son
|
||||
/// fórmulas). La engine los ordena por proximidad al ε de tolerancia.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UranianGroup {
|
||||
/// Identificadores agnósticos de los cuerpos en el grupo
|
||||
/// (ej. `["sun", "jupiter", "saturn"]`).
|
||||
pub bodies: Vec<String>,
|
||||
/// Posición en el dial de 90° (la longitud módulo 90).
|
||||
pub mod90_deg: f64,
|
||||
}
|
||||
|
||||
/// Resumen textual de un aspecto para listas legibles. La engine lo
|
||||
/// emite en paralelo con las `LineSeg` de la capa de aspectos, así
|
||||
/// el canvas no tiene que re-derivar nombres de cuerpos desde grados.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AspectSummary {
|
||||
/// Module al que pertenece — "natal", "transit", "synastry",
|
||||
/// "progression", "solar_arc", "planetary_return".
|
||||
pub module_id: String,
|
||||
/// Identificador agnóstico del cuerpo "a" — "sun", "moon", etc.
|
||||
pub from_body: String,
|
||||
pub to_body: String,
|
||||
/// Identificador del aspecto — "conjunction", "trine", etc.
|
||||
pub kind: String,
|
||||
pub orb_deg: f64,
|
||||
/// `Some(true)` = applying, `Some(false)` = separating. `None` para
|
||||
/// cross-aspects (sinastría/return) donde no se computa.
|
||||
#[serde(default)]
|
||||
pub applying: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Layer {
|
||||
pub module_id: String,
|
||||
pub kind: LayerKind,
|
||||
/// Radio normalizado [0, 1] sobre el lienzo — el canvas lo convierte
|
||||
/// a píxeles. Permite stack de anillos.
|
||||
pub ring: f32,
|
||||
#[serde(default)]
|
||||
pub z: i32,
|
||||
pub geometry: Geometry,
|
||||
#[serde(default)]
|
||||
pub glyphs: Vec<Glyph>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum LayerKind {
|
||||
SignDial,
|
||||
Houses,
|
||||
Bodies,
|
||||
Aspects,
|
||||
Lots,
|
||||
FixedStars,
|
||||
Midpoints,
|
||||
Outer,
|
||||
Custom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Geometry {
|
||||
GlyphsOnly,
|
||||
/// Anillo dividido en sectores. `cusps_deg` son los grados
|
||||
/// zodiacales donde van las divisiones radiales.
|
||||
Ring { cusps_deg: Vec<f32> },
|
||||
Lines(Vec<LineSeg>),
|
||||
Points(Vec<PointMark>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct LineSeg {
|
||||
/// Grados zodiacales del extremo "a".
|
||||
pub from_deg: f32,
|
||||
/// Grados zodiacales del extremo "b".
|
||||
pub to_deg: f32,
|
||||
/// Categoría simbólica (`"conjunction"`, `"trine"`, …) — el theme la
|
||||
/// resuelve a color.
|
||||
pub kind: String,
|
||||
pub opacity: f32,
|
||||
/// Cuerpo en el extremo "a" — populado para LineSegs de aspectos
|
||||
/// (natal × natal, cross con overlays). Vacío en `Default::default`
|
||||
/// para serde back-compat.
|
||||
#[serde(default)]
|
||||
pub from_body: String,
|
||||
/// Cuerpo en el extremo "b".
|
||||
#[serde(default)]
|
||||
pub to_body: String,
|
||||
/// Orb absoluto en grados (para tooltips).
|
||||
#[serde(default)]
|
||||
pub orb_deg: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PointMark {
|
||||
pub deg: f32,
|
||||
pub label: String,
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Glyph {
|
||||
/// Grado eclíptico [0, 360).
|
||||
pub deg: f32,
|
||||
/// Glyph simbólico — el theme/canvas lo mapea a unicode o imagen.
|
||||
/// Ej: `"sun"`, `"moon"`, `"aries"`, `"asc"`, `"mc"`.
|
||||
pub symbol: String,
|
||||
#[serde(default)]
|
||||
pub annotation: Option<String>,
|
||||
#[serde(default)]
|
||||
pub retrograde: bool,
|
||||
#[serde(default)]
|
||||
pub house: Option<u8>,
|
||||
/// Marker de dignidad esencial, set solo cuando
|
||||
/// `NatalOptions::show_dignities` está activo: `"+"` (domicilio),
|
||||
/// `"·"` (exaltación), `"−"` (exilio), `"*"` (caída).
|
||||
#[serde(default)]
|
||||
pub dignity_marker: Option<String>,
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Errores
|
||||
// =====================================================================
|
||||
@@ -246,12 +81,6 @@ pub enum EngineError {
|
||||
/// `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).
|
||||
/// Módulos overlay que pintan en el mismo slot (outer ring del wheel)
|
||||
/// y por lo tanto son **mutuamente excluyentes** a nivel de UI: al
|
||||
/// prender uno, el shell debe apagar los otros. Single source of truth
|
||||
/// — el shell y el canvas leen de acá en vez de hardcodear listas.
|
||||
pub const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PipelineRequest {
|
||||
/// `module_id = "transit"` — anillo externo con planetas al
|
||||
|
||||
Reference in New Issue
Block a user