Full section tool for the web viewport, with the gizmo + interaction shared with
desktop from one codebase.
- True-face surface pick. pickSurfaceAt had always ray-cast the instance AABB (to
skip a depth readback), so cuts sat in front of the real surface. The pick
fragment already computes the exact world_pos (it clips sections with it); now
it OUTPUTS it to a 3rd pick MRT (RGBA32F) that every pick path renders, and
pickSurfaceAt / pickSurfaceAtAsync read it back (decodeMappedPickPosition;
ray-AABB kept only as a fallback). The web async pick chains id -> normal ->
position spontaneous staging maps.
- Web tool: LMB drops a cut at the picked surface (LMB drag still orbits), K
toggles, Shift+K clears; oriented to the real MRT surface normal. Exports + a
Section / Clear cuts toolbar pair.
- Shared gizmo: lifted the section-gizmo renderer (SECTION_WGSL + thick-line AA +
quad+arrow VBO + pack + screen-space hit-test) out of the Qt-coupled
OverlayRenderer into a Qt-free SectionGizmoRenderer that ViewportCore::render
draws for BOTH desktop and web (both already render via render()). One identical
gizmo; OverlayRenderer's now-dead section code removed. Fixed 1 m size (matches
the desktop constant).
- Interaction (shared): hitTestSectionGizmo (SectionGizmoRenderer::hitTest) +
beginSectionDrag / updateSectionDrag / endSectionDrag live in ViewportCore.
Drag a gizmo arrow to slide the plane along its normal; Del/Backspace removes
the most recent cut. Desktop's ViewportWindow dropped its duplicate hit-test /
drag math + state and delegates to the core; web wires the same calls.
Tests: sectionPlaneCount add/clear/cap (Catch2, 125); web smoke "click a surface
cuts geometry, clear restores" exercises the shared gizmo + 3-MRT pick (11/11).
Desktop object-pick / marquee unaffected; BonsaiViewer builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Make orbit/pan/select mouse bindings pure data owned by ViewportCore so both
hosts and every preset share one source of truth, and add a "Web" preset. This
rounds out the matrix: the desktop gains a web-style scheme and the web inherits
all presets, with no per-platform hardcoding.
- Core: NavBindings { orbit, pan, select button + modifier } + setNavPreset
("blender" default | "rhino" | "revit" | "web") + navBindings(). Select is
preset-driven too (was hardcoded LMB) so "web" moves it to RMB. web = orbit
LMB, pan MMB, select RMB (LMB drag orbits with no click/drag ambiguity; RMB
click-selects / drag-marquees). NavMod uses "Plain" not "None" (X11 #defines
None to 0L).
- Desktop ViewportWindow: applyNavPreset sources the core table (mapped to Qt);
marquee-arm / single-pick dispatch keys off select_button_. Default stays
blender → no behaviour change.
- Desktop config: AppSettings::NavPreset gains Web + navPresetName(); the
Settings dialog lists it. This also FIXES a pre-existing gap — the preset combo
was persisted but never applied (only WGPU_NAV_PRESET env worked). MainWindow
now applies the persisted preset at startup (env override still wins) and live
on navPresetChanged, so all four presets actually work from the dialog.
- Web main_web: classifyPress routes the pressed button through navBindings()
(orbit/pan/select), defaulting to the "web" preset; context menu already
suppressed so RMB is free.
Tests: setNavPreset table (Catch2, 123 total); web smoke select tests use RMB.
BonsaiViewer builds; 9/9 web smoke.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coverage had lagged the recent feature work. Add both layers:
- test_viewport_camera (Catch2, headless): constructs ViewportCore with a mock
ViewportHost — construction/teardown touch no GPU (the wgpu teardown lives in
releaseWgpuModelGpuData, only reached with models loaded), and the camera ops
are pure — so it unit-tests the SHARED fly math fast and deterministically:
flyMove (forward step, 5x boost, opposing-key cancel, dt clamp, QE along +Z,
degenerate-pitch stays finite), flyLook (turn-in-place pins the eye, pitch
clamp), flyAdjustSpeed (x1.25/notch, [0.05,1000] clamp), toggleXray, and
hideSelected/showAll. Links the built IfcViewerCore (ViewportCore.cpp is the
monster TU that can't compile standalone). +9 cases → 122 desktop.
- smoke.spec.mjs (Playwright): fly (enter → W moves the camera → Esc exits),
x-ray (toggle translucency on/off), and hide-after-pick, driving the exported
C hooks end-to-end. +3 cases → 9 web.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>