From 556b3baeab7fded68ea3f55f6e13a3b967a58311 Mon Sep 17 00:00:00 2001 From: Adnane Belmadiaf Date: Fri, 27 Mar 2026 16:19:12 +0100 Subject: [PATCH] feat(CutterMapper): add vtkCutterMapper fixes #1967 --- Documentation/public/gallery/CutterMapper.jpg | Bin 0 -> 47187 bytes Sources/Filters/Core/Cutter/example/index.js | 47 +- .../Core/CutterMapper/example/index.js | 702 ++++++++++++++++++ .../Rendering/Core/CutterMapper/index.d.ts | 98 +++ Sources/Rendering/Core/CutterMapper/index.js | 76 ++ Sources/Rendering/Core/index.js | 2 + .../Rendering/OpenGL/CutterMapper/index.js | 358 +++++++++ Sources/Rendering/OpenGL/Profiles/Geometry.js | 1 + Sources/Rendering/OpenGL/index.js | 2 + .../Rendering/WebGPU/CutterMapper/helpers.js | 31 + .../Rendering/WebGPU/CutterMapper/index.js | 375 ++++++++++ Sources/Rendering/WebGPU/Profiles/All.js | 1 + Sources/Rendering/WebGPU/Profiles/Geometry.js | 1 + Sources/Rendering/WebGPU/index.js | 1 + 14 files changed, 1692 insertions(+), 3 deletions(-) create mode 100644 Documentation/public/gallery/CutterMapper.jpg create mode 100644 Sources/Rendering/Core/CutterMapper/example/index.js create mode 100644 Sources/Rendering/Core/CutterMapper/index.d.ts create mode 100644 Sources/Rendering/Core/CutterMapper/index.js create mode 100644 Sources/Rendering/OpenGL/CutterMapper/index.js create mode 100644 Sources/Rendering/WebGPU/CutterMapper/helpers.js create mode 100644 Sources/Rendering/WebGPU/CutterMapper/index.js diff --git a/Documentation/public/gallery/CutterMapper.jpg b/Documentation/public/gallery/CutterMapper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6a79ff7199acc4e823024fcff17570c3aa5ff573 GIT binary patch literal 47187 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 literal 0 HcmV?d00001 diff --git a/Sources/Filters/Core/Cutter/example/index.js b/Sources/Filters/Core/Cutter/example/index.js index 8835e79d5c8..a2c5d2185c7 100644 --- a/Sources/Filters/Core/Cutter/example/index.js +++ b/Sources/Filters/Core/Cutter/example/index.js @@ -5,6 +5,7 @@ import '@kitware/vtk.js/Rendering/Profiles/Geometry'; import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; import vtkCutter from '@kitware/vtk.js/Filters/Core/Cutter'; +import vtkCutterMapper from '@kitware/vtk.js/Rendering/Core/CutterMapper'; import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; import DataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper'; @@ -23,9 +24,7 @@ import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; // Standard rendering code setup // ---------------------------------------------------------------------------- -const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ - background: [0, 0, 0], -}); +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); const renderer = fullScreenRenderer.getRenderer(); const renderWindow = fullScreenRenderer.getRenderWindow(); @@ -48,6 +47,17 @@ cutProperty.setLighting(false); cutProperty.setColor(0, 1, 0); renderer.addActor(cutActor); +const gpuCutMapper = vtkCutterMapper.newInstance({ + cutFunction: plane, + cutWidth: 2.0, +}); +const gpuCutActor = vtkActor.newInstance(); +gpuCutActor.setMapper(gpuCutMapper); +const gpuCutProperty = gpuCutActor.getProperty(); +gpuCutProperty.setLighting(false); +gpuCutProperty.setColor(1.0, 0.55, 0.15); +renderer.addActor(gpuCutActor); + const cubeMapper = vtkMapper.newInstance(); cubeMapper.setScalarVisibility(false); const cubeActor = vtkActor.newInstance(); @@ -63,6 +73,9 @@ renderer.addActor(cubeActor); // ----------------------------------------------------------- const state = { + showCPU: true, + showGPU: true, + gpuWidth: 2.0, originX: 0, originY: 0, originZ: 0, @@ -74,11 +87,38 @@ const state = { const updatePlaneFunction = () => { plane.setOrigin(state.originX, state.originY, state.originZ); plane.setNormal(state.normalX, state.normalY, state.normalZ); + cutActor.setVisibility(state.showCPU); + gpuCutActor.setVisibility(state.showGPU); + gpuCutMapper.setCutWidth(state.gpuWidth); renderWindow.render(); }; const gui = new GUI(); +gui + .add(state, 'showCPU') + .name('CPU vtkCutter') + .onChange((value) => { + state.showCPU = value; + updatePlaneFunction(); + }); + +gui + .add(state, 'showGPU') + .name('GPU vtkCutterMapper') + .onChange((value) => { + state.showGPU = value; + updatePlaneFunction(); + }); + +gui + .add(state, 'gpuWidth', 0.5, 5.0, 0.1) + .name('GPU vtkCutterMapper width') + .onChange((value) => { + state.gpuWidth = Number(value); + updatePlaneFunction(); + }); + const originFolder = gui.addFolder('Origin'); originFolder .add(state, 'originX', -6, 6, 0.01) @@ -144,6 +184,7 @@ HttpDataAccessHelper.fetchBinary( const source = sceneImporter.getScene()[0].source; cutter.setInputConnection(source.getOutputPort()); cubeMapper.setInputConnection(source.getOutputPort()); + gpuCutMapper.setInputConnection(source.getOutputPort()); renderer.resetCamera(); updatePlaneFunction(); }); diff --git a/Sources/Rendering/Core/CutterMapper/example/index.js b/Sources/Rendering/Core/CutterMapper/example/index.js new file mode 100644 index 00000000000..6ddd8a1dbfc --- /dev/null +++ b/Sources/Rendering/Core/CutterMapper/example/index.js @@ -0,0 +1,702 @@ +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 GUI from 'lil-gui'; + +import vtkActor from '@kitware/vtk.js/Rendering/Core/Actor'; +import vtkBox from '@kitware/vtk.js/Common/DataModel/Box'; +import vtkCone from '@kitware/vtk.js/Common/DataModel/Cone'; +import vtkCutterMapper from '@kitware/vtk.js/Rendering/Core/CutterMapper'; +import vtkCylinder from '@kitware/vtk.js/Common/DataModel/Cylinder'; +import vtkCylinderSource from '@kitware/vtk.js/Filters/Sources/CylinderSource'; +import vtkFullScreenRenderWindow from '@kitware/vtk.js/Rendering/Misc/FullScreenRenderWindow'; +import HttpDataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper/HttpDataAccessHelper'; +import DataAccessHelper from '@kitware/vtk.js/IO/Core/DataAccessHelper'; +import vtkHttpSceneLoader from '@kitware/vtk.js/IO/Core/HttpSceneLoader'; +import vtkMapper from '@kitware/vtk.js/Rendering/Core/Mapper'; +import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane'; +import vtkPlaneSource from '@kitware/vtk.js/Filters/Sources/PlaneSource'; +import vtkProperty from '@kitware/vtk.js/Rendering/Core/Property'; +import vtkSphere from '@kitware/vtk.js/Common/DataModel/Sphere'; +import vtkSphereSource from '@kitware/vtk.js/Filters/Sources/SphereSource'; +import vtkTransform from '@kitware/vtk.js/Common/Transform/Transform'; +import { IDENTITY } from '@kitware/vtk.js/Common/Core/Math/Constants'; +import vtkCubeSource from '@kitware/vtk.js/Filters/Sources/CubeSource'; +import vtkConeSource from '@kitware/vtk.js/Filters/Sources/ConeSource'; + +// Force DataAccessHelper to have access to various data source +import '@kitware/vtk.js/IO/Core/DataAccessHelper/JSZipDataAccessHelper'; + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); + +const COLORS = { + cut: [0.0, 1.0, 0.0], + mesh: [0.72, 0.78, 0.86], + plane: [0.2, 0.75, 1.0], + sphere: [0.1, 0.55, 0.95], + box: [0.15, 0.85, 0.45], + cylinder: [1.0, 0.5, 0.15], + cone: [1.0, 0.82, 0.18], +}; + +// ---------------------------------------------------------------------------- +// Example code +// ---------------------------------------------------------------------------- + +const plane = vtkPlane.newInstance(); +const sphere = vtkSphere.newInstance(); +const box = vtkBox.newInstance(); +const cylinder = vtkCylinder.newInstance(); +const cone = vtkCone.newInstance(); +const coneTransform = vtkTransform.newInstance(); +cone.setTransform(coneTransform); + +const cutFunctions = { + plane, + sphere, + box, + cylinder, + cone, +}; + +const gpuCutMapper = vtkCutterMapper.newInstance({ + cutFunction: plane, + cutWidth: 0.5, +}); +const gpuCutActor = vtkActor.newInstance(); +gpuCutActor.setMapper(gpuCutMapper); +const gpuCutProperty = gpuCutActor.getProperty(); +gpuCutProperty.setLighting(false); +gpuCutProperty.setColor(...COLORS.cut); +renderer.addActor(gpuCutActor); + +const meshMapper = vtkMapper.newInstance(); +meshMapper.setScalarVisibility(false); +const meshActor = vtkActor.newInstance(); +meshActor.setMapper(meshMapper); +const meshProperty = meshActor.getProperty(); +meshProperty.setRepresentation(vtkProperty.Representation.WIREFRAME); +meshProperty.setLighting(false); +meshProperty.setColor(...COLORS.mesh); +meshProperty.setOpacity(0.1); +renderer.addActor(meshActor); + +const planeDebugSource = vtkPlaneSource.newInstance({ + xResolution: 1, + yResolution: 1, +}); +const planeDebugMapper = vtkMapper.newInstance(); +planeDebugMapper.setInputConnection(planeDebugSource.getOutputPort()); +const planeDebugActor = vtkActor.newInstance(); +planeDebugActor.setMapper(planeDebugMapper); +planeDebugActor.getProperty().setLighting(false); +planeDebugActor.getProperty().setColor(...COLORS.plane); +renderer.addActor(planeDebugActor); + +const sphereDebugSource = vtkSphereSource.newInstance({ + phiResolution: 48, + thetaResolution: 48, +}); +const sphereDebugMapper = vtkMapper.newInstance(); +sphereDebugMapper.setInputConnection(sphereDebugSource.getOutputPort()); +const sphereDebugActor = vtkActor.newInstance(); +sphereDebugActor.setMapper(sphereDebugMapper); +sphereDebugActor.getProperty().setLighting(false); +sphereDebugActor.getProperty().setColor(...COLORS.sphere); +renderer.addActor(sphereDebugActor); + +const boxDebugSource = vtkCubeSource.newInstance(); +const boxDebugMapper = vtkMapper.newInstance(); +boxDebugMapper.setInputConnection(boxDebugSource.getOutputPort()); +const boxDebugActor = vtkActor.newInstance(); +boxDebugActor.setMapper(boxDebugMapper); +boxDebugActor.getProperty().setLighting(false); +boxDebugActor.getProperty().setColor(...COLORS.box); +renderer.addActor(boxDebugActor); + +const cylinderDebugSource = vtkCylinderSource.newInstance({ + resolution: 64, + capping: false, +}); +const cylinderDebugMapper = vtkMapper.newInstance(); +cylinderDebugMapper.setInputConnection(cylinderDebugSource.getOutputPort()); +const cylinderDebugActor = vtkActor.newInstance(); +cylinderDebugActor.setMapper(cylinderDebugMapper); +cylinderDebugActor.getProperty().setLighting(false); +cylinderDebugActor.getProperty().setColor(...COLORS.cylinder); +renderer.addActor(cylinderDebugActor); + +const coneDebugSource = vtkConeSource.newInstance({ + resolution: 64, + capping: false, + direction: [1.0, 0.0, 0.0], +}); +const coneDebugMapper = vtkMapper.newInstance(); +coneDebugMapper.setInputConnection(coneDebugSource.getOutputPort()); +const coneDebugActor = vtkActor.newInstance(); +coneDebugActor.setMapper(coneDebugMapper); +coneDebugActor.getProperty().setLighting(false); +coneDebugActor.getProperty().setColor(...COLORS.cone); +renderer.addActor(coneDebugActor); + +const coneDebugSourceBack = vtkConeSource.newInstance({ + resolution: 64, + capping: false, + direction: [-1.0, 0.0, 0.0], +}); +const coneDebugMapperBack = vtkMapper.newInstance(); +coneDebugMapperBack.setInputConnection(coneDebugSourceBack.getOutputPort()); +const coneDebugActorBack = vtkActor.newInstance(); +coneDebugActorBack.setMapper(coneDebugMapperBack); +coneDebugActorBack.getProperty().setLighting(false); +coneDebugActorBack.getProperty().setColor(...COLORS.cone); +renderer.addActor(coneDebugActorBack); + +let modelBounds = [-1, 1, -1, 1, -1, 1]; +let modelLength = 2.5; +let modelCenter = [0.0, 0.0, 0.0]; +let modelHalfExtents = [1.0, 1.0, 1.0]; + +const state = { + showGPU: true, + gpuWidth: 0.5, + debugOpacity: 0.2, + cutType: 'plane', + planeOriginX: 0.0, + planeOriginY: 0.0, + planeOriginZ: 0.0, + planeNormalX: 1.0, + planeNormalY: 0.0, + planeNormalZ: 0.0, + sphereCenterX: 0.0, + sphereCenterY: 0.0, + sphereCenterZ: 0.0, + sphereRadius: 0.35, + boxCenterX: 0.0, + boxCenterY: 0.0, + boxCenterZ: 0.0, + boxSizeX: 0.6, + boxSizeY: 0.35, + boxSizeZ: 0.35, + cylinderCenterX: 0.0, + cylinderCenterY: 0.0, + cylinderCenterZ: 0.0, + cylinderAxisX: 1.0, + cylinderAxisY: 0.0, + cylinderAxisZ: 0.0, + cylinderRadius: 0.2, + coneCenterX: 0.0, + coneCenterY: 0.0, + coneCenterZ: 0.0, + coneAngle: 18.0, + coneRotateY: 0.0, + coneRotateZ: 0.0, +}; + +function normalizeVector3(x, y, z, fallback = [1.0, 0.0, 0.0]) { + const length = Math.hypot(x, y, z); + if (length <= 1e-6) { + return fallback; + } + return [x / length, y / length, z / length]; +} + +function cross(a, b) { + return [ + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ]; +} + +function getOffsetPosition(x, y, z) { + return [modelCenter[0] + x, modelCenter[1] + y, modelCenter[2] + z]; +} + +function updatePlane() { + const [nx, ny, nz] = normalizeVector3( + state.planeNormalX, + state.planeNormalY, + state.planeNormalZ + ); + const origin = getOffsetPosition( + state.planeOriginX, + state.planeOriginY, + state.planeOriginZ + ); + const normal = [nx, ny, nz]; + const tangentSeed = Math.abs(nz) < 0.9 ? [0.0, 0.0, 1.0] : [0.0, 1.0, 0.0]; + const tangent1 = normalizeVector3( + ...cross(normal, tangentSeed), + [1.0, 0.0, 0.0] + ); + const tangent2 = normalizeVector3( + ...cross(normal, tangent1), + [0.0, 1.0, 0.0] + ); + const halfSize = modelLength * 0.3; + plane.setOrigin(origin[0], origin[1], origin[2]); + plane.setNormal(nx, ny, nz); + planeDebugSource.setOrigin( + origin[0] - tangent1[0] * halfSize - tangent2[0] * halfSize, + origin[1] - tangent1[1] * halfSize - tangent2[1] * halfSize, + origin[2] - tangent1[2] * halfSize - tangent2[2] * halfSize + ); + planeDebugSource.setPoint1( + origin[0] + tangent1[0] * halfSize - tangent2[0] * halfSize, + origin[1] + tangent1[1] * halfSize - tangent2[1] * halfSize, + origin[2] + tangent1[2] * halfSize - tangent2[2] * halfSize + ); + planeDebugSource.setPoint2( + origin[0] - tangent1[0] * halfSize + tangent2[0] * halfSize, + origin[1] - tangent1[1] * halfSize + tangent2[1] * halfSize, + origin[2] - tangent1[2] * halfSize + tangent2[2] * halfSize + ); +} + +function updateSphere() { + const center = getOffsetPosition( + state.sphereCenterX, + state.sphereCenterY, + state.sphereCenterZ + ); + sphere.setCenter(center[0], center[1], center[2]); + sphere.setRadius(state.sphereRadius); + sphereDebugSource.setCenter(center[0], center[1], center[2]); + sphereDebugSource.setRadius(state.sphereRadius); +} + +function updateBox() { + const center = getOffsetPosition( + state.boxCenterX, + state.boxCenterY, + state.boxCenterZ + ); + const halfSizeX = state.boxSizeX / 2; + const halfSizeY = state.boxSizeY / 2; + const halfSizeZ = state.boxSizeZ / 2; + box.setBounds([ + center[0] - halfSizeX, + center[0] + halfSizeX, + center[1] - halfSizeY, + center[1] + halfSizeY, + center[2] - halfSizeZ, + center[2] + halfSizeZ, + ]); + boxDebugSource.setBounds([ + center[0] - halfSizeX, + center[0] + halfSizeX, + center[1] - halfSizeY, + center[1] + halfSizeY, + center[2] - halfSizeZ, + center[2] + halfSizeZ, + ]); +} + +function updateCylinder() { + const [axisX, axisY, axisZ] = normalizeVector3( + state.cylinderAxisX, + state.cylinderAxisY, + state.cylinderAxisZ + ); + const center = getOffsetPosition( + state.cylinderCenterX, + state.cylinderCenterY, + state.cylinderCenterZ + ); + cylinder.setCenter(center[0], center[1], center[2]); + cylinder.setAxis(axisX, axisY, axisZ); + cylinder.setRadius(state.cylinderRadius); + + cylinderDebugSource.setCenter(center[0], center[1], center[2]); + cylinderDebugSource.setRadius(state.cylinderRadius); + cylinderDebugSource.setHeight(modelLength * 1.2); + cylinderDebugSource.setDirection(axisX, axisY, axisZ); +} + +function updateCone() { + cone.setAngle(state.coneAngle); + const coneCenterX = modelCenter[0] + state.coneCenterX; + const coneCenterY = modelCenter[1] + state.coneCenterY; + const coneCenterZ = modelCenter[2] + state.coneCenterZ; + coneTransform.setMatrix(IDENTITY); + coneTransform.translate(-coneCenterX, -coneCenterY, -coneCenterZ); + coneTransform.rotateY(-state.coneRotateY); + coneTransform.rotateZ(-state.coneRotateZ); + + const coneHeight = modelLength * 0.7; + // vtkCone is an infinite double cone with its apex at the local origin. + // Each debug vtkConeSource spans from that apex to a base located one + // coneHeight away along the axis, so the base radius must use the full + // apex-to-base distance. + const coneRadius = coneHeight * Math.tan((state.coneAngle * Math.PI) / 180.0); + const [dirX, dirY, dirZ] = normalizeVector3( + Math.cos((state.coneRotateY * Math.PI) / 180.0) * + Math.cos((state.coneRotateZ * Math.PI) / 180.0), + Math.sin((state.coneRotateZ * Math.PI) / 180.0), + Math.sin((state.coneRotateY * Math.PI) / 180.0) * + Math.cos((state.coneRotateZ * Math.PI) / 180.0) + ); + const halfHeight = coneHeight / 2.0; + coneDebugSource.setHeight(coneHeight); + coneDebugSource.setRadius(coneRadius); + coneDebugSource.setCenter( + coneCenterX - dirX * halfHeight, + coneCenterY - dirY * halfHeight, + coneCenterZ - dirZ * halfHeight + ); + coneDebugSource.setDirection(dirX, dirY, dirZ); + coneDebugSourceBack.setHeight(coneHeight); + coneDebugSourceBack.setRadius(coneRadius); + coneDebugSourceBack.setCenter( + coneCenterX + dirX * halfHeight, + coneCenterY + dirY * halfHeight, + coneCenterZ + dirZ * halfHeight + ); + coneDebugSourceBack.setDirection(-dirX, -dirY, -dirZ); +} + +function updateDebugActors() { + planeDebugActor.setVisibility(state.cutType === 'plane' ? 1 : 0); + sphereDebugActor.setVisibility(state.cutType === 'sphere' ? 1 : 0); + boxDebugActor.setVisibility(state.cutType === 'box' ? 1 : 0); + cylinderDebugActor.setVisibility(state.cutType === 'cylinder' ? 1 : 0); + coneDebugActor.setVisibility(state.cutType === 'cone' ? 1 : 0); + coneDebugActorBack.setVisibility(state.cutType === 'cone' ? 1 : 0); + + [ + planeDebugActor, + sphereDebugActor, + boxDebugActor, + cylinderDebugActor, + coneDebugActor, + coneDebugActorBack, + ].forEach((actor) => { + const property = actor.getProperty(); + property.setOpacity(state.debugOpacity); + property.setRepresentation(vtkProperty.Representation.SURFACE); + }); +} + +function updateCutFunctions() { + updatePlane(); + updateSphere(); + updateBox(); + updateCylinder(); + updateCone(); +} + +function updateScene() { + updateCutFunctions(); + + const cutFunction = cutFunctions[state.cutType]; + gpuCutMapper.setCutFunction(cutFunction); + gpuCutMapper.setCutWidth(state.gpuWidth); + + gpuCutActor.setVisibility(state.showGPU); + updateDebugActors(); + renderWindow.render(); +} + +const gui = new GUI(); +const typeFolders = {}; +const positionControllers = []; +let sphereRadiusController = null; +let cylinderRadiusController = null; +let boxSizeXController = null; +let boxSizeYController = null; +let boxSizeZController = null; + +function addPositionController(folder, key, axisIndex, label) { + const controller = folder + .add( + state, + key, + -modelHalfExtents[axisIndex], + modelHalfExtents[axisIndex], + 0.01 + ) + .name(label) + .onChange((value) => { + state[key] = Number(value); + updateScene(); + }); + positionControllers.push({ controller, axisIndex }); + return controller; +} + +function updatePositionControllerRanges() { + positionControllers.forEach(({ controller, axisIndex }) => { + controller.min(-modelHalfExtents[axisIndex]); + controller.max(modelHalfExtents[axisIndex]); + controller.updateDisplay(); + }); +} + +function setFolderVisibility(folder, visible) { + if (folder?.domElement) { + folder.domElement.style.display = visible ? '' : 'none'; + } +} + +function updateVisibleTypeSettings() { + Object.entries(typeFolders).forEach(([type, folder]) => { + setFolderVisibility(folder, type === state.cutType); + }); +} + +gui + .add(state, 'cutType', Object.keys(cutFunctions)) + .name('Cut type') + .onChange((value) => { + state.cutType = value; + updateVisibleTypeSettings(); + updateScene(); + }); + +gui + .add(state, 'gpuWidth', 0.5, 5.0, 0.1) + .name('Cut width') + .onChange((value) => { + state.gpuWidth = Number(value); + updateScene(); + }); + +gui + .add(state, 'showGPU') + .name('Cut rendering') + .onChange((value) => { + state.showGPU = value; + updateScene(); + }); + +gui + .add(state, 'debugOpacity', 0.0, 1.0, 0.01) + .name('Opacity') + .onChange((value) => { + state.debugOpacity = Number(value); + updateScene(); + }); + +const planeFolder = gui.addFolder('Plane'); +typeFolders.plane = planeFolder; +addPositionController(planeFolder, 'planeOriginX', 0, 'Offset X'); +addPositionController(planeFolder, 'planeOriginY', 1, 'Offset Y'); +addPositionController(planeFolder, 'planeOriginZ', 2, 'Offset Z'); +planeFolder + .add(state, 'planeNormalX', -1.0, 1.0, 0.01) + .name('Normal X') + .onChange((value) => { + state.planeNormalX = Number(value); + updateScene(); + }); +planeFolder + .add(state, 'planeNormalY', -1.0, 1.0, 0.01) + .name('Normal Y') + .onChange((value) => { + state.planeNormalY = Number(value); + updateScene(); + }); +planeFolder + .add(state, 'planeNormalZ', -1.0, 1.0, 0.01) + .name('Normal Z') + .onChange((value) => { + state.planeNormalZ = Number(value); + updateScene(); + }); + +const sphereFolder = gui.addFolder('Sphere'); +typeFolders.sphere = sphereFolder; +addPositionController(sphereFolder, 'sphereCenterX', 0, 'Offset X'); +addPositionController(sphereFolder, 'sphereCenterY', 1, 'Offset Y'); +addPositionController(sphereFolder, 'sphereCenterZ', 2, 'Offset Z'); +sphereRadiusController = sphereFolder + .add(state, 'sphereRadius', 0.05, 1.0, 0.01) + .name('Radius'); +sphereRadiusController.onChange((value) => { + state.sphereRadius = Number(value); + updateScene(); +}); + +const boxFolder = gui.addFolder('Box'); +typeFolders.box = boxFolder; +addPositionController(boxFolder, 'boxCenterX', 0, 'Offset X'); +addPositionController(boxFolder, 'boxCenterY', 1, 'Offset Y'); +addPositionController(boxFolder, 'boxCenterZ', 2, 'Offset Z'); +boxSizeXController = boxFolder + .add(state, 'boxSizeX', 0.05, 1.5, 0.01) + .name('Size X'); +boxSizeXController.onChange((value) => { + state.boxSizeX = Number(value); + updateScene(); +}); +boxSizeYController = boxFolder + .add(state, 'boxSizeY', 0.05, 1.5, 0.01) + .name('Size Y'); +boxSizeYController.onChange((value) => { + state.boxSizeY = Number(value); + updateScene(); +}); +boxSizeZController = boxFolder + .add(state, 'boxSizeZ', 0.05, 1.5, 0.01) + .name('Size Z'); +boxSizeZController.onChange((value) => { + state.boxSizeZ = Number(value); + updateScene(); +}); + +const cylinderFolder = gui.addFolder('Cylinder'); +typeFolders.cylinder = cylinderFolder; +addPositionController(cylinderFolder, 'cylinderCenterX', 0, 'Offset X'); +addPositionController(cylinderFolder, 'cylinderCenterY', 1, 'Offset Y'); +addPositionController(cylinderFolder, 'cylinderCenterZ', 2, 'Offset Z'); +cylinderFolder + .add(state, 'cylinderAxisX', -1.0, 1.0, 0.01) + .name('Axis X') + .onChange((value) => { + state.cylinderAxisX = Number(value); + updateScene(); + }); +cylinderFolder + .add(state, 'cylinderAxisY', -1.0, 1.0, 0.01) + .name('Axis Y') + .onChange((value) => { + state.cylinderAxisY = Number(value); + updateScene(); + }); +cylinderFolder + .add(state, 'cylinderAxisZ', -1.0, 1.0, 0.01) + .name('Axis Z') + .onChange((value) => { + state.cylinderAxisZ = Number(value); + updateScene(); + }); +cylinderRadiusController = cylinderFolder + .add(state, 'cylinderRadius', 0.05, 1.0, 0.01) + .name('Radius'); +cylinderRadiusController.onChange((value) => { + state.cylinderRadius = Number(value); + updateScene(); +}); + +const coneFolder = gui.addFolder('Cone'); +typeFolders.cone = coneFolder; +addPositionController(coneFolder, 'coneCenterX', 0, 'Offset X'); +addPositionController(coneFolder, 'coneCenterY', 1, 'Offset Y'); +addPositionController(coneFolder, 'coneCenterZ', 2, 'Offset Z'); +coneFolder + .add(state, 'coneAngle', 5.0, 60.0, 0.5) + .name('Angle') + .onChange((value) => { + state.coneAngle = Number(value); + updateScene(); + }); +coneFolder + .add(state, 'coneRotateY', -180.0, 180.0, 1.0) + .name('Rotate Y') + .onChange((value) => { + state.coneRotateY = Number(value); + updateScene(); + }); +coneFolder + .add(state, 'coneRotateZ', -180.0, 180.0, 1.0) + .name('Rotate Z') + .onChange((value) => { + state.coneRotateZ = Number(value); + updateScene(); + }); + +updateVisibleTypeSettings(); + +HttpDataAccessHelper.fetchBinary( + `${__BASE_PATH__}/data/StanfordDragon.vtkjs`, + {} +).then((zipContent) => { + const dataAccessHelper = DataAccessHelper.get('zip', { + zipContent, + callback: () => { + const sceneImporter = vtkHttpSceneLoader.newInstance({ + renderer, + dataAccessHelper, + }); + sceneImporter.setUrl('index.json'); + sceneImporter.onReady(() => { + sceneImporter.getScene()[0].actor.setVisibility(false); + + const source = sceneImporter.getScene()[0].source; + meshMapper.setInputConnection(source.getOutputPort()); + gpuCutMapper.setInputConnection(source.getOutputPort()); + const inputData = source.getOutputData(); + if (inputData?.getBounds) { + modelBounds = inputData.getBounds(); + const dx = modelBounds[1] - modelBounds[0]; + const dy = modelBounds[3] - modelBounds[2]; + const dz = modelBounds[5] - modelBounds[4]; + modelLength = Math.hypot(dx, dy, dz); + const centerX = (modelBounds[0] + modelBounds[1]) / 2; + const centerY = (modelBounds[2] + modelBounds[3]) / 2; + const centerZ = (modelBounds[4] + modelBounds[5]) / 2; + modelCenter = [centerX, centerY, centerZ]; + modelHalfExtents = [ + Math.max(dx / 2, 0.01), + Math.max(dy / 2, 0.01), + Math.max(dz / 2, 0.01), + ]; + const maxSpan = Math.max(dx, dy, dz); + + state.planeOriginX = 0.0; + state.planeOriginY = 0.0; + state.planeOriginZ = 0.0; + state.sphereCenterX = 0.0; + state.sphereCenterY = 0.0; + state.sphereCenterZ = 0.0; + state.sphereRadius = maxSpan * 0.22; + if (sphereRadiusController) { + sphereRadiusController.min(maxSpan * 0.02); + sphereRadiusController.max(maxSpan * 0.6); + sphereRadiusController.updateDisplay(); + } + state.boxCenterX = 0.0; + state.boxCenterY = 0.0; + state.boxCenterZ = 0.0; + state.boxSizeX = maxSpan * 0.42; + state.boxSizeY = maxSpan * 0.42; + state.boxSizeZ = maxSpan * 0.42; + [boxSizeXController, boxSizeYController, boxSizeZController].forEach( + (controller) => { + if (controller) { + controller.min(maxSpan * 0.02); + controller.max(maxSpan * 0.8); + controller.updateDisplay(); + } + } + ); + state.cylinderCenterX = 0.0; + state.cylinderCenterY = 0.0; + state.cylinderCenterZ = 0.0; + state.cylinderRadius = maxSpan * 0.18; + if (cylinderRadiusController) { + cylinderRadiusController.min(maxSpan * 0.02); + cylinderRadiusController.max(maxSpan * 0.5); + cylinderRadiusController.updateDisplay(); + } + state.coneCenterX = 0.0; + state.coneCenterY = 0.0; + state.coneCenterZ = 0.0; + state.coneAngle = 28.0; + updatePositionControllerRanges(); + } + renderer.resetCamera(); + updateScene(); + }); + }, + }); +}); diff --git a/Sources/Rendering/Core/CutterMapper/index.d.ts b/Sources/Rendering/Core/CutterMapper/index.d.ts new file mode 100644 index 00000000000..7af56454fc4 --- /dev/null +++ b/Sources/Rendering/Core/CutterMapper/index.d.ts @@ -0,0 +1,98 @@ +import vtkImplicitFunction from '../../../Common/DataModel/ImplicitFunction'; +import vtkMapper, { IMapperInitialValues } from '../Mapper'; +import { Nullable } from '../../../types'; + +export interface ICutterMapperInitialValues extends IMapperInitialValues { + /** Implicit function evaluated by the GPU cut shader. */ + cutFunction?: Nullable; + /** Offset applied to the implicit function isosurface. */ + cutValue?: number; + /** Screen-space thickness multiplier for the displayed cut contour. */ + cutWidth?: number; +} + +export interface vtkCutterMapper extends vtkMapper { + /** + * Returns the current implicit cut function. + */ + getCutFunction(): Nullable; + + /** + * Returns the scalar offset applied to the implicit function. + * + * For a plane this acts as an additional displacement along the plane normal. + */ + getCutValue(): number; + + /** + * Returns the screen-space cut width multiplier used by the shader. + */ + getCutWidth(): number; + + /** + * Sets the implicit function used to generate the cut on the GPU. + * + * Supported implicit functions are currently plane, sphere, box, cylinder, + * and cone. + */ + setCutFunction(cutFunction: Nullable): boolean; + + /** + * Sets the scalar offset applied to the implicit function. + * + * For a plane, changing the plane origin only changes the cut when the + * displacement has a component along the plane normal. Moving the origin + * parallel to the plane describes the same plane in 3D, so the cut does not + * move. `cutValue` can also be used to offset the plane along its normal. + */ + setCutValue(cutValue: number): boolean; + + /** + * Sets the screen-space cut width multiplier used by the shader. + */ + setCutWidth(cutWidth: number): boolean; + + /** + * Returns the VTK.js class name of the supported implicit function currently in + * use, or `null` when the cut function is missing or unsupported. + */ + getSupportedImplicitFunctionName(): Nullable; +} + +/** + * Method used to decorate a given object (publicAPI+model) with vtkCutterMapper characteristics. + * + * @param publicAPI object on which methods will be bounds (public) + * @param model object on which data structure will be bounds (protected) + * @param {ICutterMapperInitialValues} [initialValues] (default: {}) + */ +export function extend( + publicAPI: object, + model: object, + initialValues?: ICutterMapperInitialValues +): void; + +/** + * Method used to create a new instance of vtkCutterMapper + * @param {ICutterMapperInitialValues} initialValues for pre-setting some of its content + */ +export function newInstance( + initialValues?: ICutterMapperInitialValues +): vtkCutterMapper; + +/** + * vtkCutterMapper renders the zero-isosurface of a supported implicit + * function on the GPU. Supported implicit functions are currently plane, + * sphere, box, cylinder, and cone. + * + * - For a plane, only displacement along the plane normal changes the cut. + * Moving the plane origin parallel to the plane still describes the same + * plane in 3D, so the cut does not move. + * - For a sphere, box, cylinder, or cone, translating the center changes the + * position of the implicit function, so the cut follows that translation. + */ +export declare const vtkCutterMapper: { + newInstance: typeof newInstance; + extend: typeof extend; +}; +export default vtkCutterMapper; diff --git a/Sources/Rendering/Core/CutterMapper/index.js b/Sources/Rendering/Core/CutterMapper/index.js new file mode 100644 index 00000000000..15ceb20536e --- /dev/null +++ b/Sources/Rendering/Core/CutterMapper/index.js @@ -0,0 +1,76 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; + +const SUPPORTED_IMPLICIT_FUNCTIONS = [ + 'vtkPlane', + 'vtkSphere', + 'vtkBox', + 'vtkCylinder', + 'vtkCone', +]; + +// ---------------------------------------------------------------------------- +// vtkCutterMapper methods +// ---------------------------------------------------------------------------- + +function vtkCutterMapper(publicAPI, model) { + model.classHierarchy.push('vtkCutterMapper'); + + const superClass = { ...publicAPI }; + + publicAPI.getMTime = () => { + let mTime = superClass.getMTime(); + if (model.cutFunction) { + mTime = Math.max(mTime, model.cutFunction.getMTime()); + const transform = model.cutFunction.getTransform?.(); + if (transform?.getMTime) { + mTime = Math.max(mTime, transform.getMTime()); + } + } + return mTime; + }; + + publicAPI.getSupportedImplicitFunctionName = () => { + const cutFunction = model.cutFunction; + if (!cutFunction || !cutFunction.isA) { + return null; + } + + return ( + SUPPORTED_IMPLICIT_FUNCTIONS.find((className) => + cutFunction.isA(className) + ) || null + ); + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + cutFunction: null, + cutValue: 0.0, + cutWidth: 1.5, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + vtkMapper.extend(publicAPI, model, initialValues); + publicAPI.setScalarVisibility(false); + + macro.setGet(publicAPI, model, ['cutFunction', 'cutValue', 'cutWidth']); + + vtkCutterMapper(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkCutterMapper'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Rendering/Core/index.js b/Sources/Rendering/Core/index.js index efd6baf6dbe..ecdb09bd718 100644 --- a/Sources/Rendering/Core/index.js +++ b/Sources/Rendering/Core/index.js @@ -13,6 +13,7 @@ import vtkCamera from './Camera'; import vtkCellPicker from './CellPicker'; import vtkColorTransferFunction from './ColorTransferFunction'; import vtkCoordinate from './Coordinate'; +import vtkCutterMapper from './CutterMapper'; import vtkCubeAxesActor from './CubeAxesActor'; import vtkFollower from './Follower'; import vtkGlyph3DMapper from './Glyph3DMapper'; @@ -61,6 +62,7 @@ export default { vtkCellPicker, vtkColorTransferFunction: { vtkColorMaps, ...vtkColorTransferFunction }, vtkCoordinate, + vtkCutterMapper, vtkCubeAxesActor, vtkFollower, vtkGlyph3DMapper, diff --git a/Sources/Rendering/OpenGL/CutterMapper/index.js b/Sources/Rendering/OpenGL/CutterMapper/index.js new file mode 100644 index 00000000000..ac91d973e65 --- /dev/null +++ b/Sources/Rendering/OpenGL/CutterMapper/index.js @@ -0,0 +1,358 @@ +import { mat4 } from 'gl-matrix'; + +import macro from 'vtk.js/Sources/macros'; +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; +import vtkShaderProgram from 'vtk.js/Sources/Rendering/OpenGL/ShaderProgram'; +import vtkOpenGLPolyDataMapper from 'vtk.js/Sources/Rendering/OpenGL/PolyDataMapper'; +import { registerOverride } from 'vtk.js/Sources/Rendering/OpenGL/ViewNodeFactory'; + +import { IDENTITY } from 'vtk.js/Sources/Common/Core/Math/Constants'; + +const { vtkErrorMacro } = macro; + +// ---------------------------------------------------------------------------- +// vtkOpenGLCutterMapper methods +// ---------------------------------------------------------------------------- + +function vtkOpenGLCutterMapper(publicAPI, model) { + model.classHierarchy.push('vtkOpenGLCutterMapper'); + + const superClass = { ...publicAPI }; + + publicAPI.replaceShaderClip = (shaders, ren, actor) => { + let VSSource = shaders.Vertex; + let FSSource = shaders.Fragment; + const functionName = model.renderable.getSupportedImplicitFunctionName(); + + if (!functionName) { + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + return; + } + + if (model.renderable.getNumberOfClippingPlanes()) { + const numClipPlanes = model.renderable.getNumberOfClippingPlanes(); + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Dec', [ + 'uniform int numClipPlanes;', + `uniform vec4 clipPlanes[${numClipPlanes}];`, + `varying float clipDistancesVSOutput[${numClipPlanes}];`, + '//VTK::Clip::Dec', + ]).result; + + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Impl', [ + `for (int planeNum = 0; planeNum < ${numClipPlanes}; planeNum++)`, + ' {', + ' if (planeNum >= numClipPlanes)', + ' {', + ' break;', + ' }', + ' clipDistancesVSOutput[planeNum] = dot(clipPlanes[planeNum], vertexMC);', + ' }', + '//VTK::Clip::Impl', + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Dec', [ + 'uniform int numClipPlanes;', + `varying float clipDistancesVSOutput[${numClipPlanes}];`, + '//VTK::Clip::Dec', + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Impl', [ + `for (int planeNum = 0; planeNum < ${numClipPlanes}; planeNum++)`, + ' {', + ' if (planeNum >= numClipPlanes)', + ' {', + ' break;', + ' }', + ' if (clipDistancesVSOutput[planeNum] < 0.0) discard;', + ' }', + '//VTK::Clip::Impl', + ]).result; + } + + let cutDistanceDec = []; + let cutDistanceImpl = []; + + switch (functionName) { + case 'vtkPlane': + cutDistanceDec = [ + 'uniform vec3 cutPlaneOrigin;', + 'uniform vec3 cutPlaneNormal;', + 'uniform float cutValue;', + ]; + cutDistanceImpl = [ + 'cutDistanceVSOutput = dot(cutPlaneNormal, cutFunctionPoint - cutPlaneOrigin) - cutValue;', + ]; + break; + case 'vtkSphere': + cutDistanceDec = [ + 'uniform vec3 cutSphereCenter;', + 'uniform vec3 cutSphereRadius;', + 'uniform int cutSphereUsesAxisRadii;', + ]; + cutDistanceImpl = [ + 'vec3 cutSphereDelta = cutFunctionPoint - cutSphereCenter;', + 'if (cutSphereUsesAxisRadii == 1) {', + ' vec3 cutSphereNormalizedDelta = cutSphereDelta / cutSphereRadius;', + ' cutDistanceVSOutput = dot(cutSphereNormalizedDelta, cutSphereNormalizedDelta) - 1.0;', + '} else {', + ' cutDistanceVSOutput = dot(cutSphereDelta, cutSphereDelta) - cutSphereRadius.x * cutSphereRadius.x;', + '}', + ]; + break; + case 'vtkBox': + cutDistanceDec = [ + 'uniform vec3 cutBoxMinPoint;', + 'uniform vec3 cutBoxMaxPoint;', + ]; + cutDistanceImpl = [ + 'float cutBoxMinDistance = -1.0e20;', + 'float cutBoxDistance = 0.0;', + 'bool cutBoxInside = true;', + 'for (int i = 0; i < 3; ++i) {', + ' float cutBoxLength = cutBoxMaxPoint[i] - cutBoxMinPoint[i];', + ' float cutBoxDist = 0.0;', + ' if (cutBoxLength != 0.0) {', + ' float cutBoxT = (cutFunctionPoint[i] - cutBoxMinPoint[i]) / cutBoxLength;', + ' if (cutBoxT < 0.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = cutBoxMinPoint[i] - cutFunctionPoint[i];', + ' } else if (cutBoxT > 1.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = cutFunctionPoint[i] - cutBoxMaxPoint[i];', + ' } else if (cutBoxT <= 0.5) {', + ' cutBoxDist = cutBoxMinPoint[i] - cutFunctionPoint[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' } else {', + ' cutBoxDist = cutFunctionPoint[i] - cutBoxMaxPoint[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' }', + ' } else {', + ' cutBoxDist = abs(cutFunctionPoint[i] - cutBoxMinPoint[i]);', + ' if (cutBoxDist > 0.0) {', + ' cutBoxInside = false;', + ' }', + ' }', + ' if (cutBoxDist > 0.0) {', + ' cutBoxDistance += cutBoxDist * cutBoxDist;', + ' }', + '}', + 'cutDistanceVSOutput = cutBoxInside ? cutBoxMinDistance : sqrt(cutBoxDistance);', + ]; + break; + case 'vtkCylinder': + cutDistanceDec = [ + 'uniform vec3 cutCylinderCenter;', + 'uniform vec3 cutCylinderAxis;', + 'uniform float cutCylinderRadius;', + ]; + cutDistanceImpl = [ + 'vec3 cutCylinderDelta = cutFunctionPoint - cutCylinderCenter;', + 'float cutCylinderProjection = dot(cutCylinderAxis, cutCylinderDelta);', + 'cutDistanceVSOutput = dot(cutCylinderDelta, cutCylinderDelta) - cutCylinderProjection * cutCylinderProjection - cutCylinderRadius * cutCylinderRadius;', + ]; + break; + case 'vtkCone': + cutDistanceDec = ['uniform float cutConeTanThetaSquared;']; + cutDistanceImpl = [ + 'cutDistanceVSOutput = cutFunctionPoint.y * cutFunctionPoint.y + cutFunctionPoint.z * cutFunctionPoint.z - cutFunctionPoint.x * cutFunctionPoint.x * cutConeTanThetaSquared;', + ]; + break; + default: + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + break; + } + + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Dec', [ + 'uniform mat4 cutFunctionMatrix;', + 'varying float cutDistanceVSOutput;', + ...cutDistanceDec, + ]).result; + + VSSource = vtkShaderProgram.substitute(VSSource, '//VTK::Clip::Impl', [ + 'vec3 cutFunctionPoint = (cutFunctionMatrix * vertexMC).xyz;', + ...cutDistanceImpl, + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Dec', [ + 'varying float cutDistanceVSOutput;', + 'uniform float cutWidth;', + ]).result; + + FSSource = vtkShaderProgram.substitute(FSSource, '//VTK::Clip::Impl', [ + 'if (abs(cutDistanceVSOutput) > max(fwidth(cutDistanceVSOutput) * cutWidth, 1e-6)) discard;', + ]).result; + + shaders.Vertex = VSSource; + shaders.Fragment = FSSource; + }; + + publicAPI.replaceShaderValues = (shaders, ren, actor) => { + superClass.replaceShaderValues(shaders, ren, actor); + + if (model.renderable.getSupportedImplicitFunctionName()) { + shaders.Fragment = vtkShaderProgram.substitute( + shaders.Fragment, + '//VTK::UniformFlow::Impl', + [ + ' float cutterDistanceDx = dFdx(cutDistanceVSOutput);', + ' float cutterDistanceDy = dFdy(cutDistanceVSOutput);', + ' float cutterDistanceGradient = length(vec2(cutterDistanceDx, cutterDistanceDy));', + ' if (cutterDistanceGradient <= 0.0 && abs(cutDistanceVSOutput) > 1e-6) discard;', + ] + ).result; + } + }; + + publicAPI.setMapperShaderParameters = (cellBO, ren, actor) => { + superClass.setMapperShaderParameters(cellBO, ren, actor); + + const cutFunction = model.renderable.getCutFunction(); + const functionName = model.renderable.getSupportedImplicitFunctionName(); + if (!cutFunction || !functionName) { + return; + } + + const shiftScaleEnabled = cellBO.getCABO().getCoordShiftAndScaleEnabled(); + const inverseShiftScaleMatrix = shiftScaleEnabled + ? cellBO.getCABO().getInverseShiftAndScaleMatrix() + : null; + const cutFunctionTransform = cutFunction.getTransform?.(); + const transformMatrix = cutFunctionTransform?.getMatrix + ? Array.from(cutFunctionTransform.getMatrix()) + : IDENTITY; + const cutFunctionMatrix = inverseShiftScaleMatrix + ? mat4.multiply(model.tmpMat4, transformMatrix, inverseShiftScaleMatrix) + : transformMatrix; + + const program = cellBO.getProgram(); + if (program.isUniformUsed('cutFunctionMatrix')) { + program.setUniformMatrix('cutFunctionMatrix', cutFunctionMatrix); + } + + switch (functionName) { + case 'vtkPlane': + if (program.isUniformUsed('cutPlaneOrigin')) { + program.setUniform3fArray('cutPlaneOrigin', cutFunction.getOrigin()); + } + if (program.isUniformUsed('cutPlaneNormal')) { + program.setUniform3fArray('cutPlaneNormal', cutFunction.getNormal()); + } + if (program.isUniformUsed('cutValue')) { + program.setUniformf('cutValue', model.renderable.getCutValue()); + } + break; + case 'vtkSphere': { + const radius = cutFunction.getRadius(); + if (program.isUniformUsed('cutSphereCenter')) { + program.setUniform3fArray('cutSphereCenter', cutFunction.getCenter()); + } + if (program.isUniformUsed('cutSphereRadius')) { + const radiusArray = model.tmpVec3A; + if (Array.isArray(radius)) { + radiusArray[0] = radius[0]; + radiusArray[1] = radius[1]; + radiusArray[2] = radius[2]; + } else { + radiusArray[0] = radius; + radiusArray[1] = radius; + radiusArray[2] = radius; + } + program.setUniform3fArray('cutSphereRadius', radiusArray); + } + if (program.isUniformUsed('cutSphereUsesAxisRadii')) { + program.setUniformi( + 'cutSphereUsesAxisRadii', + Array.isArray(radius) ? 1 : 0 + ); + } + break; + } + case 'vtkBox': { + const bounds = cutFunction.getBounds(); + if (program.isUniformUsed('cutBoxMinPoint')) { + const minPoint = model.tmpVec3A; + minPoint[0] = bounds[0]; + minPoint[1] = bounds[2]; + minPoint[2] = bounds[4]; + program.setUniform3fArray('cutBoxMinPoint', minPoint); + } + if (program.isUniformUsed('cutBoxMaxPoint')) { + const maxPoint = model.tmpVec3B; + maxPoint[0] = bounds[1]; + maxPoint[1] = bounds[3]; + maxPoint[2] = bounds[5]; + program.setUniform3fArray('cutBoxMaxPoint', maxPoint); + } + break; + } + case 'vtkCylinder': + if (program.isUniformUsed('cutCylinderCenter')) { + program.setUniform3fArray( + 'cutCylinderCenter', + cutFunction.getCenter() + ); + } + if (program.isUniformUsed('cutCylinderAxis')) { + program.setUniform3fArray('cutCylinderAxis', cutFunction.getAxis()); + } + if (program.isUniformUsed('cutCylinderRadius')) { + program.setUniformf('cutCylinderRadius', cutFunction.getRadius()); + } + break; + case 'vtkCone': + if (program.isUniformUsed('cutConeTanThetaSquared')) { + const tanTheta = Math.tan( + vtkMath.radiansFromDegrees(cutFunction.getAngle()) + ); + program.setUniformf('cutConeTanThetaSquared', tanTheta * tanTheta); + } + break; + default: + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + break; + } + + if (program.isUniformUsed('cutWidth')) { + program.setUniformf('cutWidth', model.renderable.getCutWidth()); + } + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + tmpVec3A: null, + tmpVec3B: null, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + vtkOpenGLPolyDataMapper.extend(publicAPI, model, initialValues); + + model.tmpVec3A = [0.0, 0.0, 0.0]; + model.tmpVec3B = [0.0, 0.0, 0.0]; + + vtkOpenGLCutterMapper(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkOpenGLCutterMapper'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; + +// Register ourself to OpenGL backend if imported +registerOverride('vtkCutterMapper', newInstance); diff --git a/Sources/Rendering/OpenGL/Profiles/Geometry.js b/Sources/Rendering/OpenGL/Profiles/Geometry.js index 85dcd3cc213..53700d3e4e2 100644 --- a/Sources/Rendering/OpenGL/Profiles/Geometry.js +++ b/Sources/Rendering/OpenGL/Profiles/Geometry.js @@ -6,6 +6,7 @@ import 'vtk.js/Sources/Rendering/OpenGL/Renderer'; import 'vtk.js/Sources/Rendering/OpenGL/Actor'; import 'vtk.js/Sources/Rendering/OpenGL/Actor2D'; import 'vtk.js/Sources/Rendering/OpenGL/CubeAxesActor'; +import 'vtk.js/Sources/Rendering/OpenGL/CutterMapper'; import 'vtk.js/Sources/Rendering/OpenGL/PolyDataMapper'; import 'vtk.js/Sources/Rendering/OpenGL/PolyDataMapper2D'; import 'vtk.js/Sources/Rendering/OpenGL/ScalarBarActor'; diff --git a/Sources/Rendering/OpenGL/index.js b/Sources/Rendering/OpenGL/index.js index 64e083073c8..67063257801 100644 --- a/Sources/Rendering/OpenGL/index.js +++ b/Sources/Rendering/OpenGL/index.js @@ -5,6 +5,7 @@ import vtkCamera from './Camera'; import vtkCellArrayBufferObject from './CellArrayBufferObject'; import vtkConvolution2DPass from './Convolution2DPass'; import './CubeAxesActor'; +import vtkCutterMapper from './CutterMapper'; import vtkForwardPass from './ForwardPass'; import vtkFramebuffer from './Framebuffer'; import vtkGlyph3DMapper from './Glyph3DMapper'; @@ -38,6 +39,7 @@ export default { vtkBufferObject, vtkCamera, vtkCellArrayBufferObject, + vtkCutterMapper, vtkConvolution2DPass, vtkForwardPass, vtkFramebuffer, diff --git a/Sources/Rendering/WebGPU/CutterMapper/helpers.js b/Sources/Rendering/WebGPU/CutterMapper/helpers.js new file mode 100644 index 00000000000..12f34fcc01c --- /dev/null +++ b/Sources/Rendering/WebGPU/CutterMapper/helpers.js @@ -0,0 +1,31 @@ +export function vec3ToVec4(out, x, y = 0.0) { + out[0] = x[0]; + out[1] = x[1]; + out[2] = x[2]; + out[3] = y; + return out; +} + +export function shiftVec3ToVec4(out, x, shift) { + out[0] = x[0] + shift[0]; + out[1] = x[1] + shift[1]; + out[2] = x[2] + shift[2]; + out[3] = 0.0; + return out; +} + +export function boundsToMinPoint(out, bounds, shift) { + out[0] = bounds[0] + shift[0]; + out[1] = bounds[2] + shift[1]; + out[2] = bounds[4] + shift[2]; + out[3] = 0.0; + return out; +} + +export function boundsToMaxPoint(out, bounds, shift) { + out[0] = bounds[1] + shift[0]; + out[1] = bounds[3] + shift[1]; + out[2] = bounds[5] + shift[2]; + out[3] = 0.0; + return out; +} diff --git a/Sources/Rendering/WebGPU/CutterMapper/index.js b/Sources/Rendering/WebGPU/CutterMapper/index.js new file mode 100644 index 00000000000..62b968b2bb2 --- /dev/null +++ b/Sources/Rendering/WebGPU/CutterMapper/index.js @@ -0,0 +1,375 @@ +import macro from 'vtk.js/Sources/macros'; +import vtkMath from 'vtk.js/Sources/Common/Core/Math'; +import vtkWebGPUCellArrayMapper from 'vtk.js/Sources/Rendering/WebGPU/CellArrayMapper'; +import vtkWebGPUPolyDataMapper from 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper'; +import vtkWebGPUShaderCache from 'vtk.js/Sources/Rendering/WebGPU/ShaderCache'; + +import { registerOverride } from 'vtk.js/Sources/Rendering/WebGPU/ViewNodeFactory'; + +import { + vec3ToVec4, + shiftVec3ToVec4, + boundsToMinPoint, + boundsToMaxPoint, +} from 'vtk.js/Sources/Rendering/WebGPU/CutterMapper/helpers'; + +const { vtkErrorMacro } = macro; + +function vtkWebGPUCutterCellArrayMapper(publicAPI, model) { + model.classHierarchy.push('vtkWebGPUCutterCellArrayMapper'); + + publicAPI.replaceShaderPosition = (hash, pipeline, vertexInput) => { + const functionName = model.renderable.getSupportedImplicitFunctionName(); + if (!functionName) { + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + return; + } + + const vDesc = pipeline.getShaderDescription('vertex'); + vDesc.addBuiltinOutput('vec4', '@builtin(position) Position'); + if (!vDesc.hasOutput('vertexVC')) { + vDesc.addOutput('vec4', 'vertexVC'); + } + vDesc.addOutput('f32', 'cutDistanceVS'); + + let positionImpl = []; + if (model.useRendererMatrix) { + positionImpl = [ + ' var pCoord: vec4 = rendererUBO.SCPCMatrix*mapperUBO.BCSCMatrix*vertexBC;', + ' output.vertexVC = rendererUBO.SCVCMatrix * mapperUBO.BCSCMatrix * vec4(vertexBC.xyz, 1.0);', + ]; + } else { + positionImpl = [ + ' var pCoord: vec4 = mapperUBO.BCSCMatrix*vertexBC;', + ' pCoord.x = 2.0* pCoord.x / rendererUBO.viewportSize.x - 1.0;', + ' pCoord.y = 2.0* pCoord.y / rendererUBO.viewportSize.y - 1.0;', + ' pCoord.z = 0.5 - 0.5 * pCoord.z;', + ' output.vertexVC = vec4(0.0);', + ]; + } + + let cutDistanceImpl = [' output.cutDistanceVS = 1.0;']; + switch (functionName) { + case 'vtkPlane': + cutDistanceImpl = [ + ' output.cutDistanceVS = dot(mapperUBO.CutPlaneNormal.xyz, vertexBC.xyz - mapperUBO.CutPlaneOrigin.xyz) - mapperUBO.CutValue;', + ]; + break; + case 'vtkSphere': + cutDistanceImpl = [ + ' let cutSphereDelta: vec3 = vertexBC.xyz - mapperUBO.CutSphereCenter.xyz;', + ' if (mapperUBO.CutSphereUsesAxisRadii == 1u) {', + ' let cutSphereNormalizedDelta: vec3 = cutSphereDelta / mapperUBO.CutSphereRadius.xyz;', + ' output.cutDistanceVS = dot(cutSphereNormalizedDelta, cutSphereNormalizedDelta) - 1.0;', + ' } else {', + ' output.cutDistanceVS = dot(cutSphereDelta, cutSphereDelta) - mapperUBO.CutSphereRadius.x * mapperUBO.CutSphereRadius.x;', + ' }', + ]; + break; + case 'vtkBox': + cutDistanceImpl = [ + ' var cutBoxMinDistance: f32 = -1.0e20;', + ' var cutBoxDistance: f32 = 0.0;', + ' var cutBoxInside: bool = true;', + ' for (var i: i32 = 0; i < 3; i++) {', + ' let cutBoxLength: f32 = mapperUBO.CutBoxMaxPoint[i] - mapperUBO.CutBoxMinPoint[i];', + ' var cutBoxDist: f32 = 0.0;', + ' if (cutBoxLength != 0.0) {', + ' let cutBoxT: f32 = (vertexBC[i] - mapperUBO.CutBoxMinPoint[i]) / cutBoxLength;', + ' if (cutBoxT < 0.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = mapperUBO.CutBoxMinPoint[i] - vertexBC[i];', + ' } else if (cutBoxT > 1.0) {', + ' cutBoxInside = false;', + ' cutBoxDist = vertexBC[i] - mapperUBO.CutBoxMaxPoint[i];', + ' } else if (cutBoxT <= 0.5) {', + ' cutBoxDist = mapperUBO.CutBoxMinPoint[i] - vertexBC[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' } else {', + ' cutBoxDist = vertexBC[i] - mapperUBO.CutBoxMaxPoint[i];', + ' cutBoxMinDistance = max(cutBoxMinDistance, cutBoxDist);', + ' }', + ' } else {', + ' cutBoxDist = abs(vertexBC[i] - mapperUBO.CutBoxMinPoint[i]);', + ' if (cutBoxDist > 0.0) {', + ' cutBoxInside = false;', + ' }', + ' }', + ' if (cutBoxDist > 0.0) {', + ' cutBoxDistance += cutBoxDist * cutBoxDist;', + ' }', + ' }', + ' output.cutDistanceVS = select(sqrt(cutBoxDistance), cutBoxMinDistance, cutBoxInside);', + ]; + break; + case 'vtkCylinder': + cutDistanceImpl = [ + ' let cutCylinderDelta: vec3 = vertexBC.xyz - mapperUBO.CutCylinderCenter.xyz;', + ' let cutCylinderProjection: f32 = dot(mapperUBO.CutCylinderAxis.xyz, cutCylinderDelta);', + ' output.cutDistanceVS = dot(cutCylinderDelta, cutCylinderDelta) - cutCylinderProjection * cutCylinderProjection - mapperUBO.CutCylinderRadius * mapperUBO.CutCylinderRadius;', + ]; + break; + case 'vtkCone': + cutDistanceImpl = [ + ' let cutConeDelta: vec3 = vertexBC.xyz - mapperUBO.CutConeApex.xyz;', + ' let cutConeAxial: f32 = dot(mapperUBO.CutConeAxis.xyz, cutConeDelta);', + ' let cutConeRadial2: f32 = dot(cutConeDelta, cutConeDelta) - cutConeAxial * cutConeAxial;', + ' output.cutDistanceVS = cutConeRadial2 - cutConeAxial * cutConeAxial * mapperUBO.CutConeTanThetaSquared;', + ]; + break; + default: + vtkErrorMacro( + `Unsupported implicit function ${functionName} for cutting.` + ); + break; + } + + let code = vDesc.getCode(); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ...positionImpl, + ...cutDistanceImpl, + '//VTK::Position::Impl', + ]).result; + + if (model.forceZValue) { + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + model.useRendererMatrix + ? 'pCoord = vec4(pCoord.xyz/pCoord.w, 1.0);' + : ' pCoord = vec4(pCoord.xyz/pCoord.w, 1.0);', + ' pCoord.z = mapperUBO.ZValue;', + '//VTK::Position::Impl', + ]).result; + } + + if (publicAPI.haveWideLines()) { + vDesc.addBuiltinInput('u32', '@builtin(instance_index) instanceIndex'); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ' var tmpPos: vec4 = pCoord;', + ' var numSteps: f32 = ceil(mapperUBO.LineWidth - 1.0);', + ' var offset: f32 = (mapperUBO.LineWidth - 1.0) * (f32(input.instanceIndex / 2u) - numSteps/2.0) / numSteps;', + ' var tmpPos2: vec3 = tmpPos.xyz / tmpPos.w;', + ' tmpPos2.x = tmpPos2.x + 2.0 * (f32(input.instanceIndex) % 2.0) * offset / rendererUBO.viewportSize.x;', + ' tmpPos2.y = tmpPos2.y + 2.0 * (f32(input.instanceIndex + 1u) % 2.0) * offset / rendererUBO.viewportSize.y;', + ' tmpPos2.z = min(1.0, tmpPos2.z + 0.00001);', + ' pCoord = vec4(tmpPos2.xyz * tmpPos.w, tmpPos.w);', + '//VTK::Position::Impl', + ]).result; + } + + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ' output.Position = pCoord;', + ]).result; + vDesc.setCode(code); + + const fDesc = pipeline.getShaderDescription('fragment'); + code = fDesc.getCode(); + code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [ + ' if (abs(input.cutDistanceVS) > max(fwidth(input.cutDistanceVS) * mapperUBO.CutWidth, 1.0e-6)) { discard; }', + '//VTK::Position::Impl', + ]).result; + code = vtkWebGPUShaderCache.substitute(code, '//VTK::RenderEncoder::Impl', [ + ' let cutterDistanceDx: f32 = dpdx(input.cutDistanceVS);', + ' let cutterDistanceDy: f32 = dpdy(input.cutDistanceVS);', + ' let cutterDistanceGradient: f32 = length(vec2(cutterDistanceDx, cutterDistanceDy));', + ' if (cutterDistanceGradient <= 0.0 && abs(input.cutDistanceVS) > 1.0e-6) { discard; }', + '//VTK::RenderEncoder::Impl', + ]).result; + fDesc.setCode(code); + }; + model.shaderReplacements.set( + 'replaceShaderPosition', + publicAPI.replaceShaderPosition + ); + + const superComputePipelineHash = publicAPI.computePipelineHash; + publicAPI.computePipelineHash = () => { + superComputePipelineHash(); + const functionName = + model.renderable.getSupportedImplicitFunctionName() || 'none'; + model.pipelineHash += `cf${functionName}Params`; + }; + + const superUpdateUBO = publicAPI.updateUBO; + publicAPI.updateUBO = () => { + superUpdateUBO(); + + const renderable = model.renderable; + const cutFunction = renderable.getCutFunction(); + const functionName = renderable.getSupportedImplicitFunctionName(); + if (!functionName || !cutFunction) { + return; + } + + const bufferShift = model.WebGPUActor.getBufferShift(model.WebGPURenderer); + model.UBO.setValue('CutWidth', vtkMath.max(renderable.getCutWidth(), 0.0)); + + switch (functionName) { + case 'vtkPlane': + model.UBO.setArray( + 'CutPlaneOrigin', + shiftVec3ToVec4( + model.cutPlaneOrigin, + cutFunction.getOrigin(), + bufferShift + ) + ); + model.UBO.setArray( + 'CutPlaneNormal', + vec3ToVec4(model.cutPlaneNormal, cutFunction.getNormal()) + ); + model.UBO.setValue('CutValue', renderable.getCutValue()); + break; + case 'vtkSphere': { + const radius = cutFunction.getRadius(); + const usesAxisRadii = Array.isArray(radius); + model.UBO.setArray( + 'CutSphereCenter', + shiftVec3ToVec4( + model.cutSphereCenter, + cutFunction.getCenter(), + bufferShift + ) + ); + if (usesAxisRadii) { + model.cutSphereRadius[0] = radius[0]; + model.cutSphereRadius[1] = radius[1]; + model.cutSphereRadius[2] = radius[2]; + } else { + model.cutSphereRadius[0] = radius; + model.cutSphereRadius[1] = radius; + model.cutSphereRadius[2] = radius; + } + model.cutSphereRadius[3] = 0.0; + model.UBO.setArray('CutSphereRadius', model.cutSphereRadius); + model.UBO.setValue('CutSphereUsesAxisRadii', usesAxisRadii ? 1 : 0); + break; + } + case 'vtkBox': { + const bounds = cutFunction.getBounds(); + model.UBO.setArray( + 'CutBoxMinPoint', + boundsToMinPoint(model.cutBoxMinPoint, bounds, bufferShift) + ); + model.UBO.setArray( + 'CutBoxMaxPoint', + boundsToMaxPoint(model.cutBoxMaxPoint, bounds, bufferShift) + ); + break; + } + case 'vtkCylinder': + model.UBO.setArray( + 'CutCylinderCenter', + shiftVec3ToVec4( + model.cutCylinderCenter, + cutFunction.getCenter(), + bufferShift + ) + ); + model.UBO.setArray( + 'CutCylinderAxis', + vec3ToVec4(model.cutCylinderAxis, cutFunction.getAxis()) + ); + model.UBO.setValue('CutCylinderRadius', cutFunction.getRadius()); + break; + case 'vtkCone': { + const apex = model.cutConeApex; + const axis = model.cutConeAxis; + const axisPoint = model.cutConeAxisPoint; + const inverseTransform = cutFunction.getTransform?.()?.getInverse?.(); + + apex[0] = 0.0; + apex[1] = 0.0; + apex[2] = 0.0; + axis[0] = 1.0; + axis[1] = 0.0; + axis[2] = 0.0; + axisPoint[0] = 1.0; + axisPoint[1] = 0.0; + axisPoint[2] = 0.0; + + if (inverseTransform?.transformPoint) { + inverseTransform.transformPoint(apex, apex); + inverseTransform.transformPoint(axisPoint, axisPoint); + vtkMath.subtract(axisPoint, apex, axis); + if (vtkMath.norm(axis) > 0.0) { + vtkMath.normalize(axis); + } + } + model.UBO.setArray( + 'CutConeApex', + shiftVec3ToVec4(model.cutConeApex4, apex, bufferShift) + ); + model.UBO.setArray('CutConeAxis', vec3ToVec4(model.cutConeAxis4, axis)); + const tanTheta = Math.tan( + vtkMath.radiansFromDegrees(cutFunction.getAngle()) + ); + model.UBO.setValue('CutConeTanThetaSquared', tanTheta * tanTheta); + break; + } + default: + break; + } + + model.UBO.sendIfNeeded(model.WebGPURenderWindow.getDevice()); + }; +} + +function extendCellArray(publicAPI, model, initialValues = {}) { + vtkWebGPUCellArrayMapper.extend(publicAPI, model, initialValues); + + model.UBO.addEntry('CutPlaneOrigin', 'vec4'); + model.UBO.addEntry('CutPlaneNormal', 'vec4'); + model.UBO.addEntry('CutSphereCenter', 'vec4'); + model.UBO.addEntry('CutSphereRadius', 'vec4'); + model.UBO.addEntry('CutBoxMinPoint', 'vec4'); + model.UBO.addEntry('CutBoxMaxPoint', 'vec4'); + model.UBO.addEntry('CutCylinderCenter', 'vec4'); + model.UBO.addEntry('CutCylinderAxis', 'vec4'); + model.UBO.addEntry('CutConeApex', 'vec4'); + model.UBO.addEntry('CutConeAxis', 'vec4'); + model.UBO.addEntry('CutValue', 'f32'); + model.UBO.addEntry('CutWidth', 'f32'); + model.UBO.addEntry('CutCylinderRadius', 'f32'); + model.UBO.addEntry('CutConeTanThetaSquared', 'f32'); + model.UBO.addEntry('CutSphereUsesAxisRadii', 'u32'); + + model.cutPlaneOrigin = [0.0, 0.0, 0.0, 0.0]; + model.cutPlaneNormal = [0.0, 0.0, 0.0, 0.0]; + model.cutSphereCenter = [0.0, 0.0, 0.0, 0.0]; + model.cutSphereRadius = [0.0, 0.0, 0.0, 0.0]; + model.cutBoxMinPoint = [0.0, 0.0, 0.0, 0.0]; + model.cutBoxMaxPoint = [0.0, 0.0, 0.0, 0.0]; + model.cutCylinderCenter = [0.0, 0.0, 0.0, 0.0]; + model.cutCylinderAxis = [0.0, 0.0, 0.0, 0.0]; + model.cutConeApex = [0.0, 0.0, 0.0]; + model.cutConeAxis = [1.0, 0.0, 0.0]; + model.cutConeAxisPoint = [1.0, 0.0, 0.0]; + model.cutConeApex4 = [0.0, 0.0, 0.0, 0.0]; + model.cutConeAxis4 = [1.0, 0.0, 0.0, 0.0]; + + vtkWebGPUCutterCellArrayMapper(publicAPI, model); +} + +const newCellArrayInstance = macro.newInstance( + extendCellArray, + 'vtkWebGPUCutterCellArrayMapper' +); + +function vtkWebGPUCutterMapper(publicAPI, model) { + model.classHierarchy.push('vtkWebGPUCutterMapper'); + + publicAPI.createCellArrayMapper = () => newCellArrayInstance(); +} + +export function extend(publicAPI, model, initialValues = {}) { + vtkWebGPUPolyDataMapper.extend(publicAPI, model, initialValues); + vtkWebGPUCutterMapper(publicAPI, model); +} + +export const newInstance = macro.newInstance(extend, 'vtkWebGPUCutterMapper'); + +export default { newInstance, extend }; + +registerOverride('vtkCutterMapper', newInstance); diff --git a/Sources/Rendering/WebGPU/Profiles/All.js b/Sources/Rendering/WebGPU/Profiles/All.js index d1030b1ab26..89b149cc804 100644 --- a/Sources/Rendering/WebGPU/Profiles/All.js +++ b/Sources/Rendering/WebGPU/Profiles/All.js @@ -6,6 +6,7 @@ import 'vtk.js/Sources/Rendering/WebGPU/Renderer'; import 'vtk.js/Sources/Rendering/WebGPU/Actor'; import 'vtk.js/Sources/Rendering/WebGPU/Actor2D'; import 'vtk.js/Sources/Rendering/WebGPU/CubeAxesActor'; +import 'vtk.js/Sources/Rendering/WebGPU/CutterMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper2D'; // import 'vtk.js/Sources/Rendering/WebGPU/Skybox'; diff --git a/Sources/Rendering/WebGPU/Profiles/Geometry.js b/Sources/Rendering/WebGPU/Profiles/Geometry.js index d4e596651e4..0baaac28dc4 100644 --- a/Sources/Rendering/WebGPU/Profiles/Geometry.js +++ b/Sources/Rendering/WebGPU/Profiles/Geometry.js @@ -6,6 +6,7 @@ import 'vtk.js/Sources/Rendering/WebGPU/Renderer'; import 'vtk.js/Sources/Rendering/WebGPU/Actor'; import 'vtk.js/Sources/Rendering/WebGPU/Actor2D'; import 'vtk.js/Sources/Rendering/WebGPU/CubeAxesActor'; +import 'vtk.js/Sources/Rendering/WebGPU/CutterMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper'; import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper2D'; // import 'vtk.js/Sources/Rendering/WebGPU/Skybox'; diff --git a/Sources/Rendering/WebGPU/index.js b/Sources/Rendering/WebGPU/index.js index e511771c11b..2300797a68b 100644 --- a/Sources/Rendering/WebGPU/index.js +++ b/Sources/Rendering/WebGPU/index.js @@ -3,6 +3,7 @@ import './Actor'; import './Actor2D'; import './Camera'; import './CubeAxesActor'; +import './CutterMapper'; import './ForwardPass'; import './Glyph3DMapper'; import './HardwareSelector';