feat(tahuantinsuyu): fase 21 — background compute + UranianModule
Cierre del brief original — última pieza visual (Uraniano) + perf. ## #1 — Compute en background thread render_current() pasa de bloqueante a async. La pipeline corre en cx.background_executor().spawn (no UI thread), y al terminar el update vuelve al UI vía cx.spawn. Sin esto, un drag del slider con muchos overlays bloquea el frame por hasta 200ms. Cancelación: Shell gana `render_seq: u64`. Cada render_current() incrementa el counter y captura su número; el closure async compara antes de aplicar. Si llegó un compute más nuevo en el medio (drag rápido), el viejo se descarta — evita el race donde un cómputo lento sobrescribe uno reciente y rápido. Inputs al background: Chart clonado + offset + Vec<PipelineRequest> + NatalOptions. La sesión VSOP2013 sigue siendo `static OnceLock` read-only, accesible desde cualquier thread. ## #11 — UranianModule (versión textual) Cierra la última pieza del brief original. Toggle "Uraniano (90°)" en el panel; engine detecta cuerpos natales cuya longitud módulo 90 cae dentro de ε=2° y los agrupa como "ejes". Footer renderea cada grupo como pill con los unicodes (☉ ♃ · 14.3°) bajo el header "Ejes uranianos (90°)". El algoritmo: 1. mod90 = longitude.rem_euclid(90.0) para cada placement 2. Sort por mod90 ascendente 3. Walk lineal agrupando entradas con diff(mod90) ≤ ε 4. Wrap-around check: el primer y último grupo se mergean si abarcan el cierre del dial (88→2 = solo 4° de diff modular) 5. Solo emite grupos con 2+ miembros (singletons no son fórmulas) - engine: PipelineRequest::Uranian + UranianGroup struct + build_uranian_groups helper. RenderModel gana uranian_groups field. push_overlay_meta tipo "Uraniano · N ejes" o "sin ejes". - modules: uranian::UranianModule (toggle "Activar"). Registry pasa a 9 módulos para ChartKind::Natal. Test actualizado. - shell: build_requests detecta uranian.enabled, pushea PipelineRequest::Uranian (sin parámetros). - canvas: footer agrega sección "Ejes uranianos (90°)" con pills arriba de la lista de aspectos — border angle_highlight para invitar a la lectura. La visualización geométrica completa del dial de 90° con árbol de simetría al hover queda para una fase posterior — esta versión textual cubre el caso analítico (ver qué cuerpos están "en relación uraniana") sin requerir un canvas secundario. cargo check verde, 8 tests engine + 1 test modules (9 módulos aplicables a ChartKind::Natal) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,11 @@ pub struct Shell {
|
||||
/// `module_id`. Las claves dentro del JSON dependen del módulo (la
|
||||
/// convención es `"enabled": bool` para el toggle principal).
|
||||
module_configs: HashMap<String, serde_json::Value>,
|
||||
/// Sequence counter para descartar resultados de cómputos
|
||||
/// background que llegan después de uno más reciente. Cada
|
||||
/// `render_current` lo incrementa y la closure async compara antes
|
||||
/// de aplicar el render al canvas.
|
||||
render_seq: u64,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
@@ -94,6 +99,7 @@ impl Shell {
|
||||
current_chart: None,
|
||||
current_offset_minutes: 0,
|
||||
module_configs: HashMap::new(),
|
||||
render_seq: 0,
|
||||
};
|
||||
shell.refresh_chart_options(cx);
|
||||
shell
|
||||
@@ -256,6 +262,9 @@ impl Shell {
|
||||
if module_enabled(&self.module_configs, "midpoints") {
|
||||
requests.push(PipelineRequest::Midpoints);
|
||||
}
|
||||
if module_enabled(&self.module_configs, "uranian") {
|
||||
requests.push(PipelineRequest::Uranian);
|
||||
}
|
||||
if module_enabled(&self.module_configs, "composite") {
|
||||
if let Some(partner) = self.resolve_composite_partner() {
|
||||
requests.push(PipelineRequest::Composite {
|
||||
@@ -464,34 +473,63 @@ impl Shell {
|
||||
let Some(chart) = self.current_chart.as_ref() else {
|
||||
return;
|
||||
};
|
||||
// Snapshot de inputs para mover al background. La sesión
|
||||
// VSOP2013 vive en un static `OnceLock` adentro del bridge, así
|
||||
// que es compartible read-only entre threads sin que ningún
|
||||
// dato cruce más allá del Chart clonado + requests/options.
|
||||
let chart = chart.clone();
|
||||
let offset = self.current_offset_minutes;
|
||||
let requests = self.build_requests();
|
||||
let natal_options = self.build_natal_options();
|
||||
let render = match compose_with_options(
|
||||
chart,
|
||||
self.current_offset_minutes,
|
||||
&requests,
|
||||
&natal_options,
|
||||
) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[shell] compose {} (+{}min, {} reqs): {}",
|
||||
chart.id,
|
||||
self.current_offset_minutes,
|
||||
requests.len(),
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.canvas.update(cx, |c, cx| {
|
||||
c.set_mode(
|
||||
CanvasMode::Wheel {
|
||||
render: Box::new(render),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
self.render_seq = self.render_seq.wrapping_add(1);
|
||||
let my_seq = self.render_seq;
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
// El compute corre en el background_executor — no bloquea
|
||||
// el UI thread. Para una rueda completa con varios overlays
|
||||
// puede tomar 100-200ms; sin esto, los drags del slider se
|
||||
// sentirían atorados.
|
||||
let chart_for_bg = chart.clone();
|
||||
let requests_for_bg = requests.clone();
|
||||
let opts_for_bg = natal_options.clone();
|
||||
let result = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
compose_with_options(&chart_for_bg, offset, &requests_for_bg, &opts_for_bg)
|
||||
})
|
||||
.await;
|
||||
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
// Descartar si llegó un render más nuevo en el medio.
|
||||
// Sin este check, durante un drag rápido un compute
|
||||
// viejo podría sobrescribir el más reciente.
|
||||
if this.render_seq != my_seq {
|
||||
return;
|
||||
}
|
||||
match result {
|
||||
Ok(render) => {
|
||||
this.canvas.update(cx, |c, cx| {
|
||||
c.set_mode(
|
||||
CanvasMode::Wheel {
|
||||
render: Box::new(render),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[shell] compose {} (+{}min, {} reqs): {}",
|
||||
chart.id,
|
||||
offset,
|
||||
requests.len(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn on_canvas_event(&mut self, ev: &CanvasEvent, cx: &mut Context<Self>) {
|
||||
|
||||
Reference in New Issue
Block a user