Hello everyone!
Looking at the vtkHardwareSelector example, I wondered if it is possible in vtk.js to “highlight” the faces (or cells) of a cube when hovering or clicking on it. I see that the examples use a small sphere on the corner, but this is a less obvious indicator than changing the color of the face.
In python vtk, highlighting can be done as follows:
face_ids = self.poly_data.GetCellData().GetArray("FaceID")
ids = vtk.vtkIdTypeArray()
for i in range(self.poly_data.GetNumberOfCells()):
if int(face_ids.GetTuple1(i)) == face_id:
ids.InsertNextValue(i)
selected_cells = vtk.vtkSelectionNode()
selected_cells.SetFieldType(vtk.vtkSelectionNode.CELL)
selected_cells.SetContentType(vtk.vtkSelectionNode.INDICES)
selected_cells.SetSelectionList(ids)
selection = vtk.vtkSelection()
selection.AddNode(selected_cells)
extract_selection = vtk.vtkExtractSelection()
extract_selection.SetInputData(0, self.poly_data)
extract_selection.SetInputData(1, selection)
extract_selection.Update()
geometry = vtk.vtkGeometryFilter()
geometry.SetInputConnection(extract_selection.GetOutputPort())
geometry.Update()
if geometry.GetOutput().GetNumberOfPoints() > 0:
self.highlighted_mapper.SetInputData(geometry.GetOutput())
self.highlighted_actor.VisibilityOn()
else:
self.highlighted_actor.VisibilityOff()
Is something like this possible in vtk.js and if so, where should I look? And it’s probably worth mentioning that I couldn’t find it in vtk.js vtkGeometryFilter
, vtkSelection
and vtkExtractSelection
methods.
Thanks in advance!
You can create a cell data array with an ID for each cell. It can be the index of the cell, so an increasing array from 0 to numberOfCells - 1.
Then create a lookup table that associates a cell index with its color.
When you want to highlight a cell, you just have to edit the lookup table at the right index.
1 Like
Thanks for your advice! If I understood you correctly, this is how it should work:
const apiSpecificRenderWindow = interactor.getView()
const selector = apiSpecificRenderWindow.getSelector()
selector.setRenderer(renderer)
selector.setFieldAssociation(FieldAssociations.FIELD_ASSOCIATION_CELLS)
const numCells = source.getNumberOfCells()
const cellIds = new Array(numCells).fill().map((_, i) => i)
const cellIdArray = vtkDataArray.newInstance({
values: cellIds,
name: 'CellIds',
})
source.getCellData().setScalars(cellIdArray)
const lookupTable = vtkColorTransferFunction.newInstance()
for (let i = 0; i <= source.getNumberOfCells(); i++) {
lookupTable.addRGBPoint(i, 1, 1, 1)
}
mapper.setLookupTable(lookupTable)
mapper.setScalarRange(0, numCells - 1)
mapper.setColorModeToMapScalars()
let previousCellId = -1 // Keep track of the previously selected cell
interactor.onMouseMove((e) => {
const pos = e.position
selector.setArea(pos.x - 1, pos.y - 1, pos.x + 1, pos.y + 1)
const selection = selector.select()
if (selection.length) {
const cellId = selection[0].getSelectionList()[0]
if (cellId >= 0 && cellId < source.getNumberOfCells()) {
if (cellId !== previousCellId) {
// If a cell was previously selected, revert its color to white
if (previousCellId >= 0) {
lookupTable.addRGBPoint(previousCellId, 1, 1, 1)
}
// Highlight the newly selected cell in green
lookupTable.addRGBPoint(cellId, 0, 1, 0)
// Update the previousCellId to the currently selected cell
previousCellId = cellId
renderWindow.render()
}
}
} else {
// No cell is selected, revert the previously selected cell to white
if (previousCellId >= 0) {
lookupTable.addRGBPoint(previousCellId, 1, 1, 1)
previousCellId = -1 // Reset the previous cell ID
renderWindow.render()
}
}
})
And it does work, but only with small geometries (for example, I’m currently testing a geometry with 12 cells, i.e. a simple parallelepiped), but when I decided to load a complex geometry with 80,000+ cells, the application simply froze and did not respond. It is obvious to me that this is due to two loops over the array of the number of cells, which is very resource-intensive. Is there a way to get around this? Or did I just make a mistake when transferring your words to code?
If you want to run the tests, I can share with you two of my vtp files…
Thanks!
Your lookup table is not a lookup table, it is a transfer function.
Also you add a lot of points, without ever removing any point.
With this code, you can have a lot of cells working smoothly:
import '@kitware/vtk.js/Rendering/OpenGL/Profiles/All';
import {
ColorMode,
ScalarMode,
} from '@kitware/vtk.js/Rendering/Core/Mapper/Constants';
import { FieldAssociations } from '@kitware/vtk.js/Common/DataModel/DataSet/Constants';
import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkGenericRenderWindow from '@kitware/vtk.js/Rendering/Misc/GenericRenderWindow';
import vtkHttpDataSetReader from '@kitware/vtk.js/IO/Core/HttpDataSetReader';
import vtkLookupTable from '@kitware/vtk.js/Common/Core/LookupTable';
import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper';
// use full HttpDataAccessHelper
import '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper';
// Create some control UI
const container = document.querySelector('body');
const renderWindowContainer = document.createElement('div');
container.appendChild(renderWindowContainer);
// Rendering pipeline
const lookupTable = vtkLookupTable.newInstance();
const mapper = vtkMapper.newInstance({
colorByArrayName: 'CellId',
useLookupTableScalarRange: true,
scalarMode: ScalarMode.USE_CELL_FIELD_DATA,
colorMode: ColorMode.MAP_SCALARS,
lookupTable,
});
const actor = vtkActor.newInstance({ mapper });
const grw = vtkGenericRenderWindow.newInstance();
grw.setContainer(renderWindowContainer);
grw.resize();
const renderer = grw.getRenderer();
renderer.setBackground(0.32, 0.34, 0.43);
renderer.addActor(actor);
const glwindow = grw.getApiSpecificRenderWindow();
const renderWindow = grw.getRenderWindow();
renderWindow.render();
// Create selector
const selector = glwindow.getSelector();
selector.setFieldAssociation(FieldAssociations.FIELD_ASSOCIATION_CELLS);
selector.setCaptureZValues(false);
selector.attach(glwindow, renderer);
// Read the polydata
const reader = vtkHttpDataSetReader.newInstance({ fetchGzip: true });
reader.setUrl(`${__BASE_PATH__}/data/cow.vtp`, { loadData: true }).then(() => {
const polydata = reader.getOutputData();
const nCells = polydata.getNumberOfCells();
// Create the data array with cell ids
const cellIdValues = new Float32Array(nCells);
for (let i = 0; i < nCells; ++i) {
cellIdValues[i] = i;
}
const newArray = vtkDataArray.newInstance({
name: 'CellId',
numberOfComponents: 1,
values: cellIdValues,
});
polydata.getCellData().addArray(newArray);
// Fill the lookup table
const colorTable = new Array(nCells);
const whiteColor = [255, 255, 255, 255];
const redColor = [255, 0, 0, 255];
for (let i = 0; i < nCells; ++i) {
colorTable[i] = whiteColor;
}
lookupTable.setNumberOfColors(nCells);
lookupTable.setRange(0, nCells - 1);
lookupTable.setTable(colorTable);
// Rendering
mapper.setInputData(polydata);
renderer.resetCamera();
renderWindow.render();
// Add event listener
let previousPickedCellId = 0;
function handlePicking(x, y) {
selector.setArea(x, y, x, y);
if (selector.captureBuffers()) {
const info = selector.getPixelInformation([x, y], 0, [0, 0]);
const cellId = info?.attributeID;
if (cellId != null) {
colorTable[previousPickedCellId] = whiteColor;
colorTable[cellId] = redColor;
previousPickedCellId = cellId;
lookupTable.setTable(colorTable);
}
}
}
function onMouseDown(e) {
if (e !== undefined) {
const bounds = container.getBoundingClientRect();
const [canvasWidth, canvasHeight] = glwindow.getSize();
const scaleX = canvasWidth / bounds.width;
const scaleY = canvasHeight / bounds.height;
const position = {
x: scaleX * (e.clientX - bounds.left),
y: scaleY * (bounds.height - e.clientY + bounds.top),
};
handlePicking(position.x, position.y);
}
}
container.addEventListener('mousedown', onMouseDown);
});
1 Like
This really works well, thank you very much!