From 2248eb9d0cd88837973bccf27303cff466b41442 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 7 May 2026 12:01:10 -0400 Subject: [PATCH 1/2] fix(Glyph3DMapper): clear stale source VBO shift scale When `useShiftAndScale` flips from true to false, the mapper previously left the source primitive CABOs holding the previous frame's shift/scale state. The freshly packed VBO data then used identity coordinates while the inverse shift/scale matrix still applied -- a coordinate mismatch. Pass `null, null` to `setCoordShiftAndScale` when shift/scale is no longer needed so the source CABO state is cleared. --- .../test/testGlyph3DMapperShiftScale.js | 98 +++++++++++++++++++ .../Rendering/OpenGL/Glyph3DMapper/index.js | 13 +-- 2 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 Sources/Rendering/Core/Glyph3DMapper/test/testGlyph3DMapperShiftScale.js diff --git a/Sources/Rendering/Core/Glyph3DMapper/test/testGlyph3DMapperShiftScale.js b/Sources/Rendering/Core/Glyph3DMapper/test/testGlyph3DMapperShiftScale.js new file mode 100644 index 00000000000..ac33024eed5 --- /dev/null +++ b/Sources/Rendering/Core/Glyph3DMapper/test/testGlyph3DMapperShiftScale.js @@ -0,0 +1,98 @@ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkCellArray from 'vtk.js/Sources/Common/Core/CellArray'; +import vtkGlyph3DMapper from 'vtk.js/Sources/Rendering/Core/Glyph3DMapper'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; + +function makePolyDataFromPoints(coords) { + const polyData = vtkPolyData.newInstance(); + polyData.getPoints().setData(Float32Array.from(coords), 3); + return polyData; +} + +function updatePoints(polyData, coords) { + polyData.getPoints().setData(Float32Array.from(coords), 3); + polyData.getPoints().modified(); + polyData.modified(); +} + +function makeLineGlyphSource() { + const source = makePolyDataFromPoints([-0.5, 0, 0, 0.5, 0, 0]); + source.setLines( + vtkCellArray.newInstance({ + values: new Uint32Array([2, 0, 1]), + }) + ); + return source; +} + +function getActivePrimitiveCABOs(openGLRenderWindow, mapper) { + const openGLMapper = openGLRenderWindow.getViewNodeFor(mapper); + const primitives = openGLMapper.getReferenceByName('primitives'); + return primitives + .map((primitive) => primitive.getCABO()) + .filter((cabo) => cabo.getElementCount() > 0); +} + +test.onlyIfWebGL( + 'Test vtkGlyph3DMapper clears source VBO shift/scale after shifted glyph centers', + (t) => { + const gc = testUtils.createGarbageCollector(t); + + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + + const centers = gc.registerResource( + makePolyDataFromPoints([10000000, 0, 0, 10000001, 0, 0]) + ); + const source = gc.registerResource(makeLineGlyphSource()); + const mapper = gc.registerResource(vtkGlyph3DMapper.newInstance()); + const actor = gc.registerResource(vtkActor.newInstance()); + + mapper.setInputData(centers, 0); + mapper.setInputData(source, 1); + mapper.setScalarVisibility(false); + + actor.setMapper(mapper); + renderer.addActor(actor); + + const openGLRenderWindow = gc.registerResource( + renderWindow.newAPISpecificView() + ); + openGLRenderWindow.setContainer(renderWindowContainer); + renderWindow.addView(openGLRenderWindow); + openGLRenderWindow.setSize(1, 1); + + renderWindow.render(); + + const shiftedCABOs = getActivePrimitiveCABOs(openGLRenderWindow, mapper); + t.ok(shiftedCABOs.length > 0, 'glyph source VBOs were built'); + t.ok( + shiftedCABOs.some((cabo) => cabo.getCoordShiftAndScaleEnabled()), + 'far glyph centers enable shift/scale on source VBOs' + ); + + updatePoints(centers, [-0.5, 0, 0, 0.5, 0, 0]); + renderWindow.render(); + + const unshiftedCABOs = getActivePrimitiveCABOs(openGLRenderWindow, mapper); + t.ok( + unshiftedCABOs.every((cabo) => !cabo.getCoordShiftAndScaleEnabled()), + 'source VBO shift/scale is cleared once glyph centers no longer use it' + ); + + gc.releaseResources(); + } +); diff --git a/Sources/Rendering/OpenGL/Glyph3DMapper/index.js b/Sources/Rendering/OpenGL/Glyph3DMapper/index.js index ae1205c333a..81fe0ade3ee 100644 --- a/Sources/Rendering/OpenGL/Glyph3DMapper/index.js +++ b/Sources/Rendering/OpenGL/Glyph3DMapper/index.js @@ -756,12 +756,13 @@ function vtkOpenGLGlyph3DMapper(publicAPI, model) { // apply shift + scale to primitives AFTER vtkOpenGLPolyDataMapper.buildBufferObjects // so that the Glyph3DMapper gets the last say in the shift + scale - if (useShiftAndScale) { - for (let i = primTypes.Start; i < primTypes.End; i++) { - model.primitives[i] - .getCABO() - .setCoordShiftAndScale(coordShift, coordScale); - } + for (let i = primTypes.Start; i < primTypes.End; i++) { + model.primitives[i] + .getCABO() + .setCoordShiftAndScale( + useShiftAndScale ? coordShift : null, + useShiftAndScale ? coordScale : null + ); } }; } From 46d7c46d1853d8fd77bf80ce02c6b1b7e7070233 Mon Sep 17 00:00:00 2001 From: Paul Elliott Date: Thu, 7 May 2026 12:01:10 -0400 Subject: [PATCH 2/2] fix(SphereMapper): clear stale VBO shift scale When `useShiftAndScale` flips from true to false, the mapper previously left the VBO holding the previous frame's shift/scale state. The freshly packed VBO data then used identity coordinates while the inverse shift/scale matrix still applied -- a coordinate mismatch. Pass `null, null` to `setCoordShiftAndScale` when shift/scale is no longer needed so the VBO state is cleared. --- .../test/testSphereMapperShiftScale.js | 85 +++++++++++++++++++ .../Rendering/OpenGL/SphereMapper/index.js | 7 +- 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 Sources/Rendering/Core/SphereMapper/test/testSphereMapperShiftScale.js diff --git a/Sources/Rendering/Core/SphereMapper/test/testSphereMapperShiftScale.js b/Sources/Rendering/Core/SphereMapper/test/testSphereMapperShiftScale.js new file mode 100644 index 00000000000..5083a36c602 --- /dev/null +++ b/Sources/Rendering/Core/SphereMapper/test/testSphereMapperShiftScale.js @@ -0,0 +1,85 @@ +import test from 'tape'; +import testUtils from 'vtk.js/Sources/Testing/testUtils'; + +import 'vtk.js/Sources/Rendering/Misc/RenderingAPIs'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkPolyData from 'vtk.js/Sources/Common/DataModel/PolyData'; +import vtkRenderWindow from 'vtk.js/Sources/Rendering/Core/RenderWindow'; +import vtkRenderer from 'vtk.js/Sources/Rendering/Core/Renderer'; +import vtkSphereMapper from 'vtk.js/Sources/Rendering/Core/SphereMapper'; + +function makePolyDataFromPoints(coords) { + const polyData = vtkPolyData.newInstance(); + polyData.getPoints().setData(Float32Array.from(coords), 3); + return polyData; +} + +function updatePoints(polyData, coords) { + polyData.getPoints().setData(Float32Array.from(coords), 3); + polyData.getPoints().modified(); + polyData.modified(); +} + +function getActivePrimitiveCABOs(openGLRenderWindow, mapper) { + const openGLMapper = openGLRenderWindow.getViewNodeFor(mapper); + const primitives = openGLMapper.getReferenceByName('primitives'); + return primitives + .map((primitive) => primitive.getCABO()) + .filter((cabo) => cabo.getElementCount() > 0); +} + +test.onlyIfWebGL( + 'Test vtkSphereMapper clears VBO shift/scale after shifted points', + (t) => { + const gc = testUtils.createGarbageCollector(t); + + const container = document.querySelector('body'); + const renderWindowContainer = gc.registerDOMElement( + document.createElement('div') + ); + container.appendChild(renderWindowContainer); + + const renderWindow = gc.registerResource(vtkRenderWindow.newInstance()); + const renderer = gc.registerResource(vtkRenderer.newInstance()); + renderWindow.addRenderer(renderer); + + const polyData = gc.registerResource( + makePolyDataFromPoints([10000000, 0, 0, 10000001, 0, 0]) + ); + const mapper = gc.registerResource(vtkSphereMapper.newInstance()); + const actor = gc.registerResource(vtkActor.newInstance()); + + mapper.setInputData(polyData); + mapper.setScalarVisibility(false); + + actor.setMapper(mapper); + renderer.addActor(actor); + + const openGLRenderWindow = gc.registerResource( + renderWindow.newAPISpecificView() + ); + openGLRenderWindow.setContainer(renderWindowContainer); + renderWindow.addView(openGLRenderWindow); + openGLRenderWindow.setSize(1, 1); + + renderWindow.render(); + + const shiftedCABOs = getActivePrimitiveCABOs(openGLRenderWindow, mapper); + t.ok(shiftedCABOs.length > 0, 'sphere VBOs were built'); + t.ok( + shiftedCABOs.some((cabo) => cabo.getCoordShiftAndScaleEnabled()), + 'far points enable shift/scale on VBOs' + ); + + updatePoints(polyData, [-0.5, 0, 0, 0.5, 0, 0]); + renderWindow.render(); + + const unshiftedCABOs = getActivePrimitiveCABOs(openGLRenderWindow, mapper); + t.ok( + unshiftedCABOs.every((cabo) => !cabo.getCoordShiftAndScaleEnabled()), + 'VBO shift/scale is cleared once points no longer use it' + ); + + gc.releaseResources(); + } +); diff --git a/Sources/Rendering/OpenGL/SphereMapper/index.js b/Sources/Rendering/OpenGL/SphereMapper/index.js index 5c6d8190f6a..e0392e565ea 100644 --- a/Sources/Rendering/OpenGL/SphereMapper/index.js +++ b/Sources/Rendering/OpenGL/SphereMapper/index.js @@ -296,9 +296,10 @@ function vtkOpenGLSphereMapper(publicAPI, model) { const { useShiftAndScale, coordShift, coordScale } = computeCoordShiftAndScale(points); - if (useShiftAndScale) { - vbo.setCoordShiftAndScale(coordShift, coordScale); - } + vbo.setCoordShiftAndScale( + useShiftAndScale ? coordShift : null, + useShiftAndScale ? coordScale : null + ); // // Generate points and point data for sides