Commit Graph

17 Commits

Author SHA1 Message Date
sergio cabdb2927e feat(tahuantinsuyu): fase 16 — polish místico (gradient sutil + glow en luminarias)
Dos toques pequeños que dan profundidad visual sin saturar:

1. **Gradient diagonal en el fondo del wheel**: linear_gradient 155°
   desde palette.dial_ring @ alpha 0.06 hasta palette.angle_highlight
   @ alpha 0.03. La opacidad mínima asegura que no compite con la
   geometría pintada encima; el efecto se ve sobre todo en las
   esquinas del cuadrado (afuera del círculo) y en los gaps entre
   anillos cuando no hay overlays. Da "shimmer mineral" muy velado.
   El wheel además ahora tiene rounded(12px) — perfila el cuadrado
   sin que se sienta como un container.

2. **Glow halo en luminarias natales**: paint_glow nuevo helper que
   pinta 3 fill_circle concéntricos con (radius × 5/3/1.8, alpha
   0.05/0.12/0.22). Aplicado solo a Sol y Luna del layer natal —
   son los puntos psicológicamente cargados y los que el ojo busca
   primero. El resto de planetas mantiene su dot limpio. GPUI 0.2
   no tiene radial_gradient nativo así que el shading concéntrico
   discreto cubre el rol.

- canvas: imports linear_color_stop + linear_gradient. paint_glow()
  helper. Loop de Bodies en paint_wheel detecta is_natal &&
  (sun|moon) → paint_glow antes del fill_circle del dot.

Convive con todo lo anterior: si la luminaria está oculta (toggle
[P] off), el glow también; si hay overlays con sus propios planetas,
solo las luminarias natales lucen halo (diferenciable de su contraparte
en transit/synastry/return).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:37:56 +00:00
sergio 1232e39397 feat(tahuantinsuyu): fase 15 — badges de overlays activos en el footer
Cuando hay overlays activos, debajo del info_row aparecen pills con
los nombres de cada uno (Natal, Tránsito ahora, Progresión 38.2a,
Sinastría · Ana, Saturn return 29a) — el usuario ve de un vistazo qué
está mirando sin tener que mapear los anillos manualmente.

El border de cada pill toma color según a qué slot del wheel
pertenece: outer ring (transit/synastry/planetary_return) →
palette.angle_highlight (dorado), inner overlays (progression/
solar_arc) → palette.house_cusp (tono apagado), natal → neutro.
Permite leer la pila de izquierda a derecha y ubicar visualmente cada
glyph del wheel.

- engine: nuevo OverlayMeta { module_id, label } + campo overlays:
  Vec<OverlayMeta> en RenderModel. build_render_model lo inicializa
  vacío; bridge::compose pushea un OverlayMeta por cada
  PipelineRequest después de su build_*_overlay correspondiente. Helper
  push_overlay_meta(render, id, label). Labels: "Tránsito ahora",
  "Progresión {age:.1}a", "Solar Arc {age:.1}a", "Sinastría · {name}"
  (lee partner_chart.label antes de mover el Box al builder),
  "{Body} return {age:.0}a" (usa eternal_sky body.name()).
- canvas: render_wheel separa el viejo footer en info_row (Asc/MC/ms +
  offset + hotkeys) y un badges_row opcional. badges_row aparece solo
  cuando render.overlays != empty. Pill helper centralizado: bg
  panel_alt, border 1px, text size 10, rounded 10. Border color
  decidido por module_id para correlacionar con el ring visual.

Compatible con compute_mock (que setea overlays = vec![] — ningún
mock badge). Persiste sin cambios — los configs siguen guardando su
estado, los OverlayMeta se reconstruyen en cada compose desde los
requests activos.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:34:54 +00:00
sergio 6d572c81ca 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>
2026-05-17 11:30:21 +00:00
sergio 8d95833c20 feat(tahuantinsuyu): fase 13 — Solar Return como sexto overlay
Sexto módulo siguiendo el patrón establecido. Cambio estructural:
3-way mutual exclusion para los módulos que comparten el outer ring
(transit + synastry + solar_return). Constante OUTER_RING_MODULES
abstrae el grupo para que fase 14+ pueda sumar lunar return / planet
returns sin tocar la lógica del shell.

