feat(tahuantinsuyu): fase 17 — filtros de aspectos + editor + cleanup + labels
Fase completa con 4 mejoras independientes que reusan toda la
infraestructura previa:
## A — Filtros de aspectos en NatalModule
NatalModule gana 3 controles nuevos que SÍ recomponen (a diferencia
de los show_* que solo togglean visibilidad):
- Toggle "Mayores (☌ ☍ △ □ ⚹)" default true
- Toggle "Menores (quincunx, semi-…)" default false
- Slider "Multiplicador de orbe" range 0.25..2.5 step 0.25 default 1.0
Engine API extendida sin romper la existente:
- pub struct NatalOptions { show_majors, show_minors, orb_multiplier }
- pub fn compose_with_options(chart, offset, requests, &NatalOptions)
- compose() queda como wrapper con NatalOptions::default()
- bridge::compose acepta el natal_options, construye OrbTable escalada
(build_orb_table multiplier) y filtra aspects antes de pasarlos a
build_render_model. Build_render_model dejó de filtrar majors
internamente — ahora respeta lo que recibe.
Shell wire:
- build_natal_options() lee aspect_majors/aspect_minors/orb_multiplier
desde module_configs["natal"] con defaults seguros.
- on_panel_event para natal: si key empieza con "show_" → canvas
visibility (sin recompose); otherwise → update module_configs +
persist + render_current.
- render_current pasa natal_options a compose_with_options.
## B — Editor de carta natal existente
- Store::update_chart(id, label, &birth, &config) — actualiza tres
columnas preservando id/contact_id/related/created_at_ms y todo el
module_state asociado (la FK CASCADE no se dispara por UPDATE).
- Tree: Modal::EditChart { id, form, error } reusa ChartForm que ya
manejaba el create. open_edit_chart(id, w, cx) lee la carta con
store.get_chart, pre-carga cada TextInput con el valor existente
(label, birthplace, año, mes, día, hora, min, tz, lat, lon, alt).
submit_modal::EditChart lee form, llama update_chart, preserva el
config existente (zodiac/house_system/bodies no se editan acá).
Menú contextual del chart agrega "Editar…" entre "Abrir" y
"Renombrar".
- render_chart_form ahora toma `title: &str` parameter para que el
modal muestre "Editar carta natal" vs "Nueva carta natal". El
botón cambia "Crear carta" → "Guardar cambios" según el title.
## C — Single source of truth para OUTER_RING_MODULES
- engine exporta `pub const OUTER_RING_MODULES: &[&str] = &["transit",
"synastry", "planetary_return"]`
- shell elimina su const local, importa del engine
- canvas elimina 4 listas hardcodeadas (paint_wheel outer ring active
check + glyphs overlay + aspect_endpoints match) y usa contains() o
early-return sobre el slice. Próximo módulo outer-ring solo necesita
agregarse al const, no buscar copias.
## D — Labels ASC/MC/DESC/IC en el perímetro
Cuatro centered_glyphs en radii.sign_outer * 1.06 (justo afuera del
dial zodiacal, dentro del WHEEL_MARGIN) con color angle_highlight y
font 10px. El ojo identifica los 4 ángulos inmediatamente sin tener
que mapear la línea radial gruesa al ángulo correspondiente.
Las posiciones rotan con la rueda (drag del jog-dial los lleva).
`cargo check` y `cargo test` verdes. La fase agregó 6 controles
visibles al panel del NatalModule (4 view + 2 aspect filter + 1
slider) sin tocar la arquitectura de fases 6-15.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,9 @@ use gpui::{
|
|||||||
use tahuantinsuyu_canvas::{
|
use tahuantinsuyu_canvas::{
|
||||||
AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
|
AstrologyCanvas, CanvasEvent, CanvasMode, ThumbnailItem, ThumbnailScope,
|
||||||
};
|
};
|
||||||
use tahuantinsuyu_engine::{LayerKind, PipelineRequest, compose};
|
use tahuantinsuyu_engine::{
|
||||||
|
LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, compose_with_options,
|
||||||
|
};
|
||||||
use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection};
|
use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection};
|
||||||
use tahuantinsuyu_panel::{ChartOption, ControlPanel, PanelEvent};
|
use tahuantinsuyu_panel::{ChartOption, ControlPanel, PanelEvent};
|
||||||
use tahuantinsuyu_store::Store;
|
use tahuantinsuyu_store::Store;
|
||||||
@@ -287,6 +289,28 @@ impl Shell {
|
|||||||
siblings.into_iter().find(|c| c.id != current.id)
|
siblings.into_iter().find(|c| c.id != current.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deriva las `NatalOptions` activas a partir del `module_configs["natal"]`.
|
||||||
|
/// Si la entry no existe, devuelve defaults (majors=true, minors=false,
|
||||||
|
/// multiplier=1.0).
|
||||||
|
fn build_natal_options(&self) -> NatalOptions {
|
||||||
|
let cfg = self.module_configs.get("natal");
|
||||||
|
let read_bool = |key: &str, default: bool| -> bool {
|
||||||
|
cfg.and_then(|c| c.get(key))
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(default)
|
||||||
|
};
|
||||||
|
let read_f64 = |key: &str, default: f64| -> f64 {
|
||||||
|
cfg.and_then(|c| c.get(key))
|
||||||
|
.and_then(|v| v.as_f64())
|
||||||
|
.unwrap_or(default)
|
||||||
|
};
|
||||||
|
NatalOptions {
|
||||||
|
show_majors: read_bool("aspect_majors", true),
|
||||||
|
show_minors: read_bool("aspect_minors", false),
|
||||||
|
orb_multiplier: read_f64("orb_multiplier", 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Lee `module_state` desde SQLite para la carta dada y los mergea
|
/// Lee `module_state` desde SQLite para la carta dada y los mergea
|
||||||
/// con los defaults ya cargados en `module_configs`. Los valores
|
/// con los defaults ya cargados en `module_configs`. Los valores
|
||||||
/// persistidos ganan sobre los defaults.
|
/// persistidos ganan sobre los defaults.
|
||||||
@@ -406,7 +430,13 @@ impl Shell {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let requests = self.build_requests();
|
let requests = self.build_requests();
|
||||||
let render = match compose(chart, self.current_offset_minutes, &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,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@@ -475,8 +505,9 @@ impl Shell {
|
|||||||
} => {
|
} => {
|
||||||
let bool_val = value.as_bool().unwrap_or(true);
|
let bool_val = value.as_bool().unwrap_or(true);
|
||||||
if module_id == "natal" {
|
if module_id == "natal" {
|
||||||
// Toggles puramente visuales — solo afectan visibility
|
// Distinguimos: show_* = visibility (no recompose),
|
||||||
// del render actual, sin recomponer.
|
// aspect_*/orb_* = filtros de engine (recompose +
|
||||||
|
// persist).
|
||||||
let kind = match key.as_str() {
|
let kind = match key.as_str() {
|
||||||
"show_sign_dial" => Some(LayerKind::SignDial),
|
"show_sign_dial" => Some(LayerKind::SignDial),
|
||||||
"show_houses" => Some(LayerKind::Houses),
|
"show_houses" => Some(LayerKind::Houses),
|
||||||
@@ -487,6 +518,17 @@ impl Shell {
|
|||||||
if let Some(k) = kind {
|
if let Some(k) = kind {
|
||||||
self.canvas
|
self.canvas
|
||||||
.update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx));
|
.update(cx, |c, cx| c.set_layer_visible(k, bool_val, cx));
|
||||||
|
} else {
|
||||||
|
// Filtros: actualizar module_configs + recompose.
|
||||||
|
let entry = self
|
||||||
|
.module_configs
|
||||||
|
.entry("natal".into())
|
||||||
|
.or_insert_with(|| serde_json::json!({}));
|
||||||
|
if let serde_json::Value::Object(map) = entry {
|
||||||
|
map.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
self.persist_module("natal");
|
||||||
|
self.render_current(cx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Cualquier otro módulo: actualizamos su config y
|
// Cualquier otro módulo: actualizamos su config y
|
||||||
@@ -540,9 +582,8 @@ impl Shell {
|
|||||||
// Helpers de module_configs
|
// Helpers de module_configs
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
/// Módulos que pintan en el outer ring del canvas — mutuamente
|
// OUTER_RING_MODULES viene de tahuantinsuyu_engine — single source of
|
||||||
/// excluyentes a nivel de UI. Al prender uno, los otros se apagan.
|
// truth. Shell y canvas leen del mismo slice.
|
||||||
const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"];
|
|
||||||
|
|
||||||
|
|
||||||
/// Etiqueta breve para mostrar al elegir una carta en el picker:
|
/// Etiqueta breve para mostrar al elegir una carta en el picker:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ use gpui::{
|
|||||||
linear_color_stop, linear_gradient, point, prelude::*, px,
|
linear_color_stop, linear_gradient, point, prelude::*, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tahuantinsuyu_engine::{Geometry, Layer, LayerKind, RenderModel};
|
use tahuantinsuyu_engine::{Geometry, Layer, LayerKind, OUTER_RING_MODULES, RenderModel};
|
||||||
use tahuantinsuyu_model::{ChartId, ContactId, GroupId};
|
use tahuantinsuyu_model::{ChartId, ContactId, GroupId};
|
||||||
use tahuantinsuyu_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
|
use tahuantinsuyu_theme::{AspectKind as TAspectKind, AstroPalette, Element, Planet};
|
||||||
use yahweh_theme::Theme;
|
use yahweh_theme::Theme;
|
||||||
@@ -584,9 +584,7 @@ fn render_wheel(
|
|||||||
if visible.get(&LayerKind::Outer).copied().unwrap_or(true) {
|
if visible.get(&LayerKind::Outer).copied().unwrap_or(true) {
|
||||||
for layer in &render.layers {
|
for layer in &render.layers {
|
||||||
if matches!(layer.kind, LayerKind::Outer)
|
if matches!(layer.kind, LayerKind::Outer)
|
||||||
&& (layer.module_id == "transit"
|
&& (OUTER_RING_MODULES.contains(&layer.module_id.as_str()))
|
||||||
|| layer.module_id == "synastry"
|
|
||||||
|| layer.module_id == "planetary_return")
|
|
||||||
{
|
{
|
||||||
for g in &layer.glyphs {
|
for g in &layer.glyphs {
|
||||||
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, radii.transits);
|
let (x, y) = polar_to_screen(g.deg, asc, rot_offset, radii.transits);
|
||||||
@@ -609,6 +607,29 @@ fn render_wheel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Labels ASC/MC/DESC/IC en el perímetro. Texto pequeño en el
|
||||||
|
// margen exterior (radius * 1.05) para que no se monte con los
|
||||||
|
// glifos de los signos. Color angle_highlight para que el ojo los
|
||||||
|
// reconozca como los cuatro ángulos cardinales.
|
||||||
|
let angle_labels = [
|
||||||
|
(asc, "ASC"),
|
||||||
|
(render.midheaven_deg, "MC"),
|
||||||
|
(render.descendant_deg, "DESC"),
|
||||||
|
(render.imum_coeli_deg, "IC"),
|
||||||
|
];
|
||||||
|
let label_r = r_outer * 1.06;
|
||||||
|
for (deg, label) in angle_labels {
|
||||||
|
let (x, y) = polar_to_screen(deg, asc, rot_offset, label_r);
|
||||||
|
wheel = wheel.child(centered_glyph(
|
||||||
|
cx_center + x,
|
||||||
|
cy_center + y,
|
||||||
|
32.0,
|
||||||
|
10.0,
|
||||||
|
label.into(),
|
||||||
|
palette.angle_highlight,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Header + footer + indicador de tiempo ---
|
// --- Header + footer + indicador de tiempo ---
|
||||||
let header = div()
|
let header = div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -789,12 +810,14 @@ impl Radii {
|
|||||||
|
|
||||||
/// Resuelve qué radios corresponden a una capa de aspectos según el
|
/// Resuelve qué radios corresponden a una capa de aspectos según el
|
||||||
/// `module_id`: natal-natal en `aspects`, cross con cada overlay
|
/// `module_id`: natal-natal en `aspects`, cross con cada overlay
|
||||||
/// desde `bodies` (extremo natal) al ring del módulo. Synastry y
|
/// desde `bodies` (extremo natal) al ring del módulo. Los módulos
|
||||||
/// Planetary Return comparten el outer ring de tránsito (los tres
|
/// del outer ring (OUTER_RING_MODULES) comparten el slot de
|
||||||
/// son mutuamente excluyentes a nivel de Shell).
|
/// tránsito (son mutuamente excluyentes a nivel de Shell).
|
||||||
fn aspect_endpoints(&self, module_id: &str) -> (f32, f32) {
|
fn aspect_endpoints(&self, module_id: &str) -> (f32, f32) {
|
||||||
|
if OUTER_RING_MODULES.contains(&module_id) {
|
||||||
|
return (self.bodies, self.transits);
|
||||||
|
}
|
||||||
match module_id {
|
match module_id {
|
||||||
"transit" | "synastry" | "planetary_return" => (self.bodies, self.transits),
|
|
||||||
"progression" => (self.bodies, self.progression),
|
"progression" => (self.bodies, self.progression),
|
||||||
"solar_arc" => (self.bodies, self.solar_arc),
|
"solar_arc" => (self.bodies, self.solar_arc),
|
||||||
_ => (self.aspects, self.aspects),
|
_ => (self.aspects, self.aspects),
|
||||||
@@ -1012,9 +1035,7 @@ fn paint_wheel(
|
|||||||
// si alguno de los dos está prendido, pintamos el slot.
|
// si alguno de los dos está prendido, pintamos el slot.
|
||||||
let outer_active = layers.iter().any(|l| {
|
let outer_active = layers.iter().any(|l| {
|
||||||
matches!(l.kind, LayerKind::Outer)
|
matches!(l.kind, LayerKind::Outer)
|
||||||
&& (l.module_id == "transit"
|
&& OUTER_RING_MODULES.contains(&l.module_id.as_str())
|
||||||
|| l.module_id == "synastry"
|
|
||||||
|| l.module_id == "planetary_return")
|
|
||||||
});
|
});
|
||||||
if outer_active && show(LayerKind::Outer) {
|
if outer_active && show(LayerKind::Outer) {
|
||||||
stroke_circle(
|
stroke_circle(
|
||||||
@@ -1037,9 +1058,7 @@ fn paint_wheel(
|
|||||||
let dot_r = (radii.sign_outer * 0.017).max(2.0);
|
let dot_r = (radii.sign_outer * 0.017).max(2.0);
|
||||||
for layer in layers {
|
for layer in layers {
|
||||||
if matches!(layer.kind, LayerKind::Outer)
|
if matches!(layer.kind, LayerKind::Outer)
|
||||||
&& (layer.module_id == "transit"
|
&& (OUTER_RING_MODULES.contains(&layer.module_id.as_str()))
|
||||||
|| layer.module_id == "synastry"
|
|
||||||
|| layer.module_id == "planetary_return")
|
|
||||||
{
|
{
|
||||||
for g in &layer.glyphs {
|
for g in &layer.glyphs {
|
||||||
let color = with_alpha(planet_color(palette, &g.symbol), 0.85);
|
let color = with_alpha(planet_color(palette, &g.symbol), 0.85);
|
||||||
|
|||||||
@@ -244,10 +244,20 @@ pub fn compose(
|
|||||||
chart: &Chart,
|
chart: &Chart,
|
||||||
offset_minutes: i64,
|
offset_minutes: i64,
|
||||||
requests: &[crate::PipelineRequest],
|
requests: &[crate::PipelineRequest],
|
||||||
|
natal_options: &crate::NatalOptions,
|
||||||
) -> Result<RenderModel, EngineError> {
|
) -> Result<RenderModel, EngineError> {
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?;
|
let (natal, config_e, observer) = compute_natal_chart(chart, offset_minutes)?;
|
||||||
let aspects = find_aspects(&natal, &OrbTable::modern_western());
|
let orb_table = build_orb_table(natal_options.orb_multiplier);
|
||||||
|
let all_aspects = find_aspects(&natal, &orb_table);
|
||||||
|
let aspects: Vec<Aspect> = all_aspects
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| {
|
||||||
|
let is_major = EAspectKind::MAJORS.contains(&a.kind);
|
||||||
|
(is_major && natal_options.show_majors)
|
||||||
|
|| (!is_major && natal_options.show_minors)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let mut render = build_render_model(chart, &natal, &aspects, t0);
|
let mut render = build_render_model(chart, &natal, &aspects, t0);
|
||||||
|
|
||||||
for req in requests {
|
for req in requests {
|
||||||
@@ -776,13 +786,10 @@ fn build_render_model(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ─── Capa 3: Aspects ──────────────────────────────────────────────
|
// ─── Capa 3: Aspects ──────────────────────────────────────────────
|
||||||
|
// Los aspects ya vienen filtrados por NatalOptions (majors / minors)
|
||||||
|
// desde compose(). Acá solo mapeamos a LineSeg.
|
||||||
let mut aspect_lines: Vec<LineSeg> = Vec::with_capacity(aspects.len());
|
let mut aspect_lines: Vec<LineSeg> = Vec::with_capacity(aspects.len());
|
||||||
for a in aspects {
|
for a in aspects {
|
||||||
// Solo los aspectos mayores se pintan en este pase — los menores
|
|
||||||
// saturan visualmente. Fase 4 pondrá un toggle para mostrarlos.
|
|
||||||
if !EAspectKind::MAJORS.contains(&a.kind) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let pa = natal.placement(a.a);
|
let pa = natal.placement(a.a);
|
||||||
let pb = natal.placement(a.b);
|
let pb = natal.placement(a.b);
|
||||||
if let (Some(pa), Some(pb)) = (pa, pb) {
|
if let (Some(pa), Some(pb)) = (pa, pb) {
|
||||||
@@ -834,6 +841,27 @@ fn build_render_model(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construye una `OrbTable` con los orbes default de `modern_western`
|
||||||
|
/// escalados por `multiplier`. Necesario porque eternal expone
|
||||||
|
/// `set_orb` pero no permite iterar los base orbs internos.
|
||||||
|
fn build_orb_table(multiplier: f64) -> OrbTable {
|
||||||
|
let mut t = OrbTable::modern_western();
|
||||||
|
let m = multiplier.max(0.0);
|
||||||
|
t.set_orb(EAspectKind::Conjunction, 8.0 * m);
|
||||||
|
t.set_orb(EAspectKind::Opposition, 8.0 * m);
|
||||||
|
t.set_orb(EAspectKind::Trine, 7.0 * m);
|
||||||
|
t.set_orb(EAspectKind::Square, 7.0 * m);
|
||||||
|
t.set_orb(EAspectKind::Sextile, 5.0 * m);
|
||||||
|
t.set_orb(EAspectKind::Quincunx, 2.5 * m);
|
||||||
|
t.set_orb(EAspectKind::SemiSextile, 2.0 * m);
|
||||||
|
t.set_orb(EAspectKind::SemiSquare, 2.0 * m);
|
||||||
|
t.set_orb(EAspectKind::Sesquiquadrate, 2.0 * m);
|
||||||
|
t.set_orb(EAspectKind::Quintile, 1.5 * m);
|
||||||
|
t.set_orb(EAspectKind::BiQuintile, 1.5 * m);
|
||||||
|
t.set_orb(EAspectKind::Septile, 1.5 * m);
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
fn push_overlay_meta(render: &mut RenderModel, module_id: &str, label: String) {
|
fn push_overlay_meta(render: &mut RenderModel, module_id: &str, label: String) {
|
||||||
render.overlays.push(OverlayMeta {
|
render.overlays.push(OverlayMeta {
|
||||||
module_id: module_id.to_string(),
|
module_id: module_id.to_string(),
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ pub enum EngineError {
|
|||||||
/// `tahuantinsuyu-modules` por id string. Esto deja la engine como
|
/// `tahuantinsuyu-modules` por id string. Esto deja la engine como
|
||||||
/// dueña única del cómputo (no depende del trait Module — los módulos
|
/// dueña única del cómputo (no depende del trait Module — los módulos
|
||||||
/// son sólo metadata + UI controls).
|
/// son sólo metadata + UI controls).
|
||||||
|
/// Módulos overlay que pintan en el mismo slot (outer ring del wheel)
|
||||||
|
/// y por lo tanto son **mutuamente excluyentes** a nivel de UI: al
|
||||||
|
/// prender uno, el shell debe apagar los otros. Single source of truth
|
||||||
|
/// — el shell y el canvas leen de acá en vez de hardcodear listas.
|
||||||
|
pub const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"];
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PipelineRequest {
|
pub enum PipelineRequest {
|
||||||
/// `module_id = "transit"` — anillo externo con planetas al
|
/// `module_id = "transit"` — anillo externo con planetas al
|
||||||
@@ -220,21 +226,54 @@ pub enum PipelineRequest {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
|
||||||
|
/// multiplicador de orbe usar). Es independiente de los overlays.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NatalOptions {
|
||||||
|
/// Incluir aspectos mayores (conj/opp/trine/square/sextile).
|
||||||
|
pub show_majors: bool,
|
||||||
|
/// Incluir aspectos menores (quincunx/semi-sextile/etc).
|
||||||
|
pub show_minors: bool,
|
||||||
|
/// Multiplicador uniforme sobre los orbes default. `1.0` = orbes
|
||||||
|
/// modern_western; `0.5` = tight; `2.0` = wide.
|
||||||
|
pub orb_multiplier: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NatalOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
show_majors: true,
|
||||||
|
show_minors: false,
|
||||||
|
orb_multiplier: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Composición canónica: carta natal + todos los overlays pedidos.
|
/// Composición canónica: carta natal + todos los overlays pedidos.
|
||||||
/// Es la única función que el Shell necesita llamar — `compute_at_offset`
|
/// Equivalente a `compose_with_options` con `NatalOptions::default()`.
|
||||||
/// y `compute_with_transits_at_now` quedan como atajos retrocompatibles.
|
|
||||||
pub fn compose(
|
pub fn compose(
|
||||||
chart: &Chart,
|
chart: &Chart,
|
||||||
offset_minutes: i64,
|
offset_minutes: i64,
|
||||||
requests: &[PipelineRequest],
|
requests: &[PipelineRequest],
|
||||||
|
) -> Result<RenderModel, EngineError> {
|
||||||
|
compose_with_options(chart, offset_minutes, requests, &NatalOptions::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variante que permite controlar qué aspectos natales se computan y
|
||||||
|
/// con qué multiplicador de orbe.
|
||||||
|
pub fn compose_with_options(
|
||||||
|
chart: &Chart,
|
||||||
|
offset_minutes: i64,
|
||||||
|
requests: &[PipelineRequest],
|
||||||
|
natal_options: &NatalOptions,
|
||||||
) -> Result<RenderModel, EngineError> {
|
) -> Result<RenderModel, EngineError> {
|
||||||
#[cfg(feature = "eternal-bridge")]
|
#[cfg(feature = "eternal-bridge")]
|
||||||
{
|
{
|
||||||
bridge::compose(chart, offset_minutes, requests)
|
bridge::compose(chart, offset_minutes, requests, natal_options)
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "eternal-bridge"))]
|
#[cfg(not(feature = "eternal-bridge"))]
|
||||||
{
|
{
|
||||||
let _ = (offset_minutes, requests);
|
let _ = (offset_minutes, requests, natal_options);
|
||||||
Ok(compute_mock(chart))
|
Ok(compute_mock(chart))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,28 @@ pub mod natal {
|
|||||||
default: true,
|
default: true,
|
||||||
hotkey: Some("P".into()),
|
hotkey: Some("P".into()),
|
||||||
},
|
},
|
||||||
|
// Filtros de aspectos: cambian QUÉ se computa, no QUÉ
|
||||||
|
// se pinta del render. Recompose al togglear.
|
||||||
|
Control::Toggle {
|
||||||
|
key: "aspect_majors".into(),
|
||||||
|
label: "Mayores (☌ ☍ △ □ ⚹)".into(),
|
||||||
|
default: true,
|
||||||
|
hotkey: None,
|
||||||
|
},
|
||||||
|
Control::Toggle {
|
||||||
|
key: "aspect_minors".into(),
|
||||||
|
label: "Menores (quincunx, semi-…)".into(),
|
||||||
|
default: false,
|
||||||
|
hotkey: None,
|
||||||
|
},
|
||||||
|
Control::Slider {
|
||||||
|
key: "orb_multiplier".into(),
|
||||||
|
label: "Multiplicador de orbe".into(),
|
||||||
|
min: 0.25,
|
||||||
|
max: 2.5,
|
||||||
|
step: 0.25,
|
||||||
|
default: 1.0,
|
||||||
|
},
|
||||||
Control::Slider {
|
Control::Slider {
|
||||||
key: "harmonic".into(),
|
key: "harmonic".into(),
|
||||||
label: "Armónico".into(),
|
label: "Armónico".into(),
|
||||||
|
|||||||
@@ -343,6 +343,31 @@ impl Store {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reemplaza label + birth_data + config de una carta existente,
|
||||||
|
/// preservando id / contact_id / related_chart_id / created_at_ms y
|
||||||
|
/// el `module_state` asociado (no se borra). Usado por el editor de
|
||||||
|
/// rectificación natal.
|
||||||
|
pub fn update_chart(
|
||||||
|
&self,
|
||||||
|
id: ChartId,
|
||||||
|
label: &str,
|
||||||
|
birth: &StoredBirthData,
|
||||||
|
config: &StoredChartConfig,
|
||||||
|
) -> StoreResult<()> {
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE charts SET label = ?2, birth_data_json = ?3, config_json = ?4 \
|
||||||
|
WHERE id = ?1",
|
||||||
|
params![
|
||||||
|
id.to_string(),
|
||||||
|
label,
|
||||||
|
serde_json::to_string(birth)?,
|
||||||
|
serde_json::to_string(config)?,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// Module state
|
// Module state
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
|
|||||||
@@ -112,6 +112,15 @@ enum Modal {
|
|||||||
form: ChartForm,
|
form: ChartForm,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
},
|
},
|
||||||
|
/// Editar una carta existente — reusa `ChartForm` pre-cargada.
|
||||||
|
/// El submit llama `store.update_chart(id, ...)` preservando
|
||||||
|
/// `chart.contact_id`, `related_chart_id`, `module_state` y el
|
||||||
|
/// historial.
|
||||||
|
EditChart {
|
||||||
|
id: ChartId,
|
||||||
|
form: ChartForm,
|
||||||
|
error: Option<SharedString>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChartForm {
|
struct ChartForm {
|
||||||
@@ -309,6 +318,58 @@ impl TahuantinsuyuTree {
|
|||||||
self.close_menu(cx);
|
self.close_menu(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_edit_chart(
|
||||||
|
&mut self,
|
||||||
|
id: ChartId,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
// Cargar la carta existente; si no se puede, fallamos en silencio.
|
||||||
|
let chart = match self.store.get_chart(id) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[tree] open_edit_chart {}: {}", id, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let bd = &chart.birth_data;
|
||||||
|
let form = ChartForm {
|
||||||
|
name: self.make_input("Etiqueta de la carta", &chart.label, window, cx),
|
||||||
|
place: self.make_input(
|
||||||
|
"Lugar (ciudad, país)",
|
||||||
|
bd.birthplace_label.as_deref().unwrap_or(""),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
year: self.make_input("Año", &bd.year.to_string(), window, cx),
|
||||||
|
month: self.make_input("Mes", &bd.month.to_string(), window, cx),
|
||||||
|
day: self.make_input("Día", &bd.day.to_string(), window, cx),
|
||||||
|
hour: self.make_input("Hora (0-23)", &bd.hour.to_string(), window, cx),
|
||||||
|
minute: self.make_input("Minuto", &bd.minute.to_string(), window, cx),
|
||||||
|
tz_offset_min: self.make_input(
|
||||||
|
"TZ offset (min)",
|
||||||
|
&bd.tz_offset_minutes.to_string(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
lat: self.make_input("Latitud (°)", &format!("{}", bd.latitude_deg), window, cx),
|
||||||
|
lon: self.make_input(
|
||||||
|
"Longitud (°)",
|
||||||
|
&format!("{}", bd.longitude_deg),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
alt: self.make_input("Altitud (m)", &format!("{}", bd.altitude_m), window, cx),
|
||||||
|
};
|
||||||
|
form.name.update(cx, |i, _| i.request_focus(window));
|
||||||
|
self.modal = Some(Modal::EditChart {
|
||||||
|
id,
|
||||||
|
form,
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
self.close_menu(cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn open_create_chart(
|
fn open_create_chart(
|
||||||
&mut self,
|
&mut self,
|
||||||
contact: ContactId,
|
contact: ContactId,
|
||||||
@@ -521,6 +582,56 @@ impl TahuantinsuyuTree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Modal::EditChart {
|
||||||
|
id,
|
||||||
|
form,
|
||||||
|
error: _,
|
||||||
|
} => {
|
||||||
|
let _ = value;
|
||||||
|
// Para preservar el ChartConfig original (zodiac, house
|
||||||
|
// system, bodies, etc.) leemos la carta actual y solo
|
||||||
|
// sobrescribimos label + birth_data. El editor no toca
|
||||||
|
// config — eso se haría en un futuro panel de "Config
|
||||||
|
// de carta".
|
||||||
|
let existing = match self.store.get_chart(id) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
self.modal = Some(Modal::EditChart {
|
||||||
|
id,
|
||||||
|
form,
|
||||||
|
error: Some(SharedString::from(format!("Store: {}", e))),
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match build_chart_from_form(&form, cx) {
|
||||||
|
Ok((birth, label)) => {
|
||||||
|
match self.store.update_chart(id, &label, &birth, &existing.config) {
|
||||||
|
Ok(_) => {
|
||||||
|
drop(form);
|
||||||
|
self.after_mutation(cx);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
self.modal = Some(Modal::EditChart {
|
||||||
|
id,
|
||||||
|
form,
|
||||||
|
error: Some(SharedString::from(format!("Store: {}", e))),
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(msg) => {
|
||||||
|
self.modal = Some(Modal::EditChart {
|
||||||
|
id,
|
||||||
|
form,
|
||||||
|
error: Some(SharedString::from(msg)),
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,6 +940,11 @@ impl TahuantinsuyuTree {
|
|||||||
this.close_menu(cx);
|
this.close_menu(cx);
|
||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
items = items.child(menu_item("tts-menu-edit-h", "Editar…", theme).on_click(
|
||||||
|
cx.listener(move |this, _: &ClickEvent, w, cx| {
|
||||||
|
this.open_edit_chart(id, w, cx);
|
||||||
|
}),
|
||||||
|
));
|
||||||
items = items.child(separator(theme));
|
items = items.child(separator(theme));
|
||||||
let t = menu.target.clone();
|
let t = menu.target.clone();
|
||||||
items = items.child(menu_item("tts-menu-rename-h", "Renombrar…", theme).on_click(
|
items = items.child(menu_item("tts-menu-rename-h", "Renombrar…", theme).on_click(
|
||||||
@@ -869,7 +985,12 @@ impl TahuantinsuyuTree {
|
|||||||
input.clone(),
|
input.clone(),
|
||||||
"Enter = crear — Escape = cancelar",
|
"Enter = crear — Escape = cancelar",
|
||||||
),
|
),
|
||||||
Modal::CreateChart { form, error, .. } => render_chart_form(theme, form, error.clone(), cx),
|
Modal::CreateChart { form, error, .. } => {
|
||||||
|
render_chart_form(theme, "Nueva carta natal", form, error.clone(), cx)
|
||||||
|
}
|
||||||
|
Modal::EditChart { form, error, .. } => {
|
||||||
|
render_chart_form(theme, "Editar carta natal", form, error.clone(), cx)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
@@ -945,6 +1066,7 @@ fn modal_box(
|
|||||||
|
|
||||||
fn render_chart_form(
|
fn render_chart_form(
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
|
title: &str,
|
||||||
form: &ChartForm,
|
form: &ChartForm,
|
||||||
error: Option<SharedString>,
|
error: Option<SharedString>,
|
||||||
cx: &mut Context<TahuantinsuyuTree>,
|
cx: &mut Context<TahuantinsuyuTree>,
|
||||||
@@ -991,11 +1113,15 @@ fn render_chart_form(
|
|||||||
.hover(|s| s.bg(theme.bg_button_hover()))
|
.hover(|s| s.bg(theme.bg_button_hover()))
|
||||||
.text_size(px(12.0))
|
.text_size(px(12.0))
|
||||||
.text_color(theme.fg_text)
|
.text_color(theme.fg_text)
|
||||||
.child("Crear carta")
|
.child(SharedString::from(if title.starts_with("Editar") {
|
||||||
|
"Guardar cambios"
|
||||||
|
} else {
|
||||||
|
"Crear carta"
|
||||||
|
}))
|
||||||
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
|
.on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||||
// Disparamos un submit "vacío" — el handler de submit
|
// Disparamos un submit "vacío" — el handler de submit
|
||||||
// re-lee todos los campos del form. El value que pasamos
|
// re-lee todos los campos del form. El value que pasamos
|
||||||
// se ignora dentro del branch CreateChart.
|
// se ignora dentro del branch CreateChart/EditChart.
|
||||||
this.submit_modal(String::new(), cx);
|
this.submit_modal(String::new(), cx);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1027,7 +1153,7 @@ fn render_chart_form(
|
|||||||
div()
|
div()
|
||||||
.text_size(px(14.0))
|
.text_size(px(14.0))
|
||||||
.text_color(theme.fg_text)
|
.text_color(theme.fg_text)
|
||||||
.child("Nueva carta natal"),
|
.child(SharedString::from(title.to_string())),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
Reference in New Issue
Block a user