PrivatepickPrivatepickPrivatenodePrivatenextPrivate_PrivateraycasterPrivatendcPrivate_Private_Private_Private_Private_Private_Private_Private_Private_Private_Private_Private_PrivateschedulerPrivate_Private_Private_Private_Private_Optional predicate gating whether picks should fire. Defaults to always-true. App wires this to overlayManager visibility so we skip the pick entirely when no hover tooltip would display the result.
Private_Explicit picks ignore hover/view invalidation until their result handler finishes.
Private_Private_PrivatepostOptional post-processing reference for lens distortion correction.
PrivaterendererPrivatecapabilitiesPrivatecameraPrivateonPrivate_Order-stable signature of the effectively-visible registered set at
the last pick-buffer render. Visibility can flip WITHOUT any of the
dirty-marking events firing (layers-panel toggles, embedder API) —
the visibility gate would then serve a stale cached buffer (a
re-shown layer would be unpickable until the next camera move or
commit). performPick recomputes and compares before trusting the
cache.
Number of registered pick nodes.
Monotonic generation counter for pick validity (issue #1917).
Advances on EVERY event that can make an already-delivered PickResult
no longer describe what is under the cursor: markDirty() (camera move
via the controls' change, window resize, perspective ↔ ortho swap,
a FOV edit via projection-changed, the layers-panel invalidator),
onMouseMove(), onMouseLeave(), dispose(), and the start of each
performPick. A consumer that caches a result alongside this value can
tell, in O(1) and synchronously, whether the cache still describes
reality — which is what makes click-to-act possible without a fresh GPU
readback (and therefore without spending the browser's transient user
activation on an await).
Deliberately NOT advanced by suppress: pointerdown → controls
start → suppress(true) is the FIRST half of an ordinary click, so
treating it as invalidating would make every click refuse itself.
Order-stable signature of the effectively-visible registered set, right now (issue #1917).
Companion to pickGeneration, and not redundant with it: hiding or
showing a layer from the layers panel changes what is pickable WITHOUT
dirtying the buffer — applyVisibility in ui/layers/layer-apply.ts
deliberately only calls requestRender(). performPick already
recomputes and compares this before trusting its cached buffer (see
_lastVisibleSig); exposing it lets a cached-result consumer make
the same check.
Allocate the next pick ID (incrementing counter, starts at 1). 0 = background.
The counter is checked against MAX_PICK_NODE_ID: past 2^24 the
f32 r channel of the pick buffer stops resolving consecutive ids, so
the readback would silently name the wrong node — and the vote key
(nodeId * VOTE_KEY_STRIDE) would leave the exact-integer range.
Unreachable in practice (it needs 16.7M node registrations in one
session), but the vote-key argument rests on this bound, so say so
loudly rather than assume it.
Register a main scene node and its picking shadow node.
Unregister a node by its pick ID and dispose its pick material.
Invalidate cached world-space AABBs. Call after geometry or matrixWorld changes; camera-only motion does NOT need this and should use markDirty alone.
OptionalpickId: number
Specific node to invalidate, or omit to drop all.
Set the predicate gating whether picks fire. Used by app.ts to skip picking when no hover overlay is visible (no consumer for the result).
Private_Dispose the material(s) attached to a pick mesh. Handles the rare
material: array case so a future custom-multi-material pick node
doesn't leak shaders.
Read-only snapshot of settle-scheduler timestamps and registration
count. Exposed for E2E tests (the hover-tooltip spec polls
lastPickFiredTime to verify a pick fired without reaching into
private fields). Timestamps are performance.now() values; 0
means "never". Not part of the production API surface — treat as
an observability hook, not an interaction point.
Drop all node registrations without disposing the pick
materials. Used after a WebGL context-loss event: the pick
materials' shader programs are already invalid (the context they
were compiled against is gone), and calling dispose() on them
would throw on some drivers. The caller (NodeFactory.rebuildAfterContextRestore)
is responsible for re-registering every scene node afterward,
which produces fresh pick materials against the new context.
Pick materials are registered with materialManager.register(...)
at construction (see node-factory.ts). Without unregistering
them here, repeated context-restore cycles accumulate stale
references in the materialManager registry — camera-uniform
updates would target dead materials and the
getStats().totalRegistered count grows unboundedly. We
unregister WITHOUT disposing (calls
materialManager.unregister(material) not dispose(material)),
matching the "no-dispose during context loss" contract for
visible materials.
Distinct from unregisterNode(id) which intentionally disposes
the pick material when removing a single live node.
Update camera reference (e.g., after perspective ↔ orthographic swap).
Routed through markDirty rather than setting _dirty directly:
swapping the camera reprojects every element on screen, so besides
re-rendering the pick buffer it must also advance the generation counter
and fade the now-stale tooltip. Setting _dirty alone left an
already-delivered PickResult looking valid — the same family of bug as
an FOV edit (#1916) — so a click after an ortho toggle with a stationary
cursor acted on whatever used to be under it (#1917).
Set post-processing reference for lens distortion correction.
Invalidate the cached pick buffer. Call when camera, geometry, or viewport changes. Fades the current hover overlay (matches the drag-suppression UX: while the camera is moving, tooltips hide). While a recent explicit tap pick is in flight, keep its delivery authoritative and defer that fade for at most EXPLICIT_PICK_FADE_GUARD_MS; the tap describes the frame where the finger lifted, while a stalled handler eventually fades. The rAF scheduler will fire a fresh pick once everything has been still for HOVER_SETTLE_MS.
Invalidate ONLY the cached canvas rect. Call on page scroll / layout
shifts that move the canvas without changing the 3D view: the rect
(from getBoundingClientRect()) maps event.clientX/Y into
canvas-local pick coordinates, so a stale rect after a scroll would
offset every pick. Unlike markDirty this does NOT re-render
the pick buffer or fade the tooltip — the view is unchanged, only the
canvas's screen position moved. The next mousemove lazily recomputes
the rect.
We also drop any pending settle pick: its stored coordinate is already canvas-local (converted at mousemove time against the now-stale rect), so firing it after the scroll would pick the wrong spot. Cancelling lets the next mousemove re-arm with a fresh, correctly-mapped coordinate.
Suppress or resume picking (e.g. during orbit/pan/zoom interactions).
true cancels any pending rAF; false re-enables the scheduler
and re-arms it when there's a pending cursor position. The re-arm
matters for "orbit-and-release without moving the mouse": the
camera dirtied during the suppressed window, so once it settles
for HOVER_SETTLE_MS the rAF tick fires the camera-settle re-pick
naturally — no mouse wiggle required.
Handle mouse move. Records the cursor position, fades any visible tooltip, and arms the settle scheduler. Performs zero picking work directly — the actual pick fires from the rAF loop once both the mouse and the pick buffer have been still for HOVER_SETTLE_MS.
Note: we do NOT early-return while suppressed (orbit/pan/zoom). The scheduler still tracks the latest cursor position during suppression (without scheduling a pick) so the re-pick on release uses where the cursor actually is, not a stale pre-orbit position.
Pick NOW at a viewport position — the touch counterpart of the
hover-settle path. A finger never hovers, so a tap has nothing to settle:
it bypasses the scheduler (cancelling any pending settle so the same
point is not picked twice) and runs the pick directly. Resolves after
the result has been delivered through onPickResult AND that handler
has finished (or the result was dropped as stale by the sequence guard),
so the caller can read the picked-element cache immediately afterwards.
The wait matters: the app's handler stores the cache only after an
asynchronous label fetch, so resolving on delivery alone would hand a
tap the cache as it was BEFORE its own pick landed. Honours the same setShouldPick gate as a
hover pick: with no consumer there is nothing to pick for.
Cursor left the canvas. Drop the pending position so the camera-settle re-pick path doesn't fire a stale pick when the cursor isn't even over the viewer, and cancel any pending rAF.
Clean up all resources — render target, pick materials, scene.
PrivatehasPrivateperformPerform a pick at the given screen coordinates.
If the pick buffer is dirty (camera/geometry/resize changed), re-renders all effectively visible registered nodes to the cached buffer first. Otherwise just reads from the cached buffer — zero GPU cost on hover.
PrivatedeliverHand a result to onPickResult, containing both synchronous throws and
async rejections so a failing handler is logged rather than leaked.
Returns a promise that settles once the handler has finished, so the awaited delivery in performPick — which is what lets pickAt resolve only after the picked-element cache is written — gets the same containment as the fire-and-forget fades.
The Promise.resolve is load-bearing, not ceremony — it is the narrowed
type's restatement of the truthiness guard the containment helper was
written with. The handler type is not enforced at runtime, and on a
callback that returns nothing .catch throws a TypeError that the
catch below would misreport as a handler failure while silently dropping
the wait that pickAt depends on.
PrivatecomputeSee _lastVisibleSig. Map iteration order is insertion-stable.
PrivaterenderPrivatereadbackRead back the 5x5 pick buffer and perform brightness-weighted majority voting. Returns the winning PickResult or null if all pixels are background.
Async readback (readRenderTargetPixelsAsync) works on both
WebGLRenderer and WebGPURenderer in r185. The 1-frame latency
on hover is documented in PICKING_DESIGN.md.
onPickResultis asynchronous: the app handler fetches the label / image / key before it stores the picked-element cache, so pickAt awaits the handler's promise before resolving — a caller that reads the cache "right after the pick" would otherwise race the fetch. The hover path voids the pick promise instead, so nothing there waits on it.