- engine: PipelineRequest::SolarReturn { target_age_years } +
  build_solar_return_overlay. Llama eternal_astrology::next_return
  (Sun back to natal Sun, ventana ±1.5 años) desde un instante
  ~30 días antes del cumpleaños target. Computa la carta natal
  completa al return_instant (mismo observer + config natales —
  convención clásica) y la apila como Outer + Aspects cross natal ×
  return. z=12/13. Import: `next_return` añadido a la lista de
  re-exports del bridge.
- modules: solar_return::SolarReturnModule (id "solar_return", toggle
  + slider target_age_years 0..120 step 1.0). Registry pasa a 6
  módulos para Natal.
- shell: OUTER_RING_MODULES const con los tres ids; mutual exclusion
  generalizada de pair-wise a N-way (for-loop sobre el slice). Init
  de age en apply_selection ahora incluye solar_return. build_requests
  agrega la rama. Misma estructura que progression/solar_arc en la
  rama de age handling.
- canvas: aspect_endpoints("solar_return") = (bodies, transits). Tres
  loops del outer ring (paint dots, paint glyphs, anillos guía) ahora
  aceptan los tres module_ids.

Probarlo: en el panel, slider "Edad del retorno" en valor entero (ej.
36) + toggle "Activar" = ves la carta del año cuando volviste a tu
Sol natal a los 36, con todos sus planetas en el outer ring y cross
aspects con tu natal. Cambiando el slider podés explorar año por año.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:23:25 +00:00
sergio d9e21fbedc feat(tahuantinsuyu): fase 12 — Control::ChartPicker para Synastry
Cualquier carta del DB se puede elegir como partner de sinastría
(no solo hermanas del contacto actual). El módulo SynastryModule
declara un Control::ChartPicker; el panel renderea un dropdown que
abre un popup con todas las cartas; el shell inyecta las opciones y
resuelve el partner desde la selección persistida.

- modules: nueva variante Control::ChartPicker { key, label } sin
  default — las opciones las inyecta el host. SynastryModule.controls()
  agrega un picker con key="partner_chart_id".
- store: list_all_charts() — query sin filtros, ordenada por label
  case-insensitive. Pensada para selectores cross-contact.
- panel:
  - ChartOption struct público (id + label).
  - chart_options Vec + chart_picker_value HashMap + chart_picker_open
    Option (solo uno abierto a la vez).
  - APIs públicas set_chart_options / set_chart_picker para sync.
  - set_active_kind inicializa picker_value a None ("automático").
  - render_chart_picker: botón "▾ label" en bg_button; al click toggle
    un popup absolute con (automático) + separador + cada chart_option
    clickable. select_picker emite ControlChanged con Value::String(id)
    o Value::Null.
- shell:
  - new() llama refresh_chart_options al final para popular el panel
    desde el boot.
  - refresh_chart_options() builds Vec<ChartOption> desde
    list_all_charts (label + birth_brief "YYYY-MM-DD · Lugar"). Lo
    llamamos también desde TreeEvent::HierarchyChanged para que el
    dropdown refleje altas/bajas.
  - resolve_synastry_partner reemplaza find_synastry_partner: 1) lee
    module_configs["synastry"]["partner_chart_id"] y resuelve el chart;
    2) fallback al automático (primera hermana). El fallback significa
    que el módulo sigue funcionando sin elegir manualmente.
  - sync_panel_from_configs ahora maneja Value::String / Value::Null
    → set_chart_picker, así el picker se restaura al cargar una carta.

