feat(tahuantinsuyu): fase 14 — Return abstracto + Control::Select interactivo

El módulo SolarReturn se generaliza a PlanetaryReturn parametrizable
por cuerpo (Sun/Moon/Mercury/Venus/Mars/Jupiter/Saturn/Uranus/Neptune/
Pluto). Validado contra `Control::Select`, ahora interactivo como
tercer tipo de control draggable (después de Toggle/Slider/ChartPicker).

Refactor estructural: el dropdown del ChartPicker pasa a ser
infraestructura compartida — chart_picker_value/chart_picker_open
desaparecen, reemplazados por string_state/dropdown_open que sirven
a CUALQUIER control basado en string (picker + select).
render_chart_picker y render_select ahora son thin wrappers sobre
render_dropdown(options, include_auto).

- engine:
  - PipelineRequest::SolarReturn → PipelineRequest::PlanetaryReturn
    { body: String, target_age_years }. Body como string agnóstico
    (sun/moon/jupiter/...) que el bridge mapea a eternal_sky::Body
    vía map_body — el mismo helper que ya usa StoredChartConfig.
  - build_solar_return_overlay → build_planetary_return_overlay con
    parameter `body: Body`. next_return acepta cualquier body, así que
    Moon return (mensual) y Saturn return (29 años) funcionan igual.
    Mensajes de error incluyen body.name() para diagnóstico.
- modules:
  - SolarReturnModule → PlanetaryReturnModule (mod planetary_return).
    id "planetary_return". Controles: toggle "enabled" + Select "body"
    con 10 opciones de cuerpo (Sol → Plutón) + Slider edad. label
    "Retornos planetarios".
- panel:
  - Refactor: chart_picker_value/chart_picker_open → string_state/
    dropdown_open (compartido entre ChartPicker y Select).
  - set_string(module_id, key, value, cx) — API unificada. set_chart_picker
    queda como alias retrocompatible.
  - render_dropdown(options, include_auto, …) — helper común. picker
    pasa include_auto=true (muestra "(automático)" + separador);
    select pasa include_auto=false (las options son la única opción).
  - render_select implementado — el botón muestra la option's label
    (no value); click abre dropdown; click en opción emite ControlChanged
    con Value::String(option.value).
- shell:
  - OUTER_RING_MODULES const: "solar_return" → "planetary_return".
  - build_requests para planetary_return: lee body string del
    module_configs (default "sun"), arma PipelineRequest::PlanetaryReturn.
  - apply_selection inicializa target_age + body=sun default para
    planetary_return.
  - sync_panel_from_configs strings → set_string (era set_chart_picker).

Probarlo: en el panel del módulo "Retornos planetarios", click en el
dropdown "Cuerpo" abre el popup; click en "Saturno" + slider en 29
años + toggle "Activar" = ves la carta del primer retorno de Saturno
(cuando recién terminaba la primera vuelta) en el outer ring con
cross aspects al natal.

NOTE: La persistencia con id "solar_return" de fase 13 queda huérfana
en la DB de los users que ya hayan probado. No es destructivo —
simplemente esas rows quedan sin módulo que las lea. Pre-1.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-17 11:30:21 +00:00
parent 8d95833c20
commit 6d572c81ca
6 changed files with 273 additions and 126 deletions
@@ -574,7 +574,7 @@ fn render_wheel(
if matches!(layer.kind, LayerKind::Outer)
&& (layer.module_id == "transit"
|| layer.module_id == "synastry"
|| layer.module_id == "solar_return")
|| layer.module_id == "planetary_return")
{
for g in &layer.glyphs {
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, radii.transits);
@@ -730,11 +730,11 @@ impl Radii {
/// Resuelve qué radios corresponden a una capa de aspectos según el
/// `module_id`: natal-natal en `aspects`, cross con cada overlay
/// desde `bodies` (extremo natal) al ring del módulo. Synastry y
/// Solar Return comparten el outer ring de tránsito (los tres son
/// mutuamente excluyentes a nivel de Shell).
/// Planetary Return comparten el outer ring de tránsito (los tres
/// son mutuamente excluyentes a nivel de Shell).
fn aspect_endpoints(&self, module_id: &str) -> (f32, f32) {
match module_id {
"transit" | "synastry" | "solar_return" => (self.bodies, self.transits),
"transit" | "synastry" | "planetary_return" => (self.bodies, self.transits),
"progression" => (self.bodies, self.progression),
"solar_arc" => (self.bodies, self.solar_arc),
_ => (self.aspects, self.aspects),
@@ -949,7 +949,7 @@ fn paint_wheel(
matches!(l.kind, LayerKind::Outer)
&& (l.module_id == "transit"
|| l.module_id == "synastry"
|| l.module_id == "solar_return")
|| l.module_id == "planetary_return")
});
if outer_active && show(LayerKind::Outer) {
stroke_circle(
@@ -974,7 +974,7 @@ fn paint_wheel(
if matches!(layer.kind, LayerKind::Outer)
&& (layer.module_id == "transit"
|| layer.module_id == "synastry"
|| layer.module_id == "solar_return")
|| layer.module_id == "planetary_return")
{
for g in &layer.glyphs {
let color = with_alpha(planet_color(palette, &g.symbol), 0.85);