Skip to content

fix(editable-layers): correct SelectionLayer polygon picking for elongated lassos#635

Open
llamington wants to merge 1 commit into
visgl:masterfrom
llamington:fix/selection-layer-polygon-without-pickobjects
Open

fix(editable-layers): correct SelectionLayer polygon picking for elongated lassos#635
llamington wants to merge 1 commit into
visgl:masterfrom
llamington:fix/selection-layer-polygon-without-pickobjects

Conversation

@llamington
Copy link
Copy Markdown

Summary

Fixes false-positive selection in SelectionLayer's polygon mode for elongated/diagonal/concave lassos by replacing the GPU-pick + buffered-blocker mechanism with a CPU point-in-polygon test.

The bug

When using SelectionLayer({selectionType: 'polygon', ...}), pins that sit outside the drawn lasso are returned in onSelect({pickingInfos}) as if they were inside. The failure is reliable for any lasso whose bounding box extends more than ~50 km past its own edges — long thin lassos, diagonal lassos, and concave shapes.

Repro

  1. Render a ScatterplotLayer of pins spread across a continent (e.g. the existing Manhattan demo, but at a wider zoom).
  2. Add a SelectionLayer with selectionType: 'polygon'.
  3. Draw a long thin lasso diagonally across the map.
  4. Inspect onSelect({pickingInfos}): pins clearly outside the drawn polygon are included.

Root cause

_selectPolygonObjects does this:

  1. Builds a "blocker" polygon — a turfBuffer(lasso, EXPANSION_KM = 50) polygon with the lasso subtracted (turfDifference).
  2. Calls deck.pickObjects({bbox: lassoBoundingBox, layerIds: [blockerId, ...layerIds]}) after a setTimeout(250) for the blocker to render.
  3. Filters the picks.

The blocker is what's meant to mask out pins outside the polygon. But:

  • The buffer is fixed at 50 km. Any lasso whose bbox extends >50 km past its edges has gaps where the blocker doesn't cover, and pins in those gaps get picked even though they're outside the lasso polygon.
  • The setTimeout(250) is a race; the source acknowledges it: // HACK, find a better way (line 139).
  • The picking framebuffer also drops overlapping pins at the same pixel (related issue, nebula.gl#297).

nebula.gl#132 ("Fix concave lasso select bug") has been open since 2018 with a noted TODO: "more proper implementation than the setTimeout hack." This PR is that implementation.

The fix

Replace the GPU-pick + blocker mechanism with a direct CPU point-in-polygon test:

  1. Take the drawn polygon coordinates.
  2. For every layer named in layerIds, iterate its data.
  3. Resolve each datum's position via layer.props.getPosition(...), with object.position / object.coordinates fallbacks.
  4. Run @turf/boolean-point-in-polygon (already a dependency) against the polygon.
  5. Emit a pickingInfo {object, layer, index} for each match.

The result is exact, synchronous, and free of the buffer, framebuffer, and timing constraints.

Scope

The new implementation supports layers exposing per-datum positions — ScatterplotLayer, IconLayer, TextLayer, ColumnLayer, etc. This matches:

  • The layer type in the docs example (ScatterplotLayer).
  • The layer type in examples/editable-layers/sf/app.tsx and examples/editable-layers/advanced/src/example.tsx (EditableGeoJsonLayer).

The previous implementation incidentally returned pickingInfos for PathLayer / PolygonLayer / ArcLayer via GPU picking, but the results were never meaningful — the 50 km blocker meant the lasso shape barely applied to those geometries anyway. Polygon-mode support for non-point layers was never documented, and no example demonstrates it. If maintainers want explicit support later, that's a separate feature with its own UX decisions (intersection vs containment for paths, etc.).

…gated lassos

The polygon selection path used `deck.pickObjects()` over the lasso's
bounding box, masked by a "blocker" PolygonLayer rendered as a 50 km
(`EXPANSION_KM`) buffer around the lasso. When the lasso's bbox
extended more than 50 km past its edges (elongated or diagonal lassos),
the blocker left gaps in the picking framebuffer and objects in those
gaps were incorrectly returned as selected. The approach also depended
on a `setTimeout(250)` to wait for the blocker to render and dropped
overlapping objects at the same pixel.

Replace the GPU-pick + blocker mechanism with a CPU point-in-polygon
test: iterate the data of every layer named in `layerIds`, resolve
each datum's position via `getPosition` (with `position` /
`coordinates` fallbacks), and emit pickingInfos for matches via
`@turf/boolean-point-in-polygon`. The test is exact, synchronous, and
unconstrained by buffer distance or framebuffer resolution.

Removes `EXPANSION_KM`, the blocker `PolygonLayer`, the
`pendingPolygonSelection` state, the `setTimeout`, and the
`@turf/buffer` / `@turf/difference` dependencies. Selection now
supports only layers exposing per-datum positions
(e.g. `ScatterplotLayer`, `IconLayer`, `ColumnLayer`) — matching the
layer types in the docs example and the repo's own usages.

Also addresses the long-standing concave-lasso bug filed as
nebula.gl#132.
return data.flatMap((object, index): SelectionPickingInfo[] => {
const position = extractPosition(layer, object, index, data);
if (position === null) return [];
if (!booleanPointInPolygon(point(position), selectionPolygon)) return [];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-geo may not all pass this conditional check

This works better than the current approach, so its not blocking, but these instances of turf should be triaged under a more careful pass - which may have been the original intention of the GPU-based selections?

CC @eKerney


type SelectionPickingInfo = Pick<PickingInfo, 'object' | 'layer' | 'index'>;

function extractPosition(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we want to extract a 3D position here for any reason? @felixpalmer

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