Persistencia: el partner_chart_id va al config_json (fase 11), así
que cada carta recuerda con quién hizo sinastría la última vez.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:17:35 +00:00
sergio 22e6ed6a71 feat(tahuantinsuyu): fase 11 — persistencia de module_configs por carta
Los toggles, sliders y partner pickers de cada overlay (transit,
progression, solar_arc, synastry) ahora persisten por carta en la
tabla SQLite `module_state` (que estaba creada desde fase 1 pero
sin cablear). Cambiar de carta y volver mantiene exactamente el
estado que el usuario dejó.

- shell:
  - apply_selection(Chart): tras setear defaults (target_age_years =
    edad actual), llama load_persisted_module_states(chart.id) que
    mergea sobre los defaults los valores guardados. Luego
    sync_panel_from_configs empuja todos los toggles/sliders al
    panel para reflejar el estado restaurado. Render al final.
  - load_persisted_module_states: lee list_module_states(chart_id),
    reconstruye el JSON combinado (mergea `enabled` de la columna
    SQL en el config), y lo mergea sobre lo que ya hay en
    module_configs. Vacant entries se insertan tal cual; occupied
    se patchean field-a-field para no perder defaults no guardados.
  - sync_panel_from_configs: itera module_configs, push toggle/slider
    al panel por cada key Bool/f64.
  - persist_module(module_id): extrae enabled del JSON, deja resto en
    config_json, llama upsert_module_state. Invocada desde
    on_panel_event "else" tras cada update + desde on_canvas_event
    para [T] + tras auto-disable del conflicting module en mutual
    exclusion.
- store: nuevo test module_state_roundtrip que cubre upsert/list +
  cambio de enabled vía upsert (UPSERT clause de fase 1 vuelve a
  validarse).

Flujo de usuario: ajustás el slider de progresión a 42.5 años,
activás synastry, cambiás de carta, volvés — todo está como lo
dejaste. La DB persiste por chart_id, así que distintos sujetos
mantienen estados independientes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:11:29 +00:00
sergio 97a6aab883 feat(tahuantinsuyu): fase 10 — Sinastría como overlay (bi-wheel con carta hermana)
Quinto módulo overlay. Cuando hay otra carta hermana del mismo
contacto, la sinastría pone las posiciones del partner en el outer
ring + dibuja cross aspects entre las dos personas. Mismo molde que
los overlays anteriores; única novedad: el PipelineRequest transporta
una `Chart` completa porque el partner no es derivable de la natal.

- engine: PipelineRequest::Synastry { partner_chart: Box<Chart> }.
  build_synastry_overlay(natal, partner_chart, render) llama
  compute_natal_chart sobre el partner y find_synastry_aspects entre
  los dos NatalCharts (sólo majors). Layers con module_id="synastry"
  y z=10/11. Reusa la helper compute_natal_chart de fase 5.
- modules: synastry::SynastryModule (id "synastry", toggle "Activar"
  sin hotkey por ahora). Registry agrega el quinto built-in. Test
  pasó a 5 módulos aplicables a ChartKind::Natal.
- shell: build_requests detecta synastry.enabled y llama
  find_synastry_partner — busca la primera carta hermana del contacto
  actual (mismo contact_id, distinto chart_id). Si no hay hermana,
  skip silencioso. Mutual exclusion: al prender transit o synastry
  se apaga el otro automáticamente (comparten outer ring) — sincroniza
  el toggle del panel + el layer_visibility del canvas.
- canvas: Radii::aspect_endpoints("synastry") devuelve (bodies,
  transits) — same slot que transit. Loops del outer ring aceptan
  module_id "transit" OR "synastry" (paint_wheel + glyph overlay).
  Sin radii nuevo — visualmente comparten el ring 0.82 con transit.

