diff --git a/Sources/Interaction/Manipulators/MouseCameraTrackballPanManipulatorAutoCenter/index.js b/Sources/Interaction/Manipulators/MouseCameraTrackballPanManipulatorAutoCenter/index.js new file mode 100644 index 00000000000..76b7292ed85 --- /dev/null +++ b/Sources/Interaction/Manipulators/MouseCameraTrackballPanManipulatorAutoCenter/index.js @@ -0,0 +1,144 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkMouseCameraTrackballPanManipulator from 'vtk.js/Sources/Interaction/Manipulators/MouseCameraTrackballPanManipulator'; +import { mat4, vec3 } from 'gl-matrix'; + +// ---------------------------------------------------------------------------- +// Helper functions for center of rotation adjustment +// ---------------------------------------------------------------------------- + +/** + * Transforms a vector by the transformation delta between two matrices. + * + * @param {Object} tempObjects - Temporary matrices/vectors for computation + * @param {mat4} beforeMatrix - Matrix before transformation + * @param {mat4} afterMatrix - Matrix after transformation + * @param {Array} vector - Vector to transform [x, y, z] + * @returns {Array} Transformed vector [x, y, z] + */ +function transformVectorByTransformation( + tempObjects, + beforeMatrix, + afterMatrix, + vector +) { + const { matrixA, matrixB, newCenter } = tempObjects; + + // The view matrix from vtk.js is row-major, but gl-matrix expects column-major. + // We need to transpose them before use. + mat4.transpose(matrixA, beforeMatrix); + + mat4.transpose(matrixB, afterMatrix); + mat4.invert(matrixB, matrixB); + + // Compute delta transformation matrix + mat4.multiply(matrixA, matrixB, matrixA); + + vec3.transformMat4(newCenter, vector, matrixA); + return newCenter; +} + +/** + * Computes the new center of rotation based on camera movement. + * When the camera moves (pan), the center of rotation should move + * by the same transformation. + * + * @param {Object} tempObjects - Temporary matrices/vectors for computation + * @param {Object} renderer - VTK renderer + * @param {mat4} beforeCameraMatrix - Camera view matrix before movement + * @param {Array} oldCenterOfRotation - Previous center of rotation [x, y, z] + * @returns {Array} New center of rotation [x, y, z] + */ +function computeNewCenterOfRotation( + tempObjects, + renderer, + beforeCameraMatrix, + oldCenterOfRotation +) { + const cam = renderer.getActiveCamera(); + if (!cam || !beforeCameraMatrix) { + return oldCenterOfRotation; + } + const afterMatrixRowMajor = cam.getViewMatrix(); + + return transformVectorByTransformation( + tempObjects, + beforeCameraMatrix, + afterMatrixRowMajor, + oldCenterOfRotation + ); +} + +function getCameraMatrix(renderer, tempMatrix) { + const cam = renderer.getActiveCamera(); + if (cam) { + mat4.copy(tempMatrix, cam.getViewMatrix()); + return tempMatrix; + } + return null; +} + +function vtkMouseCameraTrackballPanManipulatorAutoCenter(publicAPI, model) { + model.classHierarchy.push('vtkMouseCameraTrackballPanManipulatorAutoCenter'); + + const tempCameraMatrix = mat4.create(); + const tempComputeObjects = { + matrixA: mat4.create(), + matrixB: mat4.create(), + newCenter: vec3.create(), + }; + + const superOnMouseMove = publicAPI.onMouseMove; + + publicAPI.onMouseMove = (interactor, renderer, position) => { + if (!position) { + return; + } + const beforeCameraMatrix = getCameraMatrix(renderer, tempCameraMatrix); + + superOnMouseMove(interactor, renderer, position); + + if (beforeCameraMatrix && model.center) { + const newCenter = computeNewCenterOfRotation( + tempComputeObjects, + renderer, + beforeCameraMatrix, + model.center + ); + publicAPI.setCenter(newCenter); + + const style = interactor.getInteractorStyle(); + if (style && style.setCenterOfRotation) { + style.setCenterOfRotation(newCenter); + } + } + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = {}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Inheritance + vtkMouseCameraTrackballPanManipulator.extend(publicAPI, model, initialValues); + + // Object specific methods + vtkMouseCameraTrackballPanManipulatorAutoCenter(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance( + extend, + 'vtkMouseCameraTrackballPanManipulatorAutoCenter' +); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Interaction/Style/InteractorStyleManipulator/api.md b/Sources/Interaction/Style/InteractorStyleManipulator/api.md index 6289e4e878c..00f527447c7 100644 --- a/Sources/Interaction/Style/InteractorStyleManipulator/api.md +++ b/Sources/Interaction/Style/InteractorStyleManipulator/api.md @@ -22,6 +22,8 @@ sent to it. Also, changing the CenterOfRotation during interaction i.e. after a button press but before a button up has no effect until the next button press. The default value is [0, 0, 0]. +For automatic center adjustment during panning operations, use `MouseCameraTrackballPanManipulatorAutoCenter` instead of the standard `MouseCameraTrackballPanManipulator`. + ### rotationFactor Set/Get the rotation factor. Propagates the rotation factor to the manipulators. diff --git a/Sources/Interaction/Style/InteractorStyleManipulator/index.js b/Sources/Interaction/Style/InteractorStyleManipulator/index.js index f5067e2b05f..2e73d6d5582 100644 --- a/Sources/Interaction/Style/InteractorStyleManipulator/index.js +++ b/Sources/Interaction/Style/InteractorStyleManipulator/index.js @@ -1,7 +1,6 @@ import macro from 'vtk.js/Sources/macros'; import { MouseButton } from 'vtk.js/Sources/Rendering/Core/RenderWindowInteractor/Constants'; import vtkInteractorStyle from 'vtk.js/Sources/Rendering/Core/InteractorStyle'; -import { mat4, vec3 } from 'gl-matrix'; const { vtkDebugMacro } = macro; const { States } = vtkInteractorStyle; @@ -135,77 +134,6 @@ function dollyByFactor(interactor, renderer, factor) { } } -function getCameraMatrix(renderer, tempMatrix) { - const cam = renderer.getActiveCamera(); - if (cam) { - mat4.copy(tempMatrix, cam.getViewMatrix()); - return tempMatrix; - } - return null; -} - -/** - * Transforms a vector by the transformation delta between two matrices. - * - * @param {Object} tempObjects - Temporary matrices/vectors for computation - * @param {mat4} beforeMatrix - Matrix before transformation - * @param {mat4} afterMatrix - Matrix after transformation - * @param {Array} vector - Vector to transform [x, y, z] - * @returns {Array} Transformed vector [x, y, z] - */ -function transformVectorByTransformation( - tempObjects, - beforeMatrix, - afterMatrix, - vector -) { - const { matrixA, matrixB, newCenter } = tempObjects; - - // The view matrix from vtk.js is row-major, but gl-matrix expects column-major. - // We need to transpose them before use. - mat4.transpose(matrixA, beforeMatrix); - - mat4.transpose(matrixB, afterMatrix); - mat4.invert(matrixB, matrixB); - - // Compute delta transformation matrix - mat4.multiply(matrixA, matrixB, matrixA); - - vec3.transformMat4(newCenter, vector, matrixA); - return newCenter; -} - -/** - * Computes the new center of rotation based on camera movement. - * When the camera moves (pan), the center of rotation should move - * by the same transformation to maintain consistent rotation behavior. - * - * @param {Object} tempObjects - Temporary matrices/vectors for computation - * @param {Object} renderer - VTK renderer - * @param {mat4} beforeCameraMatrix - Camera view matrix before movement - * @param {Array} oldCenterOfRotation - Previous center of rotation [x, y, z] - * @returns {Array} New center of rotation [x, y, z] - */ -function computeNewCenterOfRotation( - tempObjects, - renderer, - beforeCameraMatrix, - oldCenterOfRotation -) { - const cam = renderer.getActiveCamera(); - if (!cam || !beforeCameraMatrix) { - return oldCenterOfRotation; - } - const afterMatrixRowMajor = cam.getViewMatrix(); - - return transformVectorByTransformation( - tempObjects, - beforeCameraMatrix, - afterMatrixRowMajor, - oldCenterOfRotation - ); -} - // ---------------------------------------------------------------------------- // Static API // ---------------------------------------------------------------------------- @@ -224,14 +152,6 @@ function vtkInteractorStyleManipulator(publicAPI, model) { // Set our className model.classHierarchy.push('vtkInteractorStyleManipulator'); - // Initialize temporary objects to reduce garbage collection - const tempCameraMatrix = mat4.create(); - const tempComputeObjects = { - matrixA: mat4.create(), - matrixB: mat4.create(), - newCenter: vec3.create(), - }; - model.currentVRManipulators = new Map(); model.mouseManipulators = []; model.keyboardManipulators = []; @@ -584,23 +504,11 @@ function vtkInteractorStyleManipulator(publicAPI, model) { publicAPI.handleMouseMove = (callData) => { model.cachedMousePosition = callData.position; if (model.currentManipulator && model.currentManipulator.onMouseMove) { - const renderer = model.getRenderer(callData); - const beforeCameraMatrix = getCameraMatrix(renderer, tempCameraMatrix); - model.currentManipulator.onMouseMove( model._interactor, - renderer, + model.getRenderer(callData), callData.position ); - - const newCenter = computeNewCenterOfRotation( - tempComputeObjects, - renderer, - beforeCameraMatrix, - model.centerOfRotation - ); - publicAPI.setCenterOfRotation(newCenter); - publicAPI.invokeInteractionEvent(INTERACTION_EVENT); } }; @@ -765,7 +673,6 @@ function vtkInteractorStyleManipulator(publicAPI, model) { //---------------------------------------------------------------------------- publicAPI.handlePan = (callData) => { const renderer = model.getRenderer(callData); - const beforeCameraMatrix = getCameraMatrix(renderer, tempCameraMatrix); let count = model.gestureManipulators.length; let actionCount = 0; @@ -776,15 +683,8 @@ function vtkInteractorStyleManipulator(publicAPI, model) { actionCount++; } } - if (actionCount) { - const newCenter = computeNewCenterOfRotation( - tempComputeObjects, - renderer, - beforeCameraMatrix, - model.centerOfRotation - ); - publicAPI.setCenterOfRotation(newCenter); + if (actionCount) { publicAPI.invokeInteractionEvent(INTERACTION_EVENT); } }; diff --git a/Sources/Interaction/Style/InteractorStyleManipulatorRotateCameraCenter/example/index.js b/Sources/Interaction/Style/InteractorStyleManipulatorRotateCameraCenter/example/index.js new file mode 100644 index 00000000000..6be2a682476 --- /dev/null +++ b/Sources/Interaction/Style/InteractorStyleManipulatorRotateCameraCenter/example/index.js @@ -0,0 +1,128 @@ +import '@kitware/vtk.js/favicon'; + +// Load the rendering pieces we want to use (for both WebGL and WebGPU) +import '@kitware/vtk.js/Rendering/Profiles/Geometry'; + +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkInteractorStyleManipulator from '@kitware/vtk.js/Interaction/Style/InteractorStyleManipulator'; +import vtkMouseCameraTrackballRotateManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballRotateManipulator'; +import vtkMouseCameraTrackballPanManipulatorAutoCenter from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballPanManipulatorAutoCenter'; +import vtkMouseCameraTrackballZoomManipulator from '@kitware/vtk.js/Interaction/Manipulators/MouseCameraTrackballZoomManipulator'; + +// This example demonstrates the MouseCameraTrackballPanManipulatorAutoCenter +// which automatically adjusts the center of rotation during panning + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +// Use standard InteractorStyleManipulator +const interactorStyle = vtkInteractorStyleManipulator.newInstance(); +fullScreenRenderer.getInteractor().setInteractorStyle(interactorStyle); + +// ---------------------------------------------------------------------------- +// Create cone (main object) +// ---------------------------------------------------------------------------- + +const coneSource = vtkConeSource.newInstance({ height: 1.0 }); +const coneMapper = vtkMapper.newInstance(); +coneMapper.setInputConnection(coneSource.getOutputPort()); + +const coneActor = vtkActor.newInstance(); +coneActor.setMapper(coneMapper); +coneActor.getProperty().setColor(0.5, 0.5, 1.0); + +// ---------------------------------------------------------------------------- +// Add actors and setup camera +// ---------------------------------------------------------------------------- + +renderer.addActor(coneActor); +renderer.resetCamera(); + +// ---------------------------------------------------------------------------- +// Setup manipulators +// ---------------------------------------------------------------------------- + +// Rotate with left button +const rotateManipulator = + vtkMouseCameraTrackballRotateManipulator.newInstance(); +rotateManipulator.setButton(1); // Left button +interactorStyle.addMouseManipulator(rotateManipulator); + +// Zoom with middle button +const middleZoomManipulator = + vtkMouseCameraTrackballZoomManipulator.newInstance(); +middleZoomManipulator.setButton(2); // Middle button +interactorStyle.addMouseManipulator(middleZoomManipulator); + +// Pan with shift + left button - with auto-adjust enabled +const shiftPanManipulator = + vtkMouseCameraTrackballPanManipulatorAutoCenter.newInstance(); +shiftPanManipulator.setButton(1); +shiftPanManipulator.setShift(true); +interactorStyle.addMouseManipulator(shiftPanManipulator); + +// Pan with right button - with auto-adjust enabled +const rightPanManipulator = + vtkMouseCameraTrackballPanManipulatorAutoCenter.newInstance(); +rightPanManipulator.setButton(3); // Right button +interactorStyle.addMouseManipulator(rightPanManipulator); + +// Zoom with mouse wheel +const wheelZoomManipulator = + vtkMouseCameraTrackballZoomManipulator.newInstance(); +wheelZoomManipulator.setScrollEnabled(true); +wheelZoomManipulator.setDragEnabled(false); +interactorStyle.addMouseManipulator(wheelZoomManipulator); + +// Can't use touch devices as they are not updating the center of rotation on pan +// interactorStyle.addGestureManipulator( +// vtkGestureCameraManipulator.newInstance() +// ); + +renderWindow.render(); + +const infoDiv = document.createElement('div'); +infoDiv.style.position = 'absolute'; +infoDiv.style.top = '10px'; +infoDiv.style.left = '10px'; +infoDiv.style.padding = '10px'; +infoDiv.style.background = 'rgba(255, 255, 255, 0.9)'; +infoDiv.style.borderRadius = '5px'; +infoDiv.style.fontFamily = 'monospace'; +infoDiv.style.maxWidth = '400px'; + +infoDiv.innerHTML = ` +
The center of rotation automatically moves with the camera during panning, maintaining consistent rotation behavior relative to the camera position.
+Controls:
+Try this: Try to rotate around the tip of the cone.
+`; +document.body.appendChild(infoDiv); + +// ----------------------------------------------------------- +// globals for debugging +// ----------------------------------------------------------- + +global.coneSource = coneSource; +global.coneActor = coneActor; +global.renderer = renderer; +global.renderWindow = renderWindow; +global.interactorStyle = interactorStyle;