Skip to content

feat(editable-layers) Snap to edges#623

Open
jake-goldsmith wants to merge 10 commits into
visgl:masterfrom
jake-goldsmith:SnapToEdges
Open

feat(editable-layers) Snap to edges#623
jake-goldsmith wants to merge 10 commits into
visgl:masterfrom
jake-goldsmith:SnapToEdges

Conversation

@jake-goldsmith
Copy link
Copy Markdown

@jake-goldsmith jake-goldsmith commented May 1, 2026

#586

This PR comprises two main changes - firstly it adds support for snapping mode to a much wider variety of modes and secondly adds support for snapping to edges.

It turned out different modes had different requirements for snapping:

  • What to snap to?
  • When to allow snapping?

I added support for three different SnappingStrategys that modes can utilise (plus not supported):

Behavior What to snap to? When to allow snapping? Example modes
SourceSnappingStrategy Features which are not selected. (Note: all selected feature vertices get a pickable snapped source) While dragging when a snap source is picked. Translate
DragSnappingStrategy Any feature which isn't the feature selected by the pointer down pick. (Note: can snap to other selected features - these modes also manage their own edit handles) When the lastMoveEvent has a pointerDownPick Modify, ResizeCircle
ClickSnappingStrategy Any feature which isn't a guide All times DrawLineString

Each strategy defines how to handle click events, how to handle movement events and how to draw guides which provides enough diversity of behaviour to meet the requirements above. Behaviour shared between different strategies lives in snapping-utils.ts.

I also added edge snapping which can be enabled by setting the edgeSnapping mode config to true.

Testing:

  • Added unit tests for some different scenarios and modes when snapping is enabled.
  • Tested manually using the advanced example.

Videos:

TranslateMode -

Translate.Mode.mp4

DrawLineStringMode -

DrawLineString.Mode.mp4

ModifyMode -

Modify.Mode.mp4

Alternative designs:

There are definitely cons to this design of specifying the behaviour on the GeoJsonEditMode because this adds extra complexity and it is also frustrating if people come up with their own custom modes which don't fit with one of these built-in behaviours.

It's also not ideal that to enable snapping you both need to wrap your mode in a SnappableMode and turn on enableSnapping. It should be sufficient to just make one change to turn on snapping.

An alternative design that we should consider is deprecating the pre-existing SnappableMode and starting from scratch. Instead of trying to apply snapping generically in a composite mode we could dictate that each mode is responsible of it's own snapping behaviour and then use gracefully constructed utility function to handle the duplicated logic where appropriate. This would give mode authors more control to specify how snapping should work specifically for that mode.

Considerations

  • 3D - I haven't verified how this approach works in 3D - I think likely not smoothly so that is something worth testing - is 3D support expected for modes in this library? I'm not sure if it is uniformly applied at the moment.
  • Performance - I will try and test this mode with more features next week to understand the performance characteristics. They are likely worse than the previous snapping mode, especially for edge snapping, because they apply more geospatial logic than before. If performance is not good we can consider investing in building an geospatial index to improve it.
  • Custom snapping pixel radius - This was recommended to be implemented in the issue but I don't think it is feasible without a big rewrite. Currently the code strongly relies on the contents of the picks and the pointerDownPicks of the lastMoveEvent and so if the snapPixelRadius the user specified diverges from the pickingRadius this will all need to be rewritten to compute the picks with the alternative distance rather than relying on the provided. Would be a big change!

Breaking Changes

Custom modes which used the SnappableMode (without extending one of the existing modes) will need to declare:

getSnappingStrategy() {
  return new SourceSnappingStrategy();
}

to get working snapping again.

@jake-goldsmith jake-goldsmith changed the title Snap to edges feat(editable-layers) Snap to edges May 7, 2026
Copy link
Copy Markdown
Collaborator

@charlieforward9 charlieforward9 left a comment

Choose a reason for hiding this comment

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

Thanks for pushing this forward. The feature direction is good, and the PR shows that different modes need different snapping behavior.

The main design question is whether this should remain a generic SnappableMode enhancement or become a broader mode-owned snapping API.