Para probarlo: creá dos cartas en el mismo contacto (ej. el sujeto +
su pareja). Abrí la primera y activá "Sinastría" en el panel. Verás
los planetas del partner en el outer ring + líneas que cruzan al
centro mostrando los aspectos entre las dos personas. Si tenés
transit prendido cuando lo activás, se apaga; al revés también.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 11:05:52 +00:00
sergio 1a3bc55016 feat(tahuantinsuyu): fase 9 — Solar Arc como segundo overlay
Confirma que la arquitectura de fase 6 escala: tres overlays simultáneos
(transit + progression + solar_arc) sin acoplamiento entre módulos, y
sin tocar el flujo del Shell salvo registrar el nuevo branch.

Tres puntos de extensión por overlay nuevo (exactamente los predichos):
1. variante en PipelineRequest
2. helper build_*_overlay en bridge + match arm en compose
3. módulo declarativo en modules/ + registro

- engine: PipelineRequest::SolarArc { target_age_years: f64 } +
  build_solar_arc_overlay que llama solar_arc_true(natal, session, age)
  → desplaza uniformemente cada placement y cusp por el arco solar
  (default ≈1°/año, vía true progressed Sun). Cross aspects natal ×
  dirigida vía find_synastry_aspects(majors). Layers con
  module_id="solar_arc" y z=8/9 (sobre todos los demás).
- modules: solar_arc::SolarArcModule con id="solar_arc", toggle
  "Activar" + slider target_age_years 0..120. Mismo shape que
  ProgressionModule. Registry.with_builtins lo registra. Test pasó a
  4 módulos aplicables a ChartKind::Natal.
- canvas: Radii.solar_arc = 0.40 (entre progression 0.48 y aspects),
  aspects shrunk a 0.32 para hacer lugar. Helpers Radii::body_ring()
  y Radii::aspect_endpoints() ahora reconocen "solar_arc". paint_wheel
  itera ambos overlays (progression + solar_arc) para dibujar dots,
  glyph overlays y anillos guía sutiles. Loop común `for (id, ring) in
  [..]` evita duplicación de código.
- shell: build_requests detecta solar_arc.enabled, agrega request con
  edad. apply_selection inicializa target_age_years para ambos
  overlays (progression + solar_arc) en current_age + sincroniza los
  sliders del panel. Helper module_age_or_current(id) factoriza la
  lectura de edad con fallback.

Activando los tres overlays al mismo tiempo el canvas se convierte en
una rueda de cinco anillos: zodíaco (1.00), tránsito (0.82), natal
(0.66-0.78), bodies natal (0.58), progression (0.48), solar arc (0.40),
con líneas de aspectos cross convergiendo desde el ring natal hacia
cada overlay simultáneamente.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:59:01 +00:00
sergio e0c5c02b8e feat(tahuantinsuyu): fase 8 — slider interactivo + slider de edad en progression
Los `Control::Slider` del panel ya no son display-only — son arrastrables
con el mismo patrón del splitter (canvas absoluto sobre el track + window
mouse handlers en cada frame). El `ProgressionModule` ahora expone un
slider de `target_age_years` (0..120) que el shell inicializa con la
edad actual del sujeto al cargar la carta.

- panel: SliderDrag struct + slider_state HashMap + slider_drag Option
  + métodos start/continue/end_slider_drag + apply_slider_position que
  calcula fraction desde la posición del mouse relativa al track y
  emite ControlChanged con el valor float. set_slider(module, key, val)
  para sincronización externa. set_active_kind ahora inicializa también
  los sliders desde sus defaults. render_slider pinta track + portion
  filled + thumb circular + canvas overlay con handlers de drag.
  Los Slider tienen un valor visible "X.X (min...max)" en el header.
- modules: ProgressionModule agrega Control::Slider target_age_years
  con range 0..120, step 0.25, default 30 (placeholder — el shell lo
  reescribe con la edad real al cargar la carta).
- shell: apply_selection(Chart) ahora calcula current_age, lo inserta
  en module_configs["progression"]["target_age_years"], y empuja al
  panel via set_slider. build_requests ya leía target_age_years desde
  el map (de fase 7), así que ahora el slider lo controla.

