feat(tahuantinsuyu): GR dual-ring + topo ascensional pegado al dial + coords
Dos cambios mayores que cierran el sistema GR/ascensional:
1. Reordenamiento radial — la capa ascensional (topocéntrico
Polich-Page) se ubica AHORA pegada al sign dial, y la
geocéntrica clásica queda más adentro. Layout outer→inner:
- sign_dial (1.00 → 0.88)
- topo_houses_outer (0.875) / topo_houses_inner (0.79) ← P-P pegadas al zodiaco
- topocentric (0.755) ← planetas topo con coords
- transits (0.71)
- houses_outer (0.66) / houses_inner (0.54) ← Placidus geo
- midpoints (0.50) / bodies (0.47) / bodies_inner (0.44) ← natal geo con coords
- pd_direct (0.495) / pd_converse (0.425) ← dual-ring GR
- aspects (0.41) / progression (0.36) / solar_arc (0.30)
Topocéntrico default ON (era OFF en la fase previa).
Coord labels ahora se pintan también en planetas topocéntricos
(label hacia adentro, no afuera, para no chocar con casas P-P).
2. Sistema GR Direcciones Primarias (dual-ring):
- Nuevo `PipelineRequest::PrimaryDirections { target_age_years }`.
- `build_primary_directions_overlay` proyecta cada cuerpo natal
con `directed_longitude` (key Naibod) en dos direcciones —
directa y conversa — y emite dos Layer Bodies con
`module_id` "pd_direct" / "pd_converse".
- Canvas: nuevos `pd_direct` y `pd_converse` en Radii; en el
render de Bodies disco más chico y alpha 0.80. Los dos anillos
se marcan con punteado fino que "abraza" el cinturón natal
por afuera y por adentro — el natal queda en el centro.
- Nuevo `PrimaryDirectionsModule` con toggle + slider de edad
(0..120, step 0.05a). Activable desde el panel.
Tests: 6 shell + 5 coord siguen verdes; el motor matemático
(eternal-astrology directed_longitude) y house system Polich-Page
están testeados desde el commit `e385ab2` en eternal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -550,6 +550,12 @@ impl Shell {
|
|||||||
if module_enabled(&self.module_configs, "topocentric") {
|
if module_enabled(&self.module_configs, "topocentric") {
|
||||||
requests.push(PipelineRequest::Topocentric);
|
requests.push(PipelineRequest::Topocentric);
|
||||||
}
|
}
|
||||||
|
if module_enabled(&self.module_configs, "primary_directions") {
|
||||||
|
let age = self.module_age_or_current("primary_directions");
|
||||||
|
requests.push(PipelineRequest::PrimaryDirections {
|
||||||
|
target_age_years: age,
|
||||||
|
});
|
||||||
|
}
|
||||||
if module_enabled(&self.module_configs, "composite") {
|
if module_enabled(&self.module_configs, "composite") {
|
||||||
if let Some(partner) = self.resolve_composite_partner() {
|
if let Some(partner) = self.resolve_composite_partner() {
|
||||||
requests.push(PipelineRequest::Composite {
|
requests.push(PipelineRequest::Composite {
|
||||||
|
|||||||
@@ -1115,22 +1115,25 @@ fn render_wheel(
|
|||||||
if matches!(layer.kind, LayerKind::Bodies) {
|
if matches!(layer.kind, LayerKind::Bodies) {
|
||||||
let is_natal = layer.module_id == "natal";
|
let is_natal = layer.module_id == "natal";
|
||||||
let is_topo = layer.module_id == "topocentric";
|
let is_topo = layer.module_id == "topocentric";
|
||||||
|
let is_pd_direct = layer.module_id == "pd_direct";
|
||||||
|
let is_pd_converse = layer.module_id == "pd_converse";
|
||||||
|
let is_pd = is_pd_direct || is_pd_converse;
|
||||||
let ring = radii.body_ring(&layer.module_id);
|
let ring = radii.body_ring(&layer.module_id);
|
||||||
let alpha = if is_natal {
|
let alpha = if is_natal {
|
||||||
1.0
|
1.0
|
||||||
} else if is_topo {
|
} else if is_topo {
|
||||||
0.75
|
0.75
|
||||||
|
} else if is_pd {
|
||||||
|
0.80
|
||||||
} else {
|
} else {
|
||||||
0.88
|
0.88
|
||||||
};
|
};
|
||||||
// Topocéntrico va con disco un poco más chico que el
|
|
||||||
// natal, y con desaturación implícita en `alpha`. El
|
|
||||||
// shift respecto al natal es lo que el ojo lee, no el
|
|
||||||
// tamaño individual.
|
|
||||||
let font_size = (if is_natal {
|
let font_size = (if is_natal {
|
||||||
18.0
|
18.0
|
||||||
} else if is_topo {
|
} else if is_topo {
|
||||||
15.0
|
15.0
|
||||||
|
} else if is_pd {
|
||||||
|
13.0
|
||||||
} else {
|
} else {
|
||||||
14.0
|
14.0
|
||||||
}) * s;
|
}) * s;
|
||||||
@@ -1138,6 +1141,8 @@ fn render_wheel(
|
|||||||
26.0
|
26.0
|
||||||
} else if is_topo {
|
} else if is_topo {
|
||||||
22.0
|
22.0
|
||||||
|
} else if is_pd {
|
||||||
|
20.0
|
||||||
} else {
|
} else {
|
||||||
22.0
|
22.0
|
||||||
}) * s;
|
}) * s;
|
||||||
@@ -1163,23 +1168,31 @@ fn render_wheel(
|
|||||||
));
|
));
|
||||||
|
|
||||||
// Coord label: grado dentro del signo + glyph del
|
// Coord label: grado dentro del signo + glyph del
|
||||||
// signo, pintado justo afuera del disco del
|
// signo, pintado afuera del disco del planeta
|
||||||
// planeta (radialmente). Sólo en natal (los
|
// (radialmente). Se pinta para el natal (afuera)
|
||||||
// overlays ya cargan info en su badge / tooltip).
|
// y para el topocéntrico (más afuera aún, hacia
|
||||||
if show_coords && is_natal {
|
// el sign dial) — los dos sistemas conviven con
|
||||||
|
// sus coords. Otros overlays (progression, solar
|
||||||
|
// arc) usan badges en el footer.
|
||||||
|
if show_coords && (is_natal || is_topo) {
|
||||||
let coord = format_coord_compact(g.deg);
|
let coord = format_coord_compact(g.deg);
|
||||||
let label_r = ring + disk_size * 0.7;
|
// Topo: label hacia ADENTRO (entre planeta y
|
||||||
|
// casas P-P arriba); natal: label hacia
|
||||||
|
// AFUERA (entre planeta y casas Placidus).
|
||||||
|
let label_r = if is_topo {
|
||||||
|
ring - disk_size * 0.7
|
||||||
|
} else {
|
||||||
|
ring + disk_size * 0.7
|
||||||
|
};
|
||||||
let (lx, ly) = polar_to_screen(g.deg, asc, rot_offset, label_r);
|
let (lx, ly) = polar_to_screen(g.deg, asc, rot_offset, label_r);
|
||||||
wheel = wheel.child(
|
wheel = wheel.child(coord_label(
|
||||||
coord_label(
|
|
||||||
cx_center + lx,
|
cx_center + lx,
|
||||||
cy_center + ly,
|
cy_center + ly,
|
||||||
coord.into(),
|
coord.into(),
|
||||||
theme.fg_muted,
|
theme.fg_muted,
|
||||||
halo_bg,
|
halo_bg,
|
||||||
9.5 * s,
|
9.5 * s,
|
||||||
),
|
));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1604,27 +1617,40 @@ fn format_offset(minutes: i64) -> String {
|
|||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct Radii {
|
struct Radii {
|
||||||
|
// Layout outer→inner. La capa **ascensional** (sistema topocéntrico
|
||||||
|
// Polich-Page) va pegada al sign dial — su lógica nace del eje
|
||||||
|
// ASC/MC y de la ascensión recta del observador, conceptualmente
|
||||||
|
// hermana del zodiaco. La capa **geocéntrica** clásica
|
||||||
|
// (casas Placidus + planetas tropicales sin paralaje) vive más
|
||||||
|
// adentro, alrededor del cinturón de planetas natales.
|
||||||
sign_outer: f32,
|
sign_outer: f32,
|
||||||
sign_inner: f32,
|
sign_inner: f32,
|
||||||
|
/// Anillo exterior de las casas Polich-Page (topocéntricas), pegado
|
||||||
|
/// al sign dial. Los cusps llegan hasta el `sign_inner`.
|
||||||
|
topo_houses_outer: f32,
|
||||||
|
topo_houses_inner: f32,
|
||||||
|
/// Carril de planetas topocéntricos — apenas dentro del anillo
|
||||||
|
/// de casas P-P. Lleva sus propios coord labels.
|
||||||
|
topocentric: f32,
|
||||||
/// Anillo de glifos de tránsito (cuando el overlay está activo).
|
/// Anillo de glifos de tránsito (cuando el overlay está activo).
|
||||||
transits: f32,
|
transits: f32,
|
||||||
|
/// Casas geocéntricas (Placidus default) — más adentro que las
|
||||||
|
/// topocéntricas, claramente diferenciables.
|
||||||
houses_outer: f32,
|
houses_outer: f32,
|
||||||
houses_inner: f32,
|
houses_inner: f32,
|
||||||
/// Anillo de midpoints — entre bodies natales y houses_inner.
|
/// Anillo de midpoints — entre cuerpos geocéntricos y `houses_inner`.
|
||||||
midpoints: f32,
|
midpoints: f32,
|
||||||
/// Anillo principal de cuerpos natales — donde se posan los
|
/// Anillo principal de cuerpos natales geocéntricos.
|
||||||
/// glyphs. Junto con `bodies_inner` forman el "cinturón" de los
|
|
||||||
/// planetas (doble línea visual).
|
|
||||||
bodies: f32,
|
bodies: f32,
|
||||||
/// Borde interior del cinturón de planetas. Marca dónde "termina"
|
/// Borde interior del cinturón de planetas geocéntricos.
|
||||||
/// la zona de cuerpos y empieza la zona de aspectos.
|
|
||||||
bodies_inner: f32,
|
bodies_inner: f32,
|
||||||
/// Cuerpos topocéntricos (capa "ascensional") — un poco hacia
|
/// Direcciones Primarias DIRECTAS (Sistema GR): ring exterior del
|
||||||
/// adentro de `bodies` para que un mismo planeta se vea como
|
/// "abrazo" GR — justo afuera del cinturón natal.
|
||||||
/// "doble glyph": natal afuera, topocéntrico justo dentro. En
|
pd_direct: f32,
|
||||||
/// Luna la separación angular es visible (~1°); en exteriores
|
/// Direcciones Primarias CONVERSAS (Sistema GR): ring interior —
|
||||||
/// los dos glyphs se superponen casi exactamente.
|
/// el cinturón natal queda entre `pd_direct` (afuera) y
|
||||||
topocentric: f32,
|
/// `pd_converse` (dentro), formando el dual-ring de rectificación.
|
||||||
|
pd_converse: f32,
|
||||||
/// Anillo interno con cuerpos progresados (overlay opcional).
|
/// Anillo interno con cuerpos progresados (overlay opcional).
|
||||||
progression: f32,
|
progression: f32,
|
||||||
/// Anillo más interno con cuerpos dirigidos por Solar Arc.
|
/// Anillo más interno con cuerpos dirigidos por Solar Arc.
|
||||||
@@ -1632,9 +1658,7 @@ struct Radii {
|
|||||||
/// Anillo de carta compuesta (midpoint Davison) con un partner.
|
/// Anillo de carta compuesta (midpoint Davison) con un partner.
|
||||||
composite: f32,
|
composite: f32,
|
||||||
/// Círculo donde anclan las líneas de aspecto entre cuerpos
|
/// Círculo donde anclan las líneas de aspecto entre cuerpos
|
||||||
/// natales. Justo dentro del cinturón de planetas, no en el
|
/// natales. Justo dentro del cinturón de planetas.
|
||||||
/// centro — así las líneas conectan cuerpos cercanos al ring
|
|
||||||
/// donde se ven, no atraviesan toda la rueda.
|
|
||||||
aspects: f32,
|
aspects: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,28 +1667,36 @@ impl Radii {
|
|||||||
Self {
|
Self {
|
||||||
sign_outer: r,
|
sign_outer: r,
|
||||||
sign_inner: r * 0.88,
|
sign_inner: r * 0.88,
|
||||||
transits: r * 0.82,
|
// Ascensional (Polich-Page): pegado al sign dial.
|
||||||
houses_outer: r * 0.78,
|
topo_houses_outer: r * 0.875,
|
||||||
houses_inner: r * 0.66,
|
topo_houses_inner: r * 0.79,
|
||||||
midpoints: r * 0.62,
|
// Carril topocéntrico de planetas: justo dentro de las
|
||||||
bodies: r * 0.60,
|
// casas P-P. Lleva sus coord labels al borde interior.
|
||||||
// bodies_inner cerca de bodies — los dos anillos juntos
|
topocentric: r * 0.755,
|
||||||
// forman un "carril" estrecho que delimita la franja de
|
// Tránsitos: ring intermedio entre las dos coronas (topo y
|
||||||
// planetas, no dos líneas separadas que confunden.
|
// geo), apenas debajo del topocéntrico.
|
||||||
bodies_inner: r * 0.57,
|
transits: r * 0.71,
|
||||||
// Topocéntrico justo bajo el carril natal: los dos
|
// Capa geocéntrica clásica más adentro — claramente
|
||||||
// glyphs comparten ancho visual, el shift relativo
|
// separada del bloque ascensional.
|
||||||
// (Luna en particular) se lee como "el natal apunta a
|
houses_outer: r * 0.66,
|
||||||
// este grado, el topo a este otro".
|
houses_inner: r * 0.54,
|
||||||
topocentric: r * 0.555,
|
midpoints: r * 0.50,
|
||||||
// aspects justo bajo el carril de cuerpos. Las líneas
|
bodies: r * 0.47,
|
||||||
// de aspecto entran a este radio, pero el círculo en sí
|
bodies_inner: r * 0.44,
|
||||||
// no se pinta — son las líneas las que importan, no
|
// Dual-ring GR — abraza el cinturón natal por afuera y por
|
||||||
// un anillo extra que sume ruido.
|
// adentro. Para Saturno los dos rings caen casi en la misma
|
||||||
aspects: r * 0.54,
|
// posición angular (poca rotación diurna afecta su lon en
|
||||||
progression: r * 0.46,
|
// años humanos); para luminarias los rings divergen
|
||||||
solar_arc: r * 0.38,
|
// visiblemente y leen "directa rumbo a este grado, conversa
|
||||||
composite: r * 0.30,
|
// hacia este otro".
|
||||||
|
pd_direct: r * 0.495,
|
||||||
|
pd_converse: r * 0.425,
|
||||||
|
// aspects justo bajo el carril natal — las líneas entran
|
||||||
|
// a este radio sin pintar el círculo (sería ruido extra).
|
||||||
|
aspects: r * 0.41,
|
||||||
|
progression: r * 0.36,
|
||||||
|
solar_arc: r * 0.30,
|
||||||
|
composite: r * 0.24,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1676,6 +1708,8 @@ impl Radii {
|
|||||||
"composite" => self.composite,
|
"composite" => self.composite,
|
||||||
"midpoints" => self.midpoints,
|
"midpoints" => self.midpoints,
|
||||||
"topocentric" => self.topocentric,
|
"topocentric" => self.topocentric,
|
||||||
|
"pd_direct" => self.pd_direct,
|
||||||
|
"pd_converse" => self.pd_converse,
|
||||||
_ => self.bodies,
|
_ => self.bodies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1762,15 +1796,31 @@ fn paint_wheel(
|
|||||||
let house_color = with_alpha(house_base, 0.85);
|
let house_color = with_alpha(house_base, 0.85);
|
||||||
stroke_circle_3d(window, cx, cy, radii.houses_outer, 1.1, house_color, theme);
|
stroke_circle_3d(window, cx, cy, radii.houses_outer, 1.1, house_color, theme);
|
||||||
stroke_circle_3d(window, cx, cy, radii.houses_inner, 1.1, house_color, theme);
|
stroke_circle_3d(window, cx, cy, radii.houses_inner, 1.1, house_color, theme);
|
||||||
|
// Si hay capa topocéntrica activa, pintar también sus dos
|
||||||
|
// anillos (con stroke más sutil que el geocéntrico, para que
|
||||||
|
// se lea como "sistema ascensional" sin competir).
|
||||||
|
if layers
|
||||||
|
.iter()
|
||||||
|
.any(|l| matches!(l.kind, LayerKind::Houses) && l.module_id == "topocentric")
|
||||||
|
{
|
||||||
|
let topo_color = with_alpha(house_base, 0.55);
|
||||||
|
stroke_circle(window, cx, cy, radii.topo_houses_outer, 0.8, topo_color);
|
||||||
|
stroke_circle(window, cx, cy, radii.topo_houses_inner, 0.8, topo_color);
|
||||||
|
}
|
||||||
|
|
||||||
for layer in layers {
|
for layer in layers {
|
||||||
if matches!(layer.kind, LayerKind::Houses) {
|
if matches!(layer.kind, LayerKind::Houses) {
|
||||||
let is_topo = layer.module_id == "topocentric";
|
let is_topo = layer.module_id == "topocentric";
|
||||||
|
let (r_in, r_out) = if is_topo {
|
||||||
|
(radii.topo_houses_inner, radii.topo_houses_outer)
|
||||||
|
} else {
|
||||||
|
(radii.houses_inner, radii.houses_outer)
|
||||||
|
};
|
||||||
if let Geometry::Ring { cusps_deg } = &layer.geometry {
|
if let Geometry::Ring { cusps_deg } = &layer.geometry {
|
||||||
for (i, c) in cusps_deg.iter().enumerate() {
|
for (i, c) in cusps_deg.iter().enumerate() {
|
||||||
let is_angle = i == 0 || i == 3 || i == 6 || i == 9;
|
let is_angle = i == 0 || i == 3 || i == 6 || i == 9;
|
||||||
let color = if is_topo {
|
let color = if is_topo {
|
||||||
with_alpha(house_base, 0.55)
|
with_alpha(house_base, 0.60)
|
||||||
} else if is_angle {
|
} else if is_angle {
|
||||||
palette.angle_highlight
|
palette.angle_highlight
|
||||||
} else {
|
} else {
|
||||||
@@ -1779,27 +1829,44 @@ fn paint_wheel(
|
|||||||
let width = if is_angle && !is_topo { 2.0 } else { 0.8 };
|
let width = if is_angle && !is_topo { 2.0 } else { 0.8 };
|
||||||
if is_topo {
|
if is_topo {
|
||||||
// Topocéntrico: cusp como línea punteada
|
// Topocéntrico: cusp como línea punteada
|
||||||
// en su propio anillo (un poco más
|
// en su propio anillo cercano al sign
|
||||||
// adentro que las casas geocéntricas) →
|
// dial — se distingue del Placidus
|
||||||
// se distingue como sistema alternativo.
|
// geocéntrico por el dash pattern y la
|
||||||
let (xi, yi) = polar_to_screen(
|
// ubicación más exterior.
|
||||||
*c,
|
|
||||||
ascendant_deg,
|
|
||||||
rot_offset_deg,
|
|
||||||
radii.houses_inner - 4.0,
|
|
||||||
);
|
|
||||||
let (xo, yo) = polar_to_screen(
|
|
||||||
*c,
|
|
||||||
ascendant_deg,
|
|
||||||
rot_offset_deg,
|
|
||||||
radii.houses_inner - 28.0,
|
|
||||||
);
|
|
||||||
paint_segment(
|
paint_segment(
|
||||||
window,
|
window,
|
||||||
cx + xi,
|
cx
|
||||||
cy + yi,
|
+ polar_to_screen(
|
||||||
cx + xo,
|
*c,
|
||||||
cy + yo,
|
ascendant_deg,
|
||||||
|
rot_offset_deg,
|
||||||
|
r_in,
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
cy
|
||||||
|
+ polar_to_screen(
|
||||||
|
*c,
|
||||||
|
ascendant_deg,
|
||||||
|
rot_offset_deg,
|
||||||
|
r_in,
|
||||||
|
)
|
||||||
|
.1,
|
||||||
|
cx
|
||||||
|
+ polar_to_screen(
|
||||||
|
*c,
|
||||||
|
ascendant_deg,
|
||||||
|
rot_offset_deg,
|
||||||
|
r_out,
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
cy
|
||||||
|
+ polar_to_screen(
|
||||||
|
*c,
|
||||||
|
ascendant_deg,
|
||||||
|
rot_offset_deg,
|
||||||
|
r_out,
|
||||||
|
)
|
||||||
|
.1,
|
||||||
color,
|
color,
|
||||||
Some((3.0, 2.5)),
|
Some((3.0, 2.5)),
|
||||||
1.0,
|
1.0,
|
||||||
@@ -1812,8 +1879,8 @@ fn paint_wheel(
|
|||||||
*c,
|
*c,
|
||||||
ascendant_deg,
|
ascendant_deg,
|
||||||
rot_offset_deg,
|
rot_offset_deg,
|
||||||
radii.houses_inner,
|
r_in,
|
||||||
radii.houses_outer,
|
r_out,
|
||||||
color,
|
color,
|
||||||
width,
|
width,
|
||||||
);
|
);
|
||||||
@@ -1866,6 +1933,35 @@ fn paint_wheel(
|
|||||||
0.6,
|
0.6,
|
||||||
with_alpha(palette.dial_ring, 0.35),
|
with_alpha(palette.dial_ring, 0.35),
|
||||||
);
|
);
|
||||||
|
// GR dual-ring: si las capas de direcciones primarias están
|
||||||
|
// presentes, marcar sus anillos para que el visual lea como
|
||||||
|
// "abrazo" del cinturón natal. La directa va punteada,
|
||||||
|
// la conversa también — la diferencia entre las dos es la
|
||||||
|
// ubicación radial (afuera vs adentro del cinturón natal).
|
||||||
|
let has_pd = layers.iter().any(|l| {
|
||||||
|
matches!(l.kind, LayerKind::Bodies)
|
||||||
|
&& (l.module_id == "pd_direct" || l.module_id == "pd_converse")
|
||||||
|
});
|
||||||
|
if has_pd {
|
||||||
|
let pd_color = with_alpha(palette.angle_highlight, 0.50);
|
||||||
|
for r in [radii.pd_direct, radii.pd_converse] {
|
||||||
|
// Pintamos el anillo como tramo punteado fino: 24
|
||||||
|
// segmentos cortos a lo largo del círculo.
|
||||||
|
let steps = 96;
|
||||||
|
for i in 0..steps {
|
||||||
|
if i % 2 != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let a0 = (i as f32) / (steps as f32) * std::f32::consts::TAU;
|
||||||
|
let a1 = ((i + 1) as f32) / (steps as f32) * std::f32::consts::TAU;
|
||||||
|
let x0 = cx + r * a0.cos();
|
||||||
|
let y0 = cy + r * a0.sin();
|
||||||
|
let x1 = cx + r * a1.cos();
|
||||||
|
let y1 = cy + r * a1.sin();
|
||||||
|
paint_segment(window, x0, y0, x1, y1, pd_color, None, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Aspectos. Cada module_id usa su par de radios — natal-natal
|
// 3. Aspectos. Cada module_id usa su par de radios — natal-natal
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ use std::sync::{Arc, OnceLock};
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use eternal_astrology::{
|
use eternal_astrology::{
|
||||||
all_lots, composite, find_aspects, find_synastry_aspects, next_return, secondary_progression,
|
all_lots, composite, directed_longitude, find_aspects, find_synastry_aspects, next_return,
|
||||||
solar_arc_true, topocentric_ecliptic, Aspect, AspectKind as EAspectKind, BirthData, BodySet,
|
primary_direction::PrimaryDirection, secondary_progression, solar_arc_true, topocentric_ecliptic,
|
||||||
ChartConfig, HouseSystem as EHouseSystem, Houses as EHouses, NatalChart, OrbTable,
|
Aspect, AspectKind as EAspectKind, BirthData, BodySet, ChartConfig,
|
||||||
Zodiac as EZodiac,
|
DirectionKey as EDirectionKey, HouseSystem as EHouseSystem, Houses as EHouses, NatalChart,
|
||||||
|
OrbTable, Zodiac as EZodiac,
|
||||||
};
|
};
|
||||||
use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig};
|
use eternal_sky::{Ayanamsha, Body, EphemerisSession, Instant as ESInstant, Observer, SessionConfig};
|
||||||
|
|
||||||
@@ -389,6 +390,14 @@ pub fn compose(
|
|||||||
"Topocéntrico (Polich-Page)".into(),
|
"Topocéntrico (Polich-Page)".into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
crate::PipelineRequest::PrimaryDirections { target_age_years } => {
|
||||||
|
build_primary_directions_overlay(&natal, *target_age_years, &mut render);
|
||||||
|
push_overlay_meta(
|
||||||
|
&mut render,
|
||||||
|
"primary_directions",
|
||||||
|
format!("GR Direcciones · {:.1}a", target_age_years),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,6 +567,69 @@ fn build_topocentric_overlay(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GR dual-ring de Direcciones Primarias: a la edad pedida, cada
|
||||||
|
/// cuerpo natal se proyecta dos veces — directa (rotación diurna
|
||||||
|
/// forward, anillo afuera) y conversa (rotación inversa, anillo
|
||||||
|
/// dentro). En rectificación, los dos rings se ven simultáneamente
|
||||||
|
/// y si un evento real cayó cerca de un ángulo, debe aparecer
|
||||||
|
/// "cruzado" con ambos arcos coincidentes — eso valida la hora.
|
||||||
|
///
|
||||||
|
/// Usa el key Naibod (0°59'08″/año) como default — convención GR.
|
||||||
|
fn build_primary_directions_overlay(
|
||||||
|
natal: &NatalChart,
|
||||||
|
target_age_years: f64,
|
||||||
|
render: &mut RenderModel,
|
||||||
|
) {
|
||||||
|
let key = EDirectionKey::Naibod;
|
||||||
|
let eps = natal.obliquity_rad;
|
||||||
|
|
||||||
|
let project = |dir: PrimaryDirection| -> Vec<Glyph> {
|
||||||
|
natal
|
||||||
|
.placements
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
let new_lon_rad = directed_longitude(
|
||||||
|
p.right_ascension_rad,
|
||||||
|
p.declination_rad,
|
||||||
|
target_age_years,
|
||||||
|
dir,
|
||||||
|
key,
|
||||||
|
eps,
|
||||||
|
);
|
||||||
|
let new_lon_deg = new_lon_rad.to_degrees() as f32;
|
||||||
|
Glyph {
|
||||||
|
deg: new_lon_deg,
|
||||||
|
symbol: body_symbol(p.body).into(),
|
||||||
|
annotation: Some(format!("{:.2}°", new_lon_deg)),
|
||||||
|
retrograde: p.longitude_rate_rad_per_day < 0.0,
|
||||||
|
house: None,
|
||||||
|
dignity_marker: None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let direct_glyphs = project(PrimaryDirection::Direct);
|
||||||
|
let converse_glyphs = project(PrimaryDirection::Converse);
|
||||||
|
|
||||||
|
render.layers.push(Layer {
|
||||||
|
module_id: "pd_direct".into(),
|
||||||
|
kind: LayerKind::Bodies,
|
||||||
|
ring: 0.0,
|
||||||
|
z: 10,
|
||||||
|
geometry: Geometry::GlyphsOnly,
|
||||||
|
glyphs: direct_glyphs,
|
||||||
|
});
|
||||||
|
render.layers.push(Layer {
|
||||||
|
module_id: "pd_converse".into(),
|
||||||
|
kind: LayerKind::Bodies,
|
||||||
|
ring: 0.0,
|
||||||
|
z: 11,
|
||||||
|
geometry: Geometry::GlyphsOnly,
|
||||||
|
glyphs: converse_glyphs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn build_progression_overlay(
|
fn build_progression_overlay(
|
||||||
natal: &NatalChart,
|
natal: &NatalChart,
|
||||||
target_age_years: f64,
|
target_age_years: f64,
|
||||||
|
|||||||
@@ -333,6 +333,14 @@ pub enum PipelineRequest {
|
|||||||
/// (~1° de shift); imperceptible en planetas exteriores. La capa
|
/// (~1° de shift); imperceptible en planetas exteriores. La capa
|
||||||
/// convive con la natal geocéntrica como overlay comparativo.
|
/// convive con la natal geocéntrica como overlay comparativo.
|
||||||
Topocentric,
|
Topocentric,
|
||||||
|
/// `module_id = "pd_direct"` + `"pd_converse"` — Direcciones
|
||||||
|
/// Primarias del Sistema GR (García Rosas). Cada cuerpo natal se
|
||||||
|
/// proyecta dos veces: hacia adelante en el tiempo diurno
|
||||||
|
/// (direct) y hacia atrás (converse). Los dos resultados a la
|
||||||
|
/// edad pedida pintan un dual-ring para rectificación en vivo.
|
||||||
|
PrimaryDirections {
|
||||||
|
target_age_years: f64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ impl Registry {
|
|||||||
r.register(Box::new(lots::LotsModule));
|
r.register(Box::new(lots::LotsModule));
|
||||||
r.register(Box::new(fixed_stars::FixedStarsModule));
|
r.register(Box::new(fixed_stars::FixedStarsModule));
|
||||||
r.register(Box::new(topocentric::TopocentricModule));
|
r.register(Box::new(topocentric::TopocentricModule));
|
||||||
|
r.register(Box::new(primary_directions::PrimaryDirectionsModule));
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -840,13 +841,13 @@ pub mod topocentric {
|
|||||||
matches!(kind, ChartKind::Natal)
|
matches!(kind, ChartKind::Natal)
|
||||||
}
|
}
|
||||||
fn enabled_by_default(&self) -> bool {
|
fn enabled_by_default(&self) -> bool {
|
||||||
false
|
true
|
||||||
}
|
}
|
||||||
fn controls(&self) -> Vec<Control> {
|
fn controls(&self) -> Vec<Control> {
|
||||||
vec![Control::Toggle {
|
vec![Control::Toggle {
|
||||||
key: "enabled".into(),
|
key: "enabled".into(),
|
||||||
label: "Activar".into(),
|
label: "Activar".into(),
|
||||||
default: false,
|
default: true,
|
||||||
hotkey: None,
|
hotkey: None,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
@@ -855,3 +856,59 @@ pub mod topocentric {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// PrimaryDirectionsModule — GR dual-ring (Direct + Converse)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
pub mod primary_directions {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Direcciones Primarias del Sistema GR (García Rosas): cada
|
||||||
|
/// cuerpo natal se proyecta en dos rings — directa (rotación
|
||||||
|
/// diurna forward) y conversa (rotación inversa). El usuario
|
||||||
|
/// scrubea `target_age_years` para ver el movimiento en vivo.
|
||||||
|
/// Útil para rectificación: un evento real debe coincidir con
|
||||||
|
/// arcos directos y conversos consistentes si la hora natal es
|
||||||
|
/// correcta.
|
||||||
|
pub struct PrimaryDirectionsModule;
|
||||||
|
|
||||||
|
impl Module for PrimaryDirectionsModule {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"primary_directions"
|
||||||
|
}
|
||||||
|
fn label(&self) -> &'static str {
|
||||||
|
"Direcciones primarias (GR)"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Dual-ring directas + conversas para rectificación en vivo."
|
||||||
|
}
|
||||||
|
fn applies_to(&self, kind: ChartKind) -> bool {
|
||||||
|
matches!(kind, ChartKind::Natal)
|
||||||
|
}
|
||||||
|
fn enabled_by_default(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn controls(&self) -> Vec<Control> {
|
||||||
|
vec![
|
||||||
|
Control::Toggle {
|
||||||
|
key: "enabled".into(),
|
||||||
|
label: "Activar".into(),
|
||||||
|
default: false,
|
||||||
|
hotkey: None,
|
||||||
|
},
|
||||||
|
Control::Slider {
|
||||||
|
key: "target_age_years".into(),
|
||||||
|
label: "Edad (años)".into(),
|
||||||
|
min: 0.0,
|
||||||
|
max: 120.0,
|
||||||
|
step: 0.05,
|
||||||
|
default: 30.0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user