Files
isokit/crates/isokit-project/src/page.rs
T
grabowski a202283a60
Deploy web Studio / deploy (push) Successful in 3m46s
Web performance: light animation, gestures on a compositor layer, no forced layouts
Measured with the new tools/bench_web.py on the web Studio's house demo, CPU slowed 6x:

- Idle: running CSS animations repainted the whole drawing every frame (99% of the main
  thread in a window). Animations now step on about ten times a second by default
  ("light", 5% idle); the new toolbar button switches light / smooth / off and the browser
  remembers it; [build] animation sets the default for built pages; view.animation().
  Pan and zoom gestures pause them. Status LEDs pause off-screen and with the rest now.
- Pan 28 -> 10 ms, zoom 69 -> 11 ms per step: gestures move the drawing as one compositor
  layer (transform) and set the viewBox once they end; --iso-k is set on the tags group
  only and only when it changes; the stage box is read once per frame.
- Hover 22 -> 15 ms per move: the tooltip is placed once per frame on its own layer and
  measured only when its text changes; the Studio's x/y readout updates ten times a second
  in a contained box; hover work in the Studio runs once per frame.
- Long object lists skip layout of rows out of sight (content-visibility).
- WASM built with opt-level 3: engine render 11.7 -> 9.4 ms for 10% more download.

Checks: Studio 102/102 (animation modes, pan as a layer, contained readout), web 87/87.
2026-10-24 20:46:35 +07:00

91 lines
3.4 KiB
Rust

//! 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),
"animation": p.manifest.build.animation,
});
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 {
// web_time: std's clock traps in the browser (wasm32-unknown-unknown)
let secs = web_time::SystemTime::now()
.duration_since(web_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)
}