summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot3d/test
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot3d/test')
-rw-r--r--src/silx/gui/plot3d/test/__init__.py25
-rw-r--r--src/silx/gui/plot3d/test/testGL.py73
-rw-r--r--src/silx/gui/plot3d/test/testScalarFieldView.py128
-rw-r--r--src/silx/gui/plot3d/test/testSceneWidget.py72
-rw-r--r--src/silx/gui/plot3d/test/testSceneWidgetPicking.py314
-rw-r--r--src/silx/gui/plot3d/test/testSceneWindow.py233
-rw-r--r--src/silx/gui/plot3d/test/testStatsWidget.py201
7 files changed, 1046 insertions, 0 deletions
diff --git a/src/silx/gui/plot3d/test/__init__.py b/src/silx/gui/plot3d/test/__init__.py
new file mode 100644
index 0000000..83491ad
--- /dev/null
+++ b/src/silx/gui/plot3d/test/__init__.py
@@ -0,0 +1,25 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""plot3d test suite."""
diff --git a/src/silx/gui/plot3d/test/testGL.py b/src/silx/gui/plot3d/test/testGL.py
new file mode 100644
index 0000000..a7309a9
--- /dev/null
+++ b/src/silx/gui/plot3d/test/testGL.py
@@ -0,0 +1,73 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# ###########################################################################*/
+"""Test OpenGL"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "10/08/2017"
+
+
+import logging
+import unittest
+
+from silx.gui._glutils import gl, OpenGLWidget
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
+
+
+_logger = logging.getLogger(__name__)
+
+
+class TestOpenGL(TestCaseQt):
+ """Tests of OpenGL widget."""
+
+ class OpenGLWidgetLogger(OpenGLWidget):
+ """Widget logging information of available OpenGL version"""
+
+ def __init__(self):
+ self._dump = False
+ super(TestOpenGL.OpenGLWidgetLogger, self).__init__(version=(1, 0))
+
+ def paintOpenGL(self):
+ """Perform the rendering and logging"""
+ if not self._dump:
+ self._dump = True
+ _logger.info('OpenGL info:')
+ _logger.info('\tQt OpenGL context version: %d.%d', *self.getOpenGLVersion())
+ _logger.info('\tGL_VERSION: %s' % gl.glGetString(gl.GL_VERSION))
+ _logger.info('\tGL_SHADING_LANGUAGE_VERSION: %s' %
+ gl.glGetString(gl.GL_SHADING_LANGUAGE_VERSION))
+ _logger.debug('\tGL_EXTENSIONS: %s' % gl.glGetString(gl.GL_EXTENSIONS))
+
+ gl.glClearColor(1., 1., 1., 1.)
+ gl.glClear(gl.GL_COLOR_BUFFER_BIT)
+
+ def testOpenGL(self):
+ """Log OpenGL version using an OpenGLWidget"""
+ super(TestOpenGL, self).setUp()
+ widget = self.OpenGLWidgetLogger()
+ widget.show()
+ widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.qWaitForWindowExposed(widget)
+ widget.close()
diff --git a/src/silx/gui/plot3d/test/testScalarFieldView.py b/src/silx/gui/plot3d/test/testScalarFieldView.py
new file mode 100644
index 0000000..e6535fc
--- /dev/null
+++ b/src/silx/gui/plot3d/test/testScalarFieldView.py
@@ -0,0 +1,128 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# ###########################################################################*/
+"""Test ScalarFieldView widget"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "17/01/2018"
+
+
+import logging
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
+
+from silx.gui.plot3d.ScalarFieldView import ScalarFieldView
+from silx.gui.plot3d.SFViewParamTree import TreeView
+
+
+_logger = logging.getLogger(__name__)
+
+
+class TestScalarFieldView(TestCaseQt, ParametricTestCase):
+ """Tests of ScalarFieldView widget."""
+
+ def setUp(self):
+ super(TestScalarFieldView, self).setUp()
+ self.widget = ScalarFieldView()
+ self.widget.show()
+
+ paramTreeWidget = TreeView()
+ paramTreeWidget.setSfView(self.widget)
+
+ dock = qt.QDockWidget()
+ dock.setWidget(paramTreeWidget)
+ self.widget.addDockWidget(qt.Qt.BottomDockWidgetArea, dock)
+
+ # Commented as it slows down the tests
+ # self.qWaitForWindowExposed(self.widget)
+
+ def tearDown(self):
+ self.qapp.processEvents()
+ self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.widget.close()
+ del self.widget
+ super(TestScalarFieldView, self).tearDown()
+
+ @staticmethod
+ def _buildData(size):
+ """Make a 3D dataset"""
+ coords = numpy.linspace(-10, 10, size)
+ z = coords.reshape(-1, 1, 1)
+ y = coords.reshape(1, -1, 1)
+ x = coords.reshape(1, 1, -1)
+ return numpy.sin(x * y * z) / (x * y * z)
+
+ def testSimple(self):
+ """Set the data and an isosurface"""
+ data = self._buildData(size=32)
+
+ self.widget.setData(data)
+ self.widget.addIsosurface(0.5, (1., 0., 0., 0.5))
+ self.widget.addIsosurface(0.7, qt.QColor('green'))
+ self.qapp.processEvents()
+
+ def testNotFinite(self):
+ """Test with NaN and inf in data set"""
+
+ # Some NaNs and inf
+ data = self._buildData(size=32)
+ data[8, :, :] = numpy.nan
+ data[16, :, :] = numpy.inf
+ data[24, :, :] = - numpy.inf
+
+ self.widget.addIsosurface(0.5, 'red')
+ self.widget.setData(data, copy=True)
+ self.qapp.processEvents()
+ self.widget.setData(None)
+
+ # All NaNs or inf
+ data = numpy.empty((4, 4, 4), dtype=numpy.float32)
+ for value in (numpy.nan, numpy.inf):
+ with self.subTest(value=str(value)):
+ data[:] = value
+ self.widget.setData(data, copy=True)
+ self.qapp.processEvents()
+
+ def testIsoSliderNormalization(self):
+ """Test set TreeView with a different isoslider normalization"""
+ data = self._buildData(size=32)
+
+ self.widget.setData(data)
+ self.widget.addIsosurface(0.5, (1., 0., 0., 0.5))
+ self.widget.addIsosurface(0.7, qt.QColor('green'))
+ self.qapp.processEvents()
+
+ # Add a second TreeView
+ paramTreeWidget = TreeView(self.widget)
+ paramTreeWidget.setIsoLevelSliderNormalization('arcsinh')
+ paramTreeWidget.setSfView(self.widget)
+
+ dock = qt.QDockWidget()
+ dock.setWidget(paramTreeWidget)
+ self.widget.addDockWidget(qt.Qt.BottomDockWidgetArea, dock)
diff --git a/src/silx/gui/plot3d/test/testSceneWidget.py b/src/silx/gui/plot3d/test/testSceneWidget.py
new file mode 100644
index 0000000..fc96781
--- /dev/null
+++ b/src/silx/gui/plot3d/test/testSceneWidget.py
@@ -0,0 +1,72 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# ###########################################################################*/
+"""Test SceneWidget"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "06/03/2019"
+
+
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
+
+from silx.gui.plot3d.SceneWidget import SceneWidget
+
+
+class TestSceneWidget(TestCaseQt, ParametricTestCase):
+ """Tests SceneWidget picking feature"""
+
+ def setUp(self):
+ super(TestSceneWidget, self).setUp()
+ self.widget = SceneWidget()
+ self.widget.show()
+ self.qWaitForWindowExposed(self.widget)
+
+ def tearDown(self):
+ self.qapp.processEvents()
+ self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.widget.close()
+ del self.widget
+ super(TestSceneWidget, self).tearDown()
+
+ def testFogEffect(self):
+ """Test fog effect on scene primitive"""
+ image = self.widget.addImage(numpy.arange(100).reshape(10, 10))
+ scatter = self.widget.add3DScatter(*numpy.random.random(4000).reshape(4, -1))
+ scatter.setTranslation(10, 10)
+ scatter.setScale(10, 10, 10)
+
+ self.widget.resetZoom('front')
+ self.qapp.processEvents()
+
+ self.widget.setFogMode(self.widget.FogMode.LINEAR)
+ self.qapp.processEvents()
+
+ self.widget.setFogMode(self.widget.FogMode.NONE)
+ self.qapp.processEvents()
diff --git a/src/silx/gui/plot3d/test/testSceneWidgetPicking.py b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py
new file mode 100644
index 0000000..d4d8db7
--- /dev/null
+++ b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py
@@ -0,0 +1,314 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# ###########################################################################*/
+"""Test SceneWidget picking feature"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "03/10/2018"
+
+
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
+
+from silx.gui.plot3d.SceneWidget import SceneWidget, items
+
+
+class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase):
+ """Tests SceneWidget picking feature"""
+
+ def setUp(self):
+ super(TestSceneWidgetPicking, self).setUp()
+ self.widget = SceneWidget()
+ self.widget.resize(300, 300)
+ self.widget.show()
+ # self.qWaitForWindowExposed(self.widget)
+
+ def tearDown(self):
+ self.qapp.processEvents()
+ self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.widget.close()
+ del self.widget
+ super(TestSceneWidgetPicking, self).tearDown()
+
+ def _widgetCenter(self):
+ """Returns widget center"""
+ size = self.widget.size()
+ return size.width() // 2, size.height() // 2
+
+ def testPickImage(self):
+ """Test picking of ImageData and ImageRgba items"""
+ imageData = items.ImageData()
+ imageData.setData(numpy.arange(100).reshape(10, 10))
+
+ imageRgba = items.ImageRgba()
+ imageRgba.setData(
+ numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3))
+
+ for item in (imageData, imageRgba):
+ with self.subTest(item=item.__class__.__name__):
+ # Add item
+ self.widget.clearItems()
+ self.widget.addItem(item)
+ self.widget.resetZoom('front')
+ self.qapp.processEvents()
+
+ # Picking on data (at widget center)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+
+ self.assertEqual(len(picking), 1)
+ self.assertIs(picking[0].getItem(), item)
+ self.assertEqual(picking[0].getPositions('ndc').shape, (1, 3))
+ data = picking[0].getData()
+ self.assertEqual(len(data), 1)
+ self.assertTrue(numpy.array_equal(
+ data,
+ item.getData()[picking[0].getIndices()]))
+
+ # Picking outside data
+ picking = list(self.widget.pickItems(1, 1))
+ self.assertEqual(len(picking), 0)
+
+ def testPickScatter(self):
+ """Test picking of Scatter2D and Scatter3D items"""
+ data = numpy.arange(100)
+
+ scatter2d = items.Scatter2D()
+ scatter2d.setData(x=data, y=data, value=data)
+
+ scatter3d = items.Scatter3D()
+ scatter3d.setData(x=data, y=data, z=data, value=data)
+
+ for item in (scatter2d, scatter3d):
+ with self.subTest(item=item.__class__.__name__):
+ # Add item
+ self.widget.clearItems()
+ self.widget.addItem(item)
+ self.widget.resetZoom('front')
+ self.qapp.processEvents()
+
+ # Picking on data (at widget center)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+
+ self.assertEqual(len(picking), 1)
+ self.assertIs(picking[0].getItem(), item)
+ nbPos = len(picking[0].getPositions('ndc'))
+ data = picking[0].getData()
+ self.assertEqual(nbPos, len(data))
+ self.assertTrue(numpy.array_equal(
+ data,
+ item.getValueData()[picking[0].getIndices()]))
+
+ # Picking outside data
+ picking = list(self.widget.pickItems(1, 1))
+ self.assertEqual(len(picking), 0)
+
+ def testPickVolume(self):
+ """Test picking of volume CutPlane and Isosurface items"""
+ for dtype in (numpy.float32, numpy.complex64):
+ with self.subTest(dtype=dtype):
+ refData = numpy.arange(10**3, dtype=dtype).reshape(10, 10, 10)
+ volume = self.widget.addVolume(refData)
+ if dtype == numpy.complex64:
+ volume.setComplexMode(volume.ComplexMode.REAL)
+ refData = numpy.real(refData)
+ self.widget.resetZoom('front')
+
+ cutplane = volume.getCutPlanes()[0]
+ if dtype == numpy.complex64:
+ cutplane.setComplexMode(volume.ComplexMode.REAL)
+ cutplane.getColormap().setVRange(0, 100)
+ cutplane.setNormal((0, 0, 1))
+
+ # Picking on data without anything displayed
+ cutplane.setVisible(False)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+ self.assertEqual(len(picking), 0)
+
+ # Picking on data with the cut plane
+ cutplane.setVisible(True)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+
+ self.assertEqual(len(picking), 1)
+ self.assertIs(picking[0].getItem(), cutplane)
+ data = picking[0].getData()
+ self.assertEqual(len(data), 1)
+ self.assertEqual(picking[0].getPositions().shape, (1, 3))
+ self.assertTrue(numpy.array_equal(
+ data,
+ refData[picking[0].getIndices()]))
+
+ # Picking on data with an isosurface
+ isosurface = volume.addIsosurface(
+ level=500, color=(1., 0., 0., .5))
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+ self.assertEqual(len(picking), 2)
+ self.assertIs(picking[0].getItem(), cutplane)
+ self.assertIs(picking[1].getItem(), isosurface)
+ self.assertEqual(picking[1].getPositions().shape, (1, 3))
+ data = picking[1].getData()
+ self.assertEqual(len(data), 1)
+ self.assertTrue(numpy.array_equal(
+ data,
+ refData[picking[1].getIndices()]))
+
+ # Picking outside data
+ picking = list(self.widget.pickItems(1, 1))
+ self.assertEqual(len(picking), 0)
+
+ self.widget.clearItems()
+
+ def testPickMesh(self):
+ """Test picking of Mesh items"""
+
+ triangles = items.Mesh()
+ triangles.setData(
+ position=((0, 0, 0), (1, 0, 0), (1, 1, 0),
+ (0, 0, 0), (1, 1, 0), (0, 1, 0)),
+ color=(1, 0, 0, 1),
+ mode='triangles')
+ triangleStrip = items.Mesh()
+ triangleStrip.setData(
+ position=(((1, 0, 0), (0, 0, 0), (1, 1, 0), (0, 1, 0))),
+ color=(0, 1, 0, 1),
+ mode='triangle_strip')
+ triangleFan = items.Mesh()
+ triangleFan.setData(
+ position=((0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0)),
+ color=(0, 0, 1, 1),
+ mode='fan')
+
+ for item in (triangles, triangleStrip, triangleFan):
+ with self.subTest(mode=item.getDrawMode()):
+ # Add item
+ self.widget.clearItems()
+ self.widget.addItem(item)
+ self.widget.resetZoom('front')
+ self.qapp.processEvents()
+
+ # Picking on data (at widget center)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+
+ self.assertEqual(len(picking), 1)
+ self.assertIs(picking[0].getItem(), item)
+ nbPos = len(picking[0].getPositions())
+ data = picking[0].getData()
+ self.assertEqual(nbPos, len(data))
+ self.assertTrue(numpy.array_equal(
+ data,
+ item.getPositionData()[picking[0].getIndices()]))
+
+ # Picking outside data
+ picking = list(self.widget.pickItems(1, 1))
+ self.assertEqual(len(picking), 0)
+
+ def testPickMeshWithIndices(self):
+ """Test picking of Mesh items defined by indices"""
+
+ triangles = items.Mesh()
+ triangles.setData(
+ position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
+ color=(1, 0, 0, 1),
+ indices=numpy.array( # dummy triangles and square
+ (0, 0, 1, 0, 1, 2, 1, 2, 3), dtype=numpy.uint8),
+ mode='triangles')
+ triangleStrip = items.Mesh()
+ triangleStrip.setData(
+ position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
+ color=(0, 1, 0, 1),
+ indices=numpy.array( # dummy triangles and square
+ (1, 0, 0, 1, 2, 3), dtype=numpy.uint8),
+ mode='triangle_strip')
+ triangleFan = items.Mesh()
+ triangleFan.setData(
+ position=((0, 0, 0), (1, 0, 0), (0, 1, 0), (1, 1, 0)),
+ color=(0, 0, 1, 1),
+ indices=numpy.array( # dummy triangle, square, dummy
+ (1, 1, 0, 2, 3, 3), dtype=numpy.uint8),
+ mode='fan')
+
+ for item in (triangles, triangleStrip, triangleFan):
+ with self.subTest(mode=item.getDrawMode()):
+ # Add item
+ self.widget.clearItems()
+ self.widget.addItem(item)
+ self.widget.resetZoom('front')
+ self.qapp.processEvents()
+
+ # Picking on data (at widget center)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+
+ self.assertEqual(len(picking), 1)
+ self.assertIs(picking[0].getItem(), item)
+ nbPos = len(picking[0].getPositions())
+ data = picking[0].getData()
+ self.assertEqual(nbPos, len(data))
+ self.assertTrue(numpy.array_equal(
+ data,
+ item.getPositionData()[picking[0].getIndices()]))
+
+ # Picking outside data
+ picking = list(self.widget.pickItems(1, 1))
+ self.assertEqual(len(picking), 0)
+
+ def testPickCylindricalMesh(self):
+ """Test picking of Box, Cylinder and Hexagon items"""
+
+ positions = numpy.array(((0., 0., 0.), (1., 1., 0.), (2., 2., 0.)))
+ box = items.Box()
+ box.setData(position=positions)
+ cylinder = items.Cylinder()
+ cylinder.setData(position=positions)
+ hexagon = items.Hexagon()
+ hexagon.setData(position=positions)
+
+ for item in (box, cylinder, hexagon):
+ with self.subTest(item=item.__class__.__name__):
+ # Add item
+ self.widget.clearItems()
+ self.widget.addItem(item)
+ self.widget.resetZoom('front')
+ self.qapp.processEvents()
+
+ # Picking on data (at widget center)
+ picking = list(self.widget.pickItems(*self._widgetCenter()))
+
+ self.assertEqual(len(picking), 1)
+ self.assertIs(picking[0].getItem(), item)
+ nbPos = len(picking[0].getPositions())
+ data = picking[0].getData()
+ print(item.__class__.__name__, [positions[1]], data)
+ self.assertTrue(numpy.all(numpy.equal(positions[1], data)))
+ self.assertEqual(nbPos, len(data))
+ self.assertTrue(numpy.array_equal(
+ data,
+ item.getPosition()[picking[0].getIndices()]))
+
+ # Picking outside data
+ picking = list(self.widget.pickItems(1, 1))
+ self.assertEqual(len(picking), 0)
diff --git a/src/silx/gui/plot3d/test/testSceneWindow.py b/src/silx/gui/plot3d/test/testSceneWindow.py
new file mode 100644
index 0000000..6b61335
--- /dev/null
+++ b/src/silx/gui/plot3d/test/testSceneWindow.py
@@ -0,0 +1,233 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019-2021 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# ###########################################################################*/
+"""Test SceneWindow"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "22/03/2019"
+
+
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui import qt
+
+from silx.gui.plot3d.SceneWindow import SceneWindow
+from silx.gui.plot3d.items import HeightMapData, HeightMapRGBA
+
+class TestSceneWindow(TestCaseQt, ParametricTestCase):
+ """Tests SceneWidget picking feature"""
+
+ def setUp(self):
+ super(TestSceneWindow, self).setUp()
+ self.window = SceneWindow()
+ self.window.show()
+ self.qWaitForWindowExposed(self.window)
+
+ def tearDown(self):
+ self.qapp.processEvents()
+ self.window.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.window.close()
+ del self.window
+ super(TestSceneWindow, self).tearDown()
+
+ def testAdd(self):
+ """Test add basic scene primitive"""
+ sceneWidget = self.window.getSceneWidget()
+ items = []
+
+ # RGB image
+ image = sceneWidget.addImage(numpy.random.random(
+ 10*10*3).astype(numpy.float32).reshape(10, 10, 3))
+ image.setLabel('RGB image')
+ items.append(image)
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+
+ # Data image
+ image = sceneWidget.addImage(
+ numpy.arange(100, dtype=numpy.float32).reshape(10, 10))
+ image.setTranslation(10.)
+ items.append(image)
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+
+ # 2D scatter
+ scatter = sceneWidget.add2DScatter(
+ *numpy.random.random(3000).astype(numpy.float32).reshape(3, -1),
+ index=0)
+ scatter.setTranslation(0, 10)
+ scatter.setScale(10, 10, 10)
+ items.insert(0, scatter)
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+
+ # 3D scatter
+ scatter = sceneWidget.add3DScatter(
+ *numpy.random.random(4000).astype(numpy.float32).reshape(4, -1))
+ scatter.setTranslation(10, 10)
+ scatter.setScale(10, 10, 10)
+ items.append(scatter)
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+
+ # 3D array of float
+ volume = sceneWidget.addVolume(
+ numpy.arange(10**3, dtype=numpy.float32).reshape(10, 10, 10))
+ volume.setTranslation(0, 0, 10)
+ volume.setRotation(45, (0, 0, 1))
+ volume.addIsosurface(500, 'red')
+ volume.getCutPlanes()[0].getColormap().setName('viridis')
+ items.append(volume)
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+
+ # 3D array of complex
+ volume = sceneWidget.addVolume(
+ numpy.arange(10**3).reshape(10, 10, 10).astype(numpy.complex64))
+ volume.setTranslation(10, 0, 10)
+ volume.setRotation(45, (0, 0, 1))
+ volume.setComplexMode(volume.ComplexMode.REAL)
+ volume.addIsosurface(500, (1., 0., 0., .5))
+ items.append(volume)
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+
+ sceneWidget.resetZoom('front')
+ self.qapp.processEvents()
+
+ def testHeightMap(self):
+ """Test height map items"""
+ sceneWidget = self.window.getSceneWidget()
+
+ height = numpy.arange(10000).reshape(100, 100) /100.
+
+ for shape in ((100, 100), (4, 5), (150, 20), (110, 110)):
+ with self.subTest(shape=shape):
+ items = []
+
+ # Colormapped data height map
+ data = numpy.arange(numpy.prod(shape)).astype(numpy.float32).reshape(shape)
+
+ heightmap = HeightMapData()
+ heightmap.setData(height)
+ heightmap.setColormappedData(data)
+ heightmap.getColormap().setName('viridis')
+ items.append(heightmap)
+ sceneWidget.addItem(heightmap)
+
+ # RGBA height map
+ colors = numpy.zeros(shape + (3,), dtype=numpy.float32)
+ colors[:, :, 1] = numpy.random.random(shape)
+
+ heightmap = HeightMapRGBA()
+ heightmap.setData(height)
+ heightmap.setColorData(colors)
+ heightmap.setTranslation(100., 0., 0.)
+ items.append(heightmap)
+ sceneWidget.addItem(heightmap)
+
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+ sceneWidget.resetZoom('front')
+ self.qapp.processEvents()
+ sceneWidget.clearItems()
+
+ def testChangeContent(self):
+ """Test add/remove/clear items"""
+ sceneWidget = self.window.getSceneWidget()
+ items = []
+
+ # Add 2 images
+ image = numpy.arange(100, dtype=numpy.float32).reshape(10, 10)
+ items.append(sceneWidget.addImage(image))
+ items.append(sceneWidget.addImage(image))
+ self.qapp.processEvents()
+ self.assertEqual(sceneWidget.getItems(), tuple(items))
+
+ # Clear
+ sceneWidget.clearItems()
+ self.qapp.processEvents()
+ self.assertEqual(sceneWidget.getItems(), ())
+
+ # Add 2 images and remove first one
+ image = numpy.arange(100, dtype=numpy.float32).reshape(10, 10)
+ sceneWidget.addImage(image)
+ items = (sceneWidget.addImage(image),)
+ self.qapp.processEvents()
+
+ sceneWidget.removeItem(sceneWidget.getItems()[0])
+ self.qapp.processEvents()
+ self.assertEqual(sceneWidget.getItems(), items)
+
+ def testColors(self):
+ """Test setting scene colors"""
+ sceneWidget = self.window.getSceneWidget()
+
+ color = qt.QColor(128, 128, 128)
+ sceneWidget.setBackgroundColor(color)
+ self.assertEqual(sceneWidget.getBackgroundColor(), color)
+
+ color = qt.QColor(0, 0, 0)
+ sceneWidget.setForegroundColor(color)
+ self.assertEqual(sceneWidget.getForegroundColor(), color)
+
+ color = qt.QColor(255, 0, 0)
+ sceneWidget.setTextColor(color)
+ self.assertEqual(sceneWidget.getTextColor(), color)
+
+ color = qt.QColor(0, 255, 0)
+ sceneWidget.setHighlightColor(color)
+ self.assertEqual(sceneWidget.getHighlightColor(), color)
+
+ self.qapp.processEvents()
+
+ def testInteractiveMode(self):
+ """Test changing interactive mode"""
+ sceneWidget = self.window.getSceneWidget()
+ center = numpy.array((sceneWidget.width() //2, sceneWidget.height() // 2))
+
+ self.mouseMove(sceneWidget, pos=center)
+ self.mouseClick(sceneWidget, qt.Qt.LeftButton, pos=center)
+
+ volume = sceneWidget.addVolume(
+ numpy.arange(10**3).astype(numpy.float32).reshape(10, 10, 10))
+ sceneWidget.selection().setCurrentItem( volume.getCutPlanes()[0])
+ sceneWidget.resetZoom('side')
+
+ for mode in (None, 'rotate', 'pan', 'panSelectedPlane'):
+ with self.subTest(mode=mode):
+ sceneWidget.setInteractiveMode(mode)
+ self.qapp.processEvents()
+ self.assertEqual(sceneWidget.getInteractiveMode(), mode)
+
+ self.mouseMove(sceneWidget, pos=center)
+ self.mousePress(sceneWidget, qt.Qt.LeftButton, pos=center)
+ self.mouseMove(sceneWidget, pos=center-10)
+ self.mouseMove(sceneWidget, pos=center-20)
+ self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center-20)
+
+ self.keyPress(sceneWidget, qt.Qt.Key_Control)
+ self.mouseMove(sceneWidget, pos=center)
+ self.mousePress(sceneWidget, qt.Qt.LeftButton, pos=center)
+ self.mouseMove(sceneWidget, pos=center-10)
+ self.mouseMove(sceneWidget, pos=center-20)
+ self.mouseRelease(sceneWidget, qt.Qt.LeftButton, pos=center-20)
+ self.keyRelease(sceneWidget, qt.Qt.Key_Control)
diff --git a/src/silx/gui/plot3d/test/testStatsWidget.py b/src/silx/gui/plot3d/test/testStatsWidget.py
new file mode 100644
index 0000000..d452eb5
--- /dev/null
+++ b/src/silx/gui/plot3d/test/testStatsWidget.py
@@ -0,0 +1,201 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# ###########################################################################*/
+"""Test silx.gui.plot.StatsWidget with SceneWidget and ScalarFieldView"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "25/01/2019"
+
+
+import unittest
+
+import numpy
+
+from silx.utils.testutils import ParametricTestCase
+from silx.gui.utils.testutils import TestCaseQt
+from silx.gui.plot.stats.stats import Stats
+from silx.gui import qt
+
+from silx.gui.plot.StatsWidget import BasicStatsWidget
+
+from silx.gui.plot3d.ScalarFieldView import ScalarFieldView
+from silx.gui.plot3d.SceneWidget import SceneWidget, items
+
+
+class TestSceneWidget(TestCaseQt, ParametricTestCase):
+ """Tests StatsWidget combined with SceneWidget"""
+
+ def setUp(self):
+ super(TestSceneWidget, self).setUp()
+ self.sceneWidget = SceneWidget()
+ self.sceneWidget.resize(300, 300)
+ self.sceneWidget.show()
+ self.statsWidget = BasicStatsWidget()
+ self.statsWidget.setPlot(self.sceneWidget)
+ # self.qWaitForWindowExposed(self.sceneWidget)
+
+ def tearDown(self):
+ Stats._getContext.cache_clear()
+ self.qapp.processEvents()
+ self.sceneWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.sceneWidget.close()
+ del self.sceneWidget
+ self.statsWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.statsWidget.close()
+ del self.statsWidget
+ super(TestSceneWidget, self).tearDown()
+
+ def test(self):
+ """Test StatsWidget with SceneWidget"""
+ # Prepare scene
+
+ # Data image
+ image = self.sceneWidget.addImage(numpy.arange(100).reshape(10, 10))
+ image.setLabel('Image')
+ # RGB image
+ imageRGB = self.sceneWidget.addImage(
+ numpy.arange(300, dtype=numpy.uint8).reshape(10, 10, 3))
+ imageRGB.setLabel('RGB Image')
+ # 2D scatter
+ data = numpy.arange(100)
+ scatter2D = self.sceneWidget.add2DScatter(x=data, y=data, value=data)
+ scatter2D.setLabel('2D Scatter')
+ # 3D scatter
+ scatter3D = self.sceneWidget.add3DScatter(x=data, y=data, z=data, value=data)
+ scatter3D.setLabel('3D Scatter')
+ # Add a group
+ group = items.GroupItem()
+ self.sceneWidget.addItem(group)
+ # 3D scalar field
+ data = numpy.arange(64**3).reshape(64, 64, 64)
+ scalarField = items.ScalarField3D()
+ scalarField.setData(data, copy=False)
+ scalarField.setLabel('3D Scalar field')
+ group.addItem(scalarField)
+
+ statsTable = self.statsWidget._getStatsTable()
+
+ # Test selection only
+ self.statsWidget.setDisplayOnlyActiveItem(True)
+ self.assertEqual(statsTable.rowCount(), 0)
+
+ self.sceneWidget.selection().setCurrentItem(group)
+ self.assertEqual(statsTable.rowCount(), 0)
+
+ for item in (image, scatter2D, scatter3D, scalarField):
+ with self.subTest('selection only', item=item.getLabel()):
+ self.sceneWidget.selection().setCurrentItem(item)
+ self.assertEqual(statsTable.rowCount(), 1)
+ self._checkItem(item)
+
+ # Test all data
+ self.statsWidget.setDisplayOnlyActiveItem(False)
+ self.assertEqual(statsTable.rowCount(), 4)
+
+ for item in (image, scatter2D, scatter3D, scalarField):
+ with self.subTest('all items', item=item.getLabel()):
+ self._checkItem(item)
+
+ def _checkItem(self, item):
+ """Check that item is in StatsTable and that stats are OK
+
+ :param silx.gui.plot3d.items.Item3D item:
+ """
+ if isinstance(item, (items.Scatter2D, items.Scatter3D)):
+ data = item.getValueData(copy=False)
+ else:
+ data = item.getData(copy=False)
+
+ statsTable = self.statsWidget._getStatsTable()
+ tableItems = statsTable._itemToTableItems(item)
+ self.assertTrue(len(tableItems) > 0)
+ self.assertEqual(tableItems['legend'].text(), item.getLabel())
+ self.assertEqual(float(tableItems['min'].text()), numpy.min(data))
+ self.assertEqual(float(tableItems['max'].text()), numpy.max(data))
+ # TODO
+
+
+class TestScalarFieldView(TestCaseQt):
+ """Tests StatsWidget combined with ScalarFieldView"""
+
+ def setUp(self):
+ super(TestScalarFieldView, self).setUp()
+ self.scalarFieldView = ScalarFieldView()
+ self.scalarFieldView.resize(300, 300)
+ self.scalarFieldView.show()
+ self.statsWidget = BasicStatsWidget()
+ self.statsWidget.setPlot(self.scalarFieldView)
+ # self.qWaitForWindowExposed(self.sceneWidget)
+
+ def tearDown(self):
+ Stats._getContext.cache_clear()
+ self.qapp.processEvents()
+ self.scalarFieldView.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.scalarFieldView.close()
+ del self.scalarFieldView
+ self.statsWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
+ self.statsWidget.close()
+ del self.statsWidget
+ super(TestScalarFieldView, self).tearDown()
+
+ def _getTextFor(self, row, name):
+ """Returns text in table at given row for column name
+
+ :param int row: Row number in the table
+ :param str name: Column id
+ :rtype: Union[str,None]
+ """
+ statsTable = self.statsWidget._getStatsTable()
+
+ for column in range(statsTable.columnCount()):
+ headerItem = statsTable.horizontalHeaderItem(column)
+ if headerItem.data(qt.Qt.UserRole) == name:
+ tableItem = statsTable.item(row, column)
+ return tableItem.text()
+
+ return None
+
+ def test(self):
+ """Test StatsWidget with ScalarFieldView"""
+ data = numpy.arange(64**3, dtype=numpy.float64).reshape(64, 64, 64)
+ self.scalarFieldView.setData(data)
+
+ statsTable = self.statsWidget._getStatsTable()
+
+ # Test selection only
+ self.statsWidget.setDisplayOnlyActiveItem(True)
+ self.assertEqual(statsTable.rowCount(), 1)
+
+ # Test all data
+ self.statsWidget.setDisplayOnlyActiveItem(False)
+ self.assertEqual(statsTable.rowCount(), 1)
+
+ for column in range(statsTable.columnCount()):
+ self.assertEqual(float(self._getTextFor(0, 'min')), numpy.min(data))
+ self.assertEqual(float(self._getTextFor(0, 'max')), numpy.max(data))
+ sum_ = numpy.sum(data)
+ comz = numpy.sum(numpy.arange(data.shape[0]) * numpy.sum(data, axis=(1, 2))) / sum_
+ comy = numpy.sum(numpy.arange(data.shape[1]) * numpy.sum(data, axis=(0, 2))) / sum_
+ comx = numpy.sum(numpy.arange(data.shape[2]) * numpy.sum(data, axis=(0, 1))) / sum_
+ self.assertEqual(self._getTextFor(0, 'COM'), str((comx, comy, comz)))