feat(cosmobiologia): GR — scrubbing live de la edad con el jog-dial

Tercer y último incremento del Sistema GR: en modo GR (direcciones
primarias activas) el jog-dial deja de rotar el wheel y pasa a
scrubear la edad en vivo.

- canvas: CanvasState::gr_active() detecta el modo; on_jog_move emite
  CanvasEvent::GrAgeDelta (años por grado de jog, sensibilidad 0.1)
  en vez de rotar; on_jog_up no aplica snap de tiempo.
- shell: scrub_gr_age acumula el delta sobre target_age_years del
  módulo primary_directions, clampa a [0,120], sincroniza el slider
  del panel y recompone — los glifos dirigidos y el HUD se mueven en
  vivo bajo el cursor.

Con esto el Sistema GR queda completo: cómputo de triggers, resaltado
de convergencias, HUD de rectificación y scrubbing live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 13:47:57 +00:00
parent 363f401b75
commit ec111a2e27
2 changed files with 67 additions and 4 deletions
+29
View File
@@ -1172,8 +1172,37 @@ impl Shell {
CanvasEvent::ExportSvgRequested => { CanvasEvent::ExportSvgRequested => {
self.export_current_to_svg(); self.export_current_to_svg();
} }
CanvasEvent::GrAgeDelta(delta) => {
self.scrub_gr_age(*delta, 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.
fn scrub_gr_age(&mut self, delta_years: f64, cx: &mut Context<Self>) {
if !module_enabled(&self.module_configs, "primary_directions") {
return;
}
let current = self.module_age_or_current("primary_directions");
let next = (current + delta_years).clamp(0.0, 120.0);
if (next - current).abs() < 1e-6 {
return;
}
let entry = self
.module_configs
.entry("primary_directions".into())
.or_insert_with(|| serde_json::json!({}));
if let serde_json::Value::Object(map) = entry {
map.insert("target_age_years".into(), serde_json::json!(next));
}
self.panel.update(cx, |p, cx| {
p.set_slider("primary_directions", "target_age_years", next, cx)
});
self.persist_module("primary_directions");
self.render_current(cx);
}
/// Recompone la carta actual + escribe el SVG a un archivo en /// Recompone la carta actual + escribe el SVG a un archivo en
/// `$XDG_DATA_HOME/cosmobiologia/exports/<label>_<short_id>.svg`. /// `$XDG_DATA_HOME/cosmobiologia/exports/<label>_<short_id>.svg`.
@@ -67,6 +67,10 @@ pub enum CanvasEvent {
/// El usuario pidió exportar el render actual como SVG. El shell /// El usuario pidió exportar el render actual como SVG. El shell
/// se encarga de escribir el archivo (la engine genera el string). /// se encarga de escribir el archivo (la engine genera el string).
ExportSvgRequested, ExportSvgRequested,
/// En modo GR (direcciones primarias activas) el jog-dial scrubea
/// la edad en vez del tiempo. Lleva el delta de edad en años; el
/// host lo acumula sobre `target_age_years` y recompone en vivo.
GrAgeDelta(f64),
} }
// ===================================================================== // =====================================================================
@@ -236,7 +240,22 @@ impl CanvasState {
pub fn is_layer_visible(&self, kind: LayerKind) -> bool { pub fn is_layer_visible(&self, kind: LayerKind) -> bool {
self.layer_visibility.get(&kind).copied().unwrap_or(true) self.layer_visibility.get(&kind).copied().unwrap_or(true)
} }
/// `true` cuando hay un overlay de direcciones primarias activo.
/// En ese modo el jog-dial scrubea la edad GR en vez del tiempo.
fn gr_active(&self) -> bool {
matches!(
&self.mode,
CanvasMode::Wheel { render }
if render.layers.iter().any(|l| l.module_id == "pd_direct")
)
} }
}
/// Sensibilidad del scrubbing GR: años de edad por grado de jog. A
/// 0.1, una vuelta completa del dial barre 36 años — fino para
/// explorar contactos sin perder rango.
const GR_AGE_PER_DEG: f32 = 0.1;
// ===================================================================== // =====================================================================
// Widget // Widget
@@ -408,6 +427,7 @@ impl AstrologyCanvas {
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
cx: &mut Context<'_, Self>, cx: &mut Context<'_, Self>,
) { ) {
let gr = self.state.gr_active();
let Some(jog) = self.state.drag_jog.as_mut() else { let Some(jog) = self.state.drag_jog.as_mut() else {
return; return;
}; };
@@ -426,10 +446,18 @@ impl AstrologyCanvas {
} }
jog.accumulated_delta_deg += delta; jog.accumulated_delta_deg += delta;
jog.last_screen_angle_deg = angle; jog.last_screen_angle_deg = angle;
let accumulated = jog.accumulated_delta_deg;
if gr {
// Modo GR: el jog scrubea la edad. No rota el wheel — el
// feedback es el movimiento de los glifos dirigidos cuando
// el shell recompone con la edad nueva.
cx.emit(CanvasEvent::GrAgeDelta((-delta * GR_AGE_PER_DEG) as f64));
} else {
// Reflejo visual durante el drag (sin recomputar). // Reflejo visual durante el drag (sin recomputar).
self.state.view_rotation_deg = jog.accumulated_delta_deg; self.state.view_rotation_deg = accumulated;
cx.notify(); cx.notify();
} }
}
/// Hit-test sobre body glyphs + house cusps. Para bodies: distancia /// Hit-test sobre body glyphs + house cusps. Para bodies: distancia
/// al centro del glyph dentro de threshold. Para cusps: el mouse /// al centro del glyph dentro de threshold. Para cusps: el mouse
@@ -662,9 +690,15 @@ impl AstrologyCanvas {
} }
fn on_jog_up(&mut self, cx: &mut Context<'_, Self>) { fn on_jog_up(&mut self, cx: &mut Context<'_, Self>) {
let gr = self.state.gr_active();
let Some(jog) = self.state.drag_jog.take() else { let Some(jog) = self.state.drag_jog.take() else {
return; return;
}; };
if gr {
// El scrub GR se aplicó en vivo durante el drag; al soltar
// no queda nada que confirmar.
return;
}
// 1° de arco ≈ 4 minutos de tiempo sideral (15°/hora). // 1° de arco ≈ 4 minutos de tiempo sideral (15°/hora).
// CW visual (delta negativa en nuestra convención) → tiempo // CW visual (delta negativa en nuestra convención) → tiempo
// hacia adelante. // hacia adelante.
@@ -1641,7 +1675,7 @@ fn render_wheel(
.text_size(px(10.0)) .text_size(px(10.0))
.text_color(theme.fg_disabled) .text_color(theme.fg_disabled)
.child( .child(
"[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [C]oords · Ctrl+drag = tiempo · [0] reset zoom · [R] reset tiempo · [S]vg", "[D]ial [H]ouses as[X]pects [P]lanets [T]ransits [C]oords · Ctrl+drag = tiempo/edad GR · [0] reset zoom · [R] reset tiempo · [S]vg",
), ),
); );