ifcviewer: marquee box-select on web + suppress the canvas context menu

Bring rubber-band box-select to the web on the Web preset's select button (RMB).

- Core: factor the pick-pass encode + rect copy out of picksInRect into
  encodeBoxPickToStaging (mirroring how single-pick shares
  encodePickReadbackToStaging), shared by the sync picksInRect (desktop) and a
  new async picksInRectAsync (web) — the latter maps the staging buffer via a
  spontaneous callback because the sync spin-map hangs the JS loop. New
  applyMarqueeToSelection (plain replace / Shift add / Ctrl remove).
- Web main_web: a select-button drag past the click threshold draws a marquee
  rubber-band (a plain DOM <div> positioned in CSS px — no GPU overlay pass,
  which the web lib lacks) and on release box-picks the rect (device px) and
  applies it to the selection. A click (no drag) still single-picks.
- Web shell.html: the #marquee div + styling, and — the reported bug — a
  contextmenu preventDefault on the canvas so RMB (now the select button) doesn't
  pop the browser menu. (Firefox still forces its native menu on Shift+RightClick;
  that's a browser escape hatch pages can't override.)

Tests: applyMarqueeToSelection replace/add/remove + id-0 (Catch2, 124 total);
web smoke marquee drag → rubber-band shown → selection changes → hidden (10/10).
Desktop picksInRect unchanged in behaviour; BonsaiViewer builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-07-03 09:08:28 +10:00
parent d08c53d756
commit cd3d70172b
6 changed files with 247 additions and 43 deletions
@@ -196,3 +196,25 @@ TEST_CASE("hideSelected hides the selection; showAll restores", "[camera][visibi
core.showAll();
REQUIRE(core.hiddenCount() == 0);
}
TEST_CASE("applyMarqueeToSelection: replace / add / remove", "[camera][selection]") {
MockHost host; ViewportCore core(&host);
// No public selection accessor, so verify via hideSelected → hiddenCount.
SECTION("plain marquee replaces the selection") {
core.applyMarqueeToSelection({1, 2, 3}, /*add*/false, /*remove*/false);
core.hideSelected();
REQUIRE(core.hiddenCount() == 3);
}
SECTION("add unions, remove subtracts") {
core.applyMarqueeToSelection({5}, false, false); // replace → {5}
core.applyMarqueeToSelection({6, 7}, true, false); // add → {5,6,7}
core.applyMarqueeToSelection({6}, false, true); // remove → {5,7}
core.hideSelected();
REQUIRE(core.hiddenCount() == 2);
}
SECTION("id 0 is ignored") {
core.applyMarqueeToSelection({0, 9, 0}, false, false);
core.hideSelected();
REQUIRE(core.hiddenCount() == 1);
}
}