Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/143.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Remote rendering 3.3d] sync camera to server on orientation widget
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ 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 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);

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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

/**
Expand Down Expand Up @@ -137,13 +144,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) => {
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;
}
Expand All @@ -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. */
Expand Down Expand Up @@ -336,3 +349,28 @@ 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();
});
});
16 changes: 16 additions & 0 deletions src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ 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();
});

// Bounding-box ids are stashed for attachSceneGraph, which is the
// point at which the live sceneGraph (needed by BoundingBoxWidget)
// becomes available.
Expand Down
21 changes: 21 additions & 0 deletions src/ansys/visor/visor-client/src/wasm/CameraGestureTracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
*/

/**
Expand Down Expand Up @@ -190,6 +195,22 @@ 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}
*/
Expand Down
11 changes: 11 additions & 0 deletions src/ansys/visor/visor-client/src/wasm/VtkScene.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ 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}
Expand Down
Loading