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:
@@ -1172,8 +1172,37 @@ impl Shell {
|
||||
CanvasEvent::ExportSvgRequested => {
|
||||
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
|
||||
/// `$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
|
||||
/// se encarga de escribir el archivo (la engine genera el string).
|
||||
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 {
|
||||
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
|
||||
@@ -408,6 +427,7 @@ impl AstrologyCanvas {
|
||||
bounds: Bounds<Pixels>,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) {
|
||||
let gr = self.state.gr_active();
|
||||
let Some(jog) = self.state.drag_jog.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -426,10 +446,18 @@ impl AstrologyCanvas {
|
||||
}
|
||||
jog.accumulated_delta_deg += delta;
|
||||
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).
|
||||
self.state.view_rotation_deg = jog.accumulated_delta_deg;
|
||||
self.state.view_rotation_deg = accumulated;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Hit-test sobre body glyphs + house cusps. Para bodies: distancia
|
||||
/// 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>) {
|
||||
let gr = self.state.gr_active();
|
||||
let Some(jog) = self.state.drag_jog.take() else {
|
||||
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).
|
||||
// CW visual (delta negativa en nuestra convención) → tiempo
|
||||
// hacia adelante.
|
||||
@@ -1641,7 +1675,7 @@ fn render_wheel(
|
||||
.text_size(px(10.0))
|
||||
.text_color(theme.fg_disabled)
|
||||
.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",
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user