forked from 1b-agent/opencadstudio-upstream
Three reviewers over the in-place save, the paste/print/capture bundle, and the upstream merge: two blockers, six majors — all fixed. In-place save (blocker + 2 majors): - A stale retained handle could receive bytes serialized for a DIFFERENT format (Save As .dwg->.dxf whose re-pick failed wrote DXF bytes through the .dwg handle) or silently save in place while the user was told a copy was downloading. The serialized format now travels to completion (tab-keyed pending map), the write requires a matching handle extension AND a live tab, and a failed re-pick drops the old handle before the download fallback. - Discarding an unsaved tab leaked its handle, and a serialization still in flight could then overwrite the real file the user just declined to save. Discard drops the handle; a tab-gone completion downloads instead of writing. - Overlapping saves had no guard (native has one): a wasm in-flight slot refuses a second save with the same localized message, cleared on every exit path. Paste/print/capture (blocker + 2 majors): - pasted_image_name built its timestamp from Date::new(0) — a CONSTANT name, so every paste overwrote every earlier paste's image (the registry is last-wins; the first entity then displayed the second paste's pixels). Real clock, milliseconds, and a collision-suffixed unique name now, with the pure logic tested. - The beforeprint snapshot read the canvas outside the frame — blank on WebGL2 (no preserveDrawingBuffer). The print image is now kept WARM: refreshed on redraw frames (throttled to 2 s, skipped when hidden), so it is ready before the browser ever prints. - A capture op in a hidden tab wedged the ENTIRE automation channel (frames never fire, the pending op blocked every settle). Intake now fails fast when hidden, and a 5 s watchdog expires a parked capture with a clear error. Merge residue (3 majors): - The 9 upstream XREF-bind tests lost in resolution are restored verbatim (the bind feature shipped untested). - The XREF manager panel reported registry-loaded xrefs as NotFound on web — stat now consults the registry first (wasm), with a regression test. - Snap: our endpoint map gained the closing-segment wraparound pair (survey-scale closed polylines kept cm-level precision on the last edge), and the 3D-snap gate no longer suppresses the 2D edge wire when the F4 system is disabled (solid edges were snappable by NEITHER system); the setting is mirrored to the scene with invalidation, and the test that encoded the old bug now pins the fallback.
141 lines
6.4 KiB
HTML
141 lines
6.4 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. The Rust side keeps the img synchronized with the WebGL canvas
|
|
via a frame-callback readback (sys::refresh_print_snapshot), so the
|
|
printout is the drawing instead of a blank page — WebGL canvases do
|
|
not composite into the print surface on their own. A `beforeprint`
|
|
listener cannot do this: wgpu runs without preserveDrawingBuffer, so
|
|
by then the drawing buffer is already cleared after composite, and
|
|
an async capture inside beforeprint finishes after printing started. */
|
|
#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, kept ALWAYS WARM by the Rust side
|
|
(sys::refresh_print_snapshot runs off the frames subscription,
|
|
throttled to ~2 s): the snapshot must exist BEFORE beforeprint
|
|
fires, because the WebGL drawing buffer is cleared after each
|
|
composite and cannot be read from a beforeprint listener. No JS
|
|
here on purpose; the print CSS above does the rest. -->
|
|
<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>
|
|
(() => {
|
|
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>
|