← ALL NOTES
METHOD·2026.08·5 MIN READ
Designing Whole-Item Movement for 3D Measurements
3d-visualization3d-design
The Problem

Scanat lets users duplicate measurements. After drawing a line or an area on a 3D model, they can copy it and move the duplicate elsewhere instead of measuring again from scratch.

The duplicate needs to move as one object. The design question was simple: what should the user drag to move it?

A length measurement has two real geometric endpoints and a floating label that shows the result, such as "328mm." Either can trigger whole-item movement, but they optimize for different things. Dragging an endpoint gives direct control over where that point lands. Dragging the label avoids introducing another mode.

The PM and I initially preferred different options. Once we compared the reasoning behind them, the trade-off became clearer: reduce mode overhead, or preserve precise endpoint placement.

Trade-offs
DimensionScheme A: drag the endpointScheme B: drag the label
Precise placementStrong. The pointer stays attached to a real geometric point, so the release position is the endpoint position.Weaker. The label is offset from the endpoint, so placement requires visual compensation.
Mode overheadRequires a distinction between endpoint adjustment and whole-item movement.No extra mode. Endpoints and labels already represent different gestures.
UI costNeeds an entry point for switching modes.No additional UI.
Dragged objectA real geometric endpoint.A rendered display label.

Both schemes behave the same for single-point adjustment: dragging an endpoint only changes that point. The only decision is what triggers whole-item movement.

Scheme B is simpler because it avoids a mode. Scheme A is more precise when the task is to move a specific endpoint from one location to another.

Final Approach

We implemented both gestures in a working prototype and compared them on iPad, iPhone, and desktop browsers. The final approach focused on improving Scheme A: keep the endpoint as the object being moved, but change the mode control from always visible to on demand.

  • Hidden by default.
  • Opened from a Move action in the bottom toolbar.
  • Automatically opened after duplication, with "Move Whole Item" selected.
  • Collapsed once dragging begins or another measurement is selected.

The design no longer depended on whether a mode should exist. The useful question was whether that mode needed to stay visible when it was not relevant.

Interaction Model

The two schemes are mutually exclusive, so the state model should express that relationship directly.

The scene uses moveScheme: 'endpoint' | 'label' to select the whole-item gesture. moveMode: 'endpoint' | 'whole' only applies when the endpoint scheme is active, and decides whether an endpoint drag adjusts one point or moves the whole measurement.

moveScheme = 'label'                     moveScheme = 'endpoint'
  │                                        │
  ├─ drag endpoint → adjust one point      ├─ moveMode = 'endpoint' → adjust one point
  └─ drag label    → move whole item       └─ moveMode = 'whole'    → move whole item

An earlier implementation modeled these states as independent booleans, which allowed conflicting combinations where both interaction paths could become active. The hierarchy removes that intermediate state.

The same interaction meaning also does not require identical mechanics across input devices:

  • Desktop: click to grab → move → click again to drop.
  • Touch: long press for 450ms → drag while holding → release to drop.

Both use the same translation and surface-snapping logic. Only the transition into the dragging state differs by input type.

Real-Device Testing

Testing on real devices exposed two issues that were easy to miss in static mockups.

1. Text contrast after a background change. The move-mode panel changed from a dark pill to a white floating card, but its title kept the old light-gray text color. The interaction worked correctly; the label was simply difficult to read on the new background. Container changes need to include any text colors that depend on that container.

2. A new gesture overlapping an existing behavior. The original spec said "tap empty space to close the panel." In the 3D scene, however, tapping empty space was already used to deselect the current measurement.

We kept those behaviors coupled: deselecting also closes the panel. The broader point is that phrases such as "tap empty space" should be checked against the rest of the scene before being treated as a new gesture.

Takeaways
  1. Define the gesture semantics before choosing the draggable object. First decide whether the user is adjusting one point or moving a whole object.
  2. Mutually exclusive interactions need an explicit state hierarchy. Do not model exclusive modes as independent states that can accidentally coexist.
  3. 3D interactions need real-device validation. Static mockups do not reveal all conflicts between input mechanics, existing scene gestures, and the physical feel of movement.
Design Discussion

I initially misunderstood the PM's argument. I thought the point was that an endpoint was simply a more meaningful object to grab. The actual concern was more specific: when users move a measurement, they may be trying to place one particular endpoint precisely at a new location.

Once we compared the schemes against that requirement, the precision advantage of endpoint dragging became clear.

A useful habit in design discussions is to restate the other person's reasoning before arguing against it. A sound conclusion still depends on both sides working from the same premise.

Engineering Notes

Opening the panel after duplication. Duplicating a measurement first selects the new copy and resets moveMode to 'endpoint'. Under the endpoint scheme, it then switches the mode to 'whole'. Rather than using a useEffect on selectedId to infer why selection changed, the UI relies on the callbacks' existing order: selection closes the panel first, and the following mode change reopens it only for the duplicate path.

JAVASCRIPT
setSelected(copy.id);
if (state.moveScheme === 'endpoint') {
  state.moveMode = 'whole';
  callbacks.onMoveModeChange?.('whole');
}

Closing the panel when dragging begins. Single-point adjustment and whole-item movement both pass through beginEndpointOrWholeMove(). The panel-close side effect therefore lives at that shared entry point instead of being repeated in mouse and long-press handlers.

JAVASCRIPT
function beginEndpointOrWholeMove(id, index, clientPoint) {
  callbacks.onMoveInteractionStart?.();
  if (state.moveScheme === 'endpoint' && state.moveMode === 'whole') {
    // whole-item movement
  } else {
    // single-point adjustment
  }
}
CONTINUE READING
Translating Figma Component Properties into Code Props2026.09Engineering a Figma Design System into Production Assets2026.09