Mecánica: si activás "Progresión secundaria", el slider arranca en la
edad actual del sujeto. Arrastralo a la izquierda y la rueda recompone
la carta progresada para esa edad simbólica — vas viendo cómo el sujeto
"evoluciona" o "involuciona" a través de su línea temporal interna,
con los planetas progresados moviéndose por el anillo interno y los
cross aspects con la natal reorganizándose en tiempo real.

Same pattern aplica de aquí en más para cualquier slider futuro
(harmonic en NatalModule, target_year en SolarArc, orb_multiplier, …).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:54:06 +00:00
sergio 42e09fd7cd feat(tahuantinsuyu): fase 7 — progresión secundaria como overlay (prueba de la arquitectura)
Valida que el refactor de fase 6 escala: agregar un overlay nuevo
(progresión secundaria, "día por año") tocó exactamente lo predicho —
una variante en PipelineRequest, un helper en bridge, un módulo
declarativo en `progression`, una línea en build_requests, y el canvas
para pintarlo. Cero cambios en el flujo de eventos del Shell.

- engine: PipelineRequest::SecondaryProgression { target_age_years: f64 }
  + build_progression_overlay(natal, age, render) que delega en
  eternal_astrology::secondary_progression(natal, session, age), pinta
  los placements progresados en un anillo interno (ring 0.48), y suma
  cross-aspects natal × progresada vía find_synastry_aspects (sólo
  mayores, opacidad × 0.7). z = 6/7 — sobre las capas natal y
  transit.
- modules: progression::ProgressionModule con id "progression", toggle
  "Activar" (sin hotkey por ahora). Registry::with_builtins lo agrega.
  El test pasó de 2 a 3 módulos para ChartKind::Natal.
- shell: build_requests detecta progression.enabled, calcula la edad
  decimal desde StoredBirthData y SystemTime::now() (current_age_years
  helper, aproximación tropical) y arma el request con esa edad.
  El resto del flujo del shell se mantiene — la abstracción funciona.
- canvas: Radii agrega `progression: r * 0.48`, `aspects` shrunk a
  `r * 0.38` para hacer lugar. Helper aspect_endpoints(module_id)
  resuelve el par (r_from, r_to) según natal/transit/progression.
  paint_wheel pinta dots progresados con alpha 0.85 + anillo guía
  sutil que delimita el slot. Glyph overlay pinta planet symbols en
  el ring de progresión con font_size 14 y box 20 (menores que el
  natal para diferenciar visualmente).

Probarlo: en el panel, activar "Progresión secundaria" — verás los
planetas progresados en un anillo interno con su retrogradación
marcada, y líneas de aspectos que cruzan desde el ring de cuerpos
natales hacia el ring progresivo. Combinable con tránsitos: ambos
overlays apilan capas en orden bodies → transits, sin colisiones.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:42:25 +00:00
sergio d4761bf238 refactor(tahuantinsuyu): fase 6 — Modules pluggables vía compose + PipelineRequest
El shell ya no carga el flag `show_transits: bool` ni hardcodea qué
pipeline corre. La engine expone una sola API `compose(chart, offset,
&[PipelineRequest])` que la shell alimenta a partir de un map
`module_configs: HashMap<String, serde_json::Value>`. Los toggles de
overlay (transit hoy, progression/synastry/solar_arc en fase 7) viven
como módulos propios en el panel.

- engine: PipelineRequest enum (variante Transit por ahora; comentarios
  con el roadmap de SecondaryProgression/SolarArc/Synastry). compose()
  es la nueva entrada canónica; compute / compute_at_offset /
  compute_with_transits_at_now quedan como atajos retrocompatibles que
  delegan en compose. bridge.rs refactor: extraído build_transit_overlay
  como helper que muta &mut RenderModel, listo para que más pipelines
  apilen capas encima.
