Skip to content

Commit 8bee63f

Browse files
committed
replace orientation widget observer with canvas mouseup mark
1 parent 38505b5 commit 8bee63f

6 files changed

Lines changed: 73 additions & 89 deletions

File tree

‎src/ansys/visor/visor-client/src/VisorFrontend.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ export class VisorFrontend {
5252
} = getPromiseResolver<UiScaffoldUtil>();
5353
let darkMode: boolean = true;
5454

55-
// TODO: uncomment these lines when the dataset addition bug is fixed
55+
// TODO: uncomment these lines once VTK is upgraded past 9.6.1. getVtkObject
56+
// on the orientation widget serializes the widget's graph, and the client-only
57+
// ids the proxy allocates then collide with the next add_dataset's objects.
58+
// Fixed after 9.6.1 by SetAllocateIdsDescending.
5659
// const orientationWidget = vtkScene.getVtkObject(vtkInfo.orientationWidgetWasmId);
5760
// use "void" here to suppress the "no await" IDE warning
5861
// void orientationWidget.SetShouldResetCamera(false);

‎src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js‎

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,23 +211,54 @@ describe('CameraGestureTracker', () => {
211211
expect(onSettled).toHaveBeenCalledWith('programmatic');
212212
});
213213

214-
// ---- the orientation widget's mark --------------------------------------
214+
// ---- a release on the wasm canvas ---------------------------------------
215215

