feat(tahuantinsuyu): fase 18 — aspect list + hover tooltips + dignidades + export SVG
Fase grande con 4 features que aprovechan toda la infraestructura ya
construida. Engine ganó 2 módulos nuevos (dignity table data-only +
svg_export), el RenderModel se enriqueció con AspectSummary y los
glyphs con dignity_marker, y el canvas trae hit-test pasivo + lista
textual + botón de export.
## C — Lista textual de aspectos
- engine: nuevo `AspectSummary { module_id, from_body, to_body, kind,
orb_deg, applying }` + campo `aspect_summary: Vec<AspectSummary>`
en RenderModel. populate_natal_aspect_summary y
populate_cross_aspect_summary se llaman desde compose por cada
pasada (natal + 4 overlays). Ordenado por orb_deg asc (los más
exactos primero).
- canvas: nuevo aspect_unicode helper (☌ ☍ △ □ ⚹ ⚻ ⚺ ∠ ⚼ Q bQ).
Footer agrega un grid flex-wrap con las top 12 entries del summary,
cada una formateada como "[module_id] ☉ △ ☾ · 2.3° A" coloreado
por palette.aspect(kind).
## A — Tooltips al hover
- canvas: nuevo HoverInfo { module_id, symbol, deg, house, retrograde,
dignity_marker, annotation, local_x, local_y } + state.hover.
on_hover_check ejecuta hit-test sobre todos los glyphs Bodies +
Outer (threshold 14px); se llama desde el handler MouseMoveEvent
cuando NO está dragging (handler reescrito para soportar drag y
hover en mismo callback). Cuando mouse sale del wheel, hover=None.
- Tooltip absoluto: "☉ Tauro · 23.4° · Casa 5 · ℞" con border
angle_highlight. Posición offset arriba-derecha del planeta,
clampada al wheel para no salirse.
## B — Dignidades esenciales clásicas
- engine: nuevo mod `dignity` con `Dignity { Rulership/Exaltation/
Detriment/Fall }` + tabla rules_classical (7 planetas tradicionales)
+ exalts_at table. `essential_dignity(body, sign_index) -> Option`.
4 tests cubren rulership/detriment/exaltation/fall + edge case
modernos (Urano/Nept/Plutón sin dignidad clásica). 4 markers:
+ (domicilio), · (exaltación), − (exilio), * (caída).
- engine: Glyph gana campo `dignity_marker: Option<String>`. Default
derive en Glyph para no romper N construction sites. bridge::
annotate_dignities mutua RenderModel post-build agregando markers
a glyphs natales según el signo de cada placement.
- NatalOptions agrega show_dignities. NatalModule.controls() agrega
Toggle "Dignidades esenciales (+ · − *)" default false.
- canvas: glyph render append dignity_marker al texto después del ᴿ.
## D — Export SVG
- engine: nuevo `pub mod svg_export` con `render_to_svg(&RenderModel)
-> String`. Reproduce la geometría del canvas en un SVG standalone
800×800 escalable: anillos zodiacales, cusps, planetas con
retrograde+dignity markers, aspectos coloreados por kind (cross
conocen rings de origen/destino vía aspect_radii), labels ASC/MC/
DESC/IC. Sin dependencias nuevas — write! sobre String. Test
asserts well-formed XML.
- canvas: CanvasEvent::ExportSvgRequested + botón pequeño "⬇ SVG"
en el header del wheel (al lado del title).
- shell: on_canvas_event ExportSvgRequested → export_current_to_svg
recompose actual + svg_export::render_to_svg + write a
$XDG_DATA_HOME/tahuantinsuyu/exports/<label>_<short_id>.svg.
Ruta logueada a stderr para que el usuario encuentre el archivo.
`cargo check` y `cargo test` verdes con 8 tests en engine
(2 existentes + 4 dignity + 1 svg + 1 mock).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ use tahuantinsuyu_canvas::{
|
||||
};
|
||||
use tahuantinsuyu_engine::{
|
||||
LayerKind, NatalOptions, OUTER_RING_MODULES, PipelineRequest, compose_with_options,
|
||||
svg_export,
|
||||
};
|
||||
use tahuantinsuyu_model::{Chart, ChartId, ModuleState, TreeSelection};
|
||||
use tahuantinsuyu_panel::{ChartOption, ControlPanel, PanelEvent};
|
||||
@@ -308,6 +309,7 @@ impl Shell {
|
||||
show_majors: read_bool("aspect_majors", true),
|
||||
show_minors: read_bool("aspect_minors", false),
|
||||
orb_multiplier: read_f64("orb_multiplier", 1.0),
|
||||
show_dignities: read_bool("show_dignities", false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +497,53 @@ impl Shell {
|
||||
CanvasEvent::ChartRequested(_) => {
|
||||
// Fase 7: doble click sobre un thumbnail abre la carta.
|
||||
}
|
||||
CanvasEvent::ExportSvgRequested => {
|
||||
self.export_current_to_svg();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recompone la carta actual + escribe el SVG a un archivo en
|
||||
/// `$XDG_DATA_HOME/tahuantinsuyu/exports/<label>_<short_id>.svg`.
|
||||
/// Logea la ruta a stderr — futuro: file save dialog GPUI.
|
||||
fn export_current_to_svg(&self) {
|
||||
let Some(chart) = self.current_chart.as_ref() else {
|
||||
eprintln!("[shell] export svg: sin carta activa");
|
||||
return;
|
||||
};
|
||||
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] export svg compose: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let svg = svg_export::render_to_svg(&render);
|
||||
let dir = directories::ProjectDirs::from("net", "gioser", "tahuantinsuyu")
|
||||
.map(|d| d.data_dir().join("exports"))
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
if let Err(e) = std::fs::create_dir_all(&dir) {
|
||||
eprintln!("[shell] mkdir {:?}: {}", dir, e);
|
||||
return;
|
||||
}
|
||||
let safe_label: String = chart
|
||||
.label
|
||||
.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
|
||||
.collect();
|
||||
let short = format!("{}", chart.id).chars().take(8).collect::<String>();
|
||||
let path = dir.join(format!("{}_{}.svg", safe_label, short));
|
||||
if let Err(e) = std::fs::write(&path, svg) {
|
||||
eprintln!("[shell] write {:?}: {}", path, e);
|
||||
} else {
|
||||
eprintln!("[shell] SVG exportado → {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user