- modules: nuevo módulo `transit::TransitModule` (id "transit", toggle
  "enabled" con hotkey [T], applies_to Natal). Sacado el toggle
  show_transits de NatalModule — ahora cada módulo declara lo suyo.
  Registry::with_builtins() registra ambos. Test asegura los dos
  aplican a Natal.
- panel: sin cambios — ya itera Registry::for_kind(kind) y renderea
  cada módulo aplicable con sus controls. La adición del TransitModule
  aparece automática como segunda card en el panel.
- shell: replace show_transits por module_configs map. build_requests()
  deriva PipelineRequest::Transit cuando module_configs["transit"]
  ["enabled"] == true. on_panel_event: toggles del NatalModule afectan
  solo visibility del canvas; toggles de otros módulos van al
  module_configs y disparan render_current. on_canvas_event: [T]
  hotkey → flip transit.enabled + sync panel + recompose. apps Cargo
  agrega serde_json como dep directa.

Todos los tests verdes. Fase 7 puede sumar overlays adicionales
(progression, solar_arc) solo agregando variantes a PipelineRequest +
helpers en bridge + módulos declarativos — sin tocar el shell.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:31:11 +00:00
sergio 4d14a4495f feat(tahuantinsuyu): fase 5 — overlay de tránsitos (bi-wheel natal × ahora)
Activá el toggle "Tránsitos (ahora)" en el panel (o hotkey [T] sobre
el wheel): la engine computa una segunda NatalChart al instante
SystemTime::now() con el mismo observer y dibuja un anillo externo de
planet glyphs encima del natal, más las cross-aspects entre ambos
charts (sólo mayores). Las líneas cross van del ring de cuerpos
natales al ring externo de tránsitos, con stroke más fino y opacidad
más baja para no taparle el ojo a las aspectos natal-natal.

- engine/bridge.rs: extraídas build_eternal_inputs y
  compute_natal_chart como helpers reutilizables. Nueva
  compute_with_transits(chart, offset, transit_at) que llama
  find_synastry_aspects entre natal y transit (AspectKind::MAJORS).
  Atajo compute_with_transits_at_now usa ESInstant::now(). Las capas
  extra van con module_id = "transit" y LayerKind::Outer /
  LayerKind::Aspects para que el canvas las distinga.
- engine/lib.rs: re-export de compute_with_transits_at_now con el
  mismo fallback al mock cuando feature `eternal-bridge` está off.
- canvas: nueva Radii::transits = 0.82, layout del wheel re-balanceado
  (houses_outer 0.78, houses_inner 0.66, bodies 0.58, aspects 0.50)
  para hacer lugar al anillo externo sin colisiones. paint_wheel:
  detecta layers de transit por module_id, pinta dots + glifos en el
  anillo nuevo + anillos guía sutiles. paint_cross_aspect_line con
  stroke 0.7 entre los dos radios. Glyph overlay para Outer ring con
  alpha 0.9 y font_size más chico que el natal. Hotkey [T] en
  on_key_down toggle LayerKind::Outer.
- modules: NatalModule.controls() agrega toggle show_transits con
  hotkey [T] (default false — no recomputar transits si nadie pidió).
- shell: nuevo show_transits flag. render_current despacha entre
  compute_at_offset y compute_with_transits_at_now según el flag.
  on_panel_event traduce ControlChanged show_transits a flip + redraw.
  on_canvas_event: el toggle de LayerKind::Outer dispara show_transits
  flip + render (no es un visibility toggle puro).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:24:36 +00:00
sergio 360797132e feat(tahuantinsuyu): fase 4 — jog-dial perimetral, hotkeys y panel interactivo
Time scrubbing por drag en el aro exterior del wheel: rota visualmente
mientras dura el drag, al soltar traduce el delta angular a minutos
(1° = 4 min sideral, CW = forward) y emite CanvasEvent::TimeOffsetChanged.
La Shell recomputa con engine::compute_at_offset y el ascendant rotado
queda en la nueva posición. Snap visual a 0° tras commit.

