86fb6ae20b
El render agnóstico ya no es un esqueleto — porta al WASM la mayoría de los detalles visuales que tenía solo el canvas gpui nativo: - palette.rs: Palette dark/light replicando AstroPalette del theme nativo, pero en Rgba (no Hsla de gpui). Métodos planet/aspect/sign para resolver color por id simbólico, + house_ring con hue-shift. - CompositionOpts extendido: palette, dial_3d, draw_ascensional_cross, show_coord_labels, show_minor_aspects. Defaults razonables. - compose_wheel ahora dibuja: background panel, dial 3D bevel (4 strokes concéntricos con alpha decreciente), subdivisiones cada 10° con sign boundaries reforzados, signos con color elemental, casas topocéntricas + geocéntricas en sus rings canónicos, cuerpos con spread anti-solapamiento + clusters + disco coloreado por planeta, coord labels "DD°MM'♈" en natal, aspectos con width inversa al orbe + filtrado opcional de minors, cruz ascensional dashed + pills ASC/MC/DESC/IC. - cosmobiologia-web: nuevo render_model_to_svg_themed(dark: bool) para que el cliente JS elija palette según preferencia del UA. Tests del módulo math siguen verdes (10/10). Smoke test del server: /api/sky.svg ahora emite 22 circles, 77 lines, 52 texts con paleta real (vs ~6 circles, 24 lines, 36 texts del esqueleto previo). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
227 lines
8.2 KiB
Rust
227 lines
8.2 KiB
Rust
//! `cosmobiologia-render` — modelo y matemática de render
|
||
//! **agnósticos de surface**. Lo consumen tanto el canvas gpui
|
||
//! (nativo, render Vulkan/Metal) como el cliente web (WASM, render
|
||
//! SVG / Canvas2D). Cualquier mejora del layout / spread / cluster /
|
||
//! coords vive acá una sola vez y aparece en ambos clientes.
|
||
//!
|
||
//! ## Por qué un crate aparte
|
||
//!
|
||
//! `cosmobiologia-engine` arrastra `eternal-sky` (VSOP2013 + I/O de
|
||
//! tablas) que **no compila a WASM** sin empaquetar 30+ MB de
|
||
//! efemérides. Los tipos del `RenderModel` en sí son serde puro y
|
||
//! sí compilan a WASM — extraerlos a este crate libera al cliente
|
||
//! web de la dependencia transitiva.
|
||
//!
|
||
//! ## Capas
|
||
//!
|
||
//! 1. **Modelo de render** — `RenderModel`, `Layer`, `Glyph`,
|
||
//! `LineSeg`, `Geometry`, `LayerKind`. Estructuras serde-friendly
|
||
//! que el engine emite y los clients consumen.
|
||
//! 2. **Matemática agnóstica** *(módulos siguientes, no en esta primera
|
||
//! versión)* — `polar_to_screen`, `spread_angles`, `find_clusters`,
|
||
//! `format_coord_compact`, `Radii`. Migran desde el canvas gpui.
|
||
//! 3. **`DrawCommand`** *(módulo siguiente)* — primitivas de pintura
|
||
//! (line, circle, glyph, pill) que cada surface traduce a su API.
|
||
|
||
#![forbid(unsafe_code)]
|
||
#![warn(rust_2018_idioms)]
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
pub use cosmobiologia_model::{Chart, ChartId, ChartKind};
|
||
|
||
pub mod draw;
|
||
pub mod math;
|
||
pub mod palette;
|
||
|
||
pub use draw::{
|
||
compose_wheel, draw_commands_to_svg, CompositionOpts, DrawCommand, Rgba, TextAnchor,
|
||
};
|
||
pub use math::{
|
||
find_clusters, format_coord_compact, polar_to_screen, spread_angles, Radii,
|
||
};
|
||
pub use palette::Palette;
|
||
|
||
// =====================================================================
|
||
// RenderModel — lo que el client renderea
|
||
// =====================================================================
|
||
|
||
/// Resultado agnóstico de un cómputo astrológico, listo para renderizar.
|
||
/// El canvas gpui y el cliente web lo consumen idénticamente: el engine
|
||
/// computa (en nativo, con eternal) y publica este struct.
|
||
#[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>,
|
||
}
|
||
|
||
/// 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"];
|