216-
test('a camera event followed by the widget mark within 300 ms reports gesture', () => {
217-
// The widget's mark can arrive after the camera events it belongs
218-
// to, so this exercises the retroactive branch of noteWidgetGesture.
219-
// The mark-first order is covered by the wheel test above, via the
220-
// same #markImpulse code path.
221-
tracker.noteCameraEvent();
216+
// A face click reaches the camera only after the release, with no button
217+
// held, so the release is what arms the window. All three raise the camera
218+
// event at 299 ms, inside the 300 ms a release arms, so what separates
219+
// them is solely whether the release armed it.
220+
221+
test('a release on the canvas, then a camera event within 300 ms, reports gesture', () => {
222+
canvas.dispatchEvent(new MouseEvent('mouseup', { button: 0, bubbles: true }));
222223
jest.advanceTimersByTime(299);
224+
tracker.noteCameraEvent();
223225

224-
tracker.noteWidgetGesture();
225226
jest.advanceTimersByTime(300);
226227

227228
expect(onSettled).toHaveBeenCalledTimes(1);
228229
expect(onSettled).toHaveBeenCalledWith('gesture');
229230
});
230231

232+
test('a release whose target is not the canvas, then the same, reports programmatic', () => {
233+
// A child of canvasDiv, so it is on the capture path and reaches the
234+
// same listener: only the target check can tell it from the canvas.
235+
const overlayButton = document.createElement('button');
236+
canvasDiv.appendChild(overlayButton);
237+
238+
overlayButton.dispatchEvent(new MouseEvent('mouseup', { button: 0, bubbles: true }));
239+
jest.advanceTimersByTime(299);
240+
tracker.noteCameraEvent();
241+
242+
jest.advanceTimersByTime(300);
243+
244+
expect(onSettled).toHaveBeenCalledTimes(1);
245+
expect(onSettled).toHaveBeenCalledWith('programmatic');
246+
});
247+
248+
test('a non-bubbling release on the canvas, then the same, reports programmatic', () => {
249+
// Exactly what applyMouseEvent fires at the canvas on every press,
250+
// release and mouseout. Its target is the canvas, so without the
251+
// bubbles check a press alone would mark this settle a gesture.
252+
canvas.dispatchEvent(new MouseEvent('mouseup', { button: 0, bubbles: false }));
253+
jest.advanceTimersByTime(299);
254+
tracker.noteCameraEvent();
255+
256+
jest.advanceTimersByTime(300);
257+
258+
expect(onSettled).toHaveBeenCalledTimes(1);
259+
expect(onSettled).toHaveBeenCalledWith('programmatic');
260+
});
261+
231262
// ---- listener management and teardown ----------------------------------
232263

233264
test('the remover returned by addSettledListener stops reports', () => {

‎src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx‎

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,7 @@ function makeFakeWasmObjects() {
103103
SetOrigin: jest.fn(async () => undefined),
104104
SetNormal: jest.fn(async () => undefined),
105105
};
106-
// The orientation widget is its own double, not the shared `widget`
107-
// above. Both are observed on `EndInteractionEvent`, and one double for
108-
// both cannot tell those two registrations apart: the plane's own test
109-
// asserts exactly one such registration, and would see two.
110-
const orientationWidget = {
111-
observe: jest.fn(),
112-
};
113-
return { actor, property, widget, orientationWidget };
106+
return { actor, property, widget };
114107
}
115108

116109
/**
@@ -144,23 +137,19 @@ async function makeRenderer(
144137
canvasDiv: document.createElement('div'),
145138
render: jest.fn(),
146139
clearObserversAndEventListeners: jest.fn(),
147-
// The tracker itself is `#private` to VtkScene, so what the renderer
148-
// can reach is this one passthrough, and this is what the orientation
149-
// registration is pinned against.
150-
noteWidgetGesture: jest.fn(),
151140
camera,
152-
getVtkObject: (wasmId: number) => {
141+
// A jest.fn, so the ids the renderer asks for are recorded: the
142+
// orientation widget's id must never be among them.
143+
getVtkObject: jest.fn((wasmId: number) => {
153144
switch (wasmId) {
154145
case ACTOR_ID:
155146
return objects.actor;
156147
case PROPERTY_ID:
157148
return objects.property;
158-
case ORIENTATION_WIDGET_ID:
159-
return objects.orientationWidget;
160149
default:
161150
return objects.widget;
162151
}
163-
},
152+
}),
164153
};
165154
const renderer = await WasmRenderer.createAsync(
166155
scene as unknown as VtkScene,
@@ -350,27 +339,15 @@ describe('WasmRenderer reports the cross-section plane on the end-of-drag event'
350339
});
351340
});
352341

353-
describe('WasmRenderer marks an orientation-widget move as a gesture', () => {
354-
// As above, the event does not exist under jsdom, so what is pinned is
355-
// the *registration*: which event the mark is bound to, that there is
356-
// exactly one of it on the orientation widget, and that the callback
357-
// marks and sends nothing. Whether the wasm widget invokes that event at
358-
// all is MC-I6's subject and no gate reaches it.
359-
test('the orientation widget is observed once on EndInteractionEvent and the callback marks a widget gesture', async () => {
360-
const sender = makeSender();
361-
const { scene, orientationWidget } = await makeRenderer(sender);
362-
363-
const endCalls = orientationWidget.observe.mock.calls.filter(
364-
(call) => call[0] === 'EndInteractionEvent'
365-
);
366-
expect(endCalls).toHaveLength(1);
367-
expect(scene.noteWidgetGesture).not.toHaveBeenCalled();
342+
describe('WasmRenderer builds no proxy of the orientation widget', () => {
343+
// Building a proxy of the orientation widget serializes its graph, and the
344+
// client-only ids that allocates collide with the next add_dataset's on
345+
// VTK 9.6.1. The gesture mark is taken at the DOM level instead.
346+
test('the orientation widget id is never requested through getVtkObject', async () => {
347+
const { scene } = await makeRenderer(makeSender());
368348

369-
await endCalls[0][1]();
349+
const requestedIds = scene.getVtkObject.mock.calls.map((call) => call[0]);
370350

371-
expect(scene.noteWidgetGesture).toHaveBeenCalledTimes(1);
372-
// The mark carries no payload and triggers no send: the report stays
373-
// the settle's, through the unchanged sync_camera path.
374-
expect(sender).not.toHaveBeenCalled();
351+
expect(requestedIds).not.toContain(ORIENTATION_WIDGET_ID);
375352
});
376353
});

‎src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts‎

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -96,21 +96,11 @@ export class WasmRenderer implements IRenderer {
9696
});
9797
});
9898

99-
/**
100-
* Attribute an orientation-widget camera move to the user.
101-
*
102-
* Clicking a face of the cube moves the camera entirely inside wasm,
103-
* with no DOM input `CameraGestureTracker` can see, so without this
104-
* mark the move settles as `programmatic` and the server drops it.
105-
* `EndInteractionEvent` (not `InteractionEvent`) fires once per
106-
* interaction rather than per animation frame. The callback only
107-
* marks the gesture; the report itself is still the settle's,
108-
* unchanged, via `sync_camera`.
109-
*/
110-
const orientationWidget = vtkScene.getVtkObject(annotation.widgets.orientationWidgetId);
111-
orientationWidget.observe('EndInteractionEvent', () => {
112-
vtkScene.noteWidgetGesture();
113-
});
99+
// No proxy of the orientation widget: getVtkObject on it serializes the
100+
// widget's graph, and the client-only ids that allocates collide with the
101+
// next add_dataset's objects. Fixed after VTK 9.6.1 by
102+
// SetAllocateIdsDescending; on that upgrade the gesture mark can move back
103+
// onto the widget's EndInteractionEvent, from CameraGestureTracker's.
114104

115105
// Bounding-box ids are stashed for attachSceneGraph, which is the
116106
// point at which the live sceneGraph (needed by BoundingBoxWidget)

‎src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js‎

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
* pending marks that report `gesture` as well.
2828
*
2929
* - The orientation widget is not a DOM input at all: its face click reaches
30-
* the camera inside wasm. It is marked explicitly, through
31-
* `noteWidgetGesture`, from the observer `WasmRenderer` registers on the
32-
* widget's `EndInteractionEvent`.
30+
* the camera inside wasm, and only after the release. A real, bubbling
31+
* release on the canvas itself therefore arms the window too, which covers
32+
* that face click without the tracker knowing the widget exists.
3333
*/
3434

3535
/**
@@ -78,6 +78,15 @@ export default class CameraGestureTracker {
7878
};
7979
const onMouseUp = /**@param {MouseEvent} e*/ (e) => {
8080
this.#heldButtons.delete(e.button);
81+
// A release on the canvas arms the window, so a move that only
82+
// reaches the camera afterwards -- an orientation-widget face
83+
// click -- still reports as a gesture. Both checks are needed:
84+
// UI over the canvas is not the canvas, and `applyMouseEvent`
85+
// fires non-bubbling `mouseup`s at the canvas on every press,
86+
// release and mouseout, which must not arm anything.
87+
if (e.target === canvas && e.bubbles) {
88+
this.#markImpulse();
89+
}
8190
};
8291
// A `mouseout` is the existing sticky-mousedown release, and a window
8392
// `blur` means the page no longer owns the input. Both clear *every*
@@ -195,21 +204,6 @@ export default class CameraGestureTracker {
195204
this.#settleTimer = setTimeout(this.#reportSettled, CAMERA_SETTLE_MS);
196205
};
197206

198-
/**
199-
* The orientation widget's end-of-interaction mark, called via
200-
* `VtkScene.noteWidgetGesture` from the `EndInteractionEvent` observer in
201-
* `WasmRenderer`. A face click involves no button, wheel or z/r key, so
202-
* without this mark the move settles as `programmatic`.
203-
*
204-
* Delegates to `#markImpulse` to reuse the same window and retroactive
205-
* stickiness as a wheel notch or z/r press, so a mark landing before or
206-
* after the camera events it belongs to is still caught.
207-
*
208-
* @return {void}
209-
*/
210-
noteWidgetGesture = () => {
211-
this.#markImpulse();
212-
};
213207

214208
/**
215209
* @return {void}

‎src/ansys/visor/visor-client/src/wasm/VtkScene.js‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -159,17 +159,6 @@ export default class VtkScene {
159159
addCameraSettledListener = (handler) => {
160160
return this.#cameraGestureTracker.addSettledListener(handler);
161161
};
162-
/**
163-
* Mark the settle window now open as a user gesture, on behalf of a wasm
164-
* widget (the orientation widget) whose interaction is not a DOM input
165-
* this scene can see. `WasmRenderer` calls this from its
166-
* `EndInteractionEvent` observer, since `#cameraGestureTracker` is private.
167-
*
168-
* @return {void}
169-
*/
170-
noteWidgetGesture = () => {
171-
this.#cameraGestureTracker?.noteWidgetGesture();
172-
};
173162
/**
174163
* @param {(actorId:number,ctrlKey:boolean,shiftKey:boolean,normX:number,normY:number)=>void} handler
175164
* @return {()=>void}

0 commit comments

Comments
 (0)