In the current implementation, modes expose getSnappingBehavior(), but SnappableMode still owns most of the behavior matrix: FromSnapSources / WhenDragging / Freehand, event rewriting, target exclusion, and guide rendering. That leaves the design partly centralized in SnappableMode and partly delegated to individual modes.

That split has a few implications:

  • If SnappableMode remains the public compatibility surface, it likely needs cleaner mode-level hooks so it can stay generic: e.g. how a mode defines snap sources, snap targets, event eligibility, and snap application.
  • If snapping becomes fully mode-owned, that is a larger public API shift. Consumers with custom modes may need to reimplement or migrate snapping behavior.
  • The second path likely needs a longer migration timeline, v9 deprecation warnings, and clearer v10 documentation.

There is also at least one concrete follow-up to check: TransformMode now wraps TranslateMode in SnappableMode, but existing priority logic checks for TranslateMode directly. That may change scale/translate interaction behavior unless the wrapper is accounted for.

Could you pick which direction you want to take here?

  1. Keep SnappableMode generic and refactor this PR so modes provide snapping policy through hooks/strategy.
  2. Treat this as the start of a mode-owned snapping API, with SnappableMode deprecation/migration planned separately.

@jake-goldsmith
Copy link
Copy Markdown
Author

@charlieforward9 I have updated the PR. I think the best direction to take is:

Keep SnappableMode generic and refactor this PR so modes provide snapping policy through hooks/strategy.

I have adapted the first version into a distinct SnappingStrategy interface to provide a simple hook that could be overriden by consumers if required.

/**
 * Encapsulates the snapping policy for a single edit mode.
 *
 * Each method is called by SnappableMode at the appropriate point in its
 * event pipeline.
 */
export interface SnappingStrategy {
  /**
   * Return the click event with snapped coordinates applied, or the original
   * event unchanged if snapping does not apply.
   */
  snapClickEvent(props: ModeProps<FeatureCollection>, event: ClickEvent): ClickEvent;

  /**
   * Return the movement event with snapped coordinates applied, or the original
   * event unchanged if snapping does not apply.
   */
  snapMovementEvent<T extends MovementEvent>(props: ModeProps<FeatureCollection>, event: T): T;

  /**
   * Returns the snapping guides for this snapping strategy.
   */
  getSnapGuides(props: ModeProps<FeatureCollection>): GuideFeatureCollection;
}

This means the behaviour is now divided as follows:

  • SnappableMode now handles only very generic interaction, delegating specific behaviour.
  • Different modes which implement SnappableEditMode can define their specific behaviour by setting a SnappingStrategy. Either one of the generic ones I have built or a completely custom one.

@jake-goldsmith
Copy link
Copy Markdown
Author

I have done some profiling of the utlity functions to try to get a sense of how fast or slow the snapping is. The results are fine for small geometries but do scale up as shapes get more complex since there isn't a lot of optimisation in the code at the moment e.g. indexing.

(All values are mean values in ms)

getSnapTargetHandles (vertex snapping)

Features \ Vertices 1 100 1,000
1 0.030 0.034 0.126
100 0.237 3.796 74.166
1,000 2.322 69.623 758.32

getSnapTargetHandles (edge snapping)

Features \ Vertices 1 100 1,000
1 0.097 0.042 0.118
100 0.604 5.731 79.059
1,000 6.506 154.97 941.49

getClosestSnapTargetHandle

Features \ Vertices 1 100 1,000
1 0.080 0.037 0.113
100 0.263 7.951 90.028
1,000 2.835 94.553 1,258.35

On the order of magnitude of 1000 vertices this seems reasonable and is probably workable around 10,000. But if we need to go up to 100,000 then it's going to take about a second to update snapping guides.

@charlieforward9 would be curious for your take on what the non-functional requirements for latency etc. should be for this feature since I imagine you have more of an insight on the different use cases for this library.

To improve the latency the natural path seems to be to bring in the rbush library for a spatial index and the challenge will be building and maintaining the spatial index if the features change.

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.

3 participants