feat(cosmobiologia): espectro de fuerza armónica — histograma clicable

Completa la feature de armónicos: además de la carta armónica, ahora
hay un espectro que guía qué armónico mirar.

- cosmobiologia-render: harmonic_spectrum computa la fuerza de cada
  armónica 1-32 (suma de cercanía a conjunción exacta de los pares de
  cuerpos en esa armónica). apply_harmonic lo puebla + expone el
  armónico activo. Campos RenderModel.harmonic / .harmonic_spectrum.
  2 tests nuevos (el pico cae en la armónica resonante).
- cosmobiologia-canvas: render_harmonic_spectrum pinta el histograma
  en el footer; cada barra es clicable y emite HarmonicSelected — un
  clic salta a esa armónica. La barra activa va resaltada.
- shell: select_harmonic fija el slider del módulo natal y recompone.
- modules: el slider de armónico pasa de 1-20 a 1-32 (rango del
  espectro).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 13:57:54 +00:00
parent 968255f4cd
commit 54de7849c5
7 changed files with 208 additions and 2 deletions
+21
View File
@@ -1176,9 +1176,30 @@ impl Shell {
CanvasEvent::GrAgeDelta(delta) => {
self.scrub_gr_age(*delta, cx);
}
CanvasEvent::HarmonicSelected(n) => {
self.select_harmonic(*n, cx);
}
}
}
/// Fija el armónico de la carta natal (clic en una barra del
/// espectro): escribe `harmonic` en `module_configs["natal"]`,
/// sincroniza el slider del panel y recompone.
fn select_harmonic(&mut self, n: u32, cx: &mut Context<Self>) {
let entry = self
.module_configs
.entry("natal".into())
.or_insert_with(|| serde_json::json!({}));
if let serde_json::Value::Object(map) = entry {
map.insert("harmonic".into(), serde_json::json!(n));
}
self.panel.update(cx, |p, cx| {
p.set_slider("natal", "harmonic", n as f64, cx)
});
self.persist_module("natal");
self.render_current(cx);
}
/// Scrubbing en vivo de la edad GR vía jog-dial. Acumula `delta`
/// sobre `target_age_years` del módulo `primary_directions`,
/// clampa a [0,120], sincroniza el slider del panel y recompone.