- title
- How should handles snap
- dated
- April-Juli '26
This labnote outlines a design & prototype of how snaps could work (and are currently implemented) in playbook. This document is meant to document the design decisions and tradeoffs, and to serve as a reference for future work.
This design makes the interactions around handles directly downstream of selection, which is similar to how handles work in most other editing tools. However, unlike most other tools, I made the draggable handles userspace entities rather than a special affordance. This is kind of weird, but I think it’s nesscesary, because it allows us to construct parts with handles in userspace.
When you select a part that has handles inside, they will become visible. This is done by setting the visibility property (which is available for all entities in the scenegraph) to peek.
When a component is selected, you can either move the whole component by dragging the pencil anywhere on screen, or, move an individual handle, by dragging it with the pencil.
When you drag a handle, any other handles that are snappable become temporarily visible. Snapping happens when you drag a handle close to another handle. Under the hood, this creates a snap propagator which maintains the snap constraint.
You can unsnap the handle by dragging it away again. The unsnapping is dependent on selection. For instance, if I snap together two line segments, and then select them both, I can move around the snapped point. If I only select one of the line segments, it will unsnap. Conceptually, I think of this as “holding” the handle inside the selection. If you want to make the snapping permanent, you can group the selected components. This will cause them to always be selected together.
You can also snap handles to curves. Under the hood, this creates a ‘point-on-curve’ propagator, which keeps the handle on the curve. You can now move, rotate and even deform the curve, and the handle will try to stay on the curve, maintaining it’s parametric position along the curve if possible.
Subtleties
On the surface this seems straightforward enough, but for this to feel good the logic is actually quite subtle:
- You can only snap to any handles that are:
- Currently
peeking - Inside a component that is a direct sibling of the selected component.
- But not to anything higher in the screen graph than the current focus
- And not to anything inside the current selection.
- Currently
- A handle can only snap to one thing at a time.
- Always prefer to snap to a handle over a curve, if it’s snapped to a curve, and there is a handle nearby, it should unsnap from the curve and snap to the handle instead.
The actual snap-to-position behaviour of the handle is done through a propagator. This, again requires some subtlety to feel good:
- When the snap propagator is created, it pulls the currently dragged handle towards the other handle, but leave the other handle where it was originally.
- If the user drags the handle too far away, we should let go and destroy the propagator.
- In any other case, the snap propagator basically acts like unification where any connected handles move together. In this case, the propagator should be optimized away, as all the snapped together handles are now behaving as one.
Conceptually we can implement this whole thing in terms of ‘tools’. This means that the selection gestures are essentially just shortcuts.
- A ‘move’ tool, which updates the position of a component
- A ‘snap’ tool, which takes two snappable entities, and creates a snap propagator between them
- An ‘unsnap’ tool, takes two snappable entities, and destroys the snap propagator between them
Trade-offs
This design takes some getting used to, but it allows you to quickly snap together components without needing to switch between different tools. It also makes sure the handles aren’t cluttering the screen when they aren’t needed. You can even unsnap overlapping shapes using the ‘pinprick’ selection gesture, which only every selects a single entity.
Other designs are possible, but I think this inevitably involves more complex gestures or switching between different tools/modes to differentiate, we explored different approaches in:
- crosscut: pause the system, to manually “pull appart” the overlapping handles (very heavy handed)
- habitat: directional ‘pull’ gesture in the direction of the line (a heuristic which isn’t always reliable)
- inkling: pseudomode (which we’re trying to avoid in playbook)