fix: close the adversarial findings on the save/paste wave

Ported from da950460 (#1717).

In-place save:
- 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: a wasm in-flight slot refuses a
  second save with the same localized message, cleared on every exit
  path.

Paste: pasted_image_name built its timestamp from Date::new(0) — a
CONSTANT name, so every paste overwrote every earlier paste's image.
Real clock, milliseconds, and a collision-suffixed unique name now.

Capture: a web capture in a hidden tab, or one with nothing redrawing,
could hold the automation slot forever. It fails fast when the page is
hidden and gives up after 5 s, on main's frame-parked capture.

The XREF manager reports registry-loaded xrefs with their size on the
web instead of NotFound.

Left out: the always-warm print snapshot (a frames subscription that
emits a message on every frame keeps the app redrawing at the display
rate), so the browser-menu print wiring from the previous commit goes
too — its beforeprint readback printed blank pages on WebGL2. The
merge-residue hunks (re-added XREF bind tests, snap endpoint map and
3D-snap gate) belong to code main already has in its own form.

(cherry picked from commit da950460)
This commit is contained in:
Afonsop authored and Hakan Seven committed 2026-10-12 12:02:05 +03:00
1 parent 835e8d469f
commit 1c0963b68d
8 files changed
+382 -90

No files matched your search

-38
View File
@@ -38,16 +38,6 @@
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" />
@@ -66,8 +56,6 @@
<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
@@ -84,32 +72,6 @@
});
})();
</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");