From d55137f68ec1a8a18fade53f14ad0c21030fc5b5 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Thu, 24 Sep 2026 07:44:35 -0700 Subject: [PATCH 1/5] sync camera to server on orientation widget --- .../jest-tests/CameraGestureTracker.test.js | 24 +++++++++++ .../WasmRendererWidgetTriggers.test.tsx | 43 ++++++++++++++++++- .../visor-client/src/renderer/WasmRenderer.ts | 31 +++++++++++++ .../src/wasm/CameraGestureTracker.js | 35 +++++++++++++++ .../visor/visor-client/src/wasm/VtkScene.js | 17 ++++++++ 5 files changed, 148 insertions(+), 2 deletions(-) diff --git a/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js b/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js index 4895803e..5b8a33ba 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js +++ b/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js @@ -211,6 +211,30 @@ describe('CameraGestureTracker', () => { expect(onSettled).toHaveBeenCalledWith('programmatic'); }); + // ---- the orientation widget's mark -------------------------------------- + + test('a camera event followed by the widget mark within 300 ms reports gesture', () => { + // The orientation widget's face click raises its camera events inside + // wasm, with no DOM input of any kind, and its end-of-interaction + // event is delivered across the wasm bridge -- so the mark can arrive + // *after* the camera events it belongs to. That is the order written + // here, and it is the one that fails if noteWidgetGesture marks + // nothing: with the settle already pending, only the retroactive + // branch can still reach this report. + // + // The mark-first order is deliberately not a second test here. It + // travels the impulse window, which 'an event within 300 ms of a + // wheel reports gesture' above already pins through the same code. + tracker.noteCameraEvent(); + jest.advanceTimersByTime(299); + + tracker.noteWidgetGesture(); + jest.advanceTimersByTime(300); + + expect(onSettled).toHaveBeenCalledTimes(1); + expect(onSettled).toHaveBeenCalledWith('gesture'); + }); + // ---- listener management and teardown ---------------------------------- test('the remover returned by addSettledListener stops reports', () => { diff --git a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx index 0ce6a698..362bd748 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx @@ -103,7 +103,14 @@ function makeFakeWasmObjects() { SetOrigin: jest.fn(async () => undefined), SetNormal: jest.fn(async () => undefined), }; - return { actor, property, widget }; + // The orientation widget is its own double, not the shared `widget` + // above. Both are observed on `EndInteractionEvent`, and one double for + // both cannot tell those two registrations apart: the plane's own test + // asserts exactly one such registration, and would see two. + const orientationWidget = { + observe: jest.fn(), + }; + return { actor, property, widget, orientationWidget }; } /** @@ -137,6 +144,10 @@ async function makeRenderer( canvasDiv: document.createElement('div'), render: jest.fn(), clearObserversAndEventListeners: jest.fn(), + // The tracker itself is `#private` to VtkScene, so what the renderer + // can reach is this one passthrough, and this is what the orientation + // registration is pinned against. + noteWidgetGesture: jest.fn(), camera, getVtkObject: (wasmId: number) => { switch (wasmId) { @@ -144,6 +155,8 @@ async function makeRenderer( return objects.actor; case PROPERTY_ID: return objects.property; + case ORIENTATION_WIDGET_ID: + return objects.orientationWidget; default: return objects.widget; } @@ -156,7 +169,7 @@ async function makeRenderer( ); const sceneGraph = makeSceneGraphDouble(actorIds); renderer.attachSceneGraph(sceneGraph as unknown as VisorSceneNodeExtended); - return { renderer, camera, sceneGraph, ...objects }; + return { renderer, camera, scene, sceneGraph, ...objects }; } /** A sender that records its calls and resolves. */ @@ -336,3 +349,29 @@ describe('WasmRenderer reports the cross-section plane on the end-of-drag event' expect(sender).not.toHaveBeenCalled(); }); }); + +describe('WasmRenderer marks an orientation-widget move as a gesture', () => { + // As above, the event does not exist under jsdom, so what is pinned is + // the *registration*: which event the mark is bound to, that there is + // exactly one of it on the orientation widget, and that the callback + // marks and sends nothing. Whether the wasm widget invokes that event at + // all is MC-I6's subject and no gate reaches it. + test('the orientation widget is observed once on EndInteractionEvent and the callback marks a widget gesture', async () => { + const sender = makeSender(); + const { scene, orientationWidget } = await makeRenderer(sender); + + const endCalls = orientationWidget.observe.mock.calls.filter( + (call) => call[0] === 'EndInteractionEvent' + ); + expect(endCalls).toHaveLength(1); + expect(scene.noteWidgetGesture).not.toHaveBeenCalled(); + + await endCalls[0][1](); + + expect(scene.noteWidgetGesture).toHaveBeenCalledTimes(1); + // The mark carries no payload and triggers no send: the report stays + // the settle's, through the unchanged sync_camera path. + expect(sender).not.toHaveBeenCalled(); + }); +}); + diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index d61fff6d..61b0a2ea 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -96,6 +96,37 @@ export class WasmRenderer implements IRenderer { }); }); + /** + * Attribute an orientation-widget camera move to the user. + * + * Clicking a face of the orientation cube moves the camera entirely + * inside wasm: no button is held on `canvasDiv`, no wheel turns and no + * z/r key is pressed, so `CameraGestureTracker` sees only camera + * events with no input and the whole move settles as `programmatic`. + * The server then drops it, and the move is lost on the next load. + * Story 3.2's camera input inventory named the drag, the z and r keys + * and the wheel, and missed this one. + * + * `EndInteractionEvent`, not `InteractionEvent`, for the reason the + * plane report gives above: the per-motion event fires many times + * across one interaction. + * + * The callback **marks and returns**. It sends nothing, reads no + * camera and is not `async`: the report is still the settle's, through + * the unchanged `sync_camera` path, with the unchanged payload. What + * this changes is the one word that path already carries. + * + * The widget animates the camera over `AnimatorTotalFrames` frames + * (20 by default, which the server-side `VisorOrientationWidget` does + * not alter). Neither that duration nor whether the end event precedes + * or follows the animation can be determined from this tree, and + * neither has to be: see `CameraGestureTracker.noteWidgetGesture`. + */ + const orientationWidget = vtkScene.getVtkObject(annotation.widgets.orientationWidgetId); + orientationWidget.observe('EndInteractionEvent', () => { + vtkScene.noteWidgetGesture(); + }); + // Bounding-box ids are stashed for attachSceneGraph, which is the // point at which the live sceneGraph (needed by BoundingBoxWidget) // becomes available. diff --git a/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js b/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js index 694c2147..cfc15806 100644 --- a/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js +++ b/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js @@ -25,6 +25,11 @@ * the camera event can arrive before the tracker has noticed the input. * To cover that, a wheel event or a z/r keydown arriving while a report is * pending marks that report `gesture` as well. + * + * - The orientation widget is not a DOM input at all: its face click reaches + * the camera inside wasm. It is marked explicitly, through + * `noteWidgetGesture`, from the observer `WasmRenderer` registers on the + * widget's `EndInteractionEvent`. */ /** @@ -190,6 +195,36 @@ export default class CameraGestureTracker { this.#settleTimer = setTimeout(this.#reportSettled, CAMERA_SETTLE_MS); }; + /** + * The orientation widget's end-of-interaction mark. + * + * Called from the `EndInteractionEvent` observer registered in + * `WasmRenderer`'s constructor, by way of `VtkScene.noteWidgetGesture`. + * A face click on the orientation cube moves the camera, but it involves + * no button held on `canvasDiv`, no wheel and no z/r key, so without this + * the whole move settles as `programmatic` and the server drops it. + * + * Deliberately the *same* window and stickiness as a wheel notch or a z/r + * press -- it delegates to `#markImpulse` rather than opening a window of + * its own -- so the settle constant keeps one meaning. That matters more + * here than it does for the wheel: the widget animates the camera over + * `AnimatorTotalFrames` frames (20, the VTK default, which the server-side + * `VisorOrientationWidget` leaves alone), and how long that runs, and + * whether the end event is invoked before or after it, cannot be + * determined from this tree. Neither has to be known, because `#sawGesture` + * is sticky: every frame restarts the settle, so the mark survives to the + * one report however many frames follow it, and a mark arriving *after* + * the last frame is caught by `#markImpulse`'s pending-report branch. + * + * Carries no camera data and sends nothing. The report itself is still the + * settle's, unchanged. + * + * @return {void} + */ + noteWidgetGesture = () => { + this.#markImpulse(); + }; + /** * @return {void} */ diff --git a/src/ansys/visor/visor-client/src/wasm/VtkScene.js b/src/ansys/visor/visor-client/src/wasm/VtkScene.js index 81d71a0e..867ab659 100644 --- a/src/ansys/visor/visor-client/src/wasm/VtkScene.js +++ b/src/ansys/visor/visor-client/src/wasm/VtkScene.js @@ -159,6 +159,23 @@ export default class VtkScene { addCameraSettledListener = (handler) => { return this.#cameraGestureTracker.addSettledListener(handler); }; + /** + * Mark the settle window now open as a user gesture, on behalf of a wasm + * widget whose interaction is not a DOM input this scene can see. + * + * The orientation widget is the one caller: `WasmRenderer` registers an + * `EndInteractionEvent` observer on it and calls this from there. The + * tracker is `#private` and this is its only way out; a field, not a + * method assigned later, because `getInstanceAsync` freezes the instance. + * + * The `?.` matches the `ModifiedEvent` call site above: a scene without a + * tracker drops the mark rather than throwing. + * + * @return {void} + */ + noteWidgetGesture = () => { + this.#cameraGestureTracker?.noteWidgetGesture(); + }; /** * @param {(actorId:number,ctrlKey:boolean,shiftKey:boolean,normX:number,normY:number)=>void} handler * @return {()=>void} From 63c615f4d085538ab31fc670ed54b9c350e50172 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:54:14 +0000 Subject: [PATCH 2/5] chore: adding changelog file 143.added.md [dependabot-skip] --- doc/changelog.d/143.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/143.added.md diff --git a/doc/changelog.d/143.added.md b/doc/changelog.d/143.added.md new file mode 100644 index 00000000..b7247c34 --- /dev/null +++ b/doc/changelog.d/143.added.md @@ -0,0 +1 @@ +[Remote rendering 3.3d] sync camera to server on orientation widget From c0d604714fbbea29815a0e2161130e60bd5e1e5d Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Thu, 24 Sep 2026 09:01:17 -0700 Subject: [PATCH 3/5] clean up docstrings --- .../jest-tests/CameraGestureTracker.test.js | 15 +++------- .../visor-client/src/renderer/WasmRenderer.ts | 29 +++++-------------- .../src/wasm/CameraGestureTracker.js | 28 +++++------------- .../visor/visor-client/src/wasm/VtkScene.js | 12 ++------ 4 files changed, 21 insertions(+), 63 deletions(-) diff --git a/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js b/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js index 5b8a33ba..318512f5 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js +++ b/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js @@ -214,17 +214,10 @@ describe('CameraGestureTracker', () => { // ---- the orientation widget's mark -------------------------------------- test('a camera event followed by the widget mark within 300 ms reports gesture', () => { - // The orientation widget's face click raises its camera events inside - // wasm, with no DOM input of any kind, and its end-of-interaction - // event is delivered across the wasm bridge -- so the mark can arrive - // *after* the camera events it belongs to. That is the order written - // here, and it is the one that fails if noteWidgetGesture marks - // nothing: with the settle already pending, only the retroactive - // branch can still reach this report. - // - // The mark-first order is deliberately not a second test here. It - // travels the impulse window, which 'an event within 300 ms of a - // wheel reports gesture' above already pins through the same code. + // The widget's mark can arrive after the camera events it belongs + // to, so this exercises the retroactive branch of noteWidgetGesture. + // The mark-first order is covered by the wheel test above, via the + // same #markImpulse code path. tracker.noteCameraEvent(); jest.advanceTimersByTime(299); diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index 61b0a2ea..b18f9054 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -99,28 +99,13 @@ export class WasmRenderer implements IRenderer { /** * Attribute an orientation-widget camera move to the user. * - * Clicking a face of the orientation cube moves the camera entirely - * inside wasm: no button is held on `canvasDiv`, no wheel turns and no - * z/r key is pressed, so `CameraGestureTracker` sees only camera - * events with no input and the whole move settles as `programmatic`. - * The server then drops it, and the move is lost on the next load. - * Story 3.2's camera input inventory named the drag, the z and r keys - * and the wheel, and missed this one. - * - * `EndInteractionEvent`, not `InteractionEvent`, for the reason the - * plane report gives above: the per-motion event fires many times - * across one interaction. - * - * The callback **marks and returns**. It sends nothing, reads no - * camera and is not `async`: the report is still the settle's, through - * the unchanged `sync_camera` path, with the unchanged payload. What - * this changes is the one word that path already carries. - * - * The widget animates the camera over `AnimatorTotalFrames` frames - * (20 by default, which the server-side `VisorOrientationWidget` does - * not alter). Neither that duration nor whether the end event precedes - * or follows the animation can be determined from this tree, and - * neither has to be: see `CameraGestureTracker.noteWidgetGesture`. + * Clicking a face of the cube moves the camera entirely inside wasm, + * with no DOM input `CameraGestureTracker` can see, so without this + * mark the move settles as `programmatic` and the server drops it. + * `EndInteractionEvent` (not `InteractionEvent`) fires once per + * interaction rather than per animation frame. The callback only + * marks the gesture; the report itself is still the settle's, + * unchanged, via `sync_camera`. */ const orientationWidget = vtkScene.getVtkObject(annotation.widgets.orientationWidgetId); orientationWidget.observe('EndInteractionEvent', () => { diff --git a/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js b/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js index cfc15806..b9895371 100644 --- a/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js +++ b/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js @@ -196,28 +196,14 @@ export default class CameraGestureTracker { }; /** - * The orientation widget's end-of-interaction mark. + * The orientation widget's end-of-interaction mark, called via + * `VtkScene.noteWidgetGesture` from the `EndInteractionEvent` observer in + * `WasmRenderer`. A face click involves no button, wheel or z/r key, so + * without this mark the move settles as `programmatic`. * - * Called from the `EndInteractionEvent` observer registered in - * `WasmRenderer`'s constructor, by way of `VtkScene.noteWidgetGesture`. - * A face click on the orientation cube moves the camera, but it involves - * no button held on `canvasDiv`, no wheel and no z/r key, so without this - * the whole move settles as `programmatic` and the server drops it. - * - * Deliberately the *same* window and stickiness as a wheel notch or a z/r - * press -- it delegates to `#markImpulse` rather than opening a window of - * its own -- so the settle constant keeps one meaning. That matters more - * here than it does for the wheel: the widget animates the camera over - * `AnimatorTotalFrames` frames (20, the VTK default, which the server-side - * `VisorOrientationWidget` leaves alone), and how long that runs, and - * whether the end event is invoked before or after it, cannot be - * determined from this tree. Neither has to be known, because `#sawGesture` - * is sticky: every frame restarts the settle, so the mark survives to the - * one report however many frames follow it, and a mark arriving *after* - * the last frame is caught by `#markImpulse`'s pending-report branch. - * - * Carries no camera data and sends nothing. The report itself is still the - * settle's, unchanged. + * Delegates to `#markImpulse` to reuse the same window and retroactive + * stickiness as a wheel notch or z/r press, so a mark landing before or + * after the camera events it belongs to is still caught. * * @return {void} */ diff --git a/src/ansys/visor/visor-client/src/wasm/VtkScene.js b/src/ansys/visor/visor-client/src/wasm/VtkScene.js index 867ab659..adce7015 100644 --- a/src/ansys/visor/visor-client/src/wasm/VtkScene.js +++ b/src/ansys/visor/visor-client/src/wasm/VtkScene.js @@ -161,15 +161,9 @@ export default class VtkScene { }; /** * Mark the settle window now open as a user gesture, on behalf of a wasm - * widget whose interaction is not a DOM input this scene can see. - * - * The orientation widget is the one caller: `WasmRenderer` registers an - * `EndInteractionEvent` observer on it and calls this from there. The - * tracker is `#private` and this is its only way out; a field, not a - * method assigned later, because `getInstanceAsync` freezes the instance. - * - * The `?.` matches the `ModifiedEvent` call site above: a scene without a - * tracker drops the mark rather than throwing. + * widget (the orientation widget) whose interaction is not a DOM input + * this scene can see. `WasmRenderer` calls this from its + * `EndInteractionEvent` observer, since `#cameraGestureTracker` is private. * * @return {void} */ From 38505b54d84e4e66ab930bde32ce93bb0f46442c Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Thu, 24 Sep 2026 09:02:07 -0700 Subject: [PATCH 4/5] prettier fix --- .../src/jest-tests/WasmRendererWidgetTriggers.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx index 362bd748..89be55cf 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx @@ -374,4 +374,3 @@ describe('WasmRenderer marks an orientation-widget move as a gesture', () => { expect(sender).not.toHaveBeenCalled(); }); }); - From 8bee63f8b83733af2863cd40acb76724d06aa0ae Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Fri, 25 Sep 2026 06:11:41 -0700 Subject: [PATCH 5/5] replace orientation widget observer with canvas mouseup mark --- .../visor/visor-client/src/VisorFrontend.tsx | 5 +- .../jest-tests/CameraGestureTracker.test.js | 47 +++++++++++++++--- .../WasmRendererWidgetTriggers.test.tsx | 49 +++++-------------- .../visor-client/src/renderer/WasmRenderer.ts | 20 ++------ .../src/wasm/CameraGestureTracker.js | 30 +++++------- .../visor/visor-client/src/wasm/VtkScene.js | 11 ----- 6 files changed, 73 insertions(+), 89 deletions(-) diff --git a/src/ansys/visor/visor-client/src/VisorFrontend.tsx b/src/ansys/visor/visor-client/src/VisorFrontend.tsx index aa59dcc3..d78a6a33 100644 --- a/src/ansys/visor/visor-client/src/VisorFrontend.tsx +++ b/src/ansys/visor/visor-client/src/VisorFrontend.tsx @@ -52,7 +52,10 @@ export class VisorFrontend { } = getPromiseResolver(); let darkMode: boolean = true; - // TODO: uncomment these lines when the dataset addition bug is fixed + // TODO: uncomment these lines once VTK is upgraded past 9.6.1. getVtkObject + // on the orientation widget serializes the widget's graph, and the client-only + // ids the proxy allocates then collide with the next add_dataset's objects. + // Fixed after 9.6.1 by SetAllocateIdsDescending. // const orientationWidget = vtkScene.getVtkObject(vtkInfo.orientationWidgetWasmId); // use "void" here to suppress the "no await" IDE warning // void orientationWidget.SetShouldResetCamera(false); diff --git a/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js b/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js index 318512f5..3c638d11 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js +++ b/src/ansys/visor/visor-client/src/jest-tests/CameraGestureTracker.test.js @@ -211,23 +211,54 @@ describe('CameraGestureTracker', () => { expect(onSettled).toHaveBeenCalledWith('programmatic'); }); - // ---- the orientation widget's mark -------------------------------------- + // ---- a release on the wasm canvas --------------------------------------- - test('a camera event followed by the widget mark within 300 ms reports gesture', () => { - // The widget's mark can arrive after the camera events it belongs - // to, so this exercises the retroactive branch of noteWidgetGesture. - // The mark-first order is covered by the wheel test above, via the - // same #markImpulse code path. - tracker.noteCameraEvent(); + // A face click reaches the camera only after the release, with no button + // held, so the release is what arms the window. All three raise the camera + // event at 299 ms, inside the 300 ms a release arms, so what separates + // them is solely whether the release armed it. + + test('a release on the canvas, then a camera event within 300 ms, reports gesture', () => { + canvas.dispatchEvent(new MouseEvent('mouseup', { button: 0, bubbles: true })); jest.advanceTimersByTime(299); + tracker.noteCameraEvent(); - tracker.noteWidgetGesture(); jest.advanceTimersByTime(300); expect(onSettled).toHaveBeenCalledTimes(1); expect(onSettled).toHaveBeenCalledWith('gesture'); }); + test('a release whose target is not the canvas, then the same, reports programmatic', () => { + // A child of canvasDiv, so it is on the capture path and reaches the + // same listener: only the target check can tell it from the canvas. + const overlayButton = document.createElement('button'); + canvasDiv.appendChild(overlayButton); + + overlayButton.dispatchEvent(new MouseEvent('mouseup', { button: 0, bubbles: true })); + jest.advanceTimersByTime(299); + tracker.noteCameraEvent(); + + jest.advanceTimersByTime(300); + + expect(onSettled).toHaveBeenCalledTimes(1); + expect(onSettled).toHaveBeenCalledWith('programmatic'); + }); + + test('a non-bubbling release on the canvas, then the same, reports programmatic', () => { + // Exactly what applyMouseEvent fires at the canvas on every press, + // release and mouseout. Its target is the canvas, so without the + // bubbles check a press alone would mark this settle a gesture. + canvas.dispatchEvent(new MouseEvent('mouseup', { button: 0, bubbles: false })); + jest.advanceTimersByTime(299); + tracker.noteCameraEvent(); + + jest.advanceTimersByTime(300); + + expect(onSettled).toHaveBeenCalledTimes(1); + expect(onSettled).toHaveBeenCalledWith('programmatic'); + }); + // ---- listener management and teardown ---------------------------------- test('the remover returned by addSettledListener stops reports', () => { diff --git a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx index 89be55cf..7ac675ef 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx @@ -103,14 +103,7 @@ function makeFakeWasmObjects() { SetOrigin: jest.fn(async () => undefined), SetNormal: jest.fn(async () => undefined), }; - // The orientation widget is its own double, not the shared `widget` - // above. Both are observed on `EndInteractionEvent`, and one double for - // both cannot tell those two registrations apart: the plane's own test - // asserts exactly one such registration, and would see two. - const orientationWidget = { - observe: jest.fn(), - }; - return { actor, property, widget, orientationWidget }; + return { actor, property, widget }; } /** @@ -144,23 +137,19 @@ async function makeRenderer( canvasDiv: document.createElement('div'), render: jest.fn(), clearObserversAndEventListeners: jest.fn(), - // The tracker itself is `#private` to VtkScene, so what the renderer - // can reach is this one passthrough, and this is what the orientation - // registration is pinned against. - noteWidgetGesture: jest.fn(), camera, - getVtkObject: (wasmId: number) => { + // A jest.fn, so the ids the renderer asks for are recorded: the + // orientation widget's id must never be among them. + getVtkObject: jest.fn((wasmId: number) => { switch (wasmId) { case ACTOR_ID: return objects.actor; case PROPERTY_ID: return objects.property; - case ORIENTATION_WIDGET_ID: - return objects.orientationWidget; default: return objects.widget; } - }, + }), }; const renderer = await WasmRenderer.createAsync( scene as unknown as VtkScene, @@ -350,27 +339,15 @@ describe('WasmRenderer reports the cross-section plane on the end-of-drag event' }); }); -describe('WasmRenderer marks an orientation-widget move as a gesture', () => { - // As above, the event does not exist under jsdom, so what is pinned is - // the *registration*: which event the mark is bound to, that there is - // exactly one of it on the orientation widget, and that the callback - // marks and sends nothing. Whether the wasm widget invokes that event at - // all is MC-I6's subject and no gate reaches it. - test('the orientation widget is observed once on EndInteractionEvent and the callback marks a widget gesture', async () => { - const sender = makeSender(); - const { scene, orientationWidget } = await makeRenderer(sender); - - const endCalls = orientationWidget.observe.mock.calls.filter( - (call) => call[0] === 'EndInteractionEvent' - ); - expect(endCalls).toHaveLength(1); - expect(scene.noteWidgetGesture).not.toHaveBeenCalled(); +describe('WasmRenderer builds no proxy of the orientation widget', () => { + // Building a proxy of the orientation widget serializes its graph, and the + // client-only ids that allocates collide with the next add_dataset's on + // VTK 9.6.1. The gesture mark is taken at the DOM level instead. + test('the orientation widget id is never requested through getVtkObject', async () => { + const { scene } = await makeRenderer(makeSender()); - await endCalls[0][1](); + const requestedIds = scene.getVtkObject.mock.calls.map((call) => call[0]); - expect(scene.noteWidgetGesture).toHaveBeenCalledTimes(1); - // The mark carries no payload and triggers no send: the report stays - // the settle's, through the unchanged sync_camera path. - expect(sender).not.toHaveBeenCalled(); + expect(requestedIds).not.toContain(ORIENTATION_WIDGET_ID); }); }); diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index b18f9054..6b901986 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -96,21 +96,11 @@ export class WasmRenderer implements IRenderer { }); }); - /** - * Attribute an orientation-widget camera move to the user. - * - * Clicking a face of the cube moves the camera entirely inside wasm, - * with no DOM input `CameraGestureTracker` can see, so without this - * mark the move settles as `programmatic` and the server drops it. - * `EndInteractionEvent` (not `InteractionEvent`) fires once per - * interaction rather than per animation frame. The callback only - * marks the gesture; the report itself is still the settle's, - * unchanged, via `sync_camera`. - */ - const orientationWidget = vtkScene.getVtkObject(annotation.widgets.orientationWidgetId); - orientationWidget.observe('EndInteractionEvent', () => { - vtkScene.noteWidgetGesture(); - }); + // No proxy of the orientation widget: getVtkObject on it serializes the + // widget's graph, and the client-only ids that allocates collide with the + // next add_dataset's objects. Fixed after VTK 9.6.1 by + // SetAllocateIdsDescending; on that upgrade the gesture mark can move back + // onto the widget's EndInteractionEvent, from CameraGestureTracker's. // Bounding-box ids are stashed for attachSceneGraph, which is the // point at which the live sceneGraph (needed by BoundingBoxWidget) diff --git a/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js b/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js index b9895371..1307b941 100644 --- a/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js +++ b/src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js @@ -27,9 +27,9 @@ * pending marks that report `gesture` as well. * * - The orientation widget is not a DOM input at all: its face click reaches - * the camera inside wasm. It is marked explicitly, through - * `noteWidgetGesture`, from the observer `WasmRenderer` registers on the - * widget's `EndInteractionEvent`. + * the camera inside wasm, and only after the release. A real, bubbling + * release on the canvas itself therefore arms the window too, which covers + * that face click without the tracker knowing the widget exists. */ /** @@ -78,6 +78,15 @@ export default class CameraGestureTracker { }; const onMouseUp = /**@param {MouseEvent} e*/ (e) => { this.#heldButtons.delete(e.button); + // A release on the canvas arms the window, so a move that only + // reaches the camera afterwards -- an orientation-widget face + // click -- still reports as a gesture. Both checks are needed: + // UI over the canvas is not the canvas, and `applyMouseEvent` + // fires non-bubbling `mouseup`s at the canvas on every press, + // release and mouseout, which must not arm anything. + if (e.target === canvas && e.bubbles) { + this.#markImpulse(); + } }; // A `mouseout` is the existing sticky-mousedown release, and a window // `blur` means the page no longer owns the input. Both clear *every* @@ -195,21 +204,6 @@ export default class CameraGestureTracker { this.#settleTimer = setTimeout(this.#reportSettled, CAMERA_SETTLE_MS); }; - /** - * The orientation widget's end-of-interaction mark, called via - * `VtkScene.noteWidgetGesture` from the `EndInteractionEvent` observer in - * `WasmRenderer`. A face click involves no button, wheel or z/r key, so - * without this mark the move settles as `programmatic`. - * - * Delegates to `#markImpulse` to reuse the same window and retroactive - * stickiness as a wheel notch or z/r press, so a mark landing before or - * after the camera events it belongs to is still caught. - * - * @return {void} - */ - noteWidgetGesture = () => { - this.#markImpulse(); - }; /** * @return {void} diff --git a/src/ansys/visor/visor-client/src/wasm/VtkScene.js b/src/ansys/visor/visor-client/src/wasm/VtkScene.js index adce7015..81d71a0e 100644 --- a/src/ansys/visor/visor-client/src/wasm/VtkScene.js +++ b/src/ansys/visor/visor-client/src/wasm/VtkScene.js @@ -159,17 +159,6 @@ export default class VtkScene { addCameraSettledListener = (handler) => { return this.#cameraGestureTracker.addSettledListener(handler); }; - /** - * Mark the settle window now open as a user gesture, on behalf of a wasm - * widget (the orientation widget) whose interaction is not a DOM input - * this scene can see. `WasmRenderer` calls this from its - * `EndInteractionEvent` observer, since `#cameraGestureTracker` is private. - * - * @return {void} - */ - noteWidgetGesture = () => { - this.#cameraGestureTracker?.noteWidgetGesture(); - }; /** * @param {(actorId:number,ctrlKey:boolean,shiftKey:boolean,normX:number,normY:number)=>void} handler * @return {()=>void}