isokit: project layer, Studio GUI, MCP server, viewer, CLI, demo project and docs
This commit is contained in:
1 parent
95c80ec6b1
commit
564aeb75f6
47 files changed
+5978
-366
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user