⎇ 0ada1050f7ea45109d7ce6d97c37c8c3344eb0e3
crates/modules/cosmobiologia/cosmobiologia-render/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(cosmobiologia): rectificador automático — escaneo GR (núcleo)
1//! `cosmobiologia-render` — modelo y matemática de render
2//! **agnósticos de surface**. Lo consumen tanto el canvas gpui
3//! (nativo, render Vulkan/Metal) como el cliente web (WASM, render
4//! SVG / Canvas2D). Cualquier mejora del layout / spread / cluster /
5//! coords vive acá una sola vez y aparece en ambos clientes.
6//!
7//! ## Por qué un crate aparte
8//!
9//! `cosmobiologia-engine` arrastra `eternal-sky` (VSOP2013 + I/O de
10//! tablas) que **no compila a WASM** sin empaquetar 30+ MB de
11//! efemérides. Los tipos del `RenderModel` en sí son serde puro y
12//! sí compilan a WASM — extraerlos a este crate libera al cliente
13//! web de la dependencia transitiva.
14//!
15//! ## Capas
16//!
17//! 1. **Modelo de render** — `RenderModel`, `Layer`, `Glyph`,
18//! `LineSeg`, `Geometry`, `LayerKind`. Estructuras serde-friendly
19//! que el engine emite y los clients consumen.
20//! 2. **Matemática agnóstica** *(módulos siguientes, no en esta primera
21//! versión)* — `polar_to_screen`, `spread_angles`, `find_clusters`,
22//! `format_coord_compact`, `Radii`. Migran desde el canvas gpui.
23//! 3. **`DrawCommand`** *(módulo siguiente)* — primitivas de pintura
24//! (line, circle, glyph, pill) que cada surface traduce a su API.
25
26#![forbid(unsafe_code)]
27#![warn(rust_2018_idioms)]
28
29use serde::{Deserialize, Serialize};
30
31pub use cosmobiologia_model::{Chart, ChartId, ChartKind};
32
33pub mod draw;
34pub mod gr;
35pub mod harmonic;
36pub mod math;
37pub mod palette;
38
39pub use draw::{
40 compose_wheel, draw_commands_to_svg, CompositionOpts, DrawCommand, Rgba, TextAnchor,
41};
42pub use gr::{compute_gr_triggers, convergencia_minima, GrDirection, GrTrigger};
43pub use harmonic::apply_harmonic;
44pub use math::{
45 find_clusters, format_coord_compact, polar_to_screen, spread_angles, Radii,
46};
47pub use palette::Palette;
48
49// =====================================================================
50// RenderModel — lo que el client renderea
51// =====================================================================
52
53/// Resultado agnóstico de un cómputo astrológico, listo para renderizar.
54/// El canvas gpui y el cliente web lo consumen idénticamente: el engine
55/// computa (en nativo, con eternal) y publica este struct.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RenderModel {
58 pub chart_id: ChartId,
59 pub chart_kind: ChartKind,
60 pub title: String,
61 #[serde(default)]
62 pub subtitle: Option<String>,
63 pub compute_ms: u64,
64
65 // ─── Ángulos del chart (grados eclípticos, 0..360) ───────────────
66 /// Ascendente — punto fijo de rotación del lienzo. La rueda se gira
67 /// de modo que el Asc cae a las 9 (lado izquierdo).
68 pub ascendant_deg: f32,
69 pub midheaven_deg: f32,
70 pub descendant_deg: f32,
71 pub imum_coeli_deg: f32,
72
73 /// Capas a pintar. Orden = z-order ascendente.
74 pub layers: Vec<Layer>,
75 /// Metadata humana por overlay activo (transit, progresión,
76 /// sinastría, retorno...). Vacío para una carta natal pura. La UI
77 /// la pinta como badges en el footer.
78 #[serde(default)]
79 pub overlays: Vec<OverlayMeta>,
80 /// Lista paralela a las LineSeg de aspectos — uno por aspecto
81 /// natal o cross. Ordenado por `orb_deg` ascendente (los más
82 /// cerrados primero). La UI lo usa para la lista textual.
83 #[serde(default)]
84 pub aspect_summary: Vec<AspectSummary>,
85 /// Grupos uranianos detectados (cuerpos en la misma posición mod 90).
86 /// Vacío sino se activó el módulo Uranian.
87 #[serde(default)]
88 pub uranian_groups: Vec<UranianGroup>,
89 /// Triggers del Sistema GR (direcciones primarias). Poblado sólo
90 /// cuando el módulo `primary_directions` está activo; ordenado por
91 /// `orb_deg` ascendente. La UI lo lista en el HUD de rectificación
92 /// y resalta los `event = true` (convergencias directo+converso).
93 #[serde(default)]
94 pub gr_triggers: Vec<GrTrigger>,
95 /// Orden de la carta armónica activa. `1` = carta natal pura.
96 #[serde(default = "default_harmonic")]
97 pub harmonic: u32,
98 /// Espectro de fuerza armónica: índice `i` = fuerza de la armónica
99 /// `i + 1`. Vacío salvo en modo armónico (`harmonic > 1`). La UI
100 /// lo pinta como histograma para guiar qué armónico mirar.
101 #[serde(default)]
102 pub harmonic_spectrum: Vec<f32>,
103}
104
105/// Default serde del campo `harmonic`: 1 (carta natal sin transformar).
106fn default_harmonic() -> u32 {
107 1
108}
109
110/// Etiqueta legible de un overlay para el footer del canvas. La engine
111/// la pushea desde cada `build_*_overlay`; el canvas solo lee y pinta.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct OverlayMeta {
114 pub module_id: String,
115 /// Etiqueta corta — ej. "Tránsito ahora", "Progresión 38.2a",
116 /// "Sinastría · Ana", "Saturn return 29a".
117 pub label: String,
118}
119
120/// Grupo de cuerpos natales que caen en la misma posición del
121/// dial uraniano de 90° (su longitud zodiacal módulo 90 es igual o
122/// muy cercana). En la astrología uraniana esto es una "fórmula" o
123/// "axis" — los cuerpos están en correspondencia simbólica directa
124/// porque comparten un cuadrante simétrico.
125///
126/// Solo se emiten grupos con 2+ miembros (los singletons no son
127/// fórmulas). La engine los ordena por proximidad al ε de tolerancia.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct UranianGroup {
130 /// Identificadores agnósticos de los cuerpos en el grupo
131 /// (ej. `["sun", "jupiter", "saturn"]`).
132 pub bodies: Vec<String>,
133 /// Posición en el dial de 90° (la longitud módulo 90).
134 pub mod90_deg: f64,
135}
136
137/// Resumen textual de un aspecto para listas legibles. La engine lo
138/// emite en paralelo con las `LineSeg` de la capa de aspectos, así
139/// el canvas no tiene que re-derivar nombres de cuerpos desde grados.
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct AspectSummary {
142 /// Module al que pertenece — "natal", "transit", "synastry",
143 /// "progression", "solar_arc", "planetary_return".
144 pub module_id: String,
145 /// Identificador agnóstico del cuerpo "a" — "sun", "moon", etc.
146 pub from_body: String,
147 pub to_body: String,
148 /// Identificador del aspecto — "conjunction", "trine", etc.
149 pub kind: String,
150 pub orb_deg: f64,
151 /// `Some(true)` = applying, `Some(false)` = separating. `None` para
152 /// cross-aspects (sinastría/return) donde no se computa.
153 #[serde(default)]
154 pub applying: Option<bool>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Layer {
159 pub module_id: String,
160 pub kind: LayerKind,
161 /// Radio normalizado [0, 1] sobre el lienzo — el canvas lo convierte
162 /// a píxeles. Permite stack de anillos.
163 pub ring: f32,
164 #[serde(default)]
165 pub z: i32,
166 pub geometry: Geometry,
167 #[serde(default)]
168 pub glyphs: Vec<Glyph>,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173pub enum LayerKind {
174 SignDial,
175 Houses,
176 Bodies,
177 Aspects,
178 Lots,
179 FixedStars,
180 Midpoints,
181 Outer,
182 Custom,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub enum Geometry {
187 GlyphsOnly,
188 /// Anillo dividido en sectores. `cusps_deg` son los grados
189 /// zodiacales donde van las divisiones radiales.
190 Ring { cusps_deg: Vec<f32> },
191 Lines(Vec<LineSeg>),
192 Points(Vec<PointMark>),
193}
194
195#[derive(Debug, Clone, Default, Serialize, Deserialize)]
196pub struct LineSeg {
197 /// Grados zodiacales del extremo "a".
198 pub from_deg: f32,
199 /// Grados zodiacales del extremo "b".
200 pub to_deg: f32,
201 /// Categoría simbólica (`"conjunction"`, `"trine"`, …) — el theme la
202 /// resuelve a color.
203 pub kind: String,
204 pub opacity: f32,
205 /// Cuerpo en el extremo "a" — populado para LineSegs de aspectos
206 /// (natal × natal, cross con overlays). Vacío en `Default::default`
207 /// para serde back-compat.
208 #[serde(default)]
209 pub from_body: String,
210 /// Cuerpo en el extremo "b".
211 #[serde(default)]
212 pub to_body: String,
213 /// Orb absoluto en grados (para tooltips).
214 #[serde(default)]
215 pub orb_deg: f32,
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct PointMark {
220 pub deg: f32,
221 pub label: String,
222 pub tag: String,
223}
224
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226pub struct Glyph {
227 /// Grado eclíptico [0, 360).
228 pub deg: f32,
229 /// Glyph simbólico — el theme/canvas lo mapea a unicode o imagen.
230 /// Ej: `"sun"`, `"moon"`, `"aries"`, `"asc"`, `"mc"`.
231 pub symbol: String,
232 #[serde(default)]
233 pub annotation: Option<String>,
234 #[serde(default)]
235 pub retrograde: bool,
236 #[serde(default)]
237 pub house: Option<u8>,
238 /// Marker de dignidad esencial, set solo cuando
239 /// `NatalOptions::show_dignities` está activo: `"+"` (domicilio),
240 /// `"·"` (exaltación), `"−"` (exilio), `"*"` (caída).
241 #[serde(default)]
242 pub dignity_marker: Option<String>,
243}
244
245/// Módulos overlay que pintan en el mismo slot (outer ring del wheel)
246/// y por lo tanto son **mutuamente excluyentes** a nivel de UI: al
247/// prender uno, el shell debe apagar los otros. Single source of truth
248/// — el shell y el canvas leen de acá en vez de hardcodear listas.
249pub const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"];