Hello,
I am trying to use InteractiveOrientationWidget in the same window with HardwareSelector,
the problem is, once hardwareSelector.getSourceDataAsync() is called, the InteractiveOrientationWidget loses interactivity.
How can I have both? or restore interactivity to the InteractiveOrientationWidget after I am done using HardwareSelector?
Here is the example, this is just the concatenation of the official example of both, with a little modification to make the selection happen only if CTRL is held.
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 { throttle } from '@kitware/vtk.js/macros';
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource';
import vtkCylinderSource from '@kitware/vtk.js/Filters/Sources/CylinderSource';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow';
import vtkGlyph3DMapper from '@kitware/vtk.js/Rendering/Core/Glyph3DMapper';
import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource';
import { FieldAssociations } from '@kitware/vtk.js/Common/DataModel/DataSet/Constants';
import { Representation } from '@kitware/vtk.js/Rendering/Core/Property/Constants';
import '@kitware/vtk.js/Rendering/Profiles/Glyph';
import vtkOrientationMarkerWidget from '@kitware/vtk.js/Interaction/Widgets/OrientationMarkerWidget';
import vtkAxesActor from '@kitware/vtk.js/Rendering/Core/AxesActor';
import vtkInteractiveOrientationWidget from '@kitware/vtk.js/Widgets/Widgets3D/InteractiveOrientationWidget';
import vtkWidgetManager from '@kitware/vtk.js/Widgets/Core/WidgetManager';
import * as vtkMath from '@kitware/vtk.js/Common/Core/Math';
// ----------------------------------------------------------------------------
// Constants
// ----------------------------------------------------------------------------
const WHITE = [1, 1, 1];
const GREEN = [0.1, 0.8, 0.1];
// ----------------------------------------------------------------------------
// Create DOM tooltip
// ----------------------------------------------------------------------------
const tooltipElem = document.createElement('div');
tooltipElem.style.position = 'absolute';
tooltipElem.style.top = 0;
tooltipElem.style.left = 0;
tooltipElem.style.width = '150px';
tooltipElem.style.padding = '10px';
tooltipElem.style.zIndex = 1;
tooltipElem.style.background = 'white';
tooltipElem.style.textAlign = 'center';
document.querySelector('body').appendChild(tooltipElem);
// ----------------------------------------------------------------------------
// Create 4 objects
// - sphere
// - sphere rendered as big points (square)
// - cone
// - cylinder with sphere as point (glyph mapper: source=cylinder, glyph=sphere)
// ----------------------------------------------------------------------------
// Sphere -------------------------------------------------
const sphereSource = vtkSphereSource.newInstance({
phiResolution: 30,
thetaResolution: 30,
});
const sphereMapper = vtkMapper.newInstance();
const sphereActor = vtkActor.newInstance();
sphereActor.setMapper(sphereMapper);
sphereMapper.setInputConnection(sphereSource.getOutputPort());
// Sphere with point representation -----------------------
const spherePointsSource = vtkSphereSource.newInstance({
phiResolution: 15,
thetaResolution: 15,
radius: 0.6,
});
const spherePointsMapper = vtkMapper.newInstance();
const spherePointsActor = vtkActor.newInstance();
spherePointsActor.setMapper(spherePointsMapper);
spherePointsMapper.setInputConnection(spherePointsSource.getOutputPort());
// Use point representation
spherePointsActor.getProperty().setRepresentation(Representation.POINTS);
spherePointsActor.getProperty().setPointSize(20);
// Cone ---------------------------------------------------
const coneSource = vtkConeSource.newInstance({ resolution: 20 });
const coneMapper = vtkMapper.newInstance();
const coneActor = vtkActor.newInstance();
coneActor.setMapper(coneMapper);
coneMapper.setInputConnection(coneSource.getOutputPort());
// Cylinder -----------------------------------------------
const cylinderSource = vtkCylinderSource.newInstance({
resolution: 10,
radius: 0.4,
height: 0.6,
direction: [1.0, 0.0, 0.0],
});
const cylinderMapper = vtkGlyph3DMapper.newInstance({
scaling: true,
scaleFactor: 0.25,
scaleMode: vtkGlyph3DMapper.ScaleModes.SCALE_BY_MAGNITUDE,
scaleArray: 'scale',
});
const cylinderActor = vtkActor.newInstance();
const cylinderGlyph = sphereSource.getOutputData();
const cylinderPointSet = cylinderSource.getOutputData();
cylinderActor.setMapper(cylinderMapper);
cylinderMapper.setInputData(cylinderPointSet, 0);
cylinderMapper.setInputData(cylinderGlyph, 1);
// Add fields to cylinderPointSet
const scaleArray = new Float32Array(cylinderPointSet.getNumberOfPoints());
scaleArray.fill(0.5);
cylinderPointSet.getPointData().addArray(
vtkDataArray.newInstance({
name: 'scale',
values: scaleArray,
})
);
// ----------------------------------------------------------------------------
// Create Picking pointer
// ----------------------------------------------------------------------------
const pointerSource = vtkSphereSource.newInstance({
phiResolution: 15,
thetaResolution: 15,
radius: 0.01,
});
const pointerMapper = vtkMapper.newInstance();
const pointerActor = vtkActor.newInstance();
pointerActor.setMapper(pointerMapper);
pointerMapper.setInputConnection(pointerSource.getOutputPort());
// ----------------------------------------------------------------------------
// Create rendering infrastructure
// ----------------------------------------------------------------------------
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance();
const renderer = fullScreenRenderer.getRenderer();
const renderWindow = renderer.getRenderWindow();
const interactor = renderWindow.getInteractor();
const apiSpecificRenderWindow = interactor.getView();
renderer.addActor(sphereActor);
renderer.addActor(spherePointsActor);
renderer.addActor(coneActor);
renderer.addActor(cylinderActor);
renderer.addActor(pointerActor);
renderer.resetCamera();
renderWindow.render();
// ----------------------------------------------------------------------------
// Create hardware selector
// ----------------------------------------------------------------------------
const hardwareSelector = apiSpecificRenderWindow.getSelector();
hardwareSelector.setCaptureZValues(true);
hardwareSelector.setFieldAssociation(
FieldAssociations.FIELD_ASSOCIATION_POINTS
);
// ----------------------------------------------------------------------------
// Create Mouse listener for picking on mouse move
// ----------------------------------------------------------------------------
function eventToWindowXY(event) {
// We know we are full screen => window.innerXXX
// Otherwise we can use pixel device ratio or else...
const { clientX, clientY } = event;
const [width, height] = apiSpecificRenderWindow.getSize();
const x = Math.round((width * clientX) / window.innerWidth);
const y = Math.round(height * (1 - clientY / window.innerHeight)); // Need to flip Y
return [x, y];
}
// ----------------------------------------------------------------------------
let needGlyphCleanup = false;
let lastProcessedActor = null;
const updateWorldPosition = (worldPosition) => {
if (lastProcessedActor) {
pointerActor.setVisibility(true);
tooltipElem.innerHTML = worldPosition.map((v) => v.toFixed(3)).join(' , ');
pointerActor.setPosition(worldPosition);
} else {
pointerActor.setVisibility(false);
tooltipElem.innerHTML = '';
}
renderWindow.render();
};
function processSelections(selections) {
if (!selections || selections.length === 0) {
renderer.getActors().forEach((a) => a.getProperty().setColor(...WHITE));
pointerActor.setVisibility(false);
renderWindow.render();
lastProcessedActor = null;
return;
}
const { worldPosition, compositeID, prop } = selections[0].getProperties();
if (lastProcessedActor === prop) {
// Skip render call when nothing change
updateWorldPosition(worldPosition);
return;
}
lastProcessedActor = prop;
// Make the picked actor green
renderer.getActors().forEach((a) => a.getProperty().setColor(...WHITE));
prop.getProperty().setColor(...GREEN);
// We hit the glyph, let's scale the picked glyph
if (prop === cylinderActor) {
scaleArray.fill(0.5);
scaleArray[compositeID] = 0.7;
cylinderPointSet.modified();
needGlyphCleanup = true;
} else if (needGlyphCleanup) {
needGlyphCleanup = false;
scaleArray.fill(0.5);
cylinderPointSet.modified();
}
// Update picture for the user so we can see the green one
updateWorldPosition(worldPosition);
}
// ----------------------------------------------------------------------------
function pickOnMouseEvent(event) {
if (interactor.isAnimating()) {
// We should not do picking when interacting with the scene
return;
}
const [x, y] = eventToWindowXY(event);
pointerActor.setVisibility(false);
if(event.ctrlKey){
hardwareSelector.getSourceDataAsync(renderer, x, y, x, y).then((result) => {
if (result) {
processSelections(result.generateSelection(x, y, x, y));
} else {
processSelections(null);
}
});
}
}
const throttleMouseHandler = throttle(pickOnMouseEvent, 100);
document.addEventListener('mousemove', throttleMouseHandler);
// ----------------------------------------------------------------------------
function majorAxis(vec3, idxA, idxB) {
const axis = [0, 0, 0];
const idx = Math.abs(vec3[idxA]) > Math.abs(vec3[idxB]) ? idxA : idxB;
const value = vec3[idx] > 0 ? 1 : -1;
axis[idx] = value;
return axis;
}
// ----------------------------------------------------------------------------
// Standard rendering code setup
// ----------------------------------------------------------------------------
const render = renderWindow.render;
const axes = vtkAxesActor.newInstance();
const orientationWidget = vtkOrientationMarkerWidget.newInstance({
actor: axes,
interactor: renderWindow.getInteractor(),
});
orientationWidget.setEnabled(true);
orientationWidget.setViewportCorner(
vtkOrientationMarkerWidget.Corners.BOTTOM_LEFT
);
orientationWidget.setViewportSize(0.15);
orientationWidget.setMinPixelSize(100);
orientationWidget.setMaxPixelSize(300);
// ----------------------------------------------------------------------------
// Add context to 3D scene for orientation
// ----------------------------------------------------------------------------
const cone = vtkConeSource.newInstance();
const mapper = vtkMapper.newInstance();
const actor = vtkActor.newInstance({ pickable: false });
actor.setMapper(mapper);
mapper.setInputConnection(cone.getOutputPort());
renderer.addActor(actor);
const camera = renderer.getActiveCamera();
// ----------------------------------------------------------------------------
// Widget manager
// ----------------------------------------------------------------------------
const widgetManager = vtkWidgetManager.newInstance();
widgetManager.setRenderer(orientationWidget.getRenderer());
const widget = vtkInteractiveOrientationWidget.newInstance();
widget.placeWidget(axes.getBounds());
widget.setBounds(axes.getBounds());
widget.setPlaceFactor(1);
const vw = widgetManager.addWidget(widget);
// Manage user interaction
vw.onOrientationChange(({ up, direction, action, event }) => {
const focalPoint = camera.getFocalPoint();
const position = camera.getPosition();
const viewUp = camera.getViewUp();
const distance = Math.sqrt(
vtkMath.distance2BetweenPoints(position, focalPoint)
);
camera.setPosition(
focalPoint[0] + direction[0] * distance,
focalPoint[1] + direction[1] * distance,
focalPoint[2] + direction[2] * distance
);
if (direction[0]) {
camera.setViewUp(majorAxis(viewUp, 1, 2));
}
if (direction[1]) {
camera.setViewUp(majorAxis(viewUp, 0, 2));
}
if (direction[2]) {
camera.setViewUp(majorAxis(viewUp, 0, 1));
}
orientationWidget.updateMarkerOrientation();
widgetManager.enablePicking();
render();
});
renderer.resetCamera();
widgetManager.enablePicking();
render();
As you can see once CTRL is pressed (which means hardwareSelector.getSourceDataAsync() is called), the widget loses interactivity.
Thank you