isokit: project layer, Studio GUI, MCP server, viewer, CLI, demo project and docs

This commit is contained in:
grabowski committed 2026-10-22 15:29:07 +07:00
1 parent 95c80ec6b1
commit 564aeb75f6
47 files changed
+5978 -366

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
//! Export: a self-contained HTML page with every view prerendered, or one
//! view as a standalone SVG file.
use crate::assets;
use crate::Project;
use isokit_core::fmt::esc;
use isokit_script::ScriptError;
use serde_json::{json, Value};
/// JSON safe to put inside `<script type="application/json">`.
fn script_json(v: &Value) -> String {
v.to_string().replace("</", "<\\/")
}
/// The static page: all levels in all four rotations, no server needed.
pub fn static_page(p: &Project) -> Result<String, ScriptError> {
let meta = p.meta();
let mut views = serde_json::Map::new();
for l in &meta.levels {
for rot in 0..4u8 {
let v = p.render(&l.id, rot, None, false)?;
views.insert(
format!("{}/{rot}", l.id),
json!({ "html": v.html, "info": v.info, "bbox": v.bbox, "focus": v.focus, "north": v.north, "boxes": v.boxes }),
);
}
}
let data = json!({
"meta": {
"title": meta.title, "note": meta.note, "levels": meta.levels,
"defaultLevel": meta.default_level, "defaultRotation": meta.default_rotation
},
"views": views,
"cfg": p.cfg(None),
});
let css = format!(
"{}\n{}",
assets::get("viewer/isokit-view.css").unwrap_or_default(),
p.theme.css()
);
let page = assets::get("viewer/page.html").unwrap_or_default();
Ok(page
.replace("{{title}}", &esc(&meta.title))
.replace("{{date}}", &today())
.replace("/*ISOKIT_CSS*/", &css)
.replace("/*ISOKIT_DATA*/", &script_json(&data))
.replace(
"/*ISOKIT_JS*/",
&assets::get("viewer/isokit-view.js").unwrap_or_default(),
))
}
/// One view as a standalone SVG (dark or light scheme, styles inlined).
pub fn svg(p: &Project, level: &str, rot: u8, light: bool) -> Result<String, ScriptError> {
let v = p.render(level, rot, None, false)?;
let css = format!(
"{}\n{}",
assets::get("viewer/isokit-view.css").unwrap_or_default(),
p.theme.css_scoped("svg.iso-svg", Some(light))
);
let b = v.bbox;
let bg = if light { "#e8f4ff" } else { "#0b1428" };
Ok(format!(
"<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"iso-svg\" viewBox=\"{} {} {} {}\" width=\"{}\" height=\"{}\">\
<style>{}</style><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" fill=\"{bg}\"/>{}</svg>\n",
b[0], b[1], b[2], b[3], (b[2] * 2.0).round(), (b[3] * 2.0).round(), css, b[0], b[1], b[2], b[3], v.html
))
}
/// Today's date (UTC) as YYYY-MM-DD, without a date crate.
pub fn today() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days = (secs / 86_400) as i64;
// civil-from-days (H. Hinnant)
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
format!("{:04}-{:02}-{:02}", if m <= 2 { y + 1 } else { y }, m, d)
}