Skip to content

Implement client-side snapping#13531

Open
markschlosseratbentley wants to merge 20 commits into
mainfrom
snapping
Open

Implement client-side snapping#13531
markschlosseratbentley wants to merge 20 commits into
mainfrom
snapping

Conversation

@markschlosseratbentley

@markschlosseratbentley markschlosseratbentley commented May 29, 2026

Copy link
Copy Markdown
Contributor

Description

Adds Scene.snap(windowPosition, width, height) which returns the best geometry hit (edges preferred) in a pixel region around the cursor (default 25×25), with a world-space position. Returns a SceneSnapResult: { object, isEdge, position, screenPosition }. The API is tagged @experimental so it can evolve based on feedback.

The behavior approximates the Nearest MicroStation snap mode documented here.

Nearest: Tentative point snaps to point on the element nearest to the pointer.

Screen.Recording.2026-05-29.at.1.18.35.PM.mov

How it works

Snapping runs as a dedicated offscreen pass, fully decoupled from Scene.pick. The normal pick framebuffer stays RGBA8 (zero diff vs main); plain pick/pickPosition/drillPick/pickAsync pay no edge-rasterization cost.

The snap pass renders into its own RGBA32F framebuffer. Each pixel carries:

  • R: pick ID (uint32 repacked from RGBA8)
  • G: isEdge flag
  • B: linear eye-space depth (-v_positionEC.z)
  • A: unused

The snap payload lives entirely in the derived-command layer (command.snapId alongside the unchanged command.pickId); there are no ifdefs in shared shaders. New files Snapping.js and SnapFramebuffer.js hold everything specific to snapping (payload format, selection policy, world-position math).

During the snap pass, edges render at 1 px (independent of the color-pass width) in all three edgeDisplayMode values, including SURFACES_ONLY, where they're visually suppressed but still snappable.

Hit selection is occlusion-gated, edges-beat-surfaces, closest-to-cursor. See selectBestHit in Snapping.js. The winning hit's eye-space depth is unprojected into the returned world position.

Only Model-pipeline primitives (3D Tiles, glTF models) are snappable. Scene.snap returns undefined (with a one-time warning) when float color attachments are unsupported (requires WebGL2 / EXT_color_buffer_float).

Possible future enhancements (deferred under @experimental)

  • Target filtering (an objectsToExclude equivalent). Plain Scene.pick has no exclusion either (only the ray-based APIs do) so this is not a stabilization blocker.
  • A snapAsync analog (mirroring the pick/pickAsync split).
  • Additional snap modes (see Consider alternate snap modes for client-side snapping #13548).

Issue number and link

Related: #13548 (brainstorming for additional snap modes)

Testing plan

Author checklist

  • I have submitted a Contributor License Agreement
  • I have added my name to CONTRIBUTORS.md
  • I have updated CHANGES.md with a short summary of my change
  • I have added or updated unit tests to ensure consistent code coverage
  • I have updated the inline documentation, and included code examples where relevant
  • I have performed a self-review of my code

AI acknowledgment

  • I used AI to generate content in this PR
  • If yes, I have reviewed the AI-generated content before submitting

If yes, I used the following Tools(s) and/or Service(s):

GitHub Copilot

If yes, I used the following Model(s):

Claude Opus 4.7
Claude Opus 4.8
Claude Fable 5

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for the pull request, @markschlosseratbentley!

✅ We can confirm we have a CLA on file for you.

@markschlosseratbentley

Copy link
Copy Markdown
Contributor Author

Filed an issue to brainstorm other snap modes: #13548

@markschlosseratbentley

Copy link
Copy Markdown
Contributor Author

Multi-frustum snapping dev sandcastle added:

Screenshot 2026-06-18 at 1 08 17 PM

@markschlosseratbentley
markschlosseratbentley marked this pull request as ready for review July 14, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants