feat(cosmobiologia): cliente web demo SSR + DrawCommand agnóstico (fase 3a)
Fase 3a — render web operativo sin WASM. Demo funcional inmediata
con server-side rendering del SVG; el cliente WASM puro se hace en
fase 3b cuando wasm-pack / wasm-bindgen-cli esté instalado.
cosmobiologia-render — nuevo módulo `draw`:
- `Rgba { r, g, b, a }` color agnóstico (no Hsla, no hex CSS).
- `DrawCommand` enum tagged-serde: `Circle`, `Line`, `Text`. Listo
para WASM o nativo — solo primitivas.
- `CompositionOpts { size, rot_offset_deg, include_bodies }`.
- `compose_wheel(model, opts) -> Vec<DrawCommand>` primera versión:
anillo zodiacal (A+B), 12 cusps cada 30°, glyphs de signos,
corona de casas (C+D), cusps de casas (Asc/IC/Desc/MC con peso
doble), house numbers, anillo de aspectos (E), líneas de
aspectos coloreadas por kind, glyphs de cuerpos natales con
disco halo.
- `draw_commands_to_svg(cmds, size) -> String` serializa la lista
a SVG inline. SVG-escape, `text-anchor` configurable, `dominant
-baseline=central` para centrar verticalmente.
Pendiente en `compose_wheel` (extender en commits siguientes,
copiando lo del canvas gpui): spread anti-solapamiento, clusters
compartidos, coord labels, dial 3D bevel, vignette, themes
PrintColor/PrintBW. Por ahora es un MVP suficiente para verificar
end-to-end y para que el usuario tenga algo visible YA.
cosmobiologia-server:
- Nuevos endpoints:
* `GET /` → HTML del cliente (single-page)
* `GET /api/sky.svg` → SVG agnóstico del "cielo ahora"
* `GET /api/charts/:id/wheel.svg` → SVG agnóstico de carta con
overlays via query (offset,
transit, prog, sa, pd)
- Página HTML embebida (`include_str!` de `static/index.html`):
* Sidebar con tree (groups → contacts → charts), click selecciona
* "⏱ Cielo ahora" siempre disponible como botón rápido
* Toolbar con input offset minutos + checkbox tránsito + botón
refresh + botón download SVG
* Botones "Nuevo grupo / Nuevo contacto" con prompt + POST
* Wheel renderizado en SVG inline, info row con título/asc/mc/ms
Smoke test:
cargo run -p cosmobiologia-server -- --port 18787
curl / → HTML (página completa)
curl /api/sky.svg → 12 KB SVG con 17 circles +
51 lines + 36 texts
curl /api/tree → árbol JSON
curl POST /api/groups → crea grupo
Browser http://127.0.0.1:8787 → wheel visible
Próximo (fase 3b): cliente cdylib WASM `cosmobiologia-web` que
reemplace el SSR — recibe RenderModel JSON, llama compose_wheel +
draw_commands_to_svg en WASM, monta SVG via DOM. Trade-off: el
SSR de hoy es 12 KB transferidos por click (sólido); WASM
descarga ~150 KB una sola vez y luego compone localmente
(scrubbing instantáneo, sin round-trip al server).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ use clap::Parser;
|
||||
use cosmobiologia_engine::{
|
||||
compose_with_options, svg_export, EngineError, NatalOptions, PipelineRequest, RenderModel,
|
||||
};
|
||||
use cosmobiologia_render::{compose_wheel, draw_commands_to_svg, CompositionOpts};
|
||||
use cosmobiologia_model::{
|
||||
Chart, ChartId, ChartKind, Contact, ContactId, Group, GroupId, StoredBirthData,
|
||||
StoredChartConfig,
|
||||
@@ -118,9 +119,17 @@ fn default_db_path() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
|
||||
fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(get_index))
|
||||
.route("/api/health", get(health))
|
||||
.route("/api/tree", get(get_tree))
|
||||
.route("/api/sky", get(get_sky))
|
||||
// El render SVG agnóstico (via `cosmobiologia-render::compose_wheel`
|
||||
// + `draw_commands_to_svg`) sirve a la fase 3 inicial: el
|
||||
// cliente recibe SVG ya compuesto, sin necesidad de WASM.
|
||||
// Cuando agreguemos el cliente WASM real, este endpoint se
|
||||
// mantiene como fallback "ver SVG sin JS".
|
||||
.route("/api/sky.svg", get(get_sky_svg))
|
||||
.route("/api/charts/:id/wheel.svg", get(get_chart_wheel_svg))
|
||||
.route("/api/groups", post(post_group))
|
||||
.route("/api/groups/:id", patch(patch_group).delete(delete_group))
|
||||
.route("/api/contacts", post(post_contact))
|
||||
@@ -139,6 +148,55 @@ fn router() -> Router<AppState> {
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Página HTML inicial
|
||||
// =====================================================================
|
||||
|
||||
const INDEX_HTML: &str = include_str!("../static/index.html");
|
||||
|
||||
async fn get_index() -> Response {
|
||||
(
|
||||
[(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
INDEX_HTML.to_string(),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// SVG render agnóstico (no es el del engine — este viene de
|
||||
// `cosmobiologia-render::compose_wheel` que es lo que mañana el
|
||||
// cliente WASM también va a usar). Útil para demos sin WASM.
|
||||
async fn get_sky_svg() -> Result<Response, ApiError> {
|
||||
let chart = build_present_sky_chart();
|
||||
let model = compose_with_options(&chart, 0, &[], &NatalOptions::default())?;
|
||||
let cmds = compose_wheel(&model, &CompositionOpts::default());
|
||||
let svg = draw_commands_to_svg(&cmds, 600.0);
|
||||
Ok((
|
||||
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
|
||||
svg,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn get_chart_wheel_svg(
|
||||
State(s): State<AppState>,
|
||||
Path(id): Path<ChartId>,
|
||||
Query(q): Query<RenderQuery>,
|
||||
) -> Result<Response, ApiError> {
|
||||
let chart = s
|
||||
.store
|
||||
.get_chart(id)
|
||||
.map_err(|_| ApiError::NotFound(format!("chart {}", id)))?;
|
||||
let model =
|
||||
compose_with_options(&chart, q.offset_min, &build_requests(&q), &NatalOptions::default())?;
|
||||
let cmds = compose_wheel(&model, &CompositionOpts::default());
|
||||
let svg = draw_commands_to_svg(&cmds, 600.0);
|
||||
Ok((
|
||||
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
|
||||
svg,
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Error
|
||||
// =====================================================================
|
||||
|
||||
Reference in New Issue
Block a user