wgpu: section cutting tool — K hotkey, click-to-add, drag arrow, Esc/Del

Mirrors GL ViewportWindow's section tool end-to-end.

Hotkeys

  K            toggle the tool active
  Shift+K      clearSectionPlanes
  Esc          deactivate the tool
  Del/Bksp     remove the most recently added plane (tool-active only)
  LMB click    pickSurfaceAt → addSectionPlaneAtSurface (no modifier)
  LMB drag    on the arrow gizmo: slide the plane along its normal

State

FrameUniforms grows by clip_count (i32) + clip_planes[6] (vec4). The
six-plane cap matches GL's MaxSectionPlanes. WGSL pads via three
scalar i32s instead of a vec3<i32> so the array starts at offset 144
to match the tightly-packed C++ struct (240 B) — vec3 would have
forced clip_planes to 160 and broken the binding-size match.

is_section_clipped(world) in WGSL evaluates all active planes and
returns true if any signals "on the positive side". Both main and
pick fragments discard with it so cuts are visible AND selection is
consistent — you can't pick something the user can't see.

Surface pick

Pick pass now emits 2 color targets: R32UInt object_id at @location(0)
and RGBA16F packed world-space normal at @location(1). Normal is
packed × 0.5 + 0.5 so unsigned-ish halfs keep the sign. pickSurfaceAt
reads both via 1×1 texel copies (RGBA16F is a color format with no
full-mip restriction, unlike Depth32Float). World position comes from
ray-AABB intersection against the picked instance's AABB — equally
accurate for "drop a plane where I clicked" and dodges the Depth32Float
copy-extent rule entirely. The pick normal is decoded into the
per-fragment surface normal so the plane lands perpendicular to the
actual triangle (not the AABB face).

Plane gizmo

Identical geometry to GL's renderSectionPlanes: 2×2 m quad outline
(white) + 1 m arrow shaft along +n (yellow-orange) + 4 arrow-head
diagonals. Drawn inside the main MSAA pass with depth LessEqual + no
depth write. Lines are rendered as screen-space-expanded thick quads
with fwidth-based AA, same technique the axis indicator uses, so the
gizmo reads against busy BIM geometry rather than disappearing as
1-px hairlines.

Drag

mousePressEvent claims a plain-LMB press if it hits an arrow gizmo
(12 logical-px grab radius, distance to the (origin, origin+n)
screen-space segment). The drag handler projects the cursor delta
onto the screen-space axis and converts to metres via
delta·axis / |axis|² — same formula GL uses. Mid-drag camera moves
keep working because the projection re-runs every frame against the
press-time origin.

Closes the click-to-add + drag halves of #30 / #60.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-30 15:18:34 +10:00
parent e6c6df905a
commit 8173074050
2 changed files with 1002 additions and 31 deletions
File diff suppressed because it is too large Load Diff