feat(cosmobiologia): corpus — las tajadas sobre la rueda
La interpretación por dominio, integrada al lienzo 2D. Tres botones (Vital / Social / Psíquico, o tecla I para ciclar): al elegir una tajada, la rueda resalta con un anillo los cuerpos de ese dominio y un panel a la derecha lista los pasajes citados —combinación, texto, fuente—, o avisa de los huecos sin texto. - El canvas carga el corpus al arrancar: corpus.ron del directorio de datos del usuario, o la plantilla ejemplo.ron embebida como fallback. - El JOIN corre con corpus_inputs (engine) + interpretar_por_dominio: cada longitud → signo, cada casa → su tajada, los aspectos puentean. - El resalte es una capa transparente sobre la rueda, sin tocar el render del wheel. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -41,8 +41,8 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use cosmobiologia_engine::{
|
use cosmobiologia_engine::{
|
||||||
Geometry, GrTrigger, Layer, LayerKind, Rectificacion, RenderModel, UranianGroup,
|
corpus_inputs, Corpus, Dominio, Geometry, GrTrigger, Layer, LayerKind, Pasaje,
|
||||||
OUTER_RING_MODULES,
|
Rectificacion, RenderModel, UranianGroup, OUTER_RING_MODULES,
|
||||||
};
|
};
|
||||||
use cosmobiologia_model::{ChartId, ContactId, GroupId};
|
use cosmobiologia_model::{ChartId, ContactId, GroupId};
|
||||||
use cosmobiologia_render::{compose_sphere, DrawCommand, Palette, SphereOpts, SphereView};
|
use cosmobiologia_render::{compose_sphere, DrawCommand, Palette, SphereOpts, SphereView};
|
||||||
@@ -173,6 +173,12 @@ pub struct CanvasState {
|
|||||||
pub sphere_view: SphereView,
|
pub sphere_view: SphereView,
|
||||||
/// Si se dibujan las figuras de constelaciones en la esfera 3D.
|
/// Si se dibujan las figuras de constelaciones en la esfera 3D.
|
||||||
pub show_constellations: bool,
|
pub show_constellations: bool,
|
||||||
|
/// El corpus de interpretación cargado (corpus.ron del usuario, o la
|
||||||
|
/// plantilla de ejemplo). `None` si no se pudo cargar ninguno.
|
||||||
|
pub corpus: Option<Corpus>,
|
||||||
|
/// Tajada vivencial activa — al elegir una, la rueda resalta sus
|
||||||
|
/// cuerpos y al costado se listan sus pasajes. `None` = apagado.
|
||||||
|
pub corpus_domain: Option<Dominio>,
|
||||||
drag_jog: Option<JogDragState>,
|
drag_jog: Option<JogDragState>,
|
||||||
drag_pan: Option<PanDragState>,
|
drag_pan: Option<PanDragState>,
|
||||||
drag_sphere: Option<SphereDragState>,
|
drag_sphere: Option<SphereDragState>,
|
||||||
@@ -261,6 +267,8 @@ impl Default for CanvasState {
|
|||||||
sphere_3d: false,
|
sphere_3d: false,
|
||||||
sphere_view: SphereView::default(),
|
sphere_view: SphereView::default(),
|
||||||
show_constellations: true,
|
show_constellations: true,
|
||||||
|
corpus: None,
|
||||||
|
corpus_domain: None,
|
||||||
drag_jog: None,
|
drag_jog: None,
|
||||||
drag_pan: None,
|
drag_pan: None,
|
||||||
drag_sphere: None,
|
drag_sphere: None,
|
||||||
@@ -309,12 +317,35 @@ impl Focusable for AstrologyCanvas {
|
|||||||
impl AstrologyCanvas {
|
impl AstrologyCanvas {
|
||||||
pub fn new(cx: &mut Context<'_, Self>) -> Self {
|
pub fn new(cx: &mut Context<'_, Self>) -> Self {
|
||||||
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
|
||||||
|
let mut state = CanvasState::default();
|
||||||
|
state.corpus = load_corpus();
|
||||||
Self {
|
Self {
|
||||||
state: CanvasState::default(),
|
state,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Elige (o apaga, si ya estaba) una tajada vivencial del corpus.
|
||||||
|
pub fn toggle_corpus_domain(&mut self, dom: Dominio, cx: &mut Context<'_, Self>) {
|
||||||
|
self.state.corpus_domain = if self.state.corpus_domain == Some(dom) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(dom)
|
||||||
|
};
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cicla la tajada activa: apagado → Vital → Social → Psíquico → …
|
||||||
|
pub fn cycle_corpus_domain(&mut self, cx: &mut Context<'_, Self>) {
|
||||||
|
self.state.corpus_domain = match self.state.corpus_domain {
|
||||||
|
None => Some(Dominio::Vital),
|
||||||
|
Some(Dominio::Vital) => Some(Dominio::Social),
|
||||||
|
Some(Dominio::Social) => Some(Dominio::Psiquico),
|
||||||
|
Some(Dominio::Psiquico) => None,
|
||||||
|
};
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn state(&self) -> &CanvasState {
|
pub fn state(&self) -> &CanvasState {
|
||||||
&self.state
|
&self.state
|
||||||
}
|
}
|
||||||
@@ -834,6 +865,10 @@ impl AstrologyCanvas {
|
|||||||
self.toggle_constellations(cx);
|
self.toggle_constellations(cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
"i" | "I" => {
|
||||||
|
self.cycle_corpus_domain(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
self.toggle_layer(kind, cx);
|
self.toggle_layer(kind, cx);
|
||||||
@@ -910,6 +945,143 @@ fn effective_r_outer(bounds: Bounds<Pixels>) -> f32 {
|
|||||||
(bw - WHEEL_MARGIN * scale * 2.0) / 2.0
|
(bw - WHEEL_MARGIN * scale * 2.0) / 2.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =====================================================================
|
||||||
|
// Corpus de interpretación — tajadas sobre la rueda
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
/// Carga el corpus: primero el `corpus.ron` del usuario en el
|
||||||
|
/// directorio de datos; si no existe o falla, la plantilla `ejemplo.ron`
|
||||||
|
/// embebida (así siempre hay algo que mostrar).
|
||||||
|
fn load_corpus() -> Option<Corpus> {
|
||||||
|
let base = std::env::var("XDG_DATA_HOME")
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
format!("{}/.local/share", std::env::var("HOME").unwrap_or_default())
|
||||||
|
});
|
||||||
|
let path = format!("{base}/cosmobiologia/corpus.ron");
|
||||||
|
if let Ok(txt) = std::fs::read_to_string(&path) {
|
||||||
|
match Corpus::desde_ron(&txt) {
|
||||||
|
Ok(c) => return Some(c),
|
||||||
|
Err(e) => eprintln!("[canvas] {path}: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Corpus::desde_ron(include_str!("../../cosmobiologia-corpus/ejemplo.ron")).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color de cada tajada vivencial.
|
||||||
|
fn domain_color(d: Dominio) -> Hsla {
|
||||||
|
match d {
|
||||||
|
Dominio::Vital => hsla(14.0 / 360.0, 0.72, 0.58, 1.0),
|
||||||
|
Dominio::Social => hsla(208.0 / 360.0, 0.62, 0.60, 1.0),
|
||||||
|
Dominio::Psiquico => hsla(280.0 / 360.0, 0.52, 0.64, 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn domain_label(d: Dominio) -> &'static str {
|
||||||
|
match d {
|
||||||
|
Dominio::Vital => "Vital",
|
||||||
|
Dominio::Social => "Social",
|
||||||
|
Dominio::Psiquico => "Psíquico",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capa transparente sobre la rueda que dibuja un anillo de resalte en
|
||||||
|
/// cada cuerpo de la tajada activa.
|
||||||
|
fn corpus_highlight_canvas(degs: Vec<f32>, asc: f32, rot: f32, color: Hsla) -> impl IntoElement {
|
||||||
|
canvas(
|
||||||
|
|_b, _w, _cx| (),
|
||||||
|
move |bounds: Bounds<Pixels>, _, window, _| {
|
||||||
|
let (cx, cy) = bounds_center(bounds);
|
||||||
|
let radii = Radii::from_outer(effective_r_outer(bounds));
|
||||||
|
for deg in °s {
|
||||||
|
let (x, y) = polar_to_screen(*deg, asc, rot, radii.bodies);
|
||||||
|
stroke_circle(window, cx + x, cy + y, 19.0, 2.2, color);
|
||||||
|
stroke_circle(window, cx + x, cy + y, 24.0, 1.0, with_alpha(color, 0.35));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.absolute()
|
||||||
|
.size_full()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El panel lateral con los pasajes de la tajada activa.
|
||||||
|
fn corpus_panel(theme: &Theme, dom: Dominio, pasajes: &[&Pasaje]) -> impl IntoElement {
|
||||||
|
let mut list = div()
|
||||||
|
.id("corpus-panel")
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.h_full()
|
||||||
|
.w(px(340.0))
|
||||||
|
.p(px(12.0))
|
||||||
|
.bg(theme.bg_panel_alt.clone())
|
||||||
|
.border_l_1()
|
||||||
|
.border_color(theme.border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(6.0))
|
||||||
|
.child(div().w(px(10.0)).h(px(10.0)).rounded_full().bg(domain_color(dom)))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"Tajada {} · {} pasajes",
|
||||||
|
domain_label(dom),
|
||||||
|
pasajes.len()
|
||||||
|
))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if pasajes.is_empty() {
|
||||||
|
list = list.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(
|
||||||
|
"Sin pasajes para esta tajada todavía. Escribilos en \
|
||||||
|
corpus.ron — ver la GUIA del corpus.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for p in pasajes {
|
||||||
|
list = list.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(3.0))
|
||||||
|
.p(px(8.0))
|
||||||
|
.rounded(px(5.0))
|
||||||
|
.bg(theme.bg_panel.clone())
|
||||||
|
.border_1()
|
||||||
|
.border_color(theme.border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(9.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(p.combinacion.to_string())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(SharedString::from(p.texto.clone())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(9.0))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!("— {}", p.fuente))),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Render
|
// Render
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
@@ -921,6 +1093,34 @@ impl Render for AstrologyCanvas {
|
|||||||
let entity = cx.entity();
|
let entity = cx.entity();
|
||||||
let focus = self.focus_handle.clone();
|
let focus = self.focus_handle.clone();
|
||||||
|
|
||||||
|
// Vista del corpus: con una tajada elegida sobre la rueda 2D, se
|
||||||
|
// calcula la interpretación por dominio (pasajes + cuerpos a
|
||||||
|
// resaltar). Los `&Pasaje` toman prestado de `state.corpus`.
|
||||||
|
let corpus_view: Option<(Dominio, Vec<&Pasaje>, Vec<f32>)> =
|
||||||
|
match &self.state.mode {
|
||||||
|
CanvasMode::Wheel { render } if !self.state.sphere_3d => {
|
||||||
|
self.state.corpus_domain.and_then(|dom| {
|
||||||
|
let corpus = self.state.corpus.as_ref()?;
|
||||||
|
let (col, asp) = corpus_inputs(render);
|
||||||
|
let por_dom = corpus.interpretar_por_dominio(&col, &asp);
|
||||||
|
let pasajes = por_dom.get(&dom).cloned().unwrap_or_default();
|
||||||
|
let degs: Vec<f32> = render
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.filter(|l| {
|
||||||
|
matches!(l.kind, LayerKind::Bodies)
|
||||||
|
&& l.module_id == "natal"
|
||||||
|
})
|
||||||
|
.flat_map(|l| l.glyphs.iter())
|
||||||
|
.filter(|g| g.house.and_then(Dominio::de_casa) == Some(dom))
|
||||||
|
.map(|g| g.deg)
|
||||||
|
.collect();
|
||||||
|
Some((dom, pasajes, degs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
let body = match &self.state.mode {
|
let body = match &self.state.mode {
|
||||||
CanvasMode::Empty => render_empty(&theme),
|
CanvasMode::Empty => render_empty(&theme),
|
||||||
CanvasMode::Wheel { render } if self.state.sphere_3d => render_sphere(
|
CanvasMode::Wheel { render } if self.state.sphere_3d => render_sphere(
|
||||||
@@ -933,7 +1133,8 @@ impl Render for AstrologyCanvas {
|
|||||||
self.state.view_pan_y,
|
self.state.view_pan_y,
|
||||||
entity,
|
entity,
|
||||||
),
|
),
|
||||||
CanvasMode::Wheel { render } => render_wheel(
|
CanvasMode::Wheel { render } => {
|
||||||
|
let wheel = render_wheel(
|
||||||
&theme,
|
&theme,
|
||||||
&palette,
|
&palette,
|
||||||
render,
|
render,
|
||||||
@@ -947,10 +1148,25 @@ impl Render for AstrologyCanvas {
|
|||||||
self.state.hover.as_ref(),
|
self.state.hover.as_ref(),
|
||||||
self.state.rectificacion.as_ref(),
|
self.state.rectificacion.as_ref(),
|
||||||
entity,
|
entity,
|
||||||
),
|
);
|
||||||
|
// Capa de resalte de la tajada activa, encima de la rueda.
|
||||||
|
match &corpus_view {
|
||||||
|
Some((dom, _, degs)) => wheel.child(corpus_highlight_canvas(
|
||||||
|
degs.clone(),
|
||||||
|
render.ascendant_deg,
|
||||||
|
self.state.view_rotation_deg,
|
||||||
|
domain_color(*dom),
|
||||||
|
)),
|
||||||
|
None => wheel,
|
||||||
|
}
|
||||||
|
}
|
||||||
CanvasMode::Thumbnails { items, .. } => render_thumbnails(&theme, items),
|
CanvasMode::Thumbnails { items, .. } => render_thumbnails(&theme, items),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let corpus_side = corpus_view
|
||||||
|
.as_ref()
|
||||||
|
.map(|(dom, pasajes, _)| corpus_panel(&theme, *dom, pasajes));
|
||||||
|
|
||||||
// Botón flotante 2D ⇄ 3D — visible solo con una carta cargada.
|
// Botón flotante 2D ⇄ 3D — visible solo con una carta cargada.
|
||||||
// Muestra el modo al que se cambiará, no el activo.
|
// Muestra el modo al que se cambiará, no el activo.
|
||||||
let sphere_toggle = matches!(self.state.mode, CanvasMode::Wheel { .. }).then(|| {
|
let sphere_toggle = matches!(self.state.mode, CanvasMode::Wheel { .. }).then(|| {
|
||||||
@@ -1009,6 +1225,50 @@ impl Render for AstrologyCanvas {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Selector de tajada del corpus — sobre la rueda 2D.
|
||||||
|
let corpus_selector = (matches!(self.state.mode, CanvasMode::Wheel { .. })
|
||||||
|
&& !self.state.sphere_3d)
|
||||||
|
.then(|| {
|
||||||
|
let mut col = div()
|
||||||
|
.absolute()
|
||||||
|
.top(px(12.0))
|
||||||
|
.left(px(12.0))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(4.0));
|
||||||
|
for d in [Dominio::Vital, Dominio::Social, Dominio::Psiquico] {
|
||||||
|
let active = self.state.corpus_domain == Some(d);
|
||||||
|
col = col.child(
|
||||||
|
div()
|
||||||
|
.px(px(10.0))
|
||||||
|
.py(px(4.0))
|
||||||
|
.rounded(px(5.0))
|
||||||
|
.bg(if active {
|
||||||
|
domain_color(d).into()
|
||||||
|
} else {
|
||||||
|
theme.bg_panel_alt.clone()
|
||||||
|
})
|
||||||
|
.border_1()
|
||||||
|
.border_color(theme.border)
|
||||||
|
.text_size(px(10.0))
|
||||||
|
.text_color(if active {
|
||||||
|
hsla(0.0, 0.0, 1.0, 0.95)
|
||||||
|
} else {
|
||||||
|
theme.fg_muted
|
||||||
|
})
|
||||||
|
.cursor_pointer()
|
||||||
|
.child(domain_label(d))
|
||||||
|
.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |this, _, _w, cx| {
|
||||||
|
this.toggle_corpus_domain(d, cx)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
col
|
||||||
|
});
|
||||||
|
|
||||||
// Depth field: capa absoluta detrás del body, ocupa todo el
|
// Depth field: capa absoluta detrás del body, ocupa todo el
|
||||||
// canvas. Vignette radial — el centro queda claro y los
|
// canvas. Vignette radial — el centro queda claro y los
|
||||||
// bordes se oscurecen, dando profundidad sin "ruido" de
|
// bordes se oscurecen, dando profundidad sin "ruido" de
|
||||||
@@ -1039,17 +1299,33 @@ impl Render for AstrologyCanvas {
|
|||||||
.relative()
|
.relative()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(depth_field)
|
.child(depth_field)
|
||||||
|
.child(match corpus_side {
|
||||||
|
// Con el corpus activo: rueda a la izquierda, panel de
|
||||||
|
// pasajes a la derecha.
|
||||||
|
Some(panel) => div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.child(body),
|
||||||
|
)
|
||||||
|
.child(panel),
|
||||||
|
None => div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.child(body),
|
.child(body),
|
||||||
)
|
})
|
||||||
.children(sphere_toggle)
|
.children(sphere_toggle)
|
||||||
.children(constellations_toggle)
|
.children(constellations_toggle)
|
||||||
|
.children(corpus_selector)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user