From 6d186a76349c13f6450a8960a4341105d5fa2bfa Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Mon, 5 Oct 2026 12:13:10 +0200 Subject: [PATCH] feat(WebGPU): extend the hardware selector API Add setArea and clearArea to the WebGPU selector. A flag records that an area was set, so an area of all zeros reads the pixel at the origin. With no area, captureBuffersAsync reads all the window. getSourceDataAsync copies only the color pixels of the area, and the depth copy stays full size. captureBuffersAsync keeps the data, and then getPixelInformation, generateSelection and isPropHit read it synchronously. Fix endSelection and destroy the read back buffers after use. Move the nearest pixel search and the selection build to Core/HardwareSelector/PixelSelectionHelper, and the view and renderer check and preRender call to vtkHardwareSelector.prepareCapture. Both backends use them. Move PassTypes to Core/HardwareSelector/Constants, so Core/Mapper does not import from OpenGL. OpenGL/HardwareSelector/Constants is deprecated. The Unicam rotate manipulator uses the point picker on WebGPU. --- .../index.js | 9 +- .../Core/HardwareSelector/Constants.d.ts | 8 + .../Core/HardwareSelector/Constants.js | 12 + .../HardwareSelector/PixelSelectionHelper.js | 163 +++++ .../Core/HardwareSelector/example/index.js | 121 ++-- .../Core/HardwareSelector/index.d.ts | 10 + .../Rendering/Core/HardwareSelector/index.js | 13 + Sources/Rendering/Core/Mapper/index.js | 2 +- .../OpenGL/HardwareSelector/Constants.d.ts | 15 +- .../OpenGL/HardwareSelector/Constants.js | 19 +- .../OpenGL/HardwareSelector/index.d.ts | 3 +- .../OpenGL/HardwareSelector/index.js | 635 ++++-------------- .../Rendering/OpenGL/PolyDataMapper/index.js | 2 +- .../WebGPU/HardwareSelector/index.d.ts | 146 ++++ .../WebGPU/HardwareSelector/index.js | 503 +++++++------- .../test/testWebGPUHardwareSelector.js | 111 +++ 16 files changed, 926 insertions(+), 846 deletions(-) create mode 100644 Sources/Rendering/Core/HardwareSelector/Constants.d.ts create mode 100644 Sources/Rendering/Core/HardwareSelector/Constants.js create mode 100644 Sources/Rendering/Core/HardwareSelector/PixelSelectionHelper.js create mode 100644 Sources/Rendering/WebGPU/HardwareSelector/index.d.ts create mode 100644 Sources/Rendering/WebGPU/HardwareSelector/test/testWebGPUHardwareSelector.js diff --git a/Sources/Interaction/Manipulators/MouseCameraUnicamRotateManipulator/index.js b/Sources/Interaction/Manipulators/MouseCameraUnicamRotateManipulator/index.js index a0127e47b77..903ad1cc9ca 100644 --- a/Sources/Interaction/Manipulators/MouseCameraUnicamRotateManipulator/index.js +++ b/Sources/Interaction/Manipulators/MouseCameraUnicamRotateManipulator/index.js @@ -268,11 +268,14 @@ function vtkMouseCameraUnicamRotateManipulator(publicAPI, model) { // to the distance from the camera's position to the focal point. // This seems like an arbitrary, but perhaps reasonable, default value. let selections = null; - if (model.useHardwareSelector) { - const selector = interactor.getView().getSelector(); + // A selector without a synchronous select() (WebGPU) uses the point + // picker. + const view = interactor.getView(); + if (model.useHardwareSelector && view.getSelector().select) { + const selector = view.getSelector(); selector.setCaptureZValues(true); selector.setFieldAssociation(FieldAssociations.FIELD_ASSOCIATION_POINTS); - selector.attach(interactor.getView(), renderer); + selector.attach(view, renderer); selector.setArea(position.x, position.y, position.x, position.y); selections = selector.select(); diff --git a/Sources/Rendering/Core/HardwareSelector/Constants.d.ts b/Sources/Rendering/Core/HardwareSelector/Constants.d.ts new file mode 100644 index 00000000000..a711b3885cf --- /dev/null +++ b/Sources/Rendering/Core/HardwareSelector/Constants.d.ts @@ -0,0 +1,8 @@ +export declare enum PassTypes { + MIN_KNOWN_PASS = 0, + ACTOR_PASS = 0, + COMPOSITE_INDEX_PASS = 1, + ID_LOW24 = 2, + ID_HIGH24 = 3, + MAX_KNOWN_PASS = 3, +} diff --git a/Sources/Rendering/Core/HardwareSelector/Constants.js b/Sources/Rendering/Core/HardwareSelector/Constants.js new file mode 100644 index 00000000000..8a63669a5c3 --- /dev/null +++ b/Sources/Rendering/Core/HardwareSelector/Constants.js @@ -0,0 +1,12 @@ +export const PassTypes = { + MIN_KNOWN_PASS: 0, + ACTOR_PASS: 0, + COMPOSITE_INDEX_PASS: 1, + ID_LOW24: 2, + ID_HIGH24: 3, + MAX_KNOWN_PASS: 3, +}; + +export default { + PassTypes, +}; diff --git a/Sources/Rendering/Core/HardwareSelector/PixelSelectionHelper.js b/Sources/Rendering/Core/HardwareSelector/PixelSelectionHelper.js new file mode 100644 index 00000000000..d7596882097 --- /dev/null +++ b/Sources/Rendering/Core/HardwareSelector/PixelSelectionHelper.js @@ -0,0 +1,163 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkSelectionNode from 'vtk.js/Sources/Common/DataModel/SelectionNode'; +import vtkDataSet from 'vtk.js/Sources/Common/DataModel/DataSet'; + +const { SelectionContent, SelectionField } = vtkSelectionNode; +const { FieldAssociations } = vtkDataSet; +const { vtkErrorMacro } = macro; + +// These functions build selections from the pixels that a backend read back. +// The backend gives readPixel(displayPosition), which decodes one pixel and +// returns null when the pixel is outside the read area or hits no prop. +// Otherwise it returns { valid: true, prop, propID, compositeID, +// attributeID } and, with captured z values, zValue and displayPosition. + +function readPixelAt(readPixel, position, outSelectedPosition) { + outSelectedPosition[0] = position[0]; + outSelectedPosition[1] = position[1]; + const info = readPixel(position); + if (info && info.valid) { + return info; + } + return null; +} + +// Look at the pixel, then at the sides of boxes that grow around it, up to +// maxDistance - 1 pixels. Returns the first hit. outSelectedPosition gets +// the position of that hit. +export function getPixelInformation( + readPixel, + inDisplayPosition, + maxDistance, + outSelectedPosition +) { + let info = readPixelAt(readPixel, inDisplayPosition, outSelectedPosition); + if (info || !(maxDistance > 0)) { + return info; + } + + const dispPos = [inDisplayPosition[0], inDisplayPosition[1]]; + const curPos = [0, 0]; + for (let dist = 1; dist < maxDistance; ++dist) { + // Vertical sides of the box. + for (let y = Math.max(dispPos[1] - dist, 0); y <= dispPos[1] + dist; ++y) { + curPos[1] = y; + if (dispPos[0] >= dist) { + curPos[0] = dispPos[0] - dist; + info = readPixelAt(readPixel, curPos, outSelectedPosition); + if (info) { + return info; + } + } + curPos[0] = dispPos[0] + dist; + info = readPixelAt(readPixel, curPos, outSelectedPosition); + if (info) { + return info; + } + } + // Horizontal sides of the box. + for ( + let x = Math.max(dispPos[0] - (dist - 1), 0); + x <= dispPos[0] + (dist - 1); + ++x + ) { + curPos[0] = x; + if (dispPos[1] >= dist) { + curPos[1] = dispPos[1] - dist; + info = readPixelAt(readPixel, curPos, outSelectedPosition); + if (info) { + return info; + } + } + curPos[1] = dispPos[1] + dist; + info = readPixelAt(readPixel, curPos, outSelectedPosition); + if (info) { + return info; + } + } + } + + // nothing hit. + outSelectedPosition[0] = inDisplayPosition[0]; + outSelectedPosition[1] = inDisplayPosition[1]; + return null; +} + +function getInfoHash(info) { + return `${info.propID} ${info.compositeID}`; +} + +function convertSelection(dataMap, options) { + const sel = []; + dataMap.forEach((value) => { + const child = vtkSelectionNode.newInstance(); + child.setContentType(SelectionContent.INDICES); + switch (options.fieldAssociation) { + case FieldAssociations.FIELD_ASSOCIATION_CELLS: + child.setFieldType(SelectionField.CELL); + break; + case FieldAssociations.FIELD_ASSOCIATION_POINTS: + child.setFieldType(SelectionField.POINT); + break; + default: + vtkErrorMacro('Unknown field association'); + } + const properties = child.getProperties(); + properties.propID = value.info.propID; + properties.prop = value.info.prop; + properties.compositeID = value.info.compositeID; + properties.attributeID = value.info.attributeID; + properties.pixelCount = value.pixelCount; + if (options.captureZValues) { + const [x, y] = value.info.displayPosition; + properties.displayPosition = [x, y, value.info.zValue]; + properties.worldPosition = options.displayToWorld( + x, + y, + value.info.zValue + ); + } + child.setSelectionList(value.attributeIDs); + sel.push(child); + }); + return sel; +} + +// Make one selection node for each prop and composite id that the pixels of +// the area fx1, fy1, fx2, fy2 hit. options has fieldAssociation, +// captureZValues and, with captureZValues, displayToWorld(x, y, z). +export function generateSelection(readPixel, fx1, fy1, fx2, fy2, options) { + const x1 = Math.floor(fx1); + const y1 = Math.floor(fy1); + const x2 = Math.floor(fx2); + const y2 = Math.floor(fy2); + + const dataMap = new Map(); + for (let yy = y1; yy <= y2; yy++) { + for (let xx = x1; xx <= x2; xx++) { + const info = readPixel([xx, yy]); + if (info && info.valid) { + const hash = getInfoHash(info); + let dmv = dataMap.get(hash); + if (!dmv) { + dmv = { info, pixelCount: 0, attributeIDs: [] }; + dataMap.set(hash, dmv); + } else if (options.captureZValues && info.zValue < dmv.info.zValue) { + dmv.info = info; + } + dmv.pixelCount++; + // A pixel can hit a prop with no attribute id: in point selection, + // the WebGL backend writes ids only where it draws the points. + if ( + info.attributeID !== undefined && + dmv.attributeIDs.indexOf(info.attributeID) === -1 + ) { + dmv.attributeIDs.push(info.attributeID); + } + } + } + } + return convertSelection(dataMap, options); +} + +export default { getPixelInformation, generateSelection }; diff --git a/Sources/Rendering/Core/HardwareSelector/example/index.js b/Sources/Rendering/Core/HardwareSelector/example/index.js index 56d6e0cef41..a1cd8b24dfe 100644 --- a/Sources/Rendering/Core/HardwareSelector/example/index.js +++ b/Sources/Rendering/Core/HardwareSelector/example/index.js @@ -3,6 +3,8 @@ import '@kitware/vtk.js/favicon'; +import GUI from 'lil-gui'; + // Load the rendering pieces we want to use (for both WebGL and WebGPU) import '@kitware/vtk.js/Rendering/Profiles/Geometry'; import '@kitware/vtk.js/Rendering/Profiles/Glyph'; @@ -24,6 +26,24 @@ import vtkMath from '@kitware/vtk.js/Common/Core/Math'; import { FieldAssociations } from '@kitware/vtk.js/Common/DataModel/DataSet/Constants'; import { Representation } from '@kitware/vtk.js/Rendering/Core/Property/Constants'; +// ---------------------------------------------------------------------------- +// Renderer selector +// ---------------------------------------------------------------------------- + +const rendererSelectorParams = { + viewAPI: + new URLSearchParams(window.location.search).get('viewAPI') || 'WebGL', +}; +const rendererSelectorGui = new GUI(); +rendererSelectorGui + .add(rendererSelectorParams, 'viewAPI', ['WebGL', 'WebGPU']) + .name('Renderer') + .onChange((api) => { + const query = new URLSearchParams(window.location.search); + query.set('viewAPI', api); + window.location.search = query.toString(); + }); + // ---------------------------------------------------------------------------- // Constants // ---------------------------------------------------------------------------- @@ -32,28 +52,34 @@ const WHITE = [1, 1, 1]; const GREEN = [0.1, 0.8, 0.1]; // ---------------------------------------------------------------------------- -// Create DOM tooltip +// Selection information in a read only text area of the GUI // ---------------------------------------------------------------------------- -const tooltipsElem = document.createElement('div'); -tooltipsElem.style.position = 'absolute'; -tooltipsElem.style.top = 0; -tooltipsElem.style.left = 0; -tooltipsElem.style.padding = '10px'; -tooltipsElem.style.zIndex = 1; -tooltipsElem.style.background = 'white'; -tooltipsElem.style.textAlign = 'center'; - -const positionTooltipElem = document.createElement('div'); -const fieldIdTooltipElem = document.createElement('div'); -const compositeIdTooltipElem = document.createElement('div'); -const propIdTooltipElem = document.createElement('div'); -tooltipsElem.appendChild(positionTooltipElem); -tooltipsElem.appendChild(propIdTooltipElem); -tooltipsElem.appendChild(fieldIdTooltipElem); -tooltipsElem.appendChild(compositeIdTooltipElem); - -document.querySelector('body').appendChild(tooltipsElem); +const pickInfo = { position: '', prop: '', field: '', composite: '' }; + +const pickInfoElem = document.createElement('textarea'); +pickInfoElem.readOnly = true; +pickInfoElem.rows = 4; +pickInfoElem.style.width = '100%'; +pickInfoElem.style.boxSizing = 'border-box'; +pickInfoElem.style.resize = 'none'; +pickInfoElem.style.font = 'inherit'; +pickInfoElem.style.color = 'inherit'; +pickInfoElem.style.background = 'var(--widget-color)'; +pickInfoElem.style.border = 'none'; +pickInfoElem.style.padding = '4px'; +rendererSelectorGui.addFolder('Selection').$children.appendChild(pickInfoElem); + +const updatePickInfo = () => { + pickInfoElem.value = [ + pickInfo.position, + pickInfo.prop, + pickInfo.field, + pickInfo.composite, + ] + .filter(Boolean) + .join('\n'); +}; // ---------------------------------------------------------------------------- // Create 4 objects @@ -198,7 +224,9 @@ pointerMapper.setInputConnection(pointerSource.getOutputPort()); // Create rendering infrastructure // ---------------------------------------------------------------------------- -const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ + viewAPI: rendererSelectorParams.viewAPI, +}); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = renderer.getRenderWindow(); const interactor = renderWindow.getInteractor(); @@ -222,12 +250,18 @@ renderWindow.render(); const hardwareSelector = apiSpecificRenderWindow.getSelector(); hardwareSelector.setCaptureZValues(true); -// TODO: bug in FIELD_ASSOCIATION_POINTS mode -// hardwareSelector.setFieldAssociation( -// FieldAssociations.FIELD_ASSOCIATION_POINTS -// ); hardwareSelector.setFieldAssociation(FieldAssociations.FIELD_ASSOCIATION_CELLS); +rendererSelectorGui + .add({ field: FieldAssociations.FIELD_ASSOCIATION_CELLS }, 'field', { + Cells: FieldAssociations.FIELD_ASSOCIATION_CELLS, + Points: FieldAssociations.FIELD_ASSOCIATION_POINTS, + }) + .name('Field') + .onChange((value) => { + hardwareSelector.setFieldAssociation(Number(value)); + }); + // ---------------------------------------------------------------------------- // Create Mouse listener for picking on mouse move // ---------------------------------------------------------------------------- @@ -247,35 +281,38 @@ function eventToWindowXY(event) { let needGlyphCleanup = false; let lastProcessedActor = null; -const updatePositionTooltip = (worldPosition) => { +const updatePositionInfo = (worldPosition) => { if (lastProcessedActor) { - positionTooltipElem.innerHTML = `Position: ${worldPosition + pickInfo.position = `Position: ${worldPosition .map((v) => v.toFixed(3)) .join(' , ')}`; } else { - positionTooltipElem.innerHTML = ''; + pickInfo.position = ''; } + updatePickInfo(); }; -const updateAssociationTooltip = (type, id) => { +const updateAssociationInfo = (type, id) => { if (type !== undefined && id !== undefined) { - fieldIdTooltipElem.innerHTML = `${type} ID: ${id}`; + pickInfo.field = `${type} ID: ${id}`; } else { - fieldIdTooltipElem.innerHTML = ''; + pickInfo.field = ''; } + updatePickInfo(); }; -const updateCompositeAndPropIdTooltip = (compositeID, propID) => { +const updateCompositeAndPropIdInfo = (compositeID, propID) => { if (compositeID !== undefined) { - compositeIdTooltipElem.innerHTML = `Composite ID: ${compositeID}`; + pickInfo.composite = `Composite ID: ${compositeID}`; } else { - compositeIdTooltipElem.innerHTML = ''; + pickInfo.composite = ''; } if (propID !== undefined) { - propIdTooltipElem.innerHTML = `Prop ID: ${propID}`; + pickInfo.prop = `Prop ID: ${propID}`; } else { - propIdTooltipElem.innerHTML = ''; + pickInfo.prop = ''; } + updatePickInfo(); }; const updateCursor = (worldPosition) => { @@ -286,16 +323,16 @@ const updateCursor = (worldPosition) => { pointerActor.setVisibility(false); } renderWindow.render(); - updatePositionTooltip(worldPosition); + updatePositionInfo(worldPosition); }; function processSelections(selections) { renderer.getActors().forEach((a) => a.getProperty().setColor(...WHITE)); if (!selections || selections.length === 0) { lastProcessedActor = null; - updateAssociationTooltip(); + updateAssociationInfo(); updateCursor(); - updateCompositeAndPropIdTooltip(); + updateCompositeAndPropIdInfo(); return; } @@ -307,7 +344,7 @@ function processSelections(selections) { attributeID, } = selections[0].getProperties(); - updateCompositeAndPropIdTooltip(compositeID, propID); + updateCompositeAndPropIdInfo(compositeID, propID); let closestCellPointWorldPosition = [...rayHitWorldPosition]; if (attributeID || attributeID === 0) { @@ -335,11 +372,11 @@ function processSelections(selections) { ...input.getPoints().getTuple(attributeID), ]; propToWorld.apply(closestCellPointWorldPosition); - updateAssociationTooltip('Point', attributeID); + updateAssociationInfo('Point', attributeID); } else { // Selecting cells const cellPoints = input.getCellPoints(attributeID); - updateAssociationTooltip('Cell', attributeID); + updateAssociationInfo('Cell', attributeID); if (cellPoints?.cellPointIds) { const pointIds = cellPoints.cellPointIds; // Find the closest cell point, and use that as cursor position diff --git a/Sources/Rendering/Core/HardwareSelector/index.d.ts b/Sources/Rendering/Core/HardwareSelector/index.d.ts index 5e67132dd2a..ea4ba3f7dcc 100644 --- a/Sources/Rendering/Core/HardwareSelector/index.d.ts +++ b/Sources/Rendering/Core/HardwareSelector/index.d.ts @@ -21,6 +21,16 @@ export interface vtkHardwareSelector extends vtkObject { fy2: number ): Promise; + /** + * Used by the backends before they render the selection. Returns false + * with an error when the view or the renderer is not set. Otherwise it + * gets the scene ready as vtkRenderWindow.render() does. + * + * @param {object} view The API specific render window. + * @param {vtkRenderer} renderer + */ + prepareCapture(view: unknown, renderer: vtkRenderer): boolean; + /** * Generates a selection. * diff --git a/Sources/Rendering/Core/HardwareSelector/index.js b/Sources/Rendering/Core/HardwareSelector/index.js index 76abb383cc0..38f7360e3b7 100644 --- a/Sources/Rendering/Core/HardwareSelector/index.js +++ b/Sources/Rendering/Core/HardwareSelector/index.js @@ -2,6 +2,7 @@ import macro from 'vtk.js/Sources/macros'; import vtkDataSet from 'vtk.js/Sources/Common/DataModel/DataSet'; const { FieldAssociations } = vtkDataSet; +const { vtkErrorMacro } = macro; // ---------------------------------------------------------------------------- // vtkHardwareSelector methods @@ -16,6 +17,18 @@ function vtkHardwareSelector(publicAPI, model) { // call this once and then make multiple calls to generateSelection. publicAPI.getSourceDataAsync = async (renderer, fx1, fy1, fx2, fy2) => {}; + // The backends call this before they render the selection. It checks + // that the view and the renderer are set, then gets the scene ready as + // vtkRenderWindow.render() does (each renderer gets a camera). + publicAPI.prepareCapture = (view, renderer) => { + if (!view || !renderer) { + vtkErrorMacro('Renderer and view must be set before calling Select.'); + return false; + } + view.getRenderable().preRender(); + return true; + }; + publicAPI.selectAsync = async (renderer, fx1, fy1, fx2, fy2) => { const srcData = await publicAPI.getSourceDataAsync( renderer, diff --git a/Sources/Rendering/Core/Mapper/index.js b/Sources/Rendering/Core/Mapper/index.js index 390220194a9..064b8d4646f 100644 --- a/Sources/Rendering/Core/Mapper/index.js +++ b/Sources/Rendering/Core/Mapper/index.js @@ -8,7 +8,7 @@ import Constants from 'vtk.js/Sources/Rendering/Core/Mapper/Constants'; import vtkDataSet from 'vtk.js/Sources/Common/DataModel/DataSet'; -import { PassTypes } from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector/Constants'; +import { PassTypes } from 'vtk.js/Sources/Rendering/Core/HardwareSelector/Constants'; const { FieldAssociations } = vtkDataSet; diff --git a/Sources/Rendering/OpenGL/HardwareSelector/Constants.d.ts b/Sources/Rendering/OpenGL/HardwareSelector/Constants.d.ts index a711b3885cf..67ef9577dd6 100644 --- a/Sources/Rendering/OpenGL/HardwareSelector/Constants.d.ts +++ b/Sources/Rendering/OpenGL/HardwareSelector/Constants.d.ts @@ -1,8 +1,7 @@ -export declare enum PassTypes { - MIN_KNOWN_PASS = 0, - ACTOR_PASS = 0, - COMPOSITE_INDEX_PASS = 1, - ID_LOW24 = 2, - ID_HIGH24 = 3, - MAX_KNOWN_PASS = 3, -} +import { PassTypes as CorePassTypes } from '../../Core/HardwareSelector/Constants'; + +/** + * @deprecated Import PassTypes from + * '@kitware/vtk.js/Rendering/Core/HardwareSelector/Constants'. + */ +export import PassTypes = CorePassTypes; diff --git a/Sources/Rendering/OpenGL/HardwareSelector/Constants.js b/Sources/Rendering/OpenGL/HardwareSelector/Constants.js index 8a63669a5c3..697a20d3894 100644 --- a/Sources/Rendering/OpenGL/HardwareSelector/Constants.js +++ b/Sources/Rendering/OpenGL/HardwareSelector/Constants.js @@ -1,12 +1,9 @@ -export const PassTypes = { - MIN_KNOWN_PASS: 0, - ACTOR_PASS: 0, - COMPOSITE_INDEX_PASS: 1, - ID_LOW24: 2, - ID_HIGH24: 3, - MAX_KNOWN_PASS: 3, -}; - -export default { +// @deprecated PassTypes is in @kitware/vtk.js/Rendering/Core/HardwareSelector/Constants. +// This module gives the same object to keep old imports working. +import CoreConstants, { PassTypes, -}; +} from 'vtk.js/Sources/Rendering/Core/HardwareSelector/Constants'; + +export { PassTypes }; + +export default CoreConstants; diff --git a/Sources/Rendering/OpenGL/HardwareSelector/index.d.ts b/Sources/Rendering/OpenGL/HardwareSelector/index.d.ts index 8d64ece42e8..c22b89e0894 100644 --- a/Sources/Rendering/OpenGL/HardwareSelector/index.d.ts +++ b/Sources/Rendering/OpenGL/HardwareSelector/index.d.ts @@ -9,7 +9,7 @@ import vtkOpenGLRenderWindow from '../../../Rendering/OpenGL/RenderWindow'; import { FieldAssociations } from '../../../Common/DataModel/DataSet/Constants'; import { EventHandler, vtkSubscription } from '../../../interfaces'; import { Nullable, Vector2, Vector3 } from '../../../types'; -import { PassTypes } from './Constants'; +import { PassTypes } from '../../Core/HardwareSelector/Constants'; type Area = [number, number, number, number]; @@ -46,7 +46,6 @@ export interface PixelInformation { attributeID?: number; } -// TODO extends vtkHardwareSelector export interface vtkOpenGLHardwareSelector extends vtkHardwareSelector { /** * Releases internal pixel buffer memory. diff --git a/Sources/Rendering/OpenGL/HardwareSelector/index.js b/Sources/Rendering/OpenGL/HardwareSelector/index.js index 8869481766a..307c96a67d4 100644 --- a/Sources/Rendering/OpenGL/HardwareSelector/index.js +++ b/Sources/Rendering/OpenGL/HardwareSelector/index.js @@ -1,21 +1,15 @@ import macro from 'vtk.js/Sources/macros'; -import Constants from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector/Constants'; +import Constants from 'vtk.js/Sources/Rendering/Core/HardwareSelector/Constants'; import vtkHardwareSelector from 'vtk.js/Sources/Rendering/Core/HardwareSelector'; import vtkOpenGLFramebuffer from 'vtk.js/Sources/Rendering/OpenGL/Framebuffer'; -import vtkSelectionNode from 'vtk.js/Sources/Common/DataModel/SelectionNode'; +import PixelSelectionHelper from 'vtk.js/Sources/Rendering/Core/HardwareSelector/PixelSelectionHelper'; import vtkDataSet from 'vtk.js/Sources/Common/DataModel/DataSet'; const { PassTypes } = Constants; -const { SelectionContent, SelectionField } = vtkSelectionNode; const { FieldAssociations } = vtkDataSet; -const { vtkErrorMacro, vtkDebugMacro } = macro; const idOffset = 1; -function getInfoHash(info) { - return `${info.propID} ${info.compositeID}`; -} - function getAlpha(xx, yy, pb, area) { if (!pb) { return 0; @@ -24,19 +18,16 @@ function getAlpha(xx, yy, pb, area) { return pb[offset + 3]; } -function convert(xx, yy, pb, area, caller = null) { +function convert(xx, yy, pb, area) { if (!pb) { return 0; } - // console.log(caller); - // console.log('convert', xx, yy, pb, area); const offset = (yy * (area[2] - area[0] + 1) + xx) * 4; const r = pb[offset]; const g = pb[offset + 1]; const b = pb[offset + 2]; - // console.log(b, g, r, (b * 256 + g) * 256 + r); return (b * 256 + g) * 256 + r; } @@ -49,287 +40,115 @@ function getID(low24, high8) { /* eslint-enable no-bitwise */ } -function getPixelInformationWithData( - buffdata, - inDisplayPosition, - maxDistance, - outSelectedPosition -) { - // Base case - const maxDist = maxDistance < 0 ? 0 : maxDistance; - if (maxDist === 0) { - outSelectedPosition[0] = inDisplayPosition[0]; - outSelectedPosition[1] = inDisplayPosition[1]; - if ( - inDisplayPosition[0] < buffdata.area[0] || - inDisplayPosition[0] > buffdata.area[2] || - inDisplayPosition[1] < buffdata.area[1] || - inDisplayPosition[1] > buffdata.area[3] - ) { - return null; - } - - // offset inDisplayPosition based on the lower-left-corner of the Area. - const displayPosition = [ - inDisplayPosition[0] - buffdata.area[0], - inDisplayPosition[1] - buffdata.area[1], - ]; - - const actorid = convert( - displayPosition[0], - displayPosition[1], - buffdata.pixBuffer[PassTypes.ACTOR_PASS], - buffdata.area, - 'getPixelInformationWithData1' - ); - if (actorid <= 0 || actorid - idOffset >= buffdata.props.length) { - // the pixel did not hit any actor. - return null; - } - - const info = {}; - info.valid = true; - - info.propID = actorid - idOffset; - info.prop = buffdata.props[info.propID]; - - let compositeID = convert( - displayPosition[0], - displayPosition[1], - buffdata.pixBuffer[PassTypes.COMPOSITE_INDEX_PASS], - buffdata.area, - 'getPixelInformationWithData2' - ); - if (compositeID < 0 || compositeID > 0xffffff) { - compositeID = 0; - } - info.compositeID = compositeID - idOffset; - if (buffdata.captureZValues) { - const offset = - (displayPosition[1] * (buffdata.area[2] - buffdata.area[0] + 1) + - displayPosition[0]) * - 4; - info.zValue = - (256 * buffdata.zBuffer[offset] + buffdata.zBuffer[offset + 1]) / - 65535.0; - info.displayPosition = inDisplayPosition; - } +// Decode one pixel of the pass buffers. buffdata has area, pixBuffer, props, +// captureZValues and zBuffer. +function readPixel(buffdata, inDisplayPosition) { + if ( + inDisplayPosition[0] < buffdata.area[0] || + inDisplayPosition[0] > buffdata.area[2] || + inDisplayPosition[1] < buffdata.area[1] || + inDisplayPosition[1] > buffdata.area[3] + ) { + return null; + } - if (buffdata.pixBuffer[PassTypes.ID_LOW24]) { - if ( - getAlpha( - displayPosition[0], - displayPosition[1], - buffdata.pixBuffer[PassTypes.ID_LOW24], - buffdata.area - ) === 0.0 - ) { - return info; - } - } + // offset inDisplayPosition based on the lower-left-corner of the Area. + const displayPosition = [ + inDisplayPosition[0] - buffdata.area[0], + inDisplayPosition[1] - buffdata.area[1], + ]; + + const actorid = convert( + displayPosition[0], + displayPosition[1], + buffdata.pixBuffer[PassTypes.ACTOR_PASS], + buffdata.area + ); + if (actorid <= 0 || actorid - idOffset >= buffdata.props.length) { + // the pixel did not hit any actor. + return null; + } - const low24 = convert( - displayPosition[0], - displayPosition[1], - buffdata.pixBuffer[PassTypes.ID_LOW24], - buffdata.area, - 'getPixelInformationWithData3' - ); - const high24 = convert( - displayPosition[0], - displayPosition[1], - buffdata.pixBuffer[PassTypes.ID_HIGH24], - buffdata.area, - 'getPixelInformationWithData4' - ); - info.attributeID = getID(low24, high24, 0); + const info = {}; + info.valid = true; - return info; - } + info.propID = actorid - idOffset; + info.prop = buffdata.props[info.propID]; - // Iterate over successively growing boxes. - // They recursively call the base case to handle single pixels. - const dispPos = [inDisplayPosition[0], inDisplayPosition[1]]; - const curPos = [0, 0]; - let info = getPixelInformationWithData( - buffdata, - inDisplayPosition, - 0, - outSelectedPosition + let compositeID = convert( + displayPosition[0], + displayPosition[1], + buffdata.pixBuffer[PassTypes.COMPOSITE_INDEX_PASS], + buffdata.area ); - if (info && info.valid) { - return info; + if (compositeID < 0 || compositeID > 0xffffff) { + compositeID = 0; } - for (let dist = 1; dist < maxDist; ++dist) { - // Vertical sides of box. - for ( - let y = dispPos[1] > dist ? dispPos[1] - dist : 0; - y <= dispPos[1] + dist; - ++y - ) { - curPos[1] = y; - if (dispPos[0] >= dist) { - curPos[0] = dispPos[0] - dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info && info.valid) { - return info; - } - } - curPos[0] = dispPos[0] + dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info && info.valid) { - return info; - } - } - // Horizontal sides of box. - for ( - let x = dispPos[0] >= dist ? dispPos[0] - (dist - 1) : 0; - x <= dispPos[0] + (dist - 1); - ++x - ) { - curPos[0] = x; - if (dispPos[1] >= dist) { - curPos[1] = dispPos[1] - dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info && info.valid) { - return info; - } - } - curPos[1] = dispPos[1] + dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info && info.valid) { - return info; - } - } + info.compositeID = compositeID - idOffset; + if (buffdata.captureZValues) { + const offset = + (displayPosition[1] * (buffdata.area[2] - buffdata.area[0] + 1) + + displayPosition[0]) * + 4; + info.zValue = + (256 * buffdata.zBuffer[offset] + buffdata.zBuffer[offset + 1]) / 65535.0; + info.displayPosition = [inDisplayPosition[0], inDisplayPosition[1]]; } - // nothing hit. - outSelectedPosition[0] = inDisplayPosition[0]; - outSelectedPosition[1] = inDisplayPosition[1]; - return null; -} - -//----------------------------------------------------------------------------- -function convertSelection( - fieldassociation, - dataMap, - captureZValues, - renderer, - openGLRenderWindow -) { - const sel = []; - - let count = 0; - dataMap.forEach((value, key) => { - const child = vtkSelectionNode.newInstance(); - child.setContentType(SelectionContent.INDICES); - switch (fieldassociation) { - case FieldAssociations.FIELD_ASSOCIATION_CELLS: - child.setFieldType(SelectionField.CELL); - break; - case FieldAssociations.FIELD_ASSOCIATION_POINTS: - child.setFieldType(SelectionField.POINT); - break; - default: - vtkErrorMacro('Unknown field association'); - } - child.getProperties().propID = value.info.propID; - child.getProperties().prop = value.info.prop; - child.getProperties().compositeID = value.info.compositeID; - child.getProperties().attributeID = value.info.attributeID; - child.getProperties().pixelCount = value.pixelCount; - if (captureZValues) { - child.getProperties().displayPosition = [ - value.info.displayPosition[0], - value.info.displayPosition[1], - value.info.zValue, - ]; - child.getProperties().worldPosition = openGLRenderWindow.displayToWorld( - value.info.displayPosition[0], - value.info.displayPosition[1], - value.info.zValue, - renderer - ); + // Skip attribute ids if alpha is zero (missed) + if (buffdata.pixBuffer[PassTypes.ID_LOW24]) { + if ( + getAlpha( + displayPosition[0], + displayPosition[1], + buffdata.pixBuffer[PassTypes.ID_LOW24], + buffdata.area + ) === 0.0 + ) { + return info; } + } - child.setSelectionList(value.attributeIDs); - sel[count] = child; - count++; - }); + const low24 = convert( + displayPosition[0], + displayPosition[1], + buffdata.pixBuffer[PassTypes.ID_LOW24], + buffdata.area + ); + const high24 = convert( + displayPosition[0], + displayPosition[1], + buffdata.pixBuffer[PassTypes.ID_HIGH24], + buffdata.area + ); + info.attributeID = getID(low24, high24); - return sel; + return info; } -//---------------------------------------------------------------------------- -function generateSelectionWithData(buffdata, fx1, fy1, fx2, fy2) { - const x1 = Math.floor(fx1); - const y1 = Math.floor(fy1); - const x2 = Math.floor(fx2); - const y2 = Math.floor(fy2); - - const dataMap = new Map(); - - const outSelectedPosition = [0, 0]; - - for (let yy = y1; yy <= y2; yy++) { - for (let xx = x1; xx <= x2; xx++) { - const pos = [xx, yy]; - const info = getPixelInformationWithData( - buffdata, - pos, - 0, - outSelectedPosition - ); - if (info && info.valid) { - const hash = getInfoHash(info); - if (!dataMap.has(hash)) { - dataMap.set(hash, { - info, - pixelCount: 1, - attributeIDs: [info.attributeID], - }); - } else { - const dmv = dataMap.get(hash); - dmv.pixelCount++; - if (buffdata.captureZValues) { - if (info.zValue < dmv.info.zValue) { - dmv.info = info; - } - } - if (dmv.attributeIDs.indexOf(info.attributeID) === -1) { - dmv.attributeIDs.push(info.attributeID); - } - } - } - } - } - return convertSelection( - buffdata.fieldAssociation, - dataMap, - buffdata.captureZValues, - buffdata.renderer, - buffdata.openGLRenderWindow - ); +// Give buffdata the functions that build selections from its pixels. +// buffdata also has fieldAssociation, renderer and openGLRenderWindow. +function addSelectionFunctions(buffdata) { + const read = (position) => readPixel(buffdata, position); + buffdata.getPixelInformation = ( + inDisplayPosition, + maxDistance, + outSelectedPosition + ) => + PixelSelectionHelper.getPixelInformation( + read, + inDisplayPosition, + maxDistance, + outSelectedPosition + ); + buffdata.generateSelection = (fx1, fy1, fx2, fy2) => + PixelSelectionHelper.generateSelection(read, fx1, fy1, fx2, fy2, { + fieldAssociation: buffdata.fieldAssociation, + captureZValues: buffdata.captureZValues, + displayToWorld: (x, y, z) => + buffdata.openGLRenderWindow.displayToWorld(x, y, z, buffdata.renderer), + }); + return buffdata; } // ---------------------------------------------------------------------------- @@ -462,15 +281,12 @@ function vtkOpenGLHardwareSelector(publicAPI, model) { renderer, openGLRenderWindow: model._openGLRenderWindow, }; - result.generateSelection = (...args) => - generateSelectionWithData(result, ...args); - return result; + return addSelectionFunctions(result); }; //---------------------------------------------------------------------------- publicAPI.captureBuffers = () => { - if (!model._renderer || !model._openGLRenderWindow) { - vtkErrorMacro('Renderer and view must be set before calling Select.'); + if (!publicAPI.prepareCapture(model._openGLRenderWindow, model._renderer)) { return false; } @@ -479,10 +295,6 @@ function vtkOpenGLHardwareSelector(publicAPI, model) { model._openGLRenderer ); - // todo revisit making selection part of core - // then we can do this in core - model._openGLRenderWindow.getRenderable().preRender(); - // int rgba[4]; // rwin.getColorBufferSizes(rgba); // if (rgba[0] < 8 || rgba[1] < 8 || rgba[2] < 8) { @@ -645,20 +457,12 @@ function vtkOpenGLHardwareSelector(publicAPI, model) { }; //---------------------------------------------------------------------------- - // TODO: make inline + // Keeps the largest attribute id given. A negative id is valid and is + // ignored: higher order cells add points to draw smooth surfaces. publicAPI.renderAttributeId = (attribid) => { - if (attribid < 0) { - // negative attribid is valid. It happens when rendering higher order - // elements where new points are added for rendering smooth surfaces. - return; + if (attribid >= 0) { + model.maxAttributeId = Math.max(model.maxAttributeId, attribid); } - - model.maxAttributeId = - attribid > model.maxAttributeId ? attribid : model.maxAttributeId; - - // if (model.currentPass < PassTypes.ID_LOW24) { - // return; // useless... - // } }; //---------------------------------------------------------------------------- @@ -678,224 +482,35 @@ function vtkOpenGLHardwareSelector(publicAPI, model) { publicAPI.delete ); - // info has - // valid - // propId - // prop - // compositeID - // attributeID - //---------------------------------------------------------------------------- + // The selection functions read the pass buffers of the last + // captureBuffers() call. info has valid, propID, prop, compositeID and + // attributeID. + const getCapturedData = () => + addSelectionFunctions({ + area: model.area, + pixBuffer: model.pixBuffer, + captureZValues: model.captureZValues, + zBuffer: model.zBuffer, + props: model.props, + fieldAssociation: model.fieldAssociation, + renderer: model._renderer, + openGLRenderWindow: model._openGLRenderWindow, + }); + publicAPI.getPixelInformation = ( inDisplayPosition, maxDistance, outSelectedPosition - ) => { - // Base case - const maxDist = maxDistance < 0 ? 0 : maxDistance; - vtkDebugMacro('getPixelInformation called', maxDist); - if (maxDist === 0) { - outSelectedPosition[0] = inDisplayPosition[0]; - outSelectedPosition[1] = inDisplayPosition[1]; - if ( - inDisplayPosition[0] < model.area[0] || - inDisplayPosition[0] > model.area[2] || - inDisplayPosition[1] < model.area[1] || - inDisplayPosition[1] > model.area[3] - ) { - return null; - } - - vtkDebugMacro(inDisplayPosition); - vtkDebugMacro(model.area); - // offset inDisplayPosition based on the lower-left-corner of the Area. - const displayPosition = [ - inDisplayPosition[0] - model.area[0], - inDisplayPosition[1] - model.area[1], - ]; - - vtkDebugMacro('adjusted displayPosition', displayPosition); - - const actorid = convert( - displayPosition[0], - displayPosition[1], - model.pixBuffer[PassTypes.ACTOR_PASS], - model.area, - 'getPixelInformation' - ); - - vtkDebugMacro('actorid', actorid); - if (actorid <= 0 || actorid - idOffset >= model.props.length) { - // the pixel did not hit any actor. - return null; - } - - const info = {}; - info.valid = true; - - info.propID = actorid - idOffset; - info.prop = model.props[info.propID]; - let compositeID = convert( - displayPosition[0], - displayPosition[1], - model.pixBuffer[PassTypes.COMPOSITE_INDEX_PASS], - model.area, - 'getPixelInformation2' - ); - if (compositeID < 0 || compositeID > 0xffffff) { - compositeID = 0; - } - info.compositeID = compositeID - idOffset; - if (model.captureZValues) { - const offset = - (displayPosition[1] * (model.area[2] - model.area[0] + 1) + - displayPosition[0]) * - 4; - info.zValue = - (256 * model.zBuffer[offset] + model.zBuffer[offset + 1]) / 65535.0; - info.displayPosition = inDisplayPosition; - } - - // Skip attribute ids if alpha is zero (missed) - if (model.pixBuffer[PassTypes.ID_LOW24]) { - if ( - getAlpha( - displayPosition[0], - displayPosition[1], - model.pixBuffer[PassTypes.ID_LOW24], - model.area - ) === 0.0 - ) { - return info; - } - } - - const low24 = convert( - displayPosition[0], - displayPosition[1], - model.pixBuffer[PassTypes.ID_LOW24], - model.area, - 'getPixelInformation3' - ); - const high24 = convert( - displayPosition[0], - displayPosition[1], - model.pixBuffer[PassTypes.ID_HIGH24], - model.area, - 'getPixelInformation4' - ); - info.attributeID = getID(low24, high24); - - return info; - } - - // Iterate over successively growing boxes. - // They recursively call the base case to handle single pixels. - const dispPos = [inDisplayPosition[0], inDisplayPosition[1]]; - const curPos = [0, 0]; - let info = publicAPI.getPixelInformation( + ) => + getCapturedData().getPixelInformation( inDisplayPosition, - 0, + maxDistance, outSelectedPosition ); - if (info && info.valid) { - return info; - } - for (let dist = 1; dist < maxDist; ++dist) { - // Vertical sides of box. - for ( - let y = dispPos[1] > dist ? dispPos[1] - dist : 0; - y <= dispPos[1] + dist; - ++y - ) { - curPos[1] = y; - if (dispPos[0] >= dist) { - curPos[0] = dispPos[0] - dist; - info = publicAPI.getPixelInformation(curPos, 0, outSelectedPosition); - if (info && info.valid) { - return info; - } - } - curPos[0] = dispPos[0] + dist; - info = publicAPI.getPixelInformation(curPos, 0, outSelectedPosition); - if (info && info.valid) { - return info; - } - } - // Horizontal sides of box. - for ( - let x = dispPos[0] >= dist ? dispPos[0] - (dist - 1) : 0; - x <= dispPos[0] + (dist - 1); - ++x - ) { - curPos[0] = x; - if (dispPos[1] >= dist) { - curPos[1] = dispPos[1] - dist; - info = publicAPI.getPixelInformation(curPos, 0, outSelectedPosition); - if (info && info.valid) { - return info; - } - } - curPos[1] = dispPos[1] + dist; - info = publicAPI.getPixelInformation(curPos, 0, outSelectedPosition); - if (info && info.valid) { - return info; - } - } - } - - // nothing hit. - outSelectedPosition[0] = inDisplayPosition[0]; - outSelectedPosition[1] = inDisplayPosition[1]; - return null; - }; - //---------------------------------------------------------------------------- - publicAPI.generateSelection = (fx1, fy1, fx2, fy2) => { - const x1 = Math.floor(fx1); - const y1 = Math.floor(fy1); - const x2 = Math.floor(fx2); - const y2 = Math.floor(fy2); - - const dataMap = new Map(); - - const outSelectedPosition = [0, 0]; - - for (let yy = y1; yy <= y2; yy++) { - for (let xx = x1; xx <= x2; xx++) { - const pos = [xx, yy]; - const info = publicAPI.getPixelInformation(pos, 0, outSelectedPosition); - if (info && info.valid) { - const hash = getInfoHash(info); - if (!dataMap.has(hash)) { - dataMap.set(hash, { - info, - pixelCount: 1, - attributeIDs: [info.attributeID], - }); - } else { - const dmv = dataMap.get(hash); - dmv.pixelCount++; - if (model.captureZValues) { - if (info.zValue < dmv.info.zValue) { - dmv.info = info; - } - } - if (dmv.attributeIDs.indexOf(info.attributeID) === -1) { - dmv.attributeIDs.push(info.attributeID); - } - } - } - } - } - return convertSelection( - model.fieldAssociation, - dataMap, - model.captureZValues, - model._renderer, - model._openGLRenderWindow - ); - }; + publicAPI.generateSelection = (fx1, fy1, fx2, fy2) => + getCapturedData().generateSelection(fx1, fy1, fx2, fy2); publicAPI.getRawPixelBuffer = (passNo) => model.rawPixBuffer[passNo]; publicAPI.getPixelBuffer = (passNo) => model.pixBuffer[passNo]; diff --git a/Sources/Rendering/OpenGL/PolyDataMapper/index.js b/Sources/Rendering/OpenGL/PolyDataMapper/index.js index 2a28fe99a5d..dae2477031c 100755 --- a/Sources/Rendering/OpenGL/PolyDataMapper/index.js +++ b/Sources/Rendering/OpenGL/PolyDataMapper/index.js @@ -18,7 +18,7 @@ import vtkReplacementShaderMapper from 'vtk.js/Sources/Rendering/OpenGL/Replacem import { registerOverride } from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; -import { PassTypes } from 'vtk.js/Sources/Rendering/OpenGL/HardwareSelector/Constants'; +import { PassTypes } from 'vtk.js/Sources/Rendering/Core/HardwareSelector/Constants'; import vtkDataSet from 'vtk.js/Sources/Common/DataModel/DataSet'; import { Resolve } from 'vtk.js/Sources/Rendering/Core/Mapper/CoincidentTopologyHelper'; diff --git a/Sources/Rendering/WebGPU/HardwareSelector/index.d.ts b/Sources/Rendering/WebGPU/HardwareSelector/index.d.ts new file mode 100644 index 00000000000..ed942a0b3e7 --- /dev/null +++ b/Sources/Rendering/WebGPU/HardwareSelector/index.d.ts @@ -0,0 +1,146 @@ +import vtkSelectionNode from '../../../Common/DataModel/SelectionNode'; +import { + IHardwareSelectorInitialValues, + vtkHardwareSelector, +} from '../../../Rendering/Core/HardwareSelector'; +import vtkProp from '../../../Rendering/Core/Prop'; +import vtkRenderer from '../../../Rendering/Core/Renderer'; +import { FieldAssociations } from '../../../Common/DataModel/DataSet/Constants'; +import { Nullable, Vector2 } from '../../../types'; +import { PixelInformation } from '../../OpenGL/HardwareSelector'; + +type Area = [number, number, number, number]; + +/** + * The pixels that getSourceDataAsync read back, with the functions that use + * them. The color buffer holds only the area. The depth buffer holds all + * the window. + */ +export interface WebGPUSourceData { + area: Area; + width: number; + height: number; + colorValues: Uint32Array; + colorBufferWidth: number; + depthValues?: Float32Array; + zbufferBufferWidth?: number; + captureZValues: boolean; + fieldAssociation: FieldAssociations; + props: vtkProp[]; + renderer: vtkRenderer; + generateSelection( + fx1: number, + fy1: number, + fx2: number, + fy2: number + ): vtkSelectionNode[]; + getPixelInformation( + inDispPos: Vector2, + maxDistance: number, + outDispPos: Vector2 + ): Nullable; + isPropHit(propID: number): boolean; +} + +export interface vtkWebGPUHardwareSelector extends vtkHardwareSelector { + /** + * Attaches a render window and a renderer to this selector. + */ + attach(webGPURenderWindow: unknown, renderer: vtkRenderer): void; + + /** + * Renders the selection pass and reads back the pixels of the area. With + * no area, all the window is read. + */ + getSourceDataAsync( + renderer?: vtkRenderer, + fx1?: number, + fy1?: number, + fx2?: number, + fy2?: number + ): Promise; + + /** + * Reads the area of setArea() and keeps the data for getPixelInformation, + * generateSelection and isPropHit. WebGPU has no synchronous + * captureBuffers() or select(). + */ + captureBuffersAsync(): Promise; + + /** + * The data of the last captureBuffersAsync() call. + */ + getCapturedData(): Nullable; + + /** + * Gets the selection information for a pixel of the captured data. + */ + getPixelInformation( + inDispPos: Vector2, + maxDistance: number, + outDispPos: Vector2 + ): Nullable; + + /** + * Generates selections in an area of the captured data. + */ + generateSelection( + fx1: number, + fy1: number, + fx2: number, + fy2: number + ): vtkSelectionNode[]; + + /** + * True if the captured data has a pixel of the prop. + */ + isPropHit(propID: number): boolean; + + /** + * Removes the captured data. + */ + releasePixBuffers(): void; + + releaseGraphicsResources(): void; + + endSelection(): void; + + /** + * The area in display pixels, with y up. The values are rounded down. + */ + setArea(area: Area): boolean; + setArea(fx1: number, fy1: number, fx2: number, fy2: number): boolean; + getArea(): Area; + + /** + * Removes the area. Then captureBuffersAsync() reads all the window. + */ + clearArea(): void; +} + +export function newInstance( + initialValues?: IHardwareSelectorInitialValues +): vtkWebGPUHardwareSelector; + +export function extend( + publicAPI: object, + model: object, + initialValues?: IHardwareSelectorInitialValues +): void; + +/** + * Gives read back selection buffers the functions that use them. + */ +export function newSourceData(fields: object): WebGPUSourceData; + +/** + * The WebGPU hardware selector renders the prop, composite and attribute ids + * to one rgba32uint texture in one pass. + */ +export const vtkWebGPUHardwareSelector: { + newInstance: typeof newInstance; + extend: typeof extend; + newSourceData: typeof newSourceData; +}; + +export default vtkWebGPUHardwareSelector; diff --git a/Sources/Rendering/WebGPU/HardwareSelector/index.js b/Sources/Rendering/WebGPU/HardwareSelector/index.js index abd2c9012db..949949f28af 100644 --- a/Sources/Rendering/WebGPU/HardwareSelector/index.js +++ b/Sources/Rendering/WebGPU/HardwareSelector/index.js @@ -1,270 +1,129 @@ import macro from 'vtk.js/Sources/macros'; import vtkHardwareSelector from 'vtk.js/Sources/Rendering/Core/HardwareSelector'; +import PixelSelectionHelper from 'vtk.js/Sources/Rendering/Core/HardwareSelector/PixelSelectionHelper'; import vtkWebGPUBuffer from 'vtk.js/Sources/Rendering/WebGPU/Buffer'; import vtkWebGPUHardwareSelectionPass from 'vtk.js/Sources/Rendering/WebGPU/HardwareSelectionPass'; -import vtkSelectionNode from 'vtk.js/Sources/Common/DataModel/SelectionNode'; -import vtkDataSet from 'vtk.js/Sources/Common/DataModel/DataSet'; -const { SelectionContent, SelectionField } = vtkSelectionNode; -const { FieldAssociations } = vtkDataSet; const { vtkErrorMacro } = macro; -function getInfoHash(info) { - return `${info.propID} ${info.compositeID}`; +// The buffers hold only the captured area. Display y goes up and texture +// rows go down, so the first buffer row is the top row of the area. +function isInArea(xx, yy, buffdata) { + const area = buffdata.area; + return xx >= area[0] && xx <= area[2] && yy >= area[1] && yy <= area[3]; +} + +function getPixelIndex(xx, yy, buffdata, rowWidth) { + return (buffdata.area[3] - yy) * rowWidth + (xx - buffdata.area[0]); } function convert(xx, yy, buffdata, channel) { - const offset = - ((buffdata.height - yy - 1) * buffdata.colorBufferWidth + xx) * 4 + channel; - return buffdata.colorValues[offset]; + const index = getPixelIndex(xx, yy, buffdata, buffdata.colorBufferWidth); + return buffdata.colorValues[index * 4 + channel]; } -function getPixelInformationWithData( - buffdata, - inDisplayPosition, - maxDistance, - outSelectedPosition -) { - // Base case - const maxDist = maxDistance < 0 ? 0 : maxDistance; - if (maxDist === 0) { - outSelectedPosition[0] = inDisplayPosition[0]; - outSelectedPosition[1] = inDisplayPosition[1]; - if ( - inDisplayPosition[0] < 0 || - inDisplayPosition[0] >= buffdata.width || - inDisplayPosition[1] < 0 || - inDisplayPosition[1] >= buffdata.height - ) { - return null; - } +// Decode one pixel of the read back buffers. +function readPixel(buffdata, inDisplayPosition) { + if (!isInArea(inDisplayPosition[0], inDisplayPosition[1], buffdata)) { + return null; + } - const actorid = convert( - inDisplayPosition[0], - inDisplayPosition[1], - buffdata, - 0 - ); + const actorid = convert( + inDisplayPosition[0], + inDisplayPosition[1], + buffdata, + 0 + ); - if (actorid <= 0 || actorid - 1 >= (buffdata.props?.length ?? 0)) { - // the pixel did not hit any actor. - return null; - } + if (actorid <= 0 || actorid - 1 >= (buffdata.props?.length ?? 0)) { + // the pixel did not hit any actor. + return null; + } - const info = {}; + const info = { valid: true }; - info.propID = actorid - 1; - info.prop = buffdata.props?.[info.propID]; + info.propID = actorid - 1; + info.prop = buffdata.props?.[info.propID]; - let compositeID = convert( - inDisplayPosition[0], - inDisplayPosition[1], - buffdata, - 1 - ); - if (compositeID < 0 || compositeID > 0xffffff) { - compositeID = 0; - } - info.compositeID = compositeID - 1; - // attributeID is stored with a +1 offset in the selection buffer so that - // 0 means "no data" while 0 remains a valid decoded attribute index. - // A buffer value < 1 means no attribute was written, fall back to compositeID. - // After fallback, subtract 1 to recover the original 0 based index. - let attributeID = convert( - inDisplayPosition[0], - inDisplayPosition[1], - buffdata, - 2 - ); - if (attributeID < 1) { - attributeID = compositeID; - } - info.attributeID = attributeID - 1; - - if (buffdata.captureZValues) { - const offset = - (buffdata.height - inDisplayPosition[1] - 1) * - buffdata.zbufferBufferWidth + - inDisplayPosition[0]; - info.zValue = buffdata.depthValues[offset]; - info.zValue = buffdata.webGPURenderer.convertToOpenGLDepth(info.zValue); - info.displayPosition = inDisplayPosition; - } - return info; + let compositeID = convert( + inDisplayPosition[0], + inDisplayPosition[1], + buffdata, + 1 + ); + if (compositeID < 0 || compositeID > 0xffffff) { + compositeID = 0; } - - // Iterate over successively growing boxes. - // They recursively call the base case to handle single pixels. - const dispPos = [inDisplayPosition[0], inDisplayPosition[1]]; - const curPos = [0, 0]; - let info = getPixelInformationWithData( + info.compositeID = compositeID - 1; + // attributeID is stored with a +1 offset in the selection buffer so that + // 0 means "no data" while 0 remains a valid decoded attribute index. + // A buffer value < 1 means no attribute was written, fall back to compositeID. + // After fallback, subtract 1 to recover the original 0 based index. + let attributeID = convert( + inDisplayPosition[0], + inDisplayPosition[1], buffdata, - inDisplayPosition, - 0, - outSelectedPosition + 2 ); - if (info) { - return info; + if (attributeID < 1) { + attributeID = compositeID; } - for (let dist = 1; dist < maxDist; ++dist) { - // Vertical sides of box. - for ( - let y = dispPos[1] > dist ? dispPos[1] - dist : 0; - y <= dispPos[1] + dist; - ++y - ) { - curPos[1] = y; - if (dispPos[0] >= dist) { - curPos[0] = dispPos[0] - dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info) { - return info; - } - } - curPos[0] = dispPos[0] + dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info) { - return info; - } - } - // Horizontal sides of box. - for ( - let x = dispPos[0] >= dist ? dispPos[0] - (dist - 1) : 0; - x <= dispPos[0] + (dist - 1); - ++x - ) { - curPos[0] = x; - if (dispPos[1] >= dist) { - curPos[1] = dispPos[1] - dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info) { - return info; - } - } - curPos[1] = dispPos[1] + dist; - info = getPixelInformationWithData( - buffdata, - curPos, - 0, - outSelectedPosition - ); - if (info) { - return info; - } - } + info.attributeID = attributeID - 1; + + if (buffdata.captureZValues) { + // A copy from a depth texture must use the full texture, so the depth + // buffer holds all the window. + const offset = + (buffdata.height - inDisplayPosition[1] - 1) * + buffdata.zbufferBufferWidth + + inDisplayPosition[0]; + info.zValue = buffdata.depthValues[offset]; + info.zValue = buffdata.webGPURenderer.convertToOpenGLDepth(info.zValue); + info.displayPosition = [inDisplayPosition[0], inDisplayPosition[1]]; } - - // nothing hit. - outSelectedPosition[0] = inDisplayPosition[0]; - outSelectedPosition[1] = inDisplayPosition[1]; - return null; -} - -//----------------------------------------------------------------------------- -function convertSelection(fieldassociation, dataMap, buffdata) { - const sel = []; - - let count = 0; - dataMap.forEach((value, key) => { - const child = vtkSelectionNode.newInstance(); - child.setContentType(SelectionContent.INDICES); - switch (fieldassociation) { - case FieldAssociations.FIELD_ASSOCIATION_CELLS: - child.setFieldType(SelectionField.CELL); - break; - case FieldAssociations.FIELD_ASSOCIATION_POINTS: - child.setFieldType(SelectionField.POINT); - break; - default: - vtkErrorMacro('Unknown field association'); - } - child.getProperties().propID = value.info.propID; - child.getProperties().prop = value.info.prop; - child.getProperties().compositeID = value.info.compositeID; - child.getProperties().attributeID = value.info.attributeID; - child.getProperties().pixelCount = value.pixelCount; - if (buffdata.captureZValues) { - child.getProperties().displayPosition = [ - value.info.displayPosition[0], - value.info.displayPosition[1], - value.info.zValue, - ]; - child.getProperties().worldPosition = - buffdata.webGPURenderWindow.displayToWorld( - value.info.displayPosition[0], - value.info.displayPosition[1], - value.info.zValue, - buffdata.renderer - ); - } - - child.setSelectionList(value.attributeIDs); - sel[count] = child; - count++; - }); - - return sel; + return info; } //---------------------------------------------------------------------------- -function generateSelectionWithData(buffdata, fx1, fy1, fx2, fy2) { - const x1 = Math.floor(fx1); - const y1 = Math.floor(fy1); - const x2 = Math.floor(fx2); - const y2 = Math.floor(fy2); - - const dataMap = new Map(); - - const outSelectedPosition = [0, 0]; - - for (let yy = y1; yy <= y2; yy++) { - for (let xx = x1; xx <= x2; xx++) { - const pos = [xx, yy]; - const info = getPixelInformationWithData( - buffdata, - pos, - 0, - outSelectedPosition - ); - if (info) { - const hash = getInfoHash(info); - if (!dataMap.has(hash)) { - dataMap.set(hash, { - info, - pixelCount: 1, - attributeIDs: [info.attributeID], - }); - } else { - const dmv = dataMap.get(hash); - dmv.pixelCount++; - if (buffdata.captureZValues) { - if (info.zValue < dmv.info.zValue) { - dmv.info = info; - } - } - if (dmv.attributeIDs.indexOf(info.attributeID) === -1) { - dmv.attributeIDs.push(info.attributeID); - } +// Give the read back buffers the functions that use them. The fields are +// area, width, height, colorValues, colorBufferWidth, props, +// fieldAssociation and captureZValues. With captureZValues, also +// depthValues, zbufferBufferWidth, renderer, webGPURenderer and +// webGPURenderWindow. +export function newSourceData(fields) { + const result = { ...fields }; + const read = (position) => readPixel(result, position); + result.generateSelection = (fx1, fy1, fx2, fy2) => + PixelSelectionHelper.generateSelection(read, fx1, fy1, fx2, fy2, { + fieldAssociation: result.fieldAssociation, + captureZValues: result.captureZValues, + displayToWorld: (x, y, z) => + result.webGPURenderWindow.displayToWorld(x, y, z, result.renderer), + }); + result.getPixelInformation = ( + inDisplayPosition, + maxDistance, + outSelectedPosition + ) => + PixelSelectionHelper.getPixelInformation( + read, + inDisplayPosition, + maxDistance, + outSelectedPosition + ); + result.isPropHit = (propID) => { + if (!result.hitProps) { + result.hitProps = new Set(); + const values = result.colorValues; + for (let i = 0; i < values.length; i += 4) { + if (values[i] > 0) { + result.hitProps.add(values[i] - 1); } } } - } - return convertSelection(buffdata.fieldAssociation, dataMap, buffdata); + return result.hitProps.has(propID); + }; + return result; } // ---------------------------------------------------------------------------- @@ -293,23 +152,104 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { //---------------------------------------------------------------------------- publicAPI.endSelection = () => { - model.WebGPURenderer.setSelector(null); + model._WebGPURenderWindow + ?.getViewNodeFor(model._renderer) + ?.setSelector(null); }; //---------------------------------------------------------------------------- - // note we ignore the x,y arguments as WebGPU has to do buffer copies - // of the entire depth bufer. We could realloc hardware selection textures - // based on the passed in size etc but it gets messy so for now we always - // render the full size window and copy it to the buffers. - publicAPI.getSourceDataAsync = async (renderer = model._renderer) => { - if (!renderer || !model._WebGPURenderWindow) { - vtkErrorMacro('Renderer and view must be set before calling Select.'); - return false; + // The area is in display pixels, with y up. setArea rounds it down. + // The _areaSet flag records that an area was given. Thus an area of all + // zeros is the pixel at the origin and not all the window. + const superSetArea = publicAPI.setArea; + publicAPI.setArea = (...args) => { + const changed = superSetArea(...args); + model._areaSet = true; + for (let i = 0; i < 4; i++) { + model.area[i] = Math.floor(model.area[i]); + } + return changed; + }; + + // Removes the area. Then captureBuffersAsync() reads all the window. + publicAPI.clearArea = () => { + model._areaSet = false; + }; + + //---------------------------------------------------------------------------- + // WebGPU reads the GPU buffers back asynchronously, so there is no + // synchronous captureBuffers() or select(). captureBuffersAsync() reads + // the area of setArea() and keeps the data. Then getPixelInformation(), + // generateSelection() and isPropHit() read the kept data synchronously. + // With no area set, all the window is read. + publicAPI.captureBuffersAsync = async () => { + const area = model.area; + let data = null; + if (model._areaSet) { + data = await publicAPI.getSourceDataAsync( + model._renderer, + area[0], + area[1], + area[2], + area[3] + ); + } else { + data = await publicAPI.getSourceDataAsync(model._renderer); } + model._capturedData = data || null; + return !!data; + }; - // todo revisit making selection part of core - // then we can do this in core - model._WebGPURenderWindow.getRenderable().preRender(); + publicAPI.getCapturedData = () => model._capturedData; + + publicAPI.releasePixBuffers = () => { + model._capturedData = null; + }; + + publicAPI.releaseGraphicsResources = () => { + publicAPI.releasePixBuffers(); + }; + + publicAPI.getPixelInformation = ( + inDisplayPosition, + maxDistance, + outSelectedPosition + ) => { + if (!model._capturedData) { + vtkErrorMacro('Call captureBuffersAsync before getPixelInformation.'); + return null; + } + return model._capturedData.getPixelInformation( + inDisplayPosition, + maxDistance, + outSelectedPosition + ); + }; + + publicAPI.generateSelection = (fx1, fy1, fx2, fy2) => { + if (!model._capturedData) { + vtkErrorMacro('Call captureBuffersAsync before generateSelection.'); + return []; + } + return model._capturedData.generateSelection(fx1, fy1, fx2, fy2); + }; + + publicAPI.isPropHit = (propID) => !!model._capturedData?.isPropHit(propID); + + //---------------------------------------------------------------------------- + // The selection pass renders the full window. Only the pixels of the area + // fx1, fy1, fx2, fy2 (display pixels, y up) are copied back to the CPU. + // With no area, all the window is copied. + publicAPI.getSourceDataAsync = async ( + renderer = model._renderer, + fx1 = undefined, + fy1 = undefined, + fx2 = undefined, + fy2 = undefined + ) => { + if (!publicAPI.prepareCapture(model._WebGPURenderWindow, renderer)) { + return false; + } if (!model._WebGPURenderWindow.getInitialized()) { model._WebGPURenderWindow.initialize(); @@ -349,22 +289,38 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { // the class as multiple calls may start before resolving // so anything specific to this request gets put into the // result object (by value in most cases) + const width = texture.getWidth(); + const height = texture.getHeight(); + const area = [0, 0, width - 1, height - 1]; + if (fx1 !== undefined) { + area[0] = Math.max(0, Math.floor(Math.min(fx1, fx2))); + area[1] = Math.max(0, Math.floor(Math.min(fy1, fy2))); + area[2] = Math.min(width - 1, Math.floor(Math.max(fx1, fx2))); + area[3] = Math.min(height - 1, Math.floor(Math.max(fy1, fy2))); + } + if (area[2] < area[0] || area[3] < area[1]) { + return false; + } + const copyWidth = area[2] - area[0] + 1; + const copyHeight = area[3] - area[1] + 1; + const copyOrigin = { x: area[0], y: height - 1 - area[3] }; + const result = { - area: [0, 0, texture.getWidth() - 1, texture.getHeight() - 1], + area, captureZValues: model.captureZValues, fieldAssociation: model.fieldAssociation, props: [...model._selectionProps], renderer, webGPURenderer, webGPURenderWindow: model._WebGPURenderWindow, - width: texture.getWidth(), - height: texture.getHeight(), + width, + height, }; // must be a multiple of 256 bytes, so 16 texels with rgba32uint - result.colorBufferWidth = 16 * Math.floor((result.width + 15) / 16); + result.colorBufferWidth = 16 * Math.floor((copyWidth + 15) / 16); result.colorBufferSizeInBytes = - result.colorBufferWidth * result.height * 4 * 4; + result.colorBufferWidth * copyHeight * 4 * 4; const colorBuffer = vtkWebGPUBuffer.newInstance({ label: 'hardwareSelectColorBuffer', }); @@ -382,27 +338,28 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { cmdEnc.copyTextureToBuffer( { texture: texture.getHandle(), + origin: copyOrigin, }, { buffer: colorBuffer.getHandle(), bytesPerRow: 16 * result.colorBufferWidth, - rowsPerImage: result.height, + rowsPerImage: copyHeight, }, { - width: result.width, - height: result.height, + width: copyWidth, + height: copyHeight, depthOrArrayLayers: 1, } ); let zbuffer; if (model.captureZValues) { - result.zbufferBufferWidth = 64 * Math.floor((result.width + 63) / 64); + result.zbufferBufferWidth = 64 * Math.floor((width + 63) / 64); zbuffer = vtkWebGPUBuffer.newInstance({ label: 'hardwareSelectDepthBuffer', }); zbuffer.setDevice(device); - result.zbufferSizeInBytes = result.height * result.zbufferBufferWidth * 4; + result.zbufferSizeInBytes = height * result.zbufferBufferWidth * 4; /* eslint-disable no-bitwise */ /* eslint-disable no-undef */ zbuffer.create( @@ -420,11 +377,11 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { { buffer: zbuffer.getHandle(), bytesPerRow: 4 * result.zbufferBufferWidth, - rowsPerImage: result.height, + rowsPerImage: height, }, { - width: result.width, - height: result.height, + width, + height, depthOrArrayLayers: 1, } ); @@ -438,6 +395,7 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { await Promise.all([cLoad, zLoad]); result.depthValues = new Float32Array(zbuffer.getMappedRange().slice()); zbuffer.unmap(); + zbuffer.getHandle().destroy(); } else { await cLoad; } @@ -445,10 +403,10 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { result.colorValues = new Uint32Array(colorBuffer.getMappedRange().slice()); colorBuffer.unmap(); + // the read back buffers are used one time only + colorBuffer.getHandle().destroy(); - result.generateSelection = (fx1, fy1, fx2, fy2) => - generateSelectionWithData(result, fx1, fy1, fx2, fy2); - return result; + return newSourceData(result); }; } @@ -458,8 +416,11 @@ function vtkWebGPUHardwareSelector(publicAPI, model) { const DEFAULT_VALUES = { // WebGPURenderWindow: null, + area: undefined, + _areaSet: false, _selectionPropMap: null, _selectionProps: null, + _capturedData: null, }; // ---------------------------------------------------------------------------- @@ -473,7 +434,13 @@ export function extend(publicAPI, model, initialValues = {}) { model._selectionPass = vtkWebGPUHardwareSelectionPass.newInstance(); model._selectionPropMap = new Map(); model._selectionProps = []; + if (model.area) { + model._areaSet = true; + } else { + model.area = [0, 0, 0, 0]; + } + macro.setGetArray(publicAPI, model, ['area'], 4); macro.setGet(publicAPI, model, ['_WebGPURenderWindow']); macro.moveToProtected(publicAPI, model, ['WebGPURenderWindow']); @@ -490,4 +457,4 @@ export const newInstance = macro.newInstance( // ---------------------------------------------------------------------------- -export default { newInstance, extend }; +export default { newInstance, extend, newSourceData }; diff --git a/Sources/Rendering/WebGPU/HardwareSelector/test/testWebGPUHardwareSelector.js b/Sources/Rendering/WebGPU/HardwareSelector/test/testWebGPUHardwareSelector.js new file mode 100644 index 00000000000..89f0b4a9a90 --- /dev/null +++ b/Sources/Rendering/WebGPU/HardwareSelector/test/testWebGPUHardwareSelector.js @@ -0,0 +1,111 @@ +import { describe, expect, it } from 'vitest'; + +import macro from 'vtk.js/Sources/macros'; +import vtkDataSet from 'vtk.js/Sources/Common/DataModel/DataSet'; +import vtkSelectionNode from 'vtk.js/Sources/Common/DataModel/SelectionNode'; +import vtkWebGPUHardwareSelector from 'vtk.js/Sources/Rendering/WebGPU/HardwareSelector'; + +const { FieldAssociations } = vtkDataSet; + +// An area of 4 x 3 pixels at display (10, 20). The pixel (11, 21) shows +// prop 0, with attribute 5 (written as 6). +function makeData() { + const area = [10, 20, 13, 22]; + const colorBufferWidth = 16; + const colorValues = new Uint32Array(colorBufferWidth * 3 * 4); + const index = (area[3] - 21) * colorBufferWidth + (11 - area[0]); + colorValues[index * 4] = 1; + colorValues[index * 4 + 2] = 6; + return vtkWebGPUHardwareSelector.newSourceData({ + area, + width: 100, + height: 100, + colorValues, + colorBufferWidth, + props: ['prop0'], + fieldAssociation: FieldAssociations.FIELD_ASSOCIATION_CELLS, + captureZValues: false, + }); +} + +// A selector that returns the given source data instead of rendering. +function newStubSelector(sourceData, calls) { + return macro.newInstance((publicAPI, model, initialValues) => { + vtkWebGPUHardwareSelector.extend(publicAPI, model, initialValues); + publicAPI.getSourceDataAsync = async (...args) => { + calls.push(args); + return sourceData; + }; + }, 'vtkStubSelector')(); +} + +describe('Test WebGPU HardwareSelector', () => { + it('reads pixels relative to the area', () => { + const data = makeData(); + const info = data.getPixelInformation([11, 21], 0, [0, 0]); + expect(info.valid).toBe(true); + expect(info.prop).toBe('prop0'); + expect(info.attributeID).toBe(5); + expect(data.getPixelInformation([12, 21], 0, [0, 0])).toBe(null); + expect(data.getPixelInformation([5, 21], 0, [0, 0])).toBe(null); + }); + + it('finds the nearest pixel within a distance', () => { + const out = [0, 0]; + const info = makeData().getPixelInformation([12, 22], 3, out); + expect(info.attributeID).toBe(5); + expect(out).toEqual([11, 21]); + }); + + it('generates a selection in the area', () => { + const sel = makeData().generateSelection(10, 20, 13, 22); + expect(sel.length).toBe(1); + expect(sel[0].getFieldType()).toBe(vtkSelectionNode.SelectionField.CELL); + expect(Array.from(sel[0].getSelectionList())).toEqual([5]); + expect(sel[0].getProperties().pixelCount).toBe(1); + }); + + it('tells which props are hit', () => { + const data = makeData(); + expect(data.isPropHit(0)).toBe(true); + expect(data.isPropHit(1)).toBe(false); + }); + + it('captures the area and keeps the data', async () => { + const calls = []; + const selector = newStubSelector(makeData(), calls); + expect(selector.getPixelInformation([11, 21], 0, [0, 0])).toBe(null); + selector.setArea(10.7, 20.2, 13.9, 22.5); + expect(selector.getArea()).toEqual([10, 20, 13, 22]); + expect(await selector.captureBuffersAsync()).toBe(true); + expect(calls[0].slice(1)).toEqual([10, 20, 13, 22]); + + expect(selector.getPixelInformation([11, 21], 0, [0, 0]).attributeID).toBe( + 5 + ); + expect(selector.generateSelection(10, 20, 13, 22).length).toBe(1); + expect(selector.isPropHit(0)).toBe(true); + + selector.releasePixBuffers(); + expect(selector.getCapturedData()).toBe(null); + expect(selector.isPropHit(0)).toBe(false); + }); + + it('reads all the window when no area is set', async () => { + const calls = []; + const selector = newStubSelector(false, calls); + expect(await selector.captureBuffersAsync()).toBe(false); + expect(calls[0].length).toBe(1); + }); + + it('reads one pixel at the origin when the area is all zeros', async () => { + const calls = []; + const selector = newStubSelector(false, calls); + selector.setArea(0, 0, 0, 0); + await selector.captureBuffersAsync(); + expect(calls[0].length).toBe(5); + selector.clearArea(); + await selector.captureBuffersAsync(); + expect(calls[1].length).toBe(1); + }); +});