ifcviewer: section-plane cut tool on web — shared gizmo, true-face pick, drag/Del

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>
This commit is contained in:
Dion Moult
2026-07-03 17:31:02 +10:00
parent 9ad10c009b
commit da5c0b7991
14 changed files with 1093 additions and 536 deletions
+22
View File
@@ -372,3 +372,25 @@ test('RMB marquee drag box-selects (Web preset)', async ({ page }) => {
expect(hiddenAfter, 'marquee was not hidden after release').toBe(true);
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
});
test('section tool: click a surface cuts geometry, clear restores', async ({ page }) => {
const gpuErrors = [];
page.on('console', (m) => { if (/Uncaptured WebGPU error|is invalid/i.test(m.text())) gpuErrors.push(m.text()); });
await ready(page);
const box = await page.locator('#viewer-canvas').boundingBox();
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
const before = await shot(page);
await page.evaluate(() => window.Module._toggle_section_c());
expect(await page.evaluate(() => window.Module._section_is_active_c())).toBe(1);
// Section tool claims LMB: a left click on the model drops a cut.
await page.mouse.click(cx, cy);
await page.waitForTimeout(700); // async surface pick + add plane + render
const cut = await shot(page);
expect(Buffer.compare(before, cut), 'section cut did not change the render').not.toBe(0);
await page.evaluate(() => window.Module._clear_section_c());
await page.waitForTimeout(400);
const cleared = await shot(page);
expect(Buffer.compare(cleared, cut), 'clearing cuts did not change the render').not.toBe(0);
expect(gpuErrors, gpuErrors.join('\n')).toEqual([]);
});