- engine: nueva variante compute_at_offset(chart, minutes) que suma
  segundos al UTC base via add_seconds + Instant::from_utc y corre la
  pipeline normal. compute() es ahora wrapper con offset=0.
- canvas: estado nuevo layer_visibility + drag_jog. Mouse handlers
  registrados desde el paint callback (mismo patrón que splitter/tiled).
  Hotkeys D/H/X/P toggle SignDial/Houses/Aspects/Bodies, R resetea
  offset. FocusHandle + click-to-focus para recibir teclas. Indicador
  ⏱ ±Xd HH:MM en el footer con color highlight cuando el offset != 0.
  paint_wheel + glyph overlays respetan layer_visibility (skip capas
  ocultas).
- modules: NatalModule.controls() ahora expone show_sign_dial /
  show_houses / show_aspects / show_bodies con hotkeys [D/H/X/P], más
  el slider de armónico.
- panel: ControlPanel mantiene toggle_state cache (module_id, key) →
  bool, inicializa desde defaults al cambiar de ChartKind. Click
  invierte el toggle visualmente y emite ControlChanged. Nuevo
  set_toggle(module, key, value) para que la Shell mantenga sync
  cuando el canvas se autotogglea por hotkey.
- shell: nuevo current_chart + current_offset_minutes. render_current()
  delega a compute_at_offset. Suscripción a CanvasEvent traduce
  TimeOffsetChanged → re-render, LayerVisibilityChanged → panel sync.
  Suscripción a PanelEvent::ControlChanged traduce show_* keys a
  set_layer_visible sobre el canvas.

Todos los tests verdes. La fase 5 sumará módulos extra (transit,
progression, synastry, uranian) + extracción de eternal de lo que falte.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:15:09 +00:00
sergio f4944218e2 fix(tahuantinsuyu-engine): leer longitude_rate_rad_per_day directo
`BodyPlacement::is_retrograde` cambió entre versiones de eternal:
en commits viejos es `pub fn is_retrograde(&self) -> bool`, en más
nuevos es `pub is_retrograde: bool`. Cualquiera de las dos formas
rompe la otra al usar `p.is_retrograde()` o `p.is_retrograde`.

Leemos el campo crudo `pub longitude_rate_rad_per_day: f64` (estable
en ambas) y aplicamos `< 0.0` localmente — el bridge queda inmune a
ese refactor upstream.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:50:48 +00:00
sergio 82fa370877 feat(tahuantinsuyu): fase 3 — engine real contra eternal + rueda pintada en GPUI
Bridge a eternal-astrology prendido por default. `engine::compute(chart)`
abre una EphemerisSession VSOP2013 (cacheada vía OnceLock global),
traduce los Stored* del modelo a BirthData/ChartConfig de eternal,
corre NatalChart::compute + find_aspects(modern_western) y devuelve un
RenderModel con cuatro capas: SignDial, Houses, Bodies, Aspects.

- tahuantinsuyu-engine: bridge.rs nuevo con map_house_system,
  map_zodiac (incl. 8 ayanamshas), map_body_set, body_symbol,
  aspect_kind_id. compute_mock se mantiene como fallback sin feature.
  Errores tipados (EngineError::Eternal). Test real verde con datos
  natales de demo.
- tahuantinsuyu-canvas: rewrite con gpui::canvas() + PathBuilder.
  Pinta: sectores zodiacales coloreados por elemento (Fire/Earth/Air/
  Water), anillos de sign-dial/houses/aspects, cusps zodiacales,
  cusps de casas (con énfasis para Asc/MC/Desc/IC), líneas radiales
  hasta el centro para los ejes, líneas de aspectos coloreadas por
  kind con opacidad por orb, dots de cuerpos.
  Glifos unicode (- signos, ☉-♇ planetas, ☊☋⚷⚸ puntos) como divs
  absolutos sobre el canvas. Marcador ᴿ cuando retrógrado.
  Rotación canónica: Asc a las 9, casas crecen contrarreloj.
