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 => {
|
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",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user