studio: version text in the status bar, favicon
Deploy web Studio / deploy (push) Successful in 3m15s

The status bar ends with "isokit <engine version>"; the web build adds
the commit (linked on Gitea), its date as a tooltip and a "+" when built
from a tree with uncommitted changes (build_web.py writes a
<meta name="isokit-build"> from GITHUB_SHA or git). /api/meta now carries
"engine". studio/favicon.svg (isometric cube in the accent green) is
served by `isokit studio` (also on /view) and copied into the web build.
check_web.py and check_studio.py check both.
This commit is contained in:
grabowski committed 2026-10-23 17:08:48 +07:00
1 parent c1d01331ac
commit c647f39059
11 files changed
+66 -4

No files matched your search

+3 -1
View File
@@ -13,7 +13,7 @@ use tiny_http::{Header, Method, Request, Response, Server};
const VIEW_PAGE: &str = r#"<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>isokit preview</title>
<script>(function(){var t=null;try{t=localStorage.getItem("isokit-theme")}catch(e){}if(t!=="light"&&t!=="dark")t=matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t);})();</script>
<link rel="stylesheet" href="/assets/viewer/isokit-view.css"><link rel="stylesheet" href="/assets/theme.css">
<link rel="icon" type="image/svg+xml" href="/assets/studio/favicon.svg"><link rel="stylesheet" href="/assets/viewer/isokit-view.css"><link rel="stylesheet" href="/assets/theme.css">
<style>:root{color-scheme:dark}html[data-theme=light]{color-scheme:light}body{margin:0;background:#0a0f1a;font:14px system-ui,sans-serif;color:#d6deeb}
html[data-theme=light] body{background:#f4f6fa;color:#1b2533}main{max-width:1320px;margin:0 auto;padding:16px}</style></head>
<body><main><div id="view"></div></main><script src="/assets/viewer/isokit-view.js"></script><script>
@@ -164,6 +164,8 @@ pub fn studio(dir: &Path, port: u16, open: bool) -> Result<()> {
"text/javascript; charset=utf-8"
} else if name.ends_with(".css") {
"text/css; charset=utf-8"
} else if name.ends_with(".svg") {
"image/svg+xml"
} else {
"text/html; charset=utf-8"
};
+1
View File
@@ -18,6 +18,7 @@ pub fn meta<S: Store>(ws: &Workspace<S>) -> Value {
v["dir"] = json!(ws.dir.display().to_string());
v["load_error"] = json!(ws.load_error);
v["script_error"] = json!(ws.project.runtime.as_ref().err());
v["engine"] = json!(env!("CARGO_PKG_VERSION"));
v
}
+2
View File
@@ -10,6 +10,7 @@ pub const PAGE: &str = include_str!("../../../viewer/page.html");
pub const STUDIO_HTML: &str = include_str!("../../../studio/index.html");
pub const STUDIO_JS: &str = include_str!("../../../studio/studio.js");
pub const STUDIO_CSS: &str = include_str!("../../../studio/studio.css");
pub const STUDIO_FAVICON: &str = include_str!("../../../studio/favicon.svg");
/// Asset by its path in the repository ("viewer/isokit-view.js",
/// "studio/studio.js"), from disk when `ISOKIT_ASSETS` is set.
@@ -26,6 +27,7 @@ pub fn get(path: &str) -> Option<Cow<'static, str>> {
"studio/index.html" => STUDIO_HTML,
"studio/studio.js" => STUDIO_JS,
"studio/studio.css" => STUDIO_CSS,
"studio/favicon.svg" => STUDIO_FAVICON,
_ => return None,
};
Some(Cow::Borrowed(s))
+1
View File
@@ -216,6 +216,7 @@ mod tests {
assert_eq!(m["title"], "isokit demo works");
assert!(m["catalog"].as_array().unwrap().len() > 20);
assert!(m["script_error"].is_null());
assert_eq!(m["engine"], env!("CARGO_PKG_VERSION"));
let n = j(&s.state())["doc"]["objects"].as_array().unwrap().len();
let v = j(&s.render("site", 1, "", true));
assert!(
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M16 2.5 29 10 16 17.5 3 10Z" fill="#3ddc84"/>
<path d="M3 10 16 17.5V30.5L3 23Z" fill="#1f9e5a"/>
<path d="M29 10 16 17.5V30.5L29 23Z" fill="#126b3c"/>
<path d="M16 2.5 29 10V23L16 30.5 3 23V10ZM3 10 16 17.5 29 10M16 17.5V30.5" fill="none" stroke="#0a0f1a" stroke-width="1.2" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

+2
View File
@@ -9,6 +9,7 @@
if(t!=="light"&&t!=="dark")t=window.matchMedia&&matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";
document.documentElement.setAttribute("data-theme",t);})();
</script>
<link rel="icon" type="image/svg+xml" href="/assets/studio/favicon.svg">
<link rel="stylesheet" href="/assets/viewer/isokit-view.css">
<link rel="stylesheet" href="/assets/theme.css">
<link rel="stylesheet" href="/assets/studio/studio.css">
@@ -51,6 +52,7 @@ document.documentElement.setAttribute("data-theme",t);})();
<span class="st-grow"></span>
<button type="button" id="warnbtn" class="st-link" hidden></button>
<span id="stats"></span>
<a id="ver" class="st-ver" href="https://git.b4l.co.th/B4L/isokit" target="_blank" rel="noopener"></a>
</footer>
</section>
<aside class="st-right" id="right"></aside>
+2
View File
@@ -52,6 +52,8 @@ input[type=checkbox] { accent-color: var(--accent); }
.st-hint { position: absolute; top: 52px; left: 50%; transform: translateX(-50%); background: var(--panel); border: 1px solid var(--accent); border-radius: 16px; padding: 4px 14px; z-index: 5; pointer-events: none; }
.st-status { display: flex; align-items: center; gap: 14px; padding: 4px 10px; background: var(--panel); border-top: 1px solid var(--stroke); color: var(--mute); font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.st-status select { padding: 1px 4px; }
.st-ver { color: var(--mute); font-size: 11px; text-decoration: none; white-space: nowrap; }
.st-ver:hover { color: var(--fg); text-decoration: underline; }
.st-link { background: none; border: 0; color: var(--danger); padding: 0; text-decoration: underline; }
.st-right h3 { margin: 0; font-size: 15px; display: flex; align-items: center; gap: 6px; }
+16
View File
@@ -356,12 +356,28 @@
if (B && B.preview) $("preview").addEventListener("click", function (e) { e.preventDefault(); B.preview(); });
if (B && B.saveTitle) $("save").title = B.saveTitle;
// engine version from /api/meta; a web build adds its commit and date
// (<meta name="isokit-build" content="<sha> <date> [dirty]">, tools/build_web.py)
function showVersion() {
var el = $("ver"), m = document.querySelector('meta[name="isokit-build"]');
var b = m ? m.getAttribute("content").split(" ") : [];
var text = "isokit " + (S.meta.engine || "?"), tip = "isokit engine " + (S.meta.engine || "?");
if (b[0]) {
text += " · " + b[0].slice(0, 7) + (b[2] ? "+" : "");
tip += ", build " + b[0] + (b[1] ? " (" + b[1] + ")" : "") + (b[2] ? ", with uncommitted changes" : "");
el.href = "https://git.b4l.co.th/B4L/isokit/commit/" + b[0];
}
el.textContent = text;
el.title = tip;
}
// ------------------------------------------------------------ start
Promise.all([api("/api/meta"), api("/api/doc")]).then(function (rs) {
S.meta = rs[0]; takeState(rs[1]);
document.title = S.meta.title + " · isokit Studio";
$("title").textContent = S.meta.title;
if (S.meta.load_error) toast(S.meta.load_error, true);
showVersion();
renderPalette(); renderLists(); mountView(); renderRight();
poll();
});
+24 -3
View File
@@ -81,6 +81,24 @@ def demo_files():
return out
def build_info():
"""'<commit> <commit date> [dirty]' for the version text in the Studio's
status bar; GITHUB_SHA (CI) wins over git. Empty when git is missing."""
def git(*args):
r = subprocess.run(["git", *args], capture_output=True, text=True, cwd=ROOT)
return r.stdout.strip() if r.returncode == 0 else ""
try:
sha = os.environ.get("GITHUB_SHA") or git("rev-parse", "HEAD")
if not sha:
return ""
parts = [sha, git("log", "-1", "--format=%cs", sha) or "-"]
if git("status", "--porcelain", "--untracked-files=no"):
parts.append("dirty")
return " ".join(parts)
except OSError:
return ""
def replace_once(s, old, new):
n = s.count(old)
assert n == 1, f"studio/index.html: expected {old!r} once, found {n}"
@@ -116,7 +134,7 @@ def main():
print(" (wasm-opt not found: the .wasm stays unoptimised by binaryen, about 15% larger)")
for src, dst in [("viewer/isokit-view.js", "viewer"), ("viewer/isokit-view.css", "viewer"),
("studio/studio.js", "studio"), ("studio/studio.css", "studio"),
("studio/studio.js", "studio"), ("studio/studio.css", "studio"), ("studio/favicon.svg", "studio"),
("web/web.js", "web"), ("web/web.css", "web")]:
(out / "assets" / dst).mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / src, out / "assets" / dst / Path(src).name)
@@ -129,8 +147,11 @@ def main():
v = "v=" + h.hexdigest()[:10]
html = (ROOT / "studio" / "index.html").read_text(encoding="utf-8")
build = build_info()
html = replace_once(html, '<title>isokit Studio</title>',
'<title>isokit Studio</title>\n<meta name="description" content="isokit Studio: edit isometric site plans in the browser.">')
'<title>isokit Studio</title>\n<meta name="description" content="isokit Studio: edit isometric site plans in the browser.">'
+ (f'\n<meta name="isokit-build" content="{build}">' if build else ""))
html = replace_once(html, 'href="/assets/studio/favicon.svg"', f'href="assets/studio/favicon.svg?{v}"')
html = replace_once(html, 'href="/assets/viewer/isokit-view.css"', f'href="assets/viewer/isokit-view.css?{v}"')
html = replace_once(html, '<link rel="stylesheet" href="/assets/theme.css">', '<style id="iso-theme"></style>')
html = replace_once(html, '<link rel="stylesheet" href="/assets/studio/studio.css">',
@@ -148,7 +169,7 @@ def main():
if p.is_file():
total += p.stat().st_size
print(f" {p.relative_to(out).as_posix():40} {p.stat().st_size / 1024:8.1f} kB")
print(f"{out} ({total / 1024 / 1024:.1f} MB, {v})")
print(f"{out} ({total / 1024 / 1024:.1f} MB, {v}, build {build or 'unknown'})")
if __name__ == "__main__":
+4
View File
@@ -48,6 +48,10 @@ with sync_playwright() as p:
n0 = len(doc()["objects"])
check(pg.locator("#objects li").count() == n0, f"object list shows {n0} objects")
check(pg.locator(".st-tile").count() >= 20, "palette lists the prefabs")
ver = pg.locator("#ver").inner_text()
check(ver == "isokit " + get("/api/meta")["engine"], "status bar shows the engine version: " + ver)
icon = urllib.request.urlopen(BASE + "/assets/studio/favicon.svg")
check(icon.headers.get_content_type() == "image/svg+xml" and b"<svg" in icon.read(), "favicon served as SVG")
pg.screenshot(path=str(OUT / "studio-start.jpg"), type="jpeg", quality=80)
# place a tree: pick it in the palette, click an empty spot of the map
+5
View File
@@ -89,6 +89,11 @@ try:
check(pg.evaluate("() => getComputedStyle(document.querySelector('.iso-svg .iso-t')).fill") not in ("", "rgb(0, 0, 0)"),
"theme CSS styles the scene")
check(pg.evaluate("() => isokit.webmcp") == 15, "15 MCP tools registered with WebMCP")
ver = pg.locator("#ver").inner_text()
check(ver.startswith("isokit " + pg.evaluate("() => isokit.engine") + " · "), "status bar shows version and build: " + ver)
icon = pg.evaluate("""async () => { const l = document.querySelector('link[rel=icon]');
const r = await fetch(l.href); return r.ok && (await r.text()).includes('<svg'); }""")
check(icon, "favicon link loads an SVG")
shot(pg, "web-start.jpg")
# ---------------------------------------------------------------- edit: place, drag, props, undo