- shell: ahora llama engine::compute() real y reporta errores por
  stderr sin caer la app.

Datos sintetizados: ascendente, MC, descendente, IC; 12 cusps de
casa según el sistema configurado; placements de los cuerpos del
BodySet con sus longitudes zodiacales, casa y flag retrógrado;
aspectos mayores con opacidad proporcional al orb.

`cargo check` y `cargo test --features eternal-bridge` verdes.
La fase 4 traerá el panel interactivo (jog-dial, toggles, sliders,
atajos teclado).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:43:11 +00:00
sergio bcb92b537e feat(tahuantinsuyu): fase 2 — CRUD UX sobre el tree (menú, modales, form natal)
Right-click sobre el explorador izquierdo abre menú contextual cuyas
opciones dependen del target (raíz, group, contact o chart). Modales
flotantes para crear/renombrar usando yahweh-widget-text-input; un
form más completo de 11 campos para la birth data al crear cartas
natales. Borrar pide confirmación por window.prompt nativo.

- tahuantinsuyu-store: rename_contact, rename_chart, move_group,
  move_contact (los `move_*` para fase posterior de drag-to-nest).
- tahuantinsuyu-tree: estado interno (Menu, Modal enum, ChartForm),
  handlers de ContextMenuRequested, render overlays.
  Soporta seis modales: rename de g/c/h, create group/contact, form
  natal completo con parseo + reporte de errores inline.
  Auto-expande el contact tras crear una carta.
  Nuevo evento TreeEvent::HierarchyChanged tras cada mutación.
- shell: maneja HierarchyChanged sin propagar selección.

`cargo check` y `cargo test` verdes. Fase 3 viene con engine real
contra eternal-astrology + pintado de la rueda.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:13:17 +00:00
sergio c48638fe87 feat(tahuantinsuyu): scaffolding del estudio astrológico (10 crates + ventana 3-panes)
Módulo nuevo `modules/tahuantinsuyu/` con 9 crates reusables + app
`apps/tahuantinsuyu` ejecutable que abre la ventana del explorador y
coordina los widgets:

- tahuantinsuyu-card: Card Brahman + spawn_sidecar (flows
  chart-request/chart-result).
- tahuantinsuyu-model: tipos agnósticos (Group/Contact/Chart,
  StoredBirthData, StoredChartConfig, ChartKind, TreeSelection).
- tahuantinsuyu-store: persistencia SQLite (rusqlite) con migración v1,
  CRUD por entidad y descenso recursivo `charts_under_group`.
- tahuantinsuyu-engine: bridge agnóstico al canvas vía `RenderModel`
  (Layer/Glyph/Geometry). Feature `eternal-bridge` (off por default)
  reservada para enchufar eternal-astrology desde ~/eternal.
- tahuantinsuyu-modules: registry de módulos pluggables (Module trait
  + Control schema) con `NatalModule` placeholder.
- tahuantinsuyu-theme: AstroPalette (elementos / modos / planetas /
  aspectos) con variantes dark + light sobre yahweh-theme.
- tahuantinsuyu-canvas: widget GPUI con CanvasState (Empty / Wheel /
  Thumbnails). Render placeholder hasta cablear la rueda real.
- tahuantinsuyu-tree: explorador izquierdo sobre yahweh-widget-tree,
  prefijos g:/c:/h: para Group/Contact/Chart.
- tahuantinsuyu-panel: control panel inferior que lee Controls de los
  módulos del registry y los pinta.
- apps/tahuantinsuyu: binario `tahuantinsuyu` (launch_app-style) con
  Shell coordinador (tree↔canvas↔panel), DB en $XDG_DATA_HOME.

Workspace Cargo.toml actualizado con los 10 miembros. `cargo check`
verde, tests unitarios verdes (model/store/engine/modules/theme/card).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 01:06:03 +00:00