Kitware/vtk-js

[Bug] InteractiveOrientationWidget stuck in active state

Antti-Palola opened this issue ยท 11 comments

Bug description

InteractiveOrientationWidget stops the main view from working anymore. Can be fixed by clicking on the widget again.

Steps to reproduce

Go to https://kitware.github.io/vtk-js/examples/InteractiveOrientationWidget.html

  • Start a quick mouse drag from orientation widget and end the drag outside the widget in the canvas area.
  • OrientationMarkerWidget triggers orientation change but keeps active state
  • Try to rotate model
  • Model is stuck

Detailed Behavior

This often happens if you want to spin your model around and start the drag too close to the widget.

getActive returns true and thus the mouse event is aborted.

Expected Behavior

The widget never gets stuck in active mode.

The same kind of behavior can be caused by starting a normal model rotate and ending the drag outside browser: model will stay in rotation mode. Could this be improved by ending LeftButtonPress event on "drags outside"? I guess this event is somehow virtually created from pointerup/pointerdown events.

Environment

  • vtk.js version: latest
  • Browsers: any
  • OS: any

I do not reproduce the error on Windows/Chrome + Windows/Firefox. Can you record a video with your mouse cursor ?

Here you go.

Macos 14.6.1
Chrome 29.0.6668.70

Screen.Recording.2024-10-11.at.9.38.26.mov.webm

Can you confirm it is a mac only problem ?

Do you reproduce the issue with a mouse ? touch pad ?
(I know there are some subtle differences with Mac mouse events)

I'm pretty sure this happens on linux and windows too, but we'll test other OSes and pointer devices on that same example and get back to you.

OS: Windows 11
Nvidia GPU
Mouse

Enregistrement.de.l.ecran.2024-10-11.101047.mp4

I still can't reproduce on my Windows laptop :-(

This happen on both FF & Chrome, note that i can still zoom in/out with the mousewheel but not able to rotate the cone

Can this be approached with a more code review heavy debugging. Wherever the internals of the mouse press handlers are created, is there a possibility that the mouseup is somehow misplaced?

I don't have a reproduction since I don't know exactly how, but it seems to happen even without touching the orientation widget. The view is stuck several times a day on my normal usage.

Four other people at our company have reported this issue.

Found an easy way to cause sticking. If you run the example locally after editing the widget bounds to not be 0.45 times the input bounds, it's like 50% times stuck when dragging from model and mouseUpping on top of indicator.

export function createInteractiveOrientationWidget(bounds) {
const widget = vtkInteractiveOrientationWidget.newInstance();
widget.placeWidget(bounds);
widget.setBounds(bounds.map((v) => v * 0.45));
return widget;

export function createInteractiveOrientationWidget(bounds) {
  const widget = vtkInteractiveOrientationWidget.newInstance();
  widget.placeWidget(bounds);
  widget.setBounds(bounds.map((v) => v));

  return widget;
}
stuck_interactiveOrientationWidget_safari.webm

MacOS 14.6, Safari 17.6 & FF 130 & Chrome 129

I've isolated the problem. There are two parts to it: how the orientation widget operates with its own renderer, and how the widget manager behaves when there are multiple renderers.

In the orientation widget example, there are two renderers: the primary one that fills the screen and contains the cone. This renderer sits in the background. The second renderer takes up a small portion of the lower left part of the viewport and contains the widget. Importantly, the boundary of this smaller renderer is larger than the orientation widget.

The vtkWidgetManager is associated to the orientation widget renderer. This means widget selection and deselection events only occur for mouse actions inside the corner renderer. When a mouse move event occurs on top of the widget, the widget is activated. When the mouse moves off of the widget but remains inside the corner renderer, the widget is deactivated.

The bug manifests when the mouse moves fast enough such that the first mouse move event occurs on top of the widget, then the next mouse move event occurs outside of the small renderer. Since no mouse move event occurred inside the small renderer but off the widget, the widget is not deactivated. No clicking is needed; just position the mouse on the widget, then move the mouse as quickly as you can to the cone. If successful, you can't move the cone anymore.

One possible solution to this is in handleEvent. If the event should not be handled, then deactivate all widgets associated with the widget manager. (Or at minimum, deactivate all widgets if the mouse event is on a different renderer.) I think this makes sense; if you're interacting on one renderer, you don't expect to be interact with widgets on another renderer.

const handleEvent = async (callData, fromTouchEvent = false) => {
if (
!model.isAnimating &&
model.pickingEnabled &&
callData.pokedRenderer === model._renderer
) {
const callID = Symbol('UpdateSelection');
model._currentUpdateSelectionCallID = callID;
await updateSelection(callData, fromTouchEvent, callID);
}
};
.