Files
OneCAD-Draft/web-app.html
T
Afonsop 6e89b36d44 feat: in-place save (File System Access), image paste, print wiring, web capture
The last high-severity web gap plus a small integration bundle.

In-place save: with the File System Access API (Chromium/Edge), Save
now works like desktop. Plain Save on a tab with a retained handle
writes straight through — no picker, no download, no "(1)" copies.
First save or format change opens showSaveFilePicker AT the confirm
gesture (the worker serialization that follows can outlast the
transient-activation window; retained handles need no activation to
write). Picker cancel cancels the save like desktop; picker or write
failure falls back to the legacy download with a note; Firefox/Safari
keep the download flow untouched (feature-detected). Handles are
keyed by tab id in a thread-local registry (Message must be Send —
JsValue cannot ride in messages or App state); closing a tab drops
its handle; Save As re-picks. Dirty clears only on successful
persistence, same race guard as before.

Image paste: Ctrl+V of a screenshot inserts it as an image reference
— the clipboard reader falls back to image/* items when neither the
entity clipboard nor text has anything (precedence preserved), the
bytes enter the image source registry as pasted-<timestamp>.<ext>,
and attach reuses the IMAGE command's model-creation path verbatim.

Browser print: beforeprint snapshots the WebGL canvas into a print-
only <img> (the capture_canvas technique), so the browser's own print
menu produces the drawing instead of a blank page.

Automation capture on web: the capture op no longer calls the
synchronous GPU-map screenshot (dead on wasm); it waits one presented
frame and reads via capture_canvas — the same workaround the save-
thumbnail path uses — delivering the PNG as a download.
2026-09-21 11:17:23 +01:00

158 lines
6.9 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<link rel="manifest" href="app.webmanifest" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex, follow" />
<meta name="theme-color" content="#1e1e1e" />
<title>Open CAD Studio Web App</title>
<link data-trunk rel="icon" href="assets/logo.svg" />
<style>
html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #1e1e1e; }
canvas { display: block; }
#loading {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 22px;
background: #1e1e1e;
color: #cfcfcf;
font-family: system-ui, sans-serif;
z-index: 9999;
transition: opacity 0.4s ease;
}
#loading.hide { opacity: 0; pointer-events: none; }
#loading .title { font-size: 22px; font-weight: 600; letter-spacing: 0.5px; }
#loading .title span { color: #3399e6; }
#loading .sub { font-size: 12px; color: #777; }
#loading .spinner {
width: 46px;
height: 46px;
border: 4px solid #2e2e2e;
border-top-color: #3399e6;
border-radius: 50%;
animation: ocs-spin 0.9s linear infinite;
}
@keyframes ocs-spin { to { transform: rotate(360deg); } }
/* Browser-menu print support: hidden on screen, the only thing shown on
paper. beforeprint snapshots the WebGL canvas into it (below), so the
printout is the drawing instead of a blank page — WebGL canvases do
not composite into the print surface on their own. */
#print-snapshot { display: none; }
@media print {
body > *:not(#print-snapshot) { display: none !important; }
html, body { overflow: visible; height: auto; background: #fff; }
#print-snapshot { display: block; max-width: 100%; height: auto; }
}
</style>
<link data-trunk rel="rust" data-bin="OpenCADStudio" />
<link data-trunk rel="copy-dir" href="web/fonts" />
<link data-trunk rel="copy-file" href="web/ocs-parse-worker.js" />
<link data-trunk rel="copy-file" href="web/discussions.json" />
<link data-trunk rel="copy-file" href="web/videos.json" />
<link data-trunk rel="copy-file" href="web/locale-labels.json" />
<link data-trunk rel="copy-file" href="web/ocs-control.js" />
<link data-trunk rel="copy-file" href="web/sw.js" />
<link data-trunk rel="copy-file" href="web/app.webmanifest" />
<link data-trunk rel="copy-file" href="web/logo.svg" />
</head>
<body>
<div id="loading">
<div class="spinner"></div>
<div class="title">Open <span>CAD</span> Studio</div>
<div class="sub">Loading…</div>
</div>
<!-- Print-only copy of the canvas; filled by beforeprint below. -->
<img id="print-snapshot" alt="" />
<script src="ocs-control.js" defer></script>
<script>
// Offline support: register the service worker. It is copied next to
// this page by Trunk (see the copy-file link in <head>), so the
// relative URL resolves to /app/sw.js and the registration scope is
// /app/ — the whole app. Registered immediately (not on load) so the
// worker starts capturing this visit's asset requests as early as
// possible. sw.js never reloads the page on update: a new worker
// waits until every tab on the old version has closed.
(() => {
if (!("serviceWorker" in navigator)) return;
navigator.serviceWorker.register("sw.js").catch(error => {
console.warn("service worker registration failed; offline support disabled:", error);
});
})();
</script>
<script>
// Print support. The drawing lives in a WebGL canvas, which browsers
// rasterize as blank on the print surface. beforeprint therefore copies
// it onto a 2D canvas (the same drawImage readback the Rust side's
// sys::capture_canvas uses — no Rust state involved, the canvas is
// located from the DOM) and shows the snapshot in the print-only
// <img>; afterprint drops it again to free the PNG.
(() => {
const snapshot = document.getElementById("print-snapshot");
if (!snapshot) return;
window.addEventListener("beforeprint", () => {
const canvas = document.querySelector("canvas");
if (!canvas || !canvas.width || !canvas.height) return;
const copy = document.createElement("canvas");
copy.width = canvas.width;
copy.height = canvas.height;
const context = copy.getContext("2d");
if (!context) return;
context.drawImage(canvas, 0, 0);
snapshot.src = copy.toDataURL("image/png");
});
window.addEventListener("afterprint", () => {
snapshot.removeAttribute("src");
});
})();
</script>
<script>
(() => {
const splash = document.getElementById("loading");
fetch("locale-labels.json").then(response => response.json()).then(labels => {
let saved;
try {
saved = JSON.parse(localStorage.getItem("opencadstudio.settings"))?.settings?.language;
} catch {}
const preferred = navigator.languages?.length ? navigator.languages : [navigator.language];
const requested = typeof saved === "string" && saved !== "system" ? [saved, ...preferred] : preferred;
const available = Object.keys(labels);
let locale = "en-US";
for (const candidate of requested) {
try {
const language = new Intl.Locale(candidate);
const match = available.find(value => value.toLowerCase() === language.baseName.toLowerCase())
|| (language.language === "zh"
? (language.maximize().script === "Hant" ? "zh-TW" : "zh-CN")
: available.find(value => value.startsWith(language.language + "-")));
if (match) { locale = match; break; }
} catch {}
}
document.title = labels[locale].title;
document.documentElement.lang = locale;
document.documentElement.dir = locale === "ar-SA" ? "rtl" : "ltr";
const caption = splash?.querySelector(".sub");
if (caption) caption.textContent = labels[locale].loading;
}).catch(() => {});
const done = () => {
if (!splash) return;
splash.classList.add("hide");
setTimeout(() => splash.remove(), 450);
};
if (document.querySelector("canvas")) return done();
const observer = new MutationObserver(() => {
if (document.querySelector("canvas")) {
observer.disconnect();
done();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();
</script>
</body>
</html>