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
|
/// `module_id`. Las claves dentro del JSON dependen del módulo (la
|
||||||
/// convención es `"enabled": bool` para el toggle principal).
|
/// convención es `"enabled": bool` para el toggle principal).
|
||||||
module_configs: HashMap<String, serde_json::Value>,
|
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 {
|
impl Shell {
|
||||||
@@ -94,6 +99,7 @@ impl Shell {
|
|||||||
current_chart: None,
|
current_chart: None,
|
||||||
current_offset_minutes: 0,
|
current_offset_minutes: 0,
|
||||||
module_configs: HashMap::new(),
|
module_configs: HashMap::new(),
|
||||||
|
render_seq: 0,
|
||||||
};
|
};
|
||||||
shell.refresh_chart_options(cx);
|
shell.refresh_chart_options(cx);
|
||||||
shell
|
shell
|
||||||
@@ -256,6 +262,9 @@ impl Shell {
|
|||||||
if module_enabled(&self.module_configs, "midpoints") {
|
if module_enabled(&self.module_configs, "midpoints") {
|
||||||
requests.push(PipelineRequest::Midpoints);
|
requests.push(PipelineRequest::Midpoints);
|
||||||
}
|
}
|
||||||
|
if module_enabled(&self.module_configs, "uranian") {
|
||||||
|
requests.push(PipelineRequest::Uranian);
|
||||||
|
}
|
||||||
if module_enabled(&self.module_configs, "composite") {
|
if module_enabled(&self.module_configs, "composite") {
|
||||||
if let Some(partner) = self.resolve_composite_partner() {
|
if let Some(partner) = self.resolve_composite_partner() {
|
||||||
requests.push(PipelineRequest::Composite {
|
requests.push(PipelineRequest::Composite {
|
||||||
@@ -464,34 +473,63 @@ impl Shell {
|
|||||||
let Some(chart) = self.current_chart.as_ref() else {
|
let Some(chart) = self.current_chart.as_ref() else {
|
||||||
return;
|
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 requests = self.build_requests();
|
||||||
let natal_options = self.build_natal_options();
|
let natal_options = self.build_natal_options();
|
||||||
let render = match compose_with_options(
|
self.render_seq = self.render_seq.wrapping_add(1);
|
||||||
chart,
|
let my_seq = self.render_seq;
|
||||||
self.current_offset_minutes,
|
|
||||||
&requests,
|
cx.spawn(async move |this, cx| {
|
||||||
&natal_options,
|
// El compute corre en el background_executor — no bloquea
|
||||||
) {
|
// el UI thread. Para una rueda completa con varios overlays
|
||||||
Ok(r) => r,
|
// puede tomar 100-200ms; sin esto, los drags del slider se
|
||||||
Err(e) => {
|
// sentirían atorados.
|
||||||
eprintln!(
|
let chart_for_bg = chart.clone();
|
||||||
"[shell] compose {} (+{}min, {} reqs): {}",
|
let requests_for_bg = requests.clone();
|
||||||
chart.id,
|
let opts_for_bg = natal_options.clone();
|
||||||
self.current_offset_minutes,
|
let result = cx
|
||||||
requests.len(),
|
.background_executor()
|
||||||
e
|
.spawn(async move {
|
||||||
);
|
compose_with_options(&chart_for_bg, offset, &requests_for_bg, &opts_for_bg)
|
||||||
return;
|
})
|
||||||
}
|
.await;
|
||||||
};
|
|
||||||
self.canvas.update(cx, |c, cx| {
|
let _ = this.update(cx, |this, cx| {
|
||||||
c.set_mode(
|
// Descartar si llegó un render más nuevo en el medio.
|
||||||
CanvasMode::Wheel {
|
// Sin este check, durante un drag rápido un compute
|
||||||
render: Box::new(render),
|
// viejo podría sobrescribir el más reciente.
|
||||||
},
|
if this.render_seq != my_seq {
|
||||||
cx,
|
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>) {
|
fn on_canvas_event(&mut self, ev: &CanvasEvent, cx: &mut Context<Self>) {
|
||||||
|
|||||||
@@ -1005,6 +1005,51 @@ fn render_wheel(
|
|||||||
footer = footer.child(b);
|
footer = footer.child(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ejes uranianos detectados (cuerpos en la misma posición mod 90).
|
||||||
|
// Aparece sólo cuando el módulo Uranian está activo y hay
|
||||||
|
// grupos. Cada grupo se muestra como pill con los unicode de los
|
||||||
|
// cuerpos + el grado dial-90.
|
||||||
|
if !render.uranian_groups.is_empty() {
|
||||||
|
let mut row = div().flex().flex_row().flex_wrap().gap(px(6.0));
|
||||||
|
for group in &render.uranian_groups {
|
||||||
|
let bodies_text: String = group
|
||||||
|
.bodies
|
||||||
|
.iter()
|
||||||
|
.map(|b| planet_unicode(b))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
row = row.child(
|
||||||
|
div()
|
||||||
|
.px(px(8.0))
|
||||||
|
.py(px(2.0))
|
||||||
|
.rounded(px(10.0))
|
||||||
|
.bg(theme.bg_panel_alt.clone())
|
||||||
|
.border_1()
|
||||||
|
.border_color(with_alpha(palette.angle_highlight, 0.6))
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"{} · {:.1}°",
|
||||||
|
bodies_text, group.mod90_deg
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
footer = footer.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(3.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(10.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child("Ejes uranianos (90°)"),
|
||||||
|
)
|
||||||
|
.child(row),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Lista textual de aspectos (top 12 por orb). Compacta, en grid
|
// Lista textual de aspectos (top 12 por orb). Compacta, en grid
|
||||||
// de 3 columnas, fonts pequeños. Solo aparece cuando hay aspectos
|
// de 3 columnas, fonts pequeños. Solo aparece cuando hay aspectos
|
||||||
// computados.
|
// computados.
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use tahuantinsuyu_model::{Chart, HouseSystem, StoredChartConfig, Zodiac};
|
|||||||
use crate::dignity::essential_dignity;
|
use crate::dignity::essential_dignity;
|
||||||
use crate::{
|
use crate::{
|
||||||
AspectSummary, EngineError, Geometry, Glyph, Layer, LayerKind, LineSeg, OverlayMeta,
|
AspectSummary, EngineError, Geometry, Glyph, Layer, LayerKind, LineSeg, OverlayMeta,
|
||||||
RenderModel,
|
RenderModel, UranianGroup,
|
||||||
};
|
};
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -343,6 +343,19 @@ pub fn compose(
|
|||||||
format!("Composite · {}", partner_label),
|
format!("Composite · {}", partner_label),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
crate::PipelineRequest::Uranian => {
|
||||||
|
build_uranian_groups(&natal, &mut render);
|
||||||
|
let n = render.uranian_groups.len();
|
||||||
|
push_overlay_meta(
|
||||||
|
&mut render,
|
||||||
|
"uranian",
|
||||||
|
if n == 0 {
|
||||||
|
"Uraniano · sin ejes".into()
|
||||||
|
} else {
|
||||||
|
format!("Uraniano · {} ejes", n)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +500,75 @@ fn build_progression_overlay(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper: detecta "ejes" del dial uraniano de 90° — cuerpos natales
|
||||||
|
/// cuya longitud módulo 90 cae dentro de una tolerancia ε (2° por
|
||||||
|
/// default). Llena `render.uranian_groups` con los grupos detectados.
|
||||||
|
fn build_uranian_groups(natal: &NatalChart, render: &mut RenderModel) {
|
||||||
|
const EPSILON: f64 = 2.0;
|
||||||
|
let mut entries: Vec<(String, f64)> = natal
|
||||||
|
.placements
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
let lon = p.longitude.longitude_deg();
|
||||||
|
let mod90 = lon.rem_euclid(90.0);
|
||||||
|
(body_symbol(p.body).to_string(), mod90)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
entries.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
|
||||||
|
let mut groups: Vec<UranianGroup> = Vec::new();
|
||||||
|
let mut current: Vec<(String, f64)> = Vec::new();
|
||||||
|
let mut anchor_mod90 = 0.0_f64;
|
||||||
|
for entry in entries {
|
||||||
|
if current.is_empty() {
|
||||||
|
anchor_mod90 = entry.1;
|
||||||
|
current.push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Distancia circular módulo 90 entre el entry y el anchor.
|
||||||
|
let mut diff = (entry.1 - anchor_mod90).abs();
|
||||||
|
if diff > 45.0 {
|
||||||
|
diff = 90.0 - diff;
|
||||||
|
}
|
||||||
|
if diff <= EPSILON {
|
||||||
|
current.push(entry);
|
||||||
|
} else {
|
||||||
|
if current.len() >= 2 {
|
||||||
|
let center = current.iter().map(|(_, m)| *m).sum::<f64>() / current.len() as f64;
|
||||||
|
groups.push(UranianGroup {
|
||||||
|
bodies: current.iter().map(|(s, _)| s.clone()).collect(),
|
||||||
|
mod90_deg: center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
anchor_mod90 = entry.1;
|
||||||
|
current = vec![entry];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current.len() >= 2 {
|
||||||
|
let center = current.iter().map(|(_, m)| *m).sum::<f64>() / current.len() as f64;
|
||||||
|
groups.push(UranianGroup {
|
||||||
|
bodies: current.iter().map(|(s, _)| s.clone()).collect(),
|
||||||
|
mod90_deg: center,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Wrap-around check: el primer y último grupo podrían ser el mismo
|
||||||
|
// (si span >88° abarcando el wrap en 90/0). Si los anchors están a
|
||||||
|
// ≤EPSILON modulo 90, mergeamos.
|
||||||
|
if groups.len() >= 2 {
|
||||||
|
let first_mod = groups[0].mod90_deg;
|
||||||
|
let last_mod = groups[groups.len() - 1].mod90_deg;
|
||||||
|
let mut diff = (first_mod - last_mod).abs();
|
||||||
|
if diff > 45.0 {
|
||||||
|
diff = 90.0 - diff;
|
||||||
|
}
|
||||||
|
if diff <= EPSILON {
|
||||||
|
let last = groups.pop().unwrap();
|
||||||
|
groups[0].bodies.extend(last.bodies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
render.uranian_groups = groups;
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper: agrega al `RenderModel` la carta compuesta (midpoint
|
/// Helper: agrega al `RenderModel` la carta compuesta (midpoint
|
||||||
/// composite, Davison 1958) entre la natal del sujeto y la carta del
|
/// composite, Davison 1958) entre la natal del sujeto y la carta del
|
||||||
/// partner. Cada planeta compuesto es el angular midpoint entre los
|
/// partner. Cada planeta compuesto es el angular midpoint entre los
|
||||||
@@ -972,6 +1054,7 @@ fn build_render_model(
|
|||||||
layers: vec![sign_dial, houses, bodies, aspects_layer],
|
layers: vec![sign_dial, houses, bodies, aspects_layer],
|
||||||
overlays: Vec::new(),
|
overlays: Vec::new(),
|
||||||
aspect_summary: Vec::new(),
|
aspect_summary: Vec::new(),
|
||||||
|
uranian_groups: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,10 @@ pub struct RenderModel {
|
|||||||
/// cerrados primero). La UI lo usa para la lista textual.
|
/// cerrados primero). La UI lo usa para la lista textual.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub aspect_summary: Vec<AspectSummary>,
|
pub aspect_summary: Vec<AspectSummary>,
|
||||||
|
/// Grupos uranianos detectados (cuerpos en la misma posición mod 90).
|
||||||
|
/// Vacío sino se activó el módulo Uranian.
|
||||||
|
#[serde(default)]
|
||||||
|
pub uranian_groups: Vec<UranianGroup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Etiqueta legible de un overlay para el footer del canvas. La engine
|
/// Etiqueta legible de un overlay para el footer del canvas. La engine
|
||||||
@@ -87,6 +91,23 @@ pub struct OverlayMeta {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Grupo de cuerpos natales que caen en la misma posición del
|
||||||
|
/// dial uraniano de 90° (su longitud zodiacal módulo 90 es igual o
|
||||||
|
/// muy cercana). En la astrología uraniana esto es una "fórmula" o
|
||||||
|
/// "axis" — los cuerpos están en correspondencia simbólica directa
|
||||||
|
/// porque comparten un cuadrante simétrico.
|
||||||
|
///
|
||||||
|
/// Solo se emiten grupos con 2+ miembros (los singletons no son
|
||||||
|
/// fórmulas). La engine los ordena por proximidad al ε de tolerancia.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UranianGroup {
|
||||||
|
/// Identificadores agnósticos de los cuerpos en el grupo
|
||||||
|
/// (ej. `["sun", "jupiter", "saturn"]`).
|
||||||
|
pub bodies: Vec<String>,
|
||||||
|
/// Posición en el dial de 90° (la longitud módulo 90).
|
||||||
|
pub mod90_deg: f64,
|
||||||
|
}
|
||||||
|
|
||||||
/// Resumen textual de un aspecto para listas legibles. La engine lo
|
/// Resumen textual de un aspecto para listas legibles. La engine lo
|
||||||
/// emite en paralelo con las `LineSeg` de la capa de aspectos, así
|
/// emite en paralelo con las `LineSeg` de la capa de aspectos, así
|
||||||
/// el canvas no tiene que re-derivar nombres de cuerpos desde grados.
|
/// el canvas no tiene que re-derivar nombres de cuerpos desde grados.
|
||||||
@@ -274,6 +295,13 @@ pub enum PipelineRequest {
|
|||||||
Composite {
|
Composite {
|
||||||
partner_chart: Box<Chart>,
|
partner_chart: Box<Chart>,
|
||||||
},
|
},
|
||||||
|
/// `module_id = "uranian"` — calcula los "ejes" del dial uraniano
|
||||||
|
/// de 90°: agrupa los cuerpos natales cuya longitud módulo 90 cae
|
||||||
|
/// dentro de una tolerancia (~2°). El resultado se publica en
|
||||||
|
/// `RenderModel.uranian_groups` para que la UI lo liste como
|
||||||
|
/// fórmulas analíticas. La visualización geométrica completa del
|
||||||
|
/// dial de 90° queda pendiente para una fase posterior.
|
||||||
|
Uranian,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
||||||
@@ -400,6 +428,7 @@ pub fn compute_mock(chart: &Chart) -> RenderModel {
|
|||||||
layers: vec![sign_dial],
|
layers: vec![sign_dial],
|
||||||
overlays: Vec::new(),
|
overlays: Vec::new(),
|
||||||
aspect_summary: Vec::new(),
|
aspect_summary: Vec::new(),
|
||||||
|
uranian_groups: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ impl Registry {
|
|||||||
r.register(Box::new(planetary_return::PlanetaryReturnModule));
|
r.register(Box::new(planetary_return::PlanetaryReturnModule));
|
||||||
r.register(Box::new(midpoints::MidpointsModule));
|
r.register(Box::new(midpoints::MidpointsModule));
|
||||||
r.register(Box::new(composite::CompositeModule));
|
r.register(Box::new(composite::CompositeModule));
|
||||||
|
r.register(Box::new(uranian::UranianModule));
|
||||||
r
|
r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,8 +664,52 @@ mod tests {
|
|||||||
assert!(r.find("planetary_return").is_some());
|
assert!(r.find("planetary_return").is_some());
|
||||||
assert!(r.find("midpoints").is_some());
|
assert!(r.find("midpoints").is_some());
|
||||||
assert!(r.find("composite").is_some());
|
assert!(r.find("composite").is_some());
|
||||||
// Natal kind tiene 8 módulos aplicables.
|
assert!(r.find("uranian").is_some());
|
||||||
assert_eq!(r.for_kind(ChartKind::Natal).len(), 8);
|
// Natal kind tiene 9 módulos aplicables.
|
||||||
|
assert_eq!(r.for_kind(ChartKind::Natal).len(), 9);
|
||||||
assert!(r.for_kind(ChartKind::Synastry).is_empty());
|
assert!(r.for_kind(ChartKind::Synastry).is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// UranianModule — ejes del dial uraniano de 90° (versión textual)
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
pub mod uranian {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Detecta "ejes" del dial uraniano: grupos de cuerpos natales cuya
|
||||||
|
/// longitud módulo 90 cae dentro de una tolerancia. Los grupos
|
||||||
|
/// resultantes se listan en el footer del canvas. La visualización
|
||||||
|
/// geométrica del dial completo de 90° queda para una fase futura.
|
||||||
|
pub struct UranianModule;
|
||||||
|
|
||||||
|
impl Module for UranianModule {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"uranian"
|
||||||
|
}
|
||||||
|
fn label(&self) -> &'static str {
|
||||||
|
"Uraniano (90°)"
|
||||||
|
}
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Ejes del dial uraniano — cuerpos en la misma posición mod 90."
|
||||||
|
}
|
||||||
|
fn applies_to(&self, kind: ChartKind) -> bool {
|
||||||
|
matches!(kind, ChartKind::Natal)
|
||||||
|
}
|
||||||
|
fn enabled_by_default(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn controls(&self) -> Vec<Control> {
|
||||||
|
vec![Control::Toggle {
|
||||||
|
key: "enabled".into(),
|
||||||
|
label: "Activar".into(),
|
||||||
|
default: false,
|
||||||
|
hotkey: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user