summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d')
-rw-r--r--silx/gui/plot3d/ParamTreeView.py546
-rw-r--r--silx/gui/plot3d/Plot3DWidget.py460
-rw-r--r--silx/gui/plot3d/Plot3DWindow.py88
-rw-r--r--silx/gui/plot3d/SFViewParamTree.py1817
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py1552
-rw-r--r--silx/gui/plot3d/SceneWidget.py687
-rw-r--r--silx/gui/plot3d/SceneWindow.py219
-rw-r--r--silx/gui/plot3d/__init__.py40
-rw-r--r--silx/gui/plot3d/_model/__init__.py35
-rw-r--r--silx/gui/plot3d/_model/core.py372
-rw-r--r--silx/gui/plot3d/_model/items.py1760
-rw-r--r--silx/gui/plot3d/_model/model.py184
-rw-r--r--silx/gui/plot3d/actions/Plot3DAction.py71
-rw-r--r--silx/gui/plot3d/actions/__init__.py34
-rw-r--r--silx/gui/plot3d/actions/io.py336
-rw-r--r--silx/gui/plot3d/actions/mode.py178
-rw-r--r--silx/gui/plot3d/actions/viewpoint.py231
-rw-r--r--silx/gui/plot3d/items/__init__.py43
-rw-r--r--silx/gui/plot3d/items/_pick.py265
-rw-r--r--silx/gui/plot3d/items/clipplane.py136
-rw-r--r--silx/gui/plot3d/items/core.py779
-rw-r--r--silx/gui/plot3d/items/image.py425
-rw-r--r--silx/gui/plot3d/items/mesh.py792
-rw-r--r--silx/gui/plot3d/items/mixins.py288
-rw-r--r--silx/gui/plot3d/items/scatter.py617
-rw-r--r--silx/gui/plot3d/items/volume.py886
-rw-r--r--silx/gui/plot3d/scene/__init__.py34
-rw-r--r--silx/gui/plot3d/scene/axes.py258
-rw-r--r--silx/gui/plot3d/scene/camera.py353
-rw-r--r--silx/gui/plot3d/scene/core.py343
-rw-r--r--silx/gui/plot3d/scene/cutplane.py390
-rw-r--r--silx/gui/plot3d/scene/event.py225
-rw-r--r--silx/gui/plot3d/scene/function.py654
-rw-r--r--silx/gui/plot3d/scene/interaction.py701
-rw-r--r--silx/gui/plot3d/scene/primitives.py2524
-rw-r--r--silx/gui/plot3d/scene/test/__init__.py43
-rw-r--r--silx/gui/plot3d/scene/test/test_transform.py91
-rw-r--r--silx/gui/plot3d/scene/test/test_utils.py275
-rw-r--r--silx/gui/plot3d/scene/text.py535
-rw-r--r--silx/gui/plot3d/scene/transform.py1027
-rw-r--r--silx/gui/plot3d/scene/utils.py662
-rw-r--r--silx/gui/plot3d/scene/viewport.py603
-rw-r--r--silx/gui/plot3d/scene/window.py430
-rw-r--r--silx/gui/plot3d/setup.py50
-rw-r--r--silx/gui/plot3d/test/__init__.py75
-rw-r--r--silx/gui/plot3d/test/testGL.py84
-rw-r--r--silx/gui/plot3d/test/testScalarFieldView.py139
-rw-r--r--silx/gui/plot3d/test/testSceneWidget.py84
-rw-r--r--silx/gui/plot3d/test/testSceneWidgetPicking.py326
-rw-r--r--silx/gui/plot3d/test/testSceneWindow.py245
-rw-r--r--silx/gui/plot3d/test/testStatsWidget.py216
-rw-r--r--silx/gui/plot3d/tools/GroupPropertiesWidget.py202
-rw-r--r--silx/gui/plot3d/tools/PositionInfoWidget.py219
-rw-r--r--silx/gui/plot3d/tools/ViewpointTools.py84
-rw-r--r--silx/gui/plot3d/tools/__init__.py34
-rw-r--r--silx/gui/plot3d/tools/test/__init__.py41
-rw-r--r--silx/gui/plot3d/tools/test/testPositionInfoWidget.py101
-rw-r--r--silx/gui/plot3d/tools/toolbars.py209
-rw-r--r--silx/gui/plot3d/utils/__init__.py28
-rw-r--r--silx/gui/plot3d/utils/mng.py121
60 files changed, 0 insertions, 24247 deletions
diff --git a/silx/gui/plot3d/ParamTreeView.py b/silx/gui/plot3d/ParamTreeView.py
deleted file mode 100644
index 8cf2b90..0000000
--- a/silx/gui/plot3d/ParamTreeView.py
+++ /dev/null
@@ -1,546 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""
-This module provides a :class:`QTreeView` dedicated to display plot3d models.
-
-This module contains:
-- :class:`ParamTreeView`: A QTreeView specific for plot3d parameters and scene.
-- :class:`ParameterTreeDelegate`: The delegate for :class:`ParamTreeView`.
-- A set of specific editors used by :class:`ParameterTreeDelegate`:
- :class:`FloatEditor`, :class:`Vector3DEditor`,
- :class:`Vector4DEditor`, :class:`IntSliderEditor`, :class:`BooleanEditor`
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "05/12/2017"
-
-
-import numbers
-import sys
-
-import six
-
-from .. import qt
-from ..widgets.FloatEdit import FloatEdit as _FloatEdit
-from ._model import visitQAbstractItemModel
-
-
-class FloatEditor(_FloatEdit):
- """Editor widget for float.
-
- :param parent: The widget's parent
- :param float value: The initial editor value
- """
-
- valueChanged = qt.Signal(float)
- """Signal emitted when the float value has changed"""
-
- def __init__(self, parent=None, value=None):
- super(FloatEditor, self).__init__(parent, value)
- self.setAlignment(qt.Qt.AlignLeft)
- self.editingFinished.connect(self._emit)
-
- def _emit(self):
- self.valueChanged.emit(self.value)
-
- value = qt.Property(float,
- fget=_FloatEdit.value,
- fset=_FloatEdit.setValue,
- user=True,
- notify=valueChanged)
- """Qt user property of the float value this widget edits"""
-
-
-class Vector3DEditor(qt.QWidget):
- """Editor widget for QVector3D.
-
- :param parent: The widget's parent
- :param flags: The widgets's flags
- """
-
- valueChanged = qt.Signal(qt.QVector3D)
- """Signal emitted when the QVector3D value has changed"""
-
- def __init__(self, parent=None, flags=qt.Qt.Widget):
- super(Vector3DEditor, self).__init__(parent, flags)
- layout = qt.QHBoxLayout(self)
- # layout.setSpacing(0)
- layout.setContentsMargins(0, 0, 0, 0)
- self.setLayout(layout)
- self._xEdit = _FloatEdit(parent=self, value=0.)
- self._xEdit.setAlignment(qt.Qt.AlignLeft)
- # self._xEdit.editingFinished.connect(self._emit)
- self._yEdit = _FloatEdit(parent=self, value=0.)
- self._yEdit.setAlignment(qt.Qt.AlignLeft)
- # self._yEdit.editingFinished.connect(self._emit)
- self._zEdit = _FloatEdit(parent=self, value=0.)
- self._zEdit.setAlignment(qt.Qt.AlignLeft)
- # self._zEdit.editingFinished.connect(self._emit)
- layout.addWidget(qt.QLabel('x:'))
- layout.addWidget(self._xEdit)
- layout.addWidget(qt.QLabel('y:'))
- layout.addWidget(self._yEdit)
- layout.addWidget(qt.QLabel('z:'))
- layout.addWidget(self._zEdit)
- layout.addStretch(1)
-
- def _emit(self):
- vector = self.value
- self.valueChanged.emit(vector)
-
- def getValue(self):
- """Returns the QVector3D value of this widget
-
- :rtype: QVector3D
- """
- return qt.QVector3D(
- self._xEdit.value(), self._yEdit.value(), self._zEdit.value())
-
- def setValue(self, value):
- """Set the QVector3D value
-
- :param QVector3D value: The new value
- """
- self._xEdit.setValue(value.x())
- self._yEdit.setValue(value.y())
- self._zEdit.setValue(value.z())
- self.valueChanged.emit(value)
-
- value = qt.Property(qt.QVector3D,
- fget=getValue,
- fset=setValue,
- user=True,
- notify=valueChanged)
- """Qt user property of the QVector3D value this widget edits"""
-
-
-class Vector4DEditor(qt.QWidget):
- """Editor widget for QVector4D.
-
- :param parent: The widget's parent
- :param flags: The widgets's flags
- """
-
- valueChanged = qt.Signal(qt.QVector4D)
- """Signal emitted when the QVector4D value has changed"""
-
- def __init__(self, parent=None, flags=qt.Qt.Widget):
- super(Vector4DEditor, self).__init__(parent, flags)
- layout = qt.QHBoxLayout(self)
- # layout.setSpacing(0)
- layout.setContentsMargins(0, 0, 0, 0)
- self.setLayout(layout)
- self._xEdit = _FloatEdit(parent=self, value=0.)
- self._xEdit.setAlignment(qt.Qt.AlignLeft)
- # self._xEdit.editingFinished.connect(self._emit)
- self._yEdit = _FloatEdit(parent=self, value=0.)
- self._yEdit.setAlignment(qt.Qt.AlignLeft)
- # self._yEdit.editingFinished.connect(self._emit)
- self._zEdit = _FloatEdit(parent=self, value=0.)
- self._zEdit.setAlignment(qt.Qt.AlignLeft)
- # self._zEdit.editingFinished.connect(self._emit)
- self._wEdit = _FloatEdit(parent=self, value=0.)
- self._wEdit.setAlignment(qt.Qt.AlignLeft)
- # self._wEdit.editingFinished.connect(self._emit)
- layout.addWidget(qt.QLabel('x:'))
- layout.addWidget(self._xEdit)
- layout.addWidget(qt.QLabel('y:'))
- layout.addWidget(self._yEdit)
- layout.addWidget(qt.QLabel('z:'))
- layout.addWidget(self._zEdit)
- layout.addWidget(qt.QLabel('w:'))
- layout.addWidget(self._wEdit)
- layout.addStretch(1)
-
- def _emit(self):
- vector = self.value
- self.valueChanged.emit(vector)
-
- def getValue(self):
- """Returns the QVector4D value of this widget
-
- :rtype: QVector4D
- """
- return qt.QVector4D(self._xEdit.value(), self._yEdit.value(),
- self._zEdit.value(), self._wEdit.value())
-
- def setValue(self, value):
- """Set the QVector4D value
-
- :param QVector4D value: The new value
- """
- self._xEdit.setValue(value.x())
- self._yEdit.setValue(value.y())
- self._zEdit.setValue(value.z())
- self._wEdit.setValue(value.w())
- self.valueChanged.emit(value)
-
- value = qt.Property(qt.QVector4D,
- fget=getValue,
- fset=setValue,
- user=True,
- notify=valueChanged)
- """Qt user property of the QVector4D value this widget edits"""
-
-
-class IntSliderEditor(qt.QSlider):
- """Slider editor widget for integer.
-
- Note: Tracking is disabled.
-
- :param parent: The widget's parent
- """
-
- def __init__(self, parent=None):
- super(IntSliderEditor, self).__init__(parent)
- self.setOrientation(qt.Qt.Horizontal)
- self.setSingleStep(1)
- self.setRange(0, 255)
- self.setValue(0)
-
-
-class BooleanEditor(qt.QCheckBox):
- """Checkbox editor for bool.
-
- This is a QCheckBox with white background.
-
- :param parent: The widget's parent
- """
-
- def __init__(self, parent=None):
- super(BooleanEditor, self).__init__(parent)
- self.setStyleSheet("background: white;")
-
-
-class ParameterTreeDelegate(qt.QStyledItemDelegate):
- """TreeView delegate specific to plot3d scene and object parameter tree.
-
- It provides additional editors.
-
- :param parent: Delegate's parent
- """
-
- EDITORS = {
- bool: BooleanEditor,
- float: FloatEditor,
- qt.QVector3D: Vector3DEditor,
- qt.QVector4D: Vector4DEditor,
- }
- """Specific editors for different type of data"""
-
- def __init__(self, parent=None):
- super(ParameterTreeDelegate, self).__init__(parent)
-
- def _fixVariant(self, data):
- """Fix PyQt4 zero vectors being stored as QPyNullVariant.
-
- :param data: Data retrieved from the model
- :return: Corresponding object
- """
- if qt.BINDING == 'PyQt4' and isinstance(data, qt.QPyNullVariant):
- typeName = data.typeName()
- if typeName == 'QVector3D':
- data = qt.QVector3D()
- elif typeName == 'QVector4D':
- data = qt.QVector4D()
- return data
-
- def paint(self, painter, option, index):
- """See :meth:`QStyledItemDelegate.paint`"""
- data = index.data(qt.Qt.DisplayRole)
- data = self._fixVariant(data)
-
- if isinstance(data, (qt.QVector3D, qt.QVector4D)):
- if isinstance(data, qt.QVector3D):
- text = '(x: %g; y: %g; z: %g)' % (data.x(), data.y(), data.z())
- elif isinstance(data, qt.QVector4D):
- text = '(%g; %g; %g; %g)' % (data.x(), data.y(), data.z(), data.w())
- else:
- text = ''
-
- painter.save()
- painter.setRenderHint(qt.QPainter.Antialiasing, True)
-
- # Select palette color group
- colorGroup = qt.QPalette.Inactive
- if option.state & qt.QStyle.State_Active:
- colorGroup = qt.QPalette.Active
- if not option.state & qt.QStyle.State_Enabled:
- colorGroup = qt.QPalette.Disabled
-
- # Draw background if selected
- if option.state & qt.QStyle.State_Selected:
- brush = option.palette.brush(colorGroup,
- qt.QPalette.Highlight)
- painter.fillRect(option.rect, brush)
-
- # Draw text
- if option.state & qt.QStyle.State_Selected:
- colorRole = qt.QPalette.HighlightedText
- else:
- colorRole = qt.QPalette.WindowText
- color = option.palette.color(colorGroup, colorRole)
- painter.setPen(qt.QPen(color))
- painter.drawText(option.rect, qt.Qt.AlignLeft, text)
-
- painter.restore()
-
- # The following commented code does the same as QPainter based code
- # but it does not work with PySide
- # self.initStyleOption(option, index)
- # option.text = text
- # widget = option.widget
- # style = qt.QApplication.style() if not widget else widget.style()
- # style.drawControl(qt.QStyle.CE_ItemViewItem, option, painter, widget)
-
- else:
- super(ParameterTreeDelegate, self).paint(painter, option, index)
-
- def _commit(self, *args):
- """Commit data to the model from editors"""
- sender = self.sender()
- self.commitData.emit(sender)
-
- def editorEvent(self, event, model, option, index):
- """See :meth:`QStyledItemDelegate.editorEvent`"""
- if (event.type() == qt.QEvent.MouseButtonPress and
- isinstance(index.data(qt.Qt.EditRole), qt.QColor)):
- initialColor = index.data(qt.Qt.EditRole)
-
- def callback(color):
- theModel = index.model()
- theModel.setData(index, color, qt.Qt.EditRole)
-
- dialog = qt.QColorDialog(self.parent())
- # dialog.setOption(qt.QColorDialog.ShowAlphaChannel, True)
- if sys.platform == 'darwin':
- # Use of native color dialog on macos might cause problems
- dialog.setOption(qt.QColorDialog.DontUseNativeDialog, True)
- dialog.setCurrentColor(initialColor)
- dialog.currentColorChanged.connect(callback)
- if dialog.exec_() == qt.QDialog.Rejected:
- # Reset color
- dialog.setCurrentColor(initialColor)
-
- return True
- else:
- return super(ParameterTreeDelegate, self).editorEvent(
- event, model, option, index)
-
- def createEditor(self, parent, option, index):
- """See :meth:`QStyledItemDelegate.createEditor`"""
- data = index.data(qt.Qt.EditRole)
- data = self._fixVariant(data)
- editorHint = index.data(qt.Qt.UserRole)
-
- if callable(editorHint):
- editor = editorHint()
- assert isinstance(editor, qt.QWidget)
- editor.setParent(parent)
-
- elif isinstance(data, numbers.Number) and editorHint is not None:
- # Use a slider
- editor = IntSliderEditor(parent)
- range_ = editorHint
- editor.setRange(*range_)
- editor.sliderReleased.connect(self._commit)
-
- elif isinstance(data, six.string_types) and editorHint is not None:
- # Use a combo box
- editor = qt.QComboBox(parent)
- if data not in editorHint:
- editor.addItem(data)
- editor.addItems(editorHint)
-
- index = editor.findText(data)
- editor.setCurrentIndex(index)
-
- editor.currentIndexChanged.connect(self._commit)
-
- else:
- # Handle overridden editors from Python
- # Mimic Qt C++ implementation
- for type_, editorClass in self.EDITORS.items():
- if isinstance(data, type_):
- editor = editorClass(parent)
- metaObject = editor.metaObject()
- userProperty = metaObject.userProperty()
- if userProperty.isValid() and userProperty.hasNotifySignal():
- notifySignal = userProperty.notifySignal()
- if hasattr(notifySignal, 'signature'): # Qt4
- signature = notifySignal.signature()
- else:
- signature = notifySignal.methodSignature()
- if qt.BINDING == 'PySide2':
- signature = signature.data()
- else:
- signature = bytes(signature)
-
- if hasattr(signature, 'decode'): # For PySide with python3
- signature = signature.decode('ascii')
- signalName = signature.split('(')[0]
-
- signal = getattr(editor, signalName)
- signal.connect(self._commit)
- break
-
- else: # Default handling for default types
- return super(ParameterTreeDelegate, self).createEditor(
- parent, option, index)
-
- editor.setAutoFillBackground(True)
- return editor
-
- def setModelData(self, editor, model, index):
- """See :meth:`QStyledItemDelegate.setModelData`"""
- if isinstance(editor, tuple(self.EDITORS.values())):
- # Special handling of Python classes
- # Translation of QStyledItemDelegate::setModelData to Python
- # To make it work with Python QVariant wrapping/unwrapping
- name = editor.metaObject().userProperty().name()
- if not name:
- pass # TODO handle the case of missing user property
- if name:
- if hasattr(editor, name):
- value = getattr(editor, name)
- else:
- value = editor.property(name)
- model.setData(index, value, qt.Qt.EditRole)
-
- else:
- super(ParameterTreeDelegate, self).setModelData(editor, model, index)
-
-
-class ParamTreeView(qt.QTreeView):
- """QTreeView specific to handle plot3d scene and object parameters.
-
- It provides additional editors and specific creation of persistent editors.
-
- :param parent: The widget's parent.
- """
-
- def __init__(self, parent=None):
- super(ParamTreeView, self).__init__(parent)
-
- header = self.header()
- header.setMinimumSectionSize(128) # For colormap pixmaps
- if hasattr(header, 'setSectionResizeMode'): # Qt5
- header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
- else: # Qt4
- header.setResizeMode(qt.QHeaderView.ResizeToContents)
-
- delegate = ParameterTreeDelegate()
- self.setItemDelegate(delegate)
-
- self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
- self.setSelectionMode(qt.QAbstractItemView.SingleSelection)
-
- self.expanded.connect(self._expanded)
-
- self.setEditTriggers(qt.QAbstractItemView.CurrentChanged |
- qt.QAbstractItemView.DoubleClicked)
-
- self.__persistentEditors = set()
-
- def _openEditorForIndex(self, index):
- """Check if it has to open a persistent editor for a specific cell.
-
- :param QModelIndex index: The cell index
- """
- if index.flags() & qt.Qt.ItemIsEditable:
- data = index.data(qt.Qt.EditRole)
- editorHint = index.data(qt.Qt.UserRole)
- if (isinstance(data, bool) or
- callable(editorHint) or
- (isinstance(data, numbers.Number) and editorHint)):
- self.openPersistentEditor(index)
- self.__persistentEditors.add(index)
-
- def _openEditors(self, parent=qt.QModelIndex()):
- """Open persistent editors in a subtree starting at parent.
-
- :param QModelIndex parent: The root of the subtree to process.
- """
- model = self.model()
- if model is not None:
- for index in visitQAbstractItemModel(model, parent):
- self._openEditorForIndex(index)
-
- def setModel(self, model):
- """Set the model this TreeView is displaying
-
- :param QAbstractItemModel model:
- """
- super(ParamTreeView, self).setModel(model)
- self._openEditors()
-
- def rowsInserted(self, parent, start, end):
- """See :meth:`QTreeView.rowsInserted`"""
- super(ParamTreeView, self).rowsInserted(parent, start, end)
- model = self.model()
- if model is not None:
- for row in range(start, end+1):
- self._openEditorForIndex(model.index(row, 1, parent))
- self._openEditors(model.index(row, 0, parent))
-
- def _expanded(self, index):
- """Handle QTreeView expanded signal"""
- name = index.data(qt.Qt.DisplayRole)
- if name == 'Transform':
- rotateIndex = self.model().index(1, 0, index)
- self.setExpanded(rotateIndex, True)
-
- def dataChanged(self, topLeft, bottomRight, roles=()):
- """Handle model dataChanged signal eventually closing editors"""
- if roles: # Qt 5
- super(ParamTreeView, self).dataChanged(topLeft, bottomRight, roles)
- else: # Qt4 compatibility
- super(ParamTreeView, self).dataChanged(topLeft, bottomRight)
- if not roles or qt.Qt.UserRole in roles: # Check editorHint update
- for row in range(topLeft.row(), bottomRight.row() + 1):
- for column in range(topLeft.column(), bottomRight.column() + 1):
- index = topLeft.sibling(row, column)
- if index.isValid():
- if self._isPersistentEditorOpen(index):
- self.closePersistentEditor(index)
- self._openEditorForIndex(index)
-
- def _isPersistentEditorOpen(self, index):
- """Returns True if a persistent editor is opened for index
-
- :param QModelIndex index:
- :rtype: bool
- """
- return index in self.__persistentEditors
-
- def selectionCommand(self, index, event=None):
- """Filter out selection of not selectable items"""
- if index.flags() & qt.Qt.ItemIsSelectable:
- return super(ParamTreeView, self).selectionCommand(index, event)
- else:
- return qt.QItemSelectionModel.NoUpdate
diff --git a/silx/gui/plot3d/Plot3DWidget.py b/silx/gui/plot3d/Plot3DWidget.py
deleted file mode 100644
index f512cd8..0000000
--- a/silx/gui/plot3d/Plot3DWidget.py
+++ /dev/null
@@ -1,460 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides a Qt widget embedding an OpenGL scene."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import enum
-import logging
-
-from silx.gui import qt
-from silx.gui.colors import rgba
-from . import actions
-
-from ...utils.enum import Enum as _Enum
-from ..utils.image import convertArrayToQImage
-
-from .. import _glutils as glu
-from .scene import interaction, primitives, transform
-from . import scene
-
-import numpy
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _OverviewViewport(scene.Viewport):
- """A scene displaying the orientation of the data in another scene.
-
- :param Camera camera: The camera to track.
- """
-
- _SIZE = 100
- """Size in pixels of the overview square"""
-
- def __init__(self, camera=None):
- super(_OverviewViewport, self).__init__()
- self.size = self._SIZE, self._SIZE
- self.background = None # Disable clear
-
- self.scene.transforms = [transform.Scale(2.5, 2.5, 2.5)]
-
- # Add a point to draw the background (in a group with depth mask)
- backgroundPoint = primitives.ColorPoints(
- x=0., y=0., z=0.,
- color=(1., 1., 1., 0.5),
- size=self._SIZE)
- backgroundPoint.marker = 'o'
- noDepthGroup = primitives.GroupNoDepth(mask=True, notest=True)
- noDepthGroup.children.append(backgroundPoint)
- self.scene.children.append(noDepthGroup)
-
- axes = primitives.Axes()
- self.scene.children.append(axes)
-
- if camera is not None:
- camera.addListener(self._cameraChanged)
-
- def _cameraChanged(self, source):
- """Listen to camera in other scene for transformation updates.
-
- Sync the overview camera to point in the same direction
- but from a sphere centered on origin.
- """
- position = -12. * source.extrinsic.direction
- self.camera.extrinsic.position = position
-
- self.camera.extrinsic.setOrientation(
- source.extrinsic.direction, source.extrinsic.up)
-
-
-class Plot3DWidget(glu.OpenGLWidget):
- """OpenGL widget with a 3D viewport and an overview."""
-
- sigInteractiveModeChanged = qt.Signal()
- """Signal emitted when the interactive mode has changed
- """
-
- sigStyleChanged = qt.Signal(str)
- """Signal emitted when the style of the scene has changed
-
- It provides the updated property.
- """
-
- sigSceneClicked = qt.Signal(float, float)
- """Signal emitted when the scene is clicked with the left mouse button.
-
- It provides the (x, y) clicked mouse position
- """
-
- @enum.unique
- class FogMode(_Enum):
- """Different mode to render the scene with fog"""
-
- NONE = 'none'
- """No fog effect"""
-
- LINEAR = 'linear'
- """Linear fog through the whole scene"""
-
- def __init__(self, parent=None, f=qt.Qt.WindowFlags()):
- self._firstRender = True
-
- super(Plot3DWidget, self).__init__(
- parent,
- alphaBufferSize=8,
- depthBufferSize=0,
- stencilBufferSize=0,
- version=(2, 1),
- f=f)
-
- self.setAutoFillBackground(False)
- self.setMouseTracking(True)
-
- self.setFocusPolicy(qt.Qt.StrongFocus)
- self._copyAction = actions.io.CopyAction(parent=self, plot3d=self)
- self.addAction(self._copyAction)
-
- self._updating = False # True if an update is requested
-
- # Main viewport
- self.viewport = scene.Viewport()
-
- self._sceneScale = transform.Scale(1., 1., 1.)
- self.viewport.scene.transforms = [self._sceneScale,
- transform.Translate(0., 0., 0.)]
-
- # Overview area
- self.overview = _OverviewViewport(self.viewport.camera)
-
- self.setBackgroundColor((0.2, 0.2, 0.2, 1.))
-
- # Window describing on screen area to render
- self._window = scene.Window(mode='framebuffer')
- self._window.viewports = [self.viewport, self.overview]
- self._window.addListener(self._redraw)
-
- self.eventHandler = None
- self.setInteractiveMode('rotate')
-
- def __clickHandler(self, *args):
- """Handle interaction state machine click"""
- x, y = args[0][:2]
- self.sigSceneClicked.emit(x, y)
-
- def setInteractiveMode(self, mode):
- """Set the interactive mode.
-
- :param str mode: The interactive mode: 'rotate', 'pan' or None
- """
- if mode == self.getInteractiveMode():
- return
-
- if mode is None:
- self.eventHandler = None
-
- elif mode == 'rotate':
- self.eventHandler = interaction.RotateCameraControl(
- self.viewport,
- orbitAroundCenter=False,
- mode='position',
- scaleTransform=self._sceneScale,
- selectCB=self.__clickHandler)
-
- elif mode == 'pan':
- self.eventHandler = interaction.PanCameraControl(
- self.viewport,
- orbitAroundCenter=False,
- mode='position',
- scaleTransform=self._sceneScale,
- selectCB=self.__clickHandler)
-
- elif isinstance(mode, interaction.StateMachine):
- self.eventHandler = mode
-
- else:
- raise ValueError('Unsupported interactive mode %s', str(mode))
-
- if (self.eventHandler is not None and
- qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier):
- self.eventHandler.handleEvent('keyPress', qt.Qt.Key_Control)
-
- self.sigInteractiveModeChanged.emit()
-
- def getInteractiveMode(self):
- """Returns the interactive mode in use.
-
- :rtype: str
- """
- if self.eventHandler is None:
- return None
- if isinstance(self.eventHandler, interaction.RotateCameraControl):
- return 'rotate'
- elif isinstance(self.eventHandler, interaction.PanCameraControl):
- return 'pan'
- else:
- return None
-
- def setProjection(self, projection):
- """Change the projection in use.
-
- :param str projection: In 'perspective', 'orthographic'.
- """
- if projection == 'orthographic':
- projection = transform.Orthographic(size=self.viewport.size)
- elif projection == 'perspective':
- projection = transform.Perspective(fovy=30.,
- size=self.viewport.size)
- else:
- raise RuntimeError('Unsupported projection: %s' % projection)
-
- self.viewport.camera.intrinsic = projection
- self.viewport.resetCamera()
-
- def getProjection(self):
- """Return the current camera projection mode as a str.
-
- See :meth:`setProjection`
- """
- projection = self.viewport.camera.intrinsic
- if isinstance(projection, transform.Orthographic):
- return 'orthographic'
- elif isinstance(projection, transform.Perspective):
- return 'perspective'
- else:
- raise RuntimeError('Unknown projection in use')
-
- def setBackgroundColor(self, color):
- """Set the background color of the OpenGL view.
-
- :param color: RGB color of the isosurface: name, #RRGGBB or RGB values
- :type color:
- QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
- """
- color = rgba(color)
- if color != self.viewport.background:
- self.viewport.background = color
- self.sigStyleChanged.emit('backgroundColor')
-
- def getBackgroundColor(self):
- """Returns the RGBA background color (QColor)."""
- return qt.QColor.fromRgbF(*self.viewport.background)
-
- def setFogMode(self, mode):
- """Set the kind of fog to use for the whole scene.
-
- :param Union[str,FogMode] mode: The mode to use
- :raise ValueError: If mode is not supported
- """
- mode = self.FogMode.from_value(mode)
- if mode != self.getFogMode():
- self.viewport.fog.isOn = mode is self.FogMode.LINEAR
- self.sigStyleChanged.emit('fogMode')
-
- def getFogMode(self):
- """Returns the kind of fog in use
-
- :return: The kind of fog in use
- :rtype: FogMode
- """
- if self.viewport.fog.isOn:
- return self.FogMode.LINEAR
- else:
- return self.FogMode.NONE
-
- def isOrientationIndicatorVisible(self):
- """Returns True if the orientation indicator is displayed.
-
- :rtype: bool
- """
- return self.overview in self._window.viewports
-
- def setOrientationIndicatorVisible(self, visible):
- """Set the orientation indicator visibility.
-
- :param bool visible: True to show
- """
- visible = bool(visible)
- if visible != self.isOrientationIndicatorVisible():
- if visible:
- self._window.viewports = [self.viewport, self.overview]
- else:
- self._window.viewports = [self.viewport]
- self.sigStyleChanged.emit('orientationIndicatorVisible')
-
- def centerScene(self):
- """Position the center of the scene at the center of rotation."""
- self.viewport.resetCamera()
-
- def resetZoom(self, face='front'):
- """Reset the camera position to a default.
-
- :param str face: The direction the camera is looking at:
- side, front, back, top, bottom, right, left.
- Default: front.
- """
- self.viewport.camera.extrinsic.reset(face=face)
- self.centerScene()
-
- def _redraw(self, source=None):
- """Viewport listener to require repaint"""
- if not self._updating:
- self._updating = True # Mark that an update is requested
- self.update() # Queued repaint (i.e., asynchronous)
-
- def sizeHint(self):
- return qt.QSize(400, 300)
-
- def initializeGL(self):
- pass
-
- def paintGL(self):
- # In case paintGL is called by the system and not through _redraw,
- # Mark as updating.
- self._updating = True
-
- # Update near and far planes only if viewport needs refresh
- if self.viewport.dirty:
- self.viewport.adjustCameraDepthExtent()
-
- self._window.render(self.context(), self.getDevicePixelRatio())
-
- if self._firstRender: # TODO remove this ugly hack
- self._firstRender = False
- self.centerScene()
- self._updating = False
-
- def resizeGL(self, width, height):
- width *= self.getDevicePixelRatio()
- height *= self.getDevicePixelRatio()
- self._window.size = width, height
- self.viewport.size = self._window.size
- overviewWidth, overviewHeight = self.overview.size
- self.overview.origin = width - overviewWidth, height - overviewHeight
-
- def grabGL(self):
- """Renders the OpenGL scene into a numpy array
-
- :returns: OpenGL scene RGB rasterization
- :rtype: QImage
- """
- if not self.isValid():
- _logger.error('OpenGL 2.1 not available, cannot save OpenGL image')
- height, width = self._window.shape
- image = numpy.zeros((height, width, 3), dtype=numpy.uint8)
-
- else:
- self.makeCurrent()
- image = self._window.grab(self.context())
-
- return convertArrayToQImage(image)
-
- def wheelEvent(self, event):
- xpixel = event.x() * self.getDevicePixelRatio()
- ypixel = event.y() * self.getDevicePixelRatio()
- if hasattr(event, 'delta'): # Qt4
- angle = event.delta() / 8.
- else: # Qt5
- angle = event.angleDelta().y() / 8.
- event.accept()
-
- if self.eventHandler is not None and angle != 0 and self.isValid():
- self.makeCurrent()
- self.eventHandler.handleEvent('wheel', xpixel, ypixel, angle)
-
- def keyPressEvent(self, event):
- keyCode = event.key()
- # No need to accept QKeyEvent
-
- converter = {
- qt.Qt.Key_Left: 'left',
- qt.Qt.Key_Right: 'right',
- qt.Qt.Key_Up: 'up',
- qt.Qt.Key_Down: 'down'
- }
- direction = converter.get(keyCode, None)
- if direction is not None:
- if event.modifiers() == qt.Qt.ControlModifier:
- self.viewport.camera.rotate(direction)
- elif event.modifiers() == qt.Qt.ShiftModifier:
- self.viewport.moveCamera(direction)
- else:
- self.viewport.orbitCamera(direction)
-
- else:
- if (keyCode == qt.Qt.Key_Control and
- self.eventHandler is not None and
- self.isValid()):
- self.eventHandler.handleEvent('keyPress', keyCode)
-
- # Key not handled, call base class implementation
- super(Plot3DWidget, self).keyPressEvent(event)
-
- def keyReleaseEvent(self, event):
- """Catch Ctrl key release"""
- keyCode = event.key()
- if (keyCode == qt.Qt.Key_Control and
- self.eventHandler is not None and
- self.isValid()):
- self.eventHandler.handleEvent('keyRelease', keyCode)
- super(Plot3DWidget, self).keyReleaseEvent(event)
-
- # Mouse events #
- _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'}
-
- def mousePressEvent(self, event):
- xpixel = event.x() * self.getDevicePixelRatio()
- ypixel = event.y() * self.getDevicePixelRatio()
- btn = self._MOUSE_BTNS[event.button()]
- event.accept()
-
- if self.eventHandler is not None and self.isValid():
- self.makeCurrent()
- self.eventHandler.handleEvent('press', xpixel, ypixel, btn)
-
- def mouseMoveEvent(self, event):
- xpixel = event.x() * self.getDevicePixelRatio()
- ypixel = event.y() * self.getDevicePixelRatio()
- event.accept()
-
- if self.eventHandler is not None and self.isValid():
- self.makeCurrent()
- self.eventHandler.handleEvent('move', xpixel, ypixel)
-
- def mouseReleaseEvent(self, event):
- xpixel = event.x() * self.getDevicePixelRatio()
- ypixel = event.y() * self.getDevicePixelRatio()
- btn = self._MOUSE_BTNS[event.button()]
- event.accept()
-
- if self.eventHandler is not None and self.isValid():
- self.makeCurrent()
- self.eventHandler.handleEvent('release', xpixel, ypixel, btn)
diff --git a/silx/gui/plot3d/Plot3DWindow.py b/silx/gui/plot3d/Plot3DWindow.py
deleted file mode 100644
index 470b966..0000000
--- a/silx/gui/plot3d/Plot3DWindow.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides a QMainWindow with a 3D scene and associated toolbar.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "26/01/2017"
-
-
-from silx.utils.proxy import docstring
-from silx.gui import qt
-
-from .Plot3DWidget import Plot3DWidget
-from .tools import OutputToolBar, InteractiveModeToolBar, ViewpointToolBar
-
-
-class Plot3DWindow(qt.QMainWindow):
- """OpenGL widget with a 3D viewport and an overview."""
-
- def __init__(self, parent=None):
- super(Plot3DWindow, self).__init__(parent)
- if parent is not None:
- # behave as a widget
- self.setWindowFlags(qt.Qt.Widget)
-
- self._plot3D = Plot3DWidget()
- self.setCentralWidget(self._plot3D)
-
- for klass in (InteractiveModeToolBar, ViewpointToolBar, OutputToolBar):
- toolbar = klass(parent=self)
- toolbar.setPlot3DWidget(self._plot3D)
- self.addToolBar(toolbar)
- self.addActions(toolbar.actions())
-
- def getPlot3DWidget(self):
- """Get the :class:`Plot3DWidget` of this window"""
- return self._plot3D
-
- # Proxy to Plot3DWidget
-
- @docstring(Plot3DWidget)
- def setProjection(self, projection):
- return self._plot3D.setProjection(projection)
-
- @docstring(Plot3DWidget)
- def getProjection(self):
- return self._plot3D.getProjection()
-
- @docstring(Plot3DWidget)
- def centerScene(self):
- return self._plot3D.centerScene()
-
- @docstring(Plot3DWidget)
- def resetZoom(self):
- return self._plot3D.resetZoom()
-
- @docstring(Plot3DWidget)
- def getBackgroundColor(self):
- return self._plot3D.getBackgroundColor()
-
- @docstring(Plot3DWidget)
- def setBackgroundColor(self, color):
- return self._plot3D.setBackgroundColor(color)
diff --git a/silx/gui/plot3d/SFViewParamTree.py b/silx/gui/plot3d/SFViewParamTree.py
deleted file mode 100644
index 4e179fc..0000000
--- a/silx/gui/plot3d/SFViewParamTree.py
+++ /dev/null
@@ -1,1817 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2020 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.
-#
-# ###########################################################################*/
-"""
-This module provides a tree widget to set/view parameters of a ScalarFieldView.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["D. N."]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-import logging
-import sys
-import weakref
-
-import numpy
-
-from silx.gui import qt
-from silx.gui.icons import getQIcon
-from silx.gui.colors import Colormap
-from silx.gui.widgets.FloatEdit import FloatEdit
-
-from .ScalarFieldView import Isosurface
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ModelColumns(object):
- NameColumn, ValueColumn, ColumnMax = range(3)
- ColumnNames = ['Name', 'Value']
-
-
-class SubjectItem(qt.QStandardItem):
- """
- Base class for observers items.
-
- Subclassing:
- ------------
- The following method can/should be reimplemented:
- - _init
- - _pullData
- - _pushData
- - _setModelData
- - _subjectChanged
- - getEditor
- - getSignals
- - leftClicked
- - queryRemove
- - setEditorData
-
- Also the following attributes are available:
- - editable
- - persistent
-
- :param subject: object that this item will be observing.
- """
-
- editable = False
- """ boolean: set to True to make the item editable. """
-
- persistent = False
- """
- boolean: set to True to make the editor persistent.
- See : Qt.QAbstractItemView.openPersistentEditor
- """
-
- def __init__(self, subject, *args):
-
- super(SubjectItem, self).__init__(*args)
-
- self.setEditable(self.editable)
-
- self.__subject = None
- self.subject = subject
-
- def setData(self, value, role=qt.Qt.UserRole, pushData=True):
- """
- Overloaded method from QStandardItem. The pushData keyword tells
- the item to push data to the subject if the role is equal to EditRole.
- This is useful to let this method know if the setData method was called
- internally or from the view.
-
- :param value: the value ti set to data
- :param role: role in the item
- :param pushData: if True push value in the existing data.
- """
- if role == qt.Qt.EditRole and pushData:
- setValue = self._pushData(value, role)
- if setValue != value:
- value = setValue
- super(SubjectItem, self).setData(value, role)
-
- @property
- def subject(self):
- """The subject this item is observing"""
- return None if self.__subject is None else self.__subject()
-
- @subject.setter
- def subject(self, subject):
- if self.__subject is not None:
- raise ValueError('Subject already set '
- ' (subject change not supported).')
- if subject is None:
- self.__subject = None
- else:
- self.__subject = weakref.ref(subject)
- if subject is not None:
- self._init()
- self._connectSignals()
-
- def _connectSignals(self):
- """
- Connects the signals. Called when the subject is set.
- """
-
- def gen_slot(_sigIdx):
- def slotfn(*args, **kwargs):
- self._subjectChanged(signalIdx=_sigIdx,
- args=args,
- kwargs=kwargs)
- return slotfn
-
- if self.__subject is not None:
- self.__slots = slots = []
-
- signals = self.getSignals()
-
- if signals:
- if not isinstance(signals, (list, tuple)):
- signals = [signals]
- for sigIdx, signal in enumerate(signals):
- slot = gen_slot(sigIdx)
- signal.connect(slot)
- slots.append((signal, slot))
-
- def _disconnectSignals(self):
- """
- Disconnects all subject's signal
- """
- if self.__slots:
- for signal, slot in self.__slots:
- try:
- signal.disconnect(slot)
- except TypeError:
- pass
-
- def _enableRow(self, enable):
- """
- Set the enabled state for this cell, or for the whole row
- if this item has a parent.
-
- :param bool enable: True if we wan't to enable the cell
- """
- parent = self.parent()
- model = self.model()
- if model is None or parent is None:
- # no parent -> no siblings
- self.setEnabled(enable)
- return
-
- for col in range(model.columnCount()):
- sibling = parent.child(self.row(), col)
- sibling.setEnabled(enable)
-
- #################################################################
- # Overloadable methods
- #################################################################
-
- def getSignals(self):
- """
- Returns the list of this items subject's signals that
- this item will be listening to.
-
- :return: list.
- """
- return None
-
- def _subjectChanged(self, signalIdx=None, args=None, kwargs=None):
- """
- Called when one of the signals is triggered. Default implementation
- just calls _pullData, compares the result to the current value stored
- as Qt.EditRole, and stores the new value if it is different. It also
- stores its str representation as Qt.DisplayRole
-
- :param signalIdx: index of the triggered signal. The value passed
- is the same as the signal position in the list returned by
- SubjectItem.getSignals.
- :param args: arguments received from the signal
- :param kwargs: keyword arguments received from the signal
- """
- data = self._pullData()
- if data == self.data(qt.Qt.EditRole):
- return
- self.setData(data, role=qt.Qt.DisplayRole, pushData=False)
- self.setData(data, role=qt.Qt.EditRole, pushData=False)
-
- def _pullData(self):
- """
- Pulls data from the subject.
-
- :return: subject data
- """
- return None
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- """
- Pushes data to the subject and returns the actual value that was stored
-
- :return: the value that was stored
- """
- return value
-
- def _init(self):
- """
- Called when the subject is set.
- :return:
- """
- self._subjectChanged()
-
- def getEditor(self, parent, option, index):
- """
- Returns the editor widget used to edit this item's data. The arguments
- are the one passed to the QStyledItemDelegate.createEditor method.
-
- :param parent: the Qt parent of the editor
- :param option:
- :param index:
- :return:
- """
- return None
-
- def setEditorData(self, editor):
- """
- This is called by the View's delegate just before the editor is shown,
- its purpose it to setup the editors contents. Return False to use
- the delegate's default behaviour.
-
- :param editor:
- :return:
- """
- return True
-
- def _setModelData(self, editor):
- """
- This is called by the View's delegate just before the editor is closed,
- its allows this item to update itself with data from the editor.
-
- :param editor:
- :return:
- """
- return False
-
- def queryRemove(self, view=None):
- """
- This is called by the view to ask this items if it (the view) can
- remove it. Return True to let the view know that the item can be
- removed.
-
- :param view:
- :return:
- """
- return False
-
- def leftClicked(self):
- """
- This method is called by the view when the item's cell if left clicked.
-
- :return:
- """
- pass
-
-
-# View settings ###############################################################
-
-class ColorItem(SubjectItem):
- """color item."""
- editable = True
- persistent = True
-
- def getEditor(self, parent, option, index):
- editor = QColorEditor(parent)
- editor.color = self.getColor()
-
- # Wrapping call in lambda is a workaround for PySide with Python 3
- editor.sigColorChanged.connect(
- lambda color: self._editorSlot(color))
- return editor
-
- def _editorSlot(self, color):
- self.setData(color, qt.Qt.EditRole)
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- self.setColor(value)
- return self.getColor()
-
- def _pullData(self):
- self.getColor()
-
- def setColor(self, color):
- """Override to implement actual color setter"""
- pass
-
-
-class BackgroundColorItem(ColorItem):
- itemName = 'Background'
-
- def setColor(self, color):
- self.subject.setBackgroundColor(color)
-
- def getColor(self):
- return self.subject.getBackgroundColor()
-
-
-class ForegroundColorItem(ColorItem):
- itemName = 'Foreground'
-
- def setColor(self, color):
- self.subject.setForegroundColor(color)
-
- def getColor(self):
- return self.subject.getForegroundColor()
-
-
-class HighlightColorItem(ColorItem):
- itemName = 'Highlight'
-
- def setColor(self, color):
- self.subject.setHighlightColor(color)
-
- def getColor(self):
- return self.subject.getHighlightColor()
-
-
-class _LightDirectionAngleBaseItem(SubjectItem):
- """Base class for directional light angle item."""
- editable = True
- persistent = True
-
- def _init(self):
- pass
-
- def getSignals(self):
- """Override to provide signals to listen"""
- raise NotImplementedError("MUST be implemented in subclass")
-
- def _pullData(self):
- """Override in subclass to get current angle"""
- raise NotImplementedError("MUST be implemented in subclass")
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- """Override in subclass to set the angle"""
- raise NotImplementedError("MUST be implemented in subclass")
-
- def getEditor(self, parent, option, index):
- editor = qt.QSlider(parent)
- editor.setOrientation(qt.Qt.Horizontal)
- editor.setMinimum(-90)
- editor.setMaximum(90)
- editor.setValue(int(self._pullData()))
-
- # Wrapping call in lambda is a workaround for PySide with Python 3
- editor.valueChanged.connect(
- lambda value: self._pushData(value))
-
- return editor
-
- def setEditorData(self, editor):
- editor.setValue(int(self._pullData()))
- return True
-
- def _setModelData(self, editor):
- value = editor.value()
- self._pushData(value)
- return True
-
-
-class LightAzimuthAngleItem(_LightDirectionAngleBaseItem):
- """Light direction azimuth angle item."""
-
- def getSignals(self):
- return self.subject.sigAzimuthAngleChanged
-
- def _pullData(self):
- return self.subject.getAzimuthAngle()
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- self.subject.setAzimuthAngle(value)
-
-
-class LightAltitudeAngleItem(_LightDirectionAngleBaseItem):
- """Light direction altitude angle item."""
-
- def getSignals(self):
- return self.subject.sigAltitudeAngleChanged
-
- def _pullData(self):
- return self.subject.getAltitudeAngle()
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- self.subject.setAltitudeAngle(value)
-
-
-class _DirectionalLightProxy(qt.QObject):
- """Proxy to handle directional light with angles rather than vector.
- """
-
- sigAzimuthAngleChanged = qt.Signal()
- """Signal sent when the azimuth angle has changed."""
-
- sigAltitudeAngleChanged = qt.Signal()
- """Signal sent when altitude angle has changed."""
-
- def __init__(self, light):
- super(_DirectionalLightProxy, self).__init__()
- self._light = light
- light.addListener(self._directionUpdated)
- self._azimuth = 0.
- self._altitude = 0.
-
- def getAzimuthAngle(self):
- """Returns the signed angle in the horizontal plane.
-
- Unit: degrees.
- The 0 angle corresponds to the axis perpendicular to the screen.
-
- :rtype: float
- """
- return self._azimuth
-
- def getAltitudeAngle(self):
- """Returns the signed vertical angle from the horizontal plane.
-
- Unit: degrees.
- Range: [-90, +90]
-
- :rtype: float
- """
- return self._altitude
-
- def setAzimuthAngle(self, angle):
- """Set the horizontal angle.
-
- :param float angle: Angle from -z axis in zx plane in degrees.
- """
- if angle != self._azimuth:
- self._azimuth = angle
- self._updateLight()
- self.sigAzimuthAngleChanged.emit()
-
- def setAltitudeAngle(self, angle):
- """Set the horizontal angle.
-
- :param float angle: Angle from -z axis in zy plane in degrees.
- """
- if angle != self._altitude:
- self._altitude = angle
- self._updateLight()
- self.sigAltitudeAngleChanged.emit()
-
- def _directionUpdated(self, *args, **kwargs):
- """Handle light direction update in the scene"""
- # Invert direction to manipulate the 'source' pointing to
- # the center of the viewport
- x, y, z = - self._light.direction
-
- # Horizontal plane is plane xz
- azimuth = numpy.degrees(numpy.arctan2(x, z))
- altitude = numpy.degrees(numpy.pi/2. - numpy.arccos(y))
-
- if (abs(azimuth - self.getAzimuthAngle()) > 0.01 and
- abs(abs(altitude) - 90.) >= 0.001): # Do not update when at zenith
- self.setAzimuthAngle(azimuth)
-
- if abs(altitude - self.getAltitudeAngle()) > 0.01:
- self.setAltitudeAngle(altitude)
-
- def _updateLight(self):
- """Update light direction in the scene"""
- azimuth = numpy.radians(self._azimuth)
- delta = numpy.pi/2. - numpy.radians(self._altitude)
- z = - numpy.sin(delta) * numpy.cos(azimuth)
- x = - numpy.sin(delta) * numpy.sin(azimuth)
- y = - numpy.cos(delta)
- self._light.direction = x, y, z
-
-
-class DirectionalLightGroup(SubjectItem):
- """
- Root Item for the directional light
- """
-
- def __init__(self,subject, *args):
- self._light = _DirectionalLightProxy(
- subject.getPlot3DWidget().viewport.light)
-
- super(DirectionalLightGroup, self).__init__(subject, *args)
-
- def _init(self):
-
- nameItem = qt.QStandardItem('Azimuth')
- nameItem.setEditable(False)
- valueItem = LightAzimuthAngleItem(self._light)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Altitude')
- nameItem.setEditable(False)
- valueItem = LightAltitudeAngleItem(self._light)
- self.appendRow([nameItem, valueItem])
-
-
-class BoundingBoxItem(SubjectItem):
- """Bounding box, axes labels and grid visibility item.
-
- Item is checkable.
- """
- itemName = 'Bounding Box'
-
- def _init(self):
- visible = self.subject.isBoundingBoxVisible()
- self.setCheckable(True)
- self.setCheckState(qt.Qt.Checked if visible else qt.Qt.Unchecked)
-
- def leftClicked(self):
- checked = (self.checkState() == qt.Qt.Checked)
- if checked != self.subject.isBoundingBoxVisible():
- self.subject.setBoundingBoxVisible(checked)
-
-
-class OrientationIndicatorItem(SubjectItem):
- """Orientation indicator visibility item.
-
- Item is checkable.
- """
- itemName = 'Axes indicator'
-
- def _init(self):
- plot3d = self.subject.getPlot3DWidget()
- visible = plot3d.isOrientationIndicatorVisible()
- self.setCheckable(True)
- self.setCheckState(qt.Qt.Checked if visible else qt.Qt.Unchecked)
-
- def leftClicked(self):
- plot3d = self.subject.getPlot3DWidget()
- checked = (self.checkState() == qt.Qt.Checked)
- if checked != plot3d.isOrientationIndicatorVisible():
- plot3d.setOrientationIndicatorVisible(checked)
-
-
-class ViewSettingsItem(qt.QStandardItem):
- """Viewport settings"""
-
- def __init__(self, subject, *args):
-
- super(ViewSettingsItem, self).__init__(*args)
-
- self.setEditable(False)
-
- classes = (BackgroundColorItem,
- ForegroundColorItem,
- HighlightColorItem,
- BoundingBoxItem,
- OrientationIndicatorItem)
- for cls in classes:
- titleItem = qt.QStandardItem(cls.itemName)
- titleItem.setEditable(False)
- self.appendRow([titleItem, cls(subject)])
-
- nameItem = DirectionalLightGroup(subject, 'Light Direction')
- valueItem = qt.QStandardItem()
- self.appendRow([nameItem, valueItem])
-
-
-# Data information ############################################################
-
-class DataChangedItem(SubjectItem):
- """
- Base class for items listening to ScalarFieldView.sigDataChanged
- """
-
- def getSignals(self):
- subject = self.subject
- if subject:
- return subject.sigDataChanged, subject.sigTransformChanged
- return None
-
- def _init(self):
- self._subjectChanged()
-
-
-class DataTypeItem(DataChangedItem):
- itemName = 'dtype'
-
- def _pullData(self):
- data = self.subject.getData(copy=False)
- return ((data is not None) and str(data.dtype)) or 'N/A'
-
-
-class DataShapeItem(DataChangedItem):
- itemName = 'size'
-
- def _pullData(self):
- data = self.subject.getData(copy=False)
- if data is None:
- return 'N/A'
- else:
- return str(list(reversed(data.shape)))
-
-
-class OffsetItem(DataChangedItem):
- itemName = 'offset'
-
- def _pullData(self):
- offset = self.subject.getTranslation()
- return ((offset is not None) and str(offset)) or 'N/A'
-
-
-class ScaleItem(DataChangedItem):
- itemName = 'scale'
-
- def _pullData(self):
- scale = self.subject.getScale()
- return ((scale is not None) and str(scale)) or 'N/A'
-
-
-class MatrixItem(DataChangedItem):
-
- def __init__(self, subject, row, *args):
- self.__row = row
- super(MatrixItem, self).__init__(subject, *args)
-
- def _pullData(self):
- matrix = self.subject.getTransformMatrix()
- return str(matrix[self.__row])
-
-
-class DataSetItem(qt.QStandardItem):
-
- def __init__(self, subject, *args):
-
- super(DataSetItem, self).__init__(*args)
-
- self.setEditable(False)
-
- klasses = [DataTypeItem, DataShapeItem, OffsetItem]
- for klass in klasses:
- titleItem = qt.QStandardItem(klass.itemName)
- titleItem.setEditable(False)
- self.appendRow([titleItem, klass(subject)])
-
- matrixItem = qt.QStandardItem('matrix')
- matrixItem.setEditable(False)
- valueItem = qt.QStandardItem()
- self.appendRow([matrixItem, valueItem])
-
- for row in range(3):
- titleItem = qt.QStandardItem()
- titleItem.setEditable(False)
- valueItem = MatrixItem(subject, row)
- matrixItem.appendRow([titleItem, valueItem])
-
- titleItem = qt.QStandardItem(ScaleItem.itemName)
- titleItem.setEditable(False)
- self.appendRow([titleItem, ScaleItem(subject)])
-
-
-# Isosurface ##################################################################
-
-class IsoSurfaceRootItem(SubjectItem):
- """
- Root (i.e : column index 0) Isosurface item.
- """
-
- def __init__(self, subject, normalization, *args):
- self._isoLevelSliderNormalization = normalization
- super(IsoSurfaceRootItem, self).__init__(subject, *args)
-
- def getSignals(self):
- subject = self.subject
- return [subject.sigColorChanged,
- subject.sigVisibilityChanged]
-
- def _subjectChanged(self, signalIdx=None, args=None, kwargs=None):
- if signalIdx == 0:
- color = self.subject.getColor()
- self.setData(color, qt.Qt.DecorationRole)
- elif signalIdx == 1:
- visible = args[0]
- self.setCheckState((visible and qt.Qt.Checked) or qt.Qt.Unchecked)
-
- def _init(self):
- self.setCheckable(True)
-
- isosurface = self.subject
- color = isosurface.getColor()
- visible = isosurface.isVisible()
- self.setData(color, qt.Qt.DecorationRole)
- self.setCheckState((visible and qt.Qt.Checked) or qt.Qt.Unchecked)
-
- nameItem = qt.QStandardItem('Level')
- sliderItem = IsoSurfaceLevelSlider(self.subject,
- self._isoLevelSliderNormalization)
- self.appendRow([nameItem, sliderItem])
-
- nameItem = qt.QStandardItem('Color')
- nameItem.setEditable(False)
- valueItem = IsoSurfaceColorItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Opacity')
- nameItem.setTextAlignment(qt.Qt.AlignLeft | qt.Qt.AlignTop)
- nameItem.setEditable(False)
- valueItem = IsoSurfaceAlphaItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem()
- nameItem.setEditable(False)
- valueItem = IsoSurfaceAlphaLegendItem(self.subject)
- valueItem.setEditable(False)
- self.appendRow([nameItem, valueItem])
-
- def queryRemove(self, view=None):
- buttons = qt.QMessageBox.Ok | qt.QMessageBox.Cancel
- ans = qt.QMessageBox.question(view,
- 'Remove isosurface',
- 'Remove the selected iso-surface?',
- buttons=buttons)
- if ans == qt.QMessageBox.Ok:
- sfview = self.subject.parent()
- if sfview:
- sfview.removeIsosurface(self.subject)
- return False
- return False
-
- def leftClicked(self):
- checked = (self.checkState() == qt.Qt.Checked)
- visible = self.subject.isVisible()
- if checked != visible:
- self.subject.setVisible(checked)
-
-
-class IsoSurfaceLevelItem(SubjectItem):
- """
- Base class for the isosurface level items.
- """
- editable = True
-
- def getSignals(self):
- subject = self.subject
- return [subject.sigLevelChanged,
- subject.sigVisibilityChanged]
-
- def getEditor(self, parent, option, index):
- return FloatEdit(parent)
-
- def setEditorData(self, editor):
- editor.setValue(self._pullData())
- return False
-
- def _setModelData(self, editor):
- self._pushData(editor.value())
- return True
-
- def _pullData(self):
- return self.subject.getLevel()
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- self.subject.setLevel(value)
- return self.subject.getLevel()
-
-
-class _IsoLevelSlider(qt.QSlider):
- """QSlider used for iso-surface level with linear scale"""
-
- def __init__(self, parent, subject, normalization):
- super(_IsoLevelSlider, self).__init__(parent=parent)
- self.subject = subject
-
- if normalization == 'arcsinh':
- self.__norm = numpy.arcsinh
- self.__invNorm = numpy.sinh
- elif normalization == 'linear':
- self.__norm = lambda x: x
- self.__invNorm = lambda x: x
- else:
- raise ValueError(
- "Unsupported normalization %s", normalization)
-
- self.sliderReleased.connect(self.__sliderReleased)
-
- self.subject.sigLevelChanged.connect(self.setLevel)
- self.subject.parent().sigDataChanged.connect(self.__dataChanged)
-
- def setLevel(self, level):
- """Set slider from iso-surface level"""
- dataRange = self.subject.parent().getDataRange()
-
- if dataRange is not None:
- min_ = self.__norm(dataRange[0])
- max_ = self.__norm(dataRange[-1])
-
- width = max_ - min_
- if width > 0:
- sliderWidth = self.maximum() - self.minimum()
- sliderPosition = sliderWidth * (self.__norm(level) - min_) / width
- self.setValue(int(sliderPosition))
-
- def __dataChanged(self):
- """Handles data update to refresh slider range if needed"""
- self.setLevel(self.subject.getLevel())
-
- def __sliderReleased(self):
- value = self.value()
- dataRange = self.subject.parent().getDataRange()
- if dataRange is not None:
- min_ = self.__norm(dataRange[0])
- max_ = self.__norm(dataRange[-1])
- width = max_ - min_
- sliderWidth = self.maximum() - self.minimum()
- level = min_ + width * value / sliderWidth
- self.subject.setLevel(self.__invNorm(level))
-
-
-class IsoSurfaceLevelSlider(IsoSurfaceLevelItem):
- """
- Isosurface level item with a slider editor.
- """
- nTicks = 1000
- persistent = True
-
- def __init__(self, subject, normalization):
- self.normalization = normalization
- super(IsoSurfaceLevelSlider, self).__init__(subject)
-
- def getEditor(self, parent, option, index):
- editor = _IsoLevelSlider(parent, self.subject, self.normalization)
- editor.setOrientation(qt.Qt.Horizontal)
- editor.setMinimum(0)
- editor.setMaximum(self.nTicks)
-
- editor.setSingleStep(1)
-
- editor.setLevel(self.subject.getLevel())
- return editor
-
- def setEditorData(self, editor):
- return True
-
- def _setModelData(self, editor):
- return True
-
-
-class IsoSurfaceColorItem(SubjectItem):
- """
- Isosurface color item.
- """
- editable = True
- persistent = True
-
- def getSignals(self):
- return self.subject.sigColorChanged
-
- def getEditor(self, parent, option, index):
- editor = QColorEditor(parent)
- color = self.subject.getColor()
- color.setAlpha(255)
- editor.color = color
- # Wrapping call in lambda is a workaround for PySide with Python 3
- editor.sigColorChanged.connect(
- lambda color: self.__editorChanged(color))
- return editor
-
- def __editorChanged(self, color):
- color.setAlpha(self.subject.getColor().alpha())
- self.subject.setColor(color)
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- self.subject.setColor(value)
- return self.subject.getColor()
-
-
-class QColorEditor(qt.QWidget):
- """
- QColor editor.
- """
- sigColorChanged = qt.Signal(object)
-
- color = property(lambda self: qt.QColor(self.__color))
-
- @color.setter
- def color(self, color):
- self._setColor(color)
- self.__previousColor = color
-
- def __init__(self, *args, **kwargs):
- super(QColorEditor, self).__init__(*args, **kwargs)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- button = qt.QToolButton()
- icon = qt.QIcon(qt.QPixmap(32, 32))
- button.setIcon(icon)
- layout.addWidget(button)
- button.clicked.connect(self.__showColorDialog)
- layout.addStretch(1)
-
- self.__color = None
- self.__previousColor = None
-
- def sizeHint(self):
- return qt.QSize(0, 0)
-
- def _setColor(self, qColor):
- button = self.findChild(qt.QToolButton)
- pixmap = qt.QPixmap(32, 32)
- pixmap.fill(qColor)
- button.setIcon(qt.QIcon(pixmap))
- self.__color = qColor
-
- def __showColorDialog(self):
- dialog = qt.QColorDialog(parent=self)
- if sys.platform == 'darwin':
- # Use of native color dialog on macos might cause problems
- dialog.setOption(qt.QColorDialog.DontUseNativeDialog, True)
-
- self.__previousColor = self.__color
- dialog.setAttribute(qt.Qt.WA_DeleteOnClose)
- dialog.setModal(True)
- dialog.currentColorChanged.connect(self.__colorChanged)
- dialog.finished.connect(self.__dialogClosed)
- dialog.show()
-
- def __colorChanged(self, color):
- self.__color = color
- self._setColor(color)
- self.sigColorChanged.emit(color)
-
- def __dialogClosed(self, result):
- if result == qt.QDialog.Rejected:
- self.__colorChanged(self.__previousColor)
- self.__previousColor = None
-
-
-class IsoSurfaceAlphaItem(SubjectItem):
- """
- Isosurface alpha item.
- """
- editable = True
- persistent = True
-
- def _init(self):
- pass
-
- def getSignals(self):
- return self.subject.sigColorChanged
-
- def getEditor(self, parent, option, index):
- editor = qt.QSlider(parent)
- editor.setOrientation(qt.Qt.Horizontal)
- editor.setMinimum(0)
- editor.setMaximum(255)
-
- color = self.subject.getColor()
- editor.setValue(color.alpha())
-
- # Wrapping call in lambda is a workaround for PySide with Python 3
- editor.valueChanged.connect(
- lambda value: self.__editorChanged(value))
-
- return editor
-
- def __editorChanged(self, value):
- color = self.subject.getColor()
- color.setAlpha(value)
- self.subject.setColor(color)
-
- def setEditorData(self, editor):
- return True
-
- def _setModelData(self, editor):
- return True
-
-
-class IsoSurfaceAlphaLegendItem(SubjectItem):
- """Legend to place under opacity slider"""
-
- editable = False
- persistent = True
-
- def getEditor(self, parent, option, index):
- layout = qt.QHBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
- layout.addWidget(qt.QLabel('0'))
- layout.addStretch(1)
- layout.addWidget(qt.QLabel('1'))
-
- editor = qt.QWidget(parent)
- editor.setLayout(layout)
- return editor
-
-
-class IsoSurfaceCount(SubjectItem):
- """
- Item displaying the number of isosurfaces.
- """
-
- def getSignals(self):
- subject = self.subject
- return [subject.sigIsosurfaceAdded, subject.sigIsosurfaceRemoved]
-
- def _pullData(self):
- return len(self.subject.getIsosurfaces())
-
-
-class IsoSurfaceAddRemoveWidget(qt.QWidget):
-
- sigViewTask = qt.Signal(str)
- """Signal for the tree view to perform some task"""
-
- def __init__(self, parent, item):
- super(IsoSurfaceAddRemoveWidget, self).__init__(parent)
- self._item = item
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- addBtn = qt.QToolButton(self)
- addBtn.setText('+')
- addBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
- layout.addWidget(addBtn)
- addBtn.clicked.connect(self.__addClicked)
-
- removeBtn = qt.QToolButton(self)
- removeBtn.setText('-')
- removeBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
- layout.addWidget(removeBtn)
- removeBtn.clicked.connect(self.__removeClicked)
-
- layout.addStretch(1)
-
- def __addClicked(self):
- sfview = self._item.subject
- if not sfview:
- return
- dataRange = sfview.getDataRange()
- if dataRange is None:
- dataRange = [0, 1]
-
- sfview.addIsosurface(
- numpy.mean((dataRange[0], dataRange[-1])), '#0000FF')
-
- def __removeClicked(self):
- self.sigViewTask.emit('remove_iso')
-
-
-class IsoSurfaceAddRemoveItem(SubjectItem):
- """
- Item displaying a simple QToolButton allowing to add an isosurface.
- """
- persistent = True
-
- def getEditor(self, parent, option, index):
- return IsoSurfaceAddRemoveWidget(parent, self)
-
-
-class IsoSurfaceGroup(SubjectItem):
- """
- Root item for the list of isosurface items.
- """
-
- def __init__(self, subject, normalization, *args):
- self._isoLevelSliderNormalization = normalization
- super(IsoSurfaceGroup, self).__init__(subject, *args)
-
- def getSignals(self):
- subject = self.subject
- return [subject.sigIsosurfaceAdded, subject.sigIsosurfaceRemoved]
-
- def _subjectChanged(self, signalIdx=None, args=None, kwargs=None):
- if signalIdx == 0:
- if len(args) >= 1:
- isosurface = args[0]
- if not isinstance(isosurface, Isosurface):
- raise ValueError('Expected an isosurface instance.')
- self.__addIsosurface(isosurface)
- else:
- raise ValueError('Expected an isosurface instance.')
- elif signalIdx == 1:
- if len(args) >= 1:
- isosurface = args[0]
- if not isinstance(isosurface, Isosurface):
- raise ValueError('Expected an isosurface instance.')
- self.__removeIsosurface(isosurface)
- else:
- raise ValueError('Expected an isosurface instance.')
-
- def __addIsosurface(self, isosurface):
- valueItem = IsoSurfaceRootItem(
- subject=isosurface,
- normalization=self._isoLevelSliderNormalization)
- nameItem = IsoSurfaceLevelItem(subject=isosurface)
- self.insertRow(max(0, self.rowCount() - 1), [valueItem, nameItem])
-
- def __removeIsosurface(self, isosurface):
- for row in range(self.rowCount()):
- child = self.child(row)
- subject = getattr(child, 'subject', None)
- if subject == isosurface:
- self.takeRow(row)
- break
-
- def _init(self):
- nameItem = IsoSurfaceAddRemoveItem(self.subject)
- valueItem = qt.QStandardItem()
- valueItem.setEditable(False)
- self.appendRow([nameItem, valueItem])
-
- subject = self.subject
- isosurfaces = subject.getIsosurfaces()
- for isosurface in isosurfaces:
- self.__addIsosurface(isosurface)
-
-
-# Cutting Plane ###############################################################
-
-class ColormapBase(SubjectItem):
- """
- Mixin class for colormap items.
- """
-
- def getSignals(self):
- return [self.subject.getCutPlanes()[0].sigColormapChanged]
-
-
-class PlaneMinRangeItem(ColormapBase):
- """
- colormap minVal item.
- Editor is a QLineEdit with a QDoubleValidator
- """
- editable = True
-
- def _pullData(self):
- colormap = self.subject.getCutPlanes()[0].getColormap()
- auto = colormap.isAutoscale()
- if auto == self.isEnabled():
- self._enableRow(not auto)
- return colormap.getVMin()
-
- def _pushData(self, value, role=qt.Qt.UserRole):
- self._setVMin(value)
-
- def _setVMin(self, value):
- colormap = self.subject.getCutPlanes()[0].getColormap()
- vMin = value
- vMax = colormap.getVMax()
-
- if vMax is not None and value > vMax:
- vMin = vMax
- vMax = value
- colormap.setVRange(vMin, vMax)
-
- def getEditor(self, parent, option, index):
- return FloatEdit(parent)
-
- def setEditorData(self, editor):
- editor.setValue(self._pullData())
- return True
-
- def _setModelData(self, editor):
- value = editor.value()
- self._setVMin(value)
- return True
-
-
-class PlaneMaxRangeItem(ColormapBase):
- """
- colormap maxVal item.
- Editor is a QLineEdit with a QDoubleValidator
- """
- editable = True
-
- def _pullData(self):
- colormap = self.subject.getCutPlanes()[0].getColormap()
- auto = colormap.isAutoscale()
- if auto == self.isEnabled():
- self._enableRow(not auto)
- return self.subject.getCutPlanes()[0].getColormap().getVMax()
-
- def _setVMax(self, value):
- colormap = self.subject.getCutPlanes()[0].getColormap()
- vMin = colormap.getVMin()
- vMax = value
- if vMin is not None and value < vMin:
- vMax = vMin
- vMin = value
- colormap.setVRange(vMin, vMax)
-
- def getEditor(self, parent, option, index):
- return FloatEdit(parent)
-
- def setEditorData(self, editor):
- editor.setText(str(self._pullData()))
- return True
-
- def _setModelData(self, editor):
- value = editor.value()
- self._setVMax(value)
- return True
-
-
-class PlaneOrientationItem(SubjectItem):
- """
- Plane orientation item.
- Editor is a QComboBox.
- """
- editable = True
-
- _PLANE_ACTIONS = (
- ('3d-plane-normal-x', 'Plane 0',
- 'Set plane perpendicular to red axis', (1., 0., 0.)),
- ('3d-plane-normal-y', 'Plane 1',
- 'Set plane perpendicular to green axis', (0., 1., 0.)),
- ('3d-plane-normal-z', 'Plane 2',
- 'Set plane perpendicular to blue axis', (0., 0., 1.)),
- )
-
- def getSignals(self):
- return [self.subject.getCutPlanes()[0].sigPlaneChanged]
-
- def _pullData(self):
- currentNormal = self.subject.getCutPlanes()[0].getNormal(
- coordinates='scene')
- for _, text, _, normal in self._PLANE_ACTIONS:
- if numpy.allclose(normal, currentNormal):
- return text
- return ''
-
- def getEditor(self, parent, option, index):
- editor = qt.QComboBox(parent)
- for iconName, text, tooltip, normal in self._PLANE_ACTIONS:
- editor.addItem(getQIcon(iconName), text)
-
- # Wrapping call in lambda is a workaround for PySide with Python 3
- editor.currentIndexChanged[int].connect(
- lambda index: self.__editorChanged(index))
- return editor
-
- def __editorChanged(self, index):
- normal = self._PLANE_ACTIONS[index][3]
- plane = self.subject.getCutPlanes()[0]
- plane.setNormal(normal, coordinates='scene')
- plane.moveToCenter()
-
- def setEditorData(self, editor):
- currentText = self._pullData()
- index = 0
- for normIdx, (_, text, _, _) in enumerate(self._PLANE_ACTIONS):
- if text == currentText:
- index = normIdx
- break
- editor.setCurrentIndex(index)
- return True
-
- def _setModelData(self, editor):
- return True
-
-
-class PlaneInterpolationItem(SubjectItem):
- """Toggle cut plane interpolation method: nearest or linear.
-
- Item is checkable
- """
-
- def _init(self):
- interpolation = self.subject.getCutPlanes()[0].getInterpolation()
- self.setCheckable(True)
- self.setCheckState(
- qt.Qt.Checked if interpolation == 'linear' else qt.Qt.Unchecked)
- self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)
-
- def getSignals(self):
- return [self.subject.getCutPlanes()[0].sigInterpolationChanged]
-
- def leftClicked(self):
- checked = self.checkState() == qt.Qt.Checked
- self._setInterpolation('linear' if checked else 'nearest')
-
- def _pullData(self):
- interpolation = self.subject.getCutPlanes()[0].getInterpolation()
- self._setInterpolation(interpolation)
- return interpolation[0].upper() + interpolation[1:]
-
- def _setInterpolation(self, interpolation):
- self.subject.getCutPlanes()[0].setInterpolation(interpolation)
-
-
-class PlaneDisplayBelowMinItem(SubjectItem):
- """Toggle whether to display or not values <= colormap min of the cut plane
-
- Item is checkable
- """
-
- def _init(self):
- display = self.subject.getCutPlanes()[0].getDisplayValuesBelowMin()
- self.setCheckable(True)
- self.setCheckState(
- qt.Qt.Checked if display else qt.Qt.Unchecked)
- self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)
-
- def getSignals(self):
- return [self.subject.getCutPlanes()[0].sigTransparencyChanged]
-
- def leftClicked(self):
- checked = self.checkState() == qt.Qt.Checked
- self._setDisplayValuesBelowMin(checked)
-
- def _pullData(self):
- display = self.subject.getCutPlanes()[0].getDisplayValuesBelowMin()
- self._setDisplayValuesBelowMin(display)
- return "Displayed" if display else "Hidden"
-
- def _setDisplayValuesBelowMin(self, display):
- self.subject.getCutPlanes()[0].setDisplayValuesBelowMin(display)
-
-
-class PlaneColormapItem(ColormapBase):
- """
- colormap name item.
- Editor is a QComboBox
- """
- editable = True
-
- listValues = ['gray', 'reversed gray',
- 'temperature', 'red',
- 'green', 'blue',
- 'viridis', 'magma', 'inferno', 'plasma']
-
- def getEditor(self, parent, option, index):
- editor = qt.QComboBox(parent)
- editor.addItems(self.listValues)
-
- # Wrapping call in lambda is a workaround for PySide with Python 3
- editor.currentIndexChanged[int].connect(
- lambda index: self.__editorChanged(index))
-
- return editor
-
- def __editorChanged(self, index):
- colormapName = self.listValues[index]
- colormap = self.subject.getCutPlanes()[0].getColormap()
- colormap.setName(colormapName)
-
- def setEditorData(self, editor):
- colormapName = self.subject.getCutPlanes()[0].getColormap().getName()
- try:
- index = self.listValues.index(colormapName)
- except ValueError:
- _logger.error('Unsupported colormap: %s', colormapName)
- else:
- editor.setCurrentIndex(index)
- return True
-
- def _setModelData(self, editor):
- self.__editorChanged(editor.currentIndex())
- return True
-
- def _pullData(self):
- return self.subject.getCutPlanes()[0].getColormap().getName()
-
-
-class PlaneAutoScaleItem(ColormapBase):
- """
- colormap autoscale item.
- Item is checkable.
- """
-
- def _init(self):
- colorMap = self.subject.getCutPlanes()[0].getColormap()
- self.setCheckable(True)
- self.setCheckState((colorMap.isAutoscale() and qt.Qt.Checked)
- or qt.Qt.Unchecked)
- self.setData(self._pullData(), role=qt.Qt.DisplayRole, pushData=False)
-
- def leftClicked(self):
- checked = (self.checkState() == qt.Qt.Checked)
- self._setAutoScale(checked)
-
- def _setAutoScale(self, auto):
- view3d = self.subject
- colormap = view3d.getCutPlanes()[0].getColormap()
-
- if auto != colormap.isAutoscale():
- if auto:
- vMin = vMax = None
- else:
- dataRange = view3d.getDataRange()
- if dataRange is None:
- vMin = vMax = None
- else:
- vMin, vMax = dataRange[0], dataRange[-1]
- colormap.setVRange(vMin, vMax)
-
- def _pullData(self):
- auto = self.subject.getCutPlanes()[0].getColormap().isAutoscale()
- self._setAutoScale(auto)
- if auto:
- data = 'Auto'
- else:
- data = 'User'
- return data
-
-
-class NormalizationNode(ColormapBase):
- """
- colormap normalization item.
- Item is a QComboBox.
- """
- editable = True
- listValues = list(Colormap.NORMALIZATIONS)
-
- def getEditor(self, parent, option, index):
- editor = qt.QComboBox(parent)
- editor.addItems(self.listValues)
-
- # Wrapping call in lambda is a workaround for PySide with Python 3
- editor.currentIndexChanged[int].connect(
- lambda index: self.__editorChanged(index))
-
- return editor
-
- def __editorChanged(self, index):
- colorMap = self.subject.getCutPlanes()[0].getColormap()
- normalization = self.listValues[index]
- self.subject.getCutPlanes()[0].setColormap(name=colorMap.getName(),
- norm=normalization,
- vmin=colorMap.getVMin(),
- vmax=colorMap.getVMax())
-
- def setEditorData(self, editor):
- normalization = self.subject.getCutPlanes()[0].getColormap().getNormalization()
- index = self.listValues.index(normalization)
- editor.setCurrentIndex(index)
- return True
-
- def _setModelData(self, editor):
- self.__editorChanged(editor.currentIndex())
- return True
-
- def _pullData(self):
- return self.subject.getCutPlanes()[0].getColormap().getNormalization()
-
-
-class PlaneGroup(SubjectItem):
- """
- Root Item for the plane items.
- """
- def _init(self):
- valueItem = qt.QStandardItem()
- valueItem.setEditable(False)
- nameItem = PlaneVisibleItem(self.subject, 'Visible')
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Colormap')
- nameItem.setEditable(False)
- valueItem = PlaneColormapItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Normalization')
- nameItem.setEditable(False)
- valueItem = NormalizationNode(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Orientation')
- nameItem.setEditable(False)
- valueItem = PlaneOrientationItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Interpolation')
- nameItem.setEditable(False)
- valueItem = PlaneInterpolationItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Autoscale')
- nameItem.setEditable(False)
- valueItem = PlaneAutoScaleItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Min')
- nameItem.setEditable(False)
- valueItem = PlaneMinRangeItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Max')
- nameItem.setEditable(False)
- valueItem = PlaneMaxRangeItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
- nameItem = qt.QStandardItem('Values<=Min')
- nameItem.setEditable(False)
- valueItem = PlaneDisplayBelowMinItem(self.subject)
- self.appendRow([nameItem, valueItem])
-
-
-class PlaneVisibleItem(SubjectItem):
- """
- Plane visibility item.
- Item is checkable.
- """
- def _init(self):
- plane = self.subject.getCutPlanes()[0]
- self.setCheckable(True)
- self.setCheckState((plane.isVisible() and qt.Qt.Checked)
- or qt.Qt.Unchecked)
-
- def leftClicked(self):
- plane = self.subject.getCutPlanes()[0]
- checked = (self.checkState() == qt.Qt.Checked)
- if checked != plane.isVisible():
- plane.setVisible(checked)
- if plane.isVisible():
- plane.moveToCenter()
-
-
-# Tree ########################################################################
-
-class ItemDelegate(qt.QStyledItemDelegate):
- """
- Delegate for the QTreeView filled with SubjectItems.
- """
-
- sigDelegateEvent = qt.Signal(str)
-
- def __init__(self, parent=None):
- super(ItemDelegate, self).__init__(parent)
-
- def createEditor(self, parent, option, index):
- item = index.model().itemFromIndex(index)
- if item:
- if isinstance(item, SubjectItem):
- editor = item.getEditor(parent, option, index)
- if editor:
- editor.setAutoFillBackground(True)
- if hasattr(editor, 'sigViewTask'):
- editor.sigViewTask.connect(self.__viewTask)
- return editor
-
- editor = super(ItemDelegate, self).createEditor(parent,
- option,
- index)
- return editor
-
- def updateEditorGeometry(self, editor, option, index):
- editor.setGeometry(option.rect)
-
- def setEditorData(self, editor, index):
- item = index.model().itemFromIndex(index)
- if item:
- if isinstance(item, SubjectItem) and item.setEditorData(editor):
- return
- super(ItemDelegate, self).setEditorData(editor, index)
-
- def setModelData(self, editor, model, index):
- item = index.model().itemFromIndex(index)
- if isinstance(item, SubjectItem) and item._setModelData(editor):
- return
- super(ItemDelegate, self).setModelData(editor, model, index)
-
- def __viewTask(self, task):
- self.sigDelegateEvent.emit(task)
-
-
-class TreeView(qt.QTreeView):
- """
- TreeView displaying the SubjectItems for the ScalarFieldView.
- """
-
- def __init__(self, parent=None):
- super(TreeView, self).__init__(parent)
- self.__openedIndex = None
- self._isoLevelSliderNormalization = 'linear'
-
- self.setIconSize(qt.QSize(16, 16))
-
- header = self.header()
- if hasattr(header, 'setSectionResizeMode'): # Qt5
- header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
- else: # Qt4
- header.setResizeMode(qt.QHeaderView.ResizeToContents)
-
- delegate = ItemDelegate()
- self.setItemDelegate(delegate)
- delegate.sigDelegateEvent.connect(self.__delegateEvent)
- self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
- self.setSelectionMode(qt.QAbstractItemView.SingleSelection)
-
- self.clicked.connect(self.__clicked)
-
- def setSfView(self, sfView):
- """
- Sets the ScalarFieldView this view is controlling.
-
- :param sfView: A `ScalarFieldView`
- """
- model = qt.QStandardItemModel()
- model.setColumnCount(ModelColumns.ColumnMax)
- model.setHorizontalHeaderLabels(['Name', 'Value'])
-
- item = qt.QStandardItem()
- item.setEditable(False)
- model.appendRow([ViewSettingsItem(sfView, 'Style'), item])
-
- item = qt.QStandardItem()
- item.setEditable(False)
- model.appendRow([DataSetItem(sfView, 'Data'), item])
-
- item = IsoSurfaceCount(sfView)
- item.setEditable(False)
- model.appendRow([IsoSurfaceGroup(sfView,
- self._isoLevelSliderNormalization,
- 'Isosurfaces'),
- item])
-
- item = qt.QStandardItem()
- item.setEditable(False)
- model.appendRow([PlaneGroup(sfView, 'Cutting Plane'), item])
-
- self.setModel(model)
-
- def setModel(self, model):
- """
- Reimplementation of the QTreeView.setModel method. It connects the
- rowsRemoved signal and opens the persistent editors.
-
- :param qt.QStandardItemModel model: the model
- """
-
- prevModel = self.model()
- if prevModel:
- self.__openPersistentEditors(qt.QModelIndex(), False)
- try:
- prevModel.rowsRemoved.disconnect(self.rowsRemoved)
- except TypeError:
- pass
-
- super(TreeView, self).setModel(model)
- model.rowsRemoved.connect(self.rowsRemoved)
- self.__openPersistentEditors(qt.QModelIndex())
-
- def __openPersistentEditors(self, parent=None, openEditor=True):
- """
- Opens or closes the items persistent editors.
-
- :param qt.QModelIndex parent: starting index, or None if the whole tree
- is to be considered.
- :param bool openEditor: True to open the editors, False to close them.
- """
- model = self.model()
-
- if not model:
- return
-
- if not parent or not parent.isValid():
- parent = self.model().invisibleRootItem().index()
-
- if openEditor:
- meth = self.openPersistentEditor
- else:
- meth = self.closePersistentEditor
-
- curParent = parent
- children = [model.index(row, 0, curParent)
- for row in range(model.rowCount(curParent))]
-
- columnCount = model.columnCount()
-
- while len(children) > 0:
- curParent = children.pop(-1)
-
- children.extend([model.index(row, 0, curParent)
- for row in range(model.rowCount(curParent))])
-
- for colIdx in range(columnCount):
- sibling = model.sibling(curParent.row(),
- colIdx,
- curParent)
- item = model.itemFromIndex(sibling)
- if isinstance(item, SubjectItem) and item.persistent:
- meth(sibling)
-
- def rowsAboutToBeRemoved(self, parent, start, end):
- """
- Reimplementation of the QTreeView.rowsAboutToBeRemoved. Closes all
- persistent editors under parent.
-
- :param qt.QModelIndex parent: Parent index
- :param int start: Start index from parent index (inclusive)
- :param int end: End index from parent index (inclusive)
- """
- self.__openPersistentEditors(parent, False)
- super(TreeView, self).rowsAboutToBeRemoved(parent, start, end)
-
- def rowsRemoved(self, parent, start, end):
- """
- Called when QTreeView.rowsRemoved is emitted. Opens all persistent
- editors under parent.
-
- :param qt.QModelIndex parent: Parent index
- :param int start: Start index from parent index (inclusive)
- :param int end: End index from parent index (inclusive)
- """
- super(TreeView, self).rowsRemoved(parent, start, end)
- self.__openPersistentEditors(parent, True)
-
- def rowsInserted(self, parent, start, end):
- """
- Reimplementation of the QTreeView.rowsInserted. Opens all persistent
- editors under parent.
-
- :param qt.QModelIndex parent: Parent index
- :param int start: Start index from parent index
- :param int end: End index from parent index
- """
- self.__openPersistentEditors(parent, False)
- super(TreeView, self).rowsInserted(parent, start, end)
- self.__openPersistentEditors(parent)
-
- def keyReleaseEvent(self, event):
- """
- Reimplementation of the QTreeView.keyReleaseEvent.
- At the moment only Key_Delete is handled. It calls the selected item's
- queryRemove method, and deleted the item if needed.
-
- :param qt.QKeyEvent event: A key event
- """
-
- # TODO : better filtering
- key = event.key()
- modifiers = event.modifiers()
-
- if key == qt.Qt.Key_Delete and modifiers == qt.Qt.NoModifier:
- self.__removeIsosurfaces()
-
- super(TreeView, self).keyReleaseEvent(event)
-
- def __removeIsosurfaces(self):
- model = self.model()
- selected = self.selectedIndexes()
- items = []
- # WARNING : the selection mode is set to single, so we re not
- # supposed to have more than one item here.
- # Multiple selection deletion has not been tested.
- # Watch out for index invalidation
- for index in selected:
- leftIndex = model.sibling(index.row(), 0, index)
- leftItem = model.itemFromIndex(leftIndex)
- if isinstance(leftItem, SubjectItem) and leftItem not in items:
- items.append(leftItem)
-
- isos = [item for item in items if isinstance(item, IsoSurfaceRootItem)]
- if isos:
- for iso in isos:
- if iso.queryRemove(self):
- parentItem = iso.parent()
- parentItem.removeRow(iso.row())
- else:
- qt.QMessageBox.information(
- self,
- 'Remove isosurface',
- 'Select an iso-surface to remove it')
-
- def __clicked(self, index):
- """
- Called when the QTreeView.clicked signal is emitted. Calls the item's
- leftClick method.
-
- :param qt.QIndex index: An index
- """
- item = self.model().itemFromIndex(index)
- if isinstance(item, SubjectItem):
- item.leftClicked()
-
- def __delegateEvent(self, task):
- if task == 'remove_iso':
- self.__removeIsosurfaces()
-
- def setIsoLevelSliderNormalization(self, normalization):
- """Set the normalization for iso level slider
-
- This MUST be called *before* :meth:`setSfView` to have an effect.
-
- :param str normalization: Either 'linear' or 'arcsinh'
- """
- assert normalization in ('linear', 'arcsinh')
- self._isoLevelSliderNormalization = normalization
diff --git a/silx/gui/plot3d/ScalarFieldView.py b/silx/gui/plot3d/ScalarFieldView.py
deleted file mode 100644
index b2bb254..0000000
--- a/silx/gui/plot3d/ScalarFieldView.py
+++ /dev/null
@@ -1,1552 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2020 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.
-#
-# ###########################################################################*/
-"""This module provides a window to view a 3D scalar field.
-
-It supports iso-surfaces, a cutting plane and the definition of
-a region of interest.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-import re
-import logging
-import time
-from collections import deque
-
-import numpy
-
-from silx.gui import qt, icons
-from silx.gui.colors import rgba
-from silx.gui.colors import Colormap
-
-from silx.math.marchingcubes import MarchingCubes
-from silx.math.combo import min_max
-
-from .scene import axes, cutplane, interaction, primitives, transform
-from . import scene
-from .Plot3DWindow import Plot3DWindow
-from .tools import InteractiveModeToolBar
-
-_logger = logging.getLogger(__name__)
-
-
-class Isosurface(qt.QObject):
- """Class representing an iso-surface
-
- :param parent: The View widget this iso-surface belongs to
- """
-
- sigLevelChanged = qt.Signal(float)
- """Signal emitted when the iso-surface level has changed.
-
- This signal provides the new level value (might be nan).
- """
-
- sigColorChanged = qt.Signal()
- """Signal emitted when the iso-surface color has changed"""
-
- sigVisibilityChanged = qt.Signal(bool)
- """Signal emitted when the iso-surface visibility has changed.
-
- This signal provides the new visibility status.
- """
-
- def __init__(self, parent):
- super(Isosurface, self).__init__(parent=parent)
- self._level = float('nan')
- self._autoLevelFunction = None
- self._color = rgba('#FFD700FF')
- self._data = None
- self._group = scene.Group()
-
- def _setData(self, data, copy=True):
- """Set the data set from which to build the iso-surface.
-
- :param numpy.ndarray data: The 3D dataset or None
- :param bool copy: True to make a copy, False to use as is if possible
- """
- if data is None:
- self._data = None
- else:
- self._data = numpy.array(data, copy=copy, order='C')
-
- self._update()
-
- def _get3DPrimitive(self):
- """Return the group containing the mesh of the iso-surface if any"""
- return self._group
-
- def isVisible(self):
- """Returns True if iso-surface is visible, else False"""
- return self._group.visible
-
- def setVisible(self, visible):
- """Set the visibility of the iso-surface in the view.
-
- :param bool visible: True to show the iso-surface, False to hide
- """
- visible = bool(visible)
- if visible != self._group.visible:
- self._group.visible = visible
- self.sigVisibilityChanged.emit(visible)
-
- def getLevel(self):
- """Return the level of this iso-surface (float)"""
- return self._level
-
- def setLevel(self, level):
- """Set the value at which to build the iso-surface.
-
- Setting this value reset auto-level function
-
- :param float level: The value at which to build the iso-surface
- """
- self._autoLevelFunction = None
- level = float(level)
- if level != self._level:
- self._level = level
- self._update()
- self.sigLevelChanged.emit(level)
-
- def isAutoLevel(self):
- """True if iso-level is rebuild for each data set."""
- return self.getAutoLevelFunction() is not None
-
- def getAutoLevelFunction(self):
- """Return the function computing the iso-level (callable or None)"""
- return self._autoLevelFunction
-
- def setAutoLevelFunction(self, autoLevel):
- """Set the function used to compute the iso-level.
-
- WARNING: The function might get called in a thread.
-
- :param callable autoLevel:
- A function taking a 3D numpy.ndarray of float32 and returning
- a float used as iso-level.
- Example: numpy.mean(data) + numpy.std(data)
- """
- assert callable(autoLevel)
- self._autoLevelFunction = autoLevel
- self._update()
-
- def getColor(self):
- """Return the color of this iso-surface (QColor)"""
- return qt.QColor.fromRgbF(*self._color)
-
- def setColor(self, color):
- """Set the color of the iso-surface
-
- :param color: RGBA color of the isosurface
- :type color: QColor, str or array-like of 4 float in [0., 1.]
- """
- color = rgba(color)
- if color != self._color:
- self._color = color
- if len(self._group.children) != 0:
- self._group.children[0].setAttribute('color', self._color)
- self.sigColorChanged.emit()
-
- def _update(self):
- """Update underlying mesh"""
- self._group.children = []
-
- if self._data is None:
- if self.isAutoLevel():
- self._level = float('nan')
-
- else:
- if self.isAutoLevel():
- st = time.time()
- try:
- level = float(self.getAutoLevelFunction()(self._data))
-
- except Exception:
- module = self.getAutoLevelFunction().__module__
- name = self.getAutoLevelFunction().__name__
- _logger.error(
- "Error while executing iso level function %s.%s",
- module,
- name,
- exc_info=True)
- level = float('nan')
-
- else:
- _logger.info(
- 'Computed iso-level in %f s.', time.time() - st)
-
- if level != self._level:
- self._level = level
- self.sigLevelChanged.emit(level)
-
- if not numpy.isfinite(self._level):
- return
-
- st = time.time()
- vertices, normals, indices = MarchingCubes(
- self._data,
- isolevel=self._level)
- _logger.info('Computed iso-surface in %f s.', time.time() - st)
-
- if len(vertices) == 0:
- return
- else:
- mesh = primitives.Mesh3D(vertices,
- colors=self._color,
- normals=normals,
- mode='triangles',
- indices=indices)
- self._group.children = [mesh]
-
-
-class SelectedRegion(object):
- """Selection of a 3D region aligned with the axis.
-
- :param arrayRange: Range of the selection in the array
- ((zmin, zmax), (ymin, ymax), (xmin, xmax))
- :param dataBBox: Bounding box of the selection in data coordinates
- ((xmin, xmax), (ymin, ymax), (zmin, zmax))
- :param translation: Offset from array to data coordinates (ox, oy, oz)
- :param scale: Scale from array to data coordinates (sx, sy, sz)
- """
-
- def __init__(self, arrayRange, dataBBox,
- translation=(0., 0., 0.),
- scale=(1., 1., 1.)):
- self._arrayRange = numpy.array(arrayRange, copy=True, dtype=numpy.int64)
- assert self._arrayRange.shape == (3, 2)
- assert numpy.all(self._arrayRange[:, 1] >= self._arrayRange[:, 0])
-
- self._dataRange = dataBBox
-
- self._translation = numpy.array(translation, dtype=numpy.float32)
- assert self._translation.shape == (3,)
- self._scale = numpy.array(scale, dtype=numpy.float32)
- assert self._scale.shape == (3,)
-
- def getArrayRange(self):
- """Returns array ranges of the selection: 3x2 array of int
-
- :return: A numpy array with ((zmin, zmax), (ymin, ymax), (xmin, xmax))
- :rtype: numpy.ndarray
- """
- return self._arrayRange.copy()
-
- def getArraySlices(self):
- """Slices corresponding to the selected range in the array
-
- :return: A numpy array with (zslice, yslice, zslice)
- :rtype: numpy.ndarray
- """
- return (slice(*self._arrayRange[0]),
- slice(*self._arrayRange[1]),
- slice(*self._arrayRange[2]))
-
- def getDataRange(self):
- """Range in the data coordinates of the selection: 3x2 array of float
-
- When the transform matrix is not the identity matrix
- (e.g., rotation, skew) the returned range is the one of the selected
- region bounding box in data coordinates.
-
- :return: A numpy array with ((xmin, xmax), (ymin, ymax), (zmin, zmax))
- :rtype: numpy.ndarray
- """
- return self._dataRange.copy()
-
- def getDataScale(self):
- """Scale from array to data coordinates: (sx, sy, sz)
-
- :return: A numpy array with (sx, sy, sz)
- :rtype: numpy.ndarray
- """
- return self._scale.copy()
-
- def getDataTranslation(self):
- """Offset from array to data coordinates: (ox, oy, oz)
-
- :return: A numpy array with (ox, oy, oz)
- :rtype: numpy.ndarray
- """
- return self._translation.copy()
-
-
-class CutPlane(qt.QObject):
- """Class representing a cutting plane
-
- :param ~silx.gui.plot3d.ScalarFieldView.ScalarFieldView sfView:
- Widget in which the cut plane is applied.
- """
-
- sigVisibilityChanged = qt.Signal(bool)
- """Signal emitted when the cut visibility has changed.
-
- This signal provides the new visibility status.
- """
-
- sigDataChanged = qt.Signal()
- """Signal emitted when the data this plane is cutting has changed."""
-
- sigPlaneChanged = qt.Signal()
- """Signal emitted when the cut plane has moved"""
-
- sigColormapChanged = qt.Signal(Colormap)
- """Signal emitted when the colormap has changed
-
- This signal provides the new colormap.
- """
-
- sigTransparencyChanged = qt.Signal()
- """Signal emitted when the transparency of the plane has changed.
-
- This signal is emitted when calling :meth:`setDisplayValuesBelowMin`.
- """
-
- sigInterpolationChanged = qt.Signal(str)
- """Signal emitted when the cut plane interpolation has changed
-
- This signal provides the new interpolation mode.
- """
-
- def __init__(self, sfView):
- super(CutPlane, self).__init__(parent=sfView)
-
- self._dataRange = None
- self._visible = False
-
- self.__syncPlane = True
-
- # Plane stroke on the outer bounding box
- self._planeStroke = primitives.PlaneInGroup(normal=(0, 1, 0))
- self._planeStroke.visible = self._visible
- self._planeStroke.addListener(self._planeChanged)
- self._planeStroke.plane.addListener(self._planePositionChanged)
-
- # Plane with texture on the data bounding box
- self._dataPlane = cutplane.CutPlane(normal=(0, 1, 0))
- self._dataPlane.strokeVisible = False
- self._dataPlane.alpha = 1.
- self._dataPlane.visible = self._visible
- self._dataPlane.plane.addListener(self._planePositionChanged)
-
- self._colormap = Colormap(
- name='gray', normalization='linear', vmin=None, vmax=None)
- self.getColormap().sigChanged.connect(self._colormapChanged)
- self._updateSceneColormap()
-
- sfView.sigDataChanged.connect(self._sfViewDataChanged)
- sfView.sigTransformChanged.connect(self._sfViewTransformChanged)
-
- def _get3DPrimitives(self):
- """Return the cut plane scene node."""
- return self._planeStroke, self._dataPlane
-
- def _keepPlaneInBBox(self):
- """Makes sure the plane intersect its parent bounding box if any"""
- bounds = self._planeStroke.parent.bounds(dataBounds=True)
- if bounds is not None:
- self._planeStroke.plane.point = numpy.clip(
- self._planeStroke.plane.point,
- a_min=bounds[0], a_max=bounds[1])
-
- @staticmethod
- def _syncPlanes(master, slave):
- """Move slave PlaneInGroup so that it is coplanar with master.
-
- :param PlaneInGroup master: Reference PlaneInGroup
- :param PlaneInGroup slave: PlaneInGroup to align
- """
- masterToSlave = transform.StaticTransformList([
- slave.objectToSceneTransform.inverse(),
- master.objectToSceneTransform])
-
- point = masterToSlave.transformPoint(
- master.plane.point)
- normal = masterToSlave.transformNormal(
- master.plane.normal)
- slave.plane.setPlane(point, normal)
-
- def _sfViewDataChanged(self):
- """Handle data change in the ScalarFieldView this plane belongs to"""
- self._dataPlane.setData(self.sender().getData(), copy=False)
-
- # Store data range info as 3-tuple of values
- self._dataRange = self.sender().getDataRange()
-
- self.sigDataChanged.emit()
-
- # Update colormap range when autoscale
- if self.getColormap().isAutoscale():
- self._updateSceneColormap()
-
- self._keepPlaneInBBox()
-
- def _sfViewTransformChanged(self):
- """Handle transform changed in the ScalarFieldView"""
- self._keepPlaneInBBox()
- self._syncPlanes(master=self._planeStroke,
- slave=self._dataPlane)
- self.sigPlaneChanged.emit()
-
- def _planeChanged(self, source, *args, **kwargs):
- """Handle events from the plane primitive"""
- # Using _visible for now, until scene as more info in events
- if source.visible != self._visible:
- self._visible = source.visible
- self.sigVisibilityChanged.emit(source.visible)
-
- def _planePositionChanged(self, source, *args, **kwargs):
- """Handle update of cut plane position and normal"""
- if self.__syncPlane:
- self.__syncPlane = False
- if source is self._planeStroke.plane:
- self._syncPlanes(master=self._planeStroke,
- slave=self._dataPlane)
- elif source is self._dataPlane.plane:
- self._syncPlanes(master=self._dataPlane,
- slave=self._planeStroke)
- else:
- _logger.error('Received an unknown object %s',
- str(source))
-
- if self._planeStroke.visible or self._dataPlane.visible:
- self.sigPlaneChanged.emit()
-
- self.__syncPlane = True
-
- # Plane position
-
- def moveToCenter(self):
- """Move cut plane to center of data set"""
- self._planeStroke.moveToCenter()
-
- def isValid(self):
- """Returns whether the cut plane is defined or not (bool)"""
- return self._planeStroke.isValid
-
- def _plane(self, coordinates='array'):
- """Returns the scene plane to set.
-
- :param str coordinates: The coordinate system to use:
- Either 'scene' or 'array' (default)
- :rtype: Plane
- :raise ValueError: If coordinates is not correct
- """
- if coordinates == 'scene':
- return self._planeStroke.plane
- elif coordinates == 'array':
- return self._dataPlane.plane
- else:
- raise ValueError(
- 'Unsupported coordinates: %s' % str(coordinates))
-
- def getNormal(self, coordinates='array'):
- """Returns the normal of the plane (as a unit vector)
-
- :param str coordinates: The coordinate system to use:
- Either 'scene' or 'array' (default)
- :return: Normal (nx, ny, nz), vector is 0 if no plane is defined
- :rtype: numpy.ndarray
- :raise ValueError: If coordinates is not correct
- """
- return self._plane(coordinates).normal
-
- def setNormal(self, normal, coordinates='array'):
- """Set the normal of the plane.
-
- :param normal: 3-tuple of float: nx, ny, nz
- :param str coordinates: The coordinate system to use:
- Either 'scene' or 'array' (default)
- :raise ValueError: If coordinates is not correct
- """
- self._plane(coordinates).normal = normal
-
- def getPoint(self, coordinates='array'):
- """Returns a point on the plane.
-
- :param str coordinates: The coordinate system to use:
- Either 'scene' or 'array' (default)
- :return: (x, y, z)
- :rtype: numpy.ndarray
- :raise ValueError: If coordinates is not correct
- """
- return self._plane(coordinates).point
-
- def setPoint(self, point, constraint=True, coordinates='array'):
- """Set a point contained in the plane.
-
- Warning: The plane might not intersect the bounding box of the data.
-
- :param point: (x, y, z) position
- :type point: 3-tuple of float
- :param bool constraint:
- True (default) to make sure the plane intersect data bounding box,
- False to set the plane without any constraint.
- :raise ValueError: If coordinates is not correc
- """
- self._plane(coordinates).point = point
- if constraint:
- self._keepPlaneInBBox()
-
- def getParameters(self, coordinates='array'):
- """Returns the plane equation parameters: a*x + b*y + c*z + d = 0
-
- :param str coordinates: The coordinate system to use:
- Either 'scene' or 'array' (default)
- :return: Plane equation parameters: (a, b, c, d)
- :rtype: numpy.ndarray
- :raise ValueError: If coordinates is not correct
- """
- return self._plane(coordinates).parameters
-
- def setParameters(self, parameters, constraint=True, coordinates='array'):
- """Set the plane equation parameters: a*x + b*y + c*z + d = 0
-
- Warning: The plane might not intersect the bounding box of the data.
-
- :param parameters: (a, b, c, d) plane equation parameters.
- :type parameters: 4-tuple of float
- :param bool constraint:
- True (default) to make sure the plane intersect data bounding box,
- False to set the plane without any constraint.
- :raise ValueError: If coordinates is not correc
- """
- self._plane(coordinates).parameters = parameters
- if constraint:
- self._keepPlaneInBBox()
-
- # Visibility
-
- def isVisible(self):
- """Returns True if the plane is visible, False otherwise"""
- return self._planeStroke.visible
-
- def setVisible(self, visible):
- """Set the visibility of the plane
-
- :param bool visible: True to make plane visible
- """
- visible = bool(visible)
- self._planeStroke.visible = visible
- self._dataPlane.visible = visible
-
- # Border stroke
-
- def getStrokeColor(self):
- """Returns the color of the plane border (QColor)"""
- return qt.QColor.fromRgbF(*self._planeStroke.color)
-
- def setStrokeColor(self, color):
- """Set the color of the plane border.
-
- :param color: RGB color: name, #RRGGBB or RGB values
- :type color:
- QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
- """
- color = rgba(color)
- self._planeStroke.color = color
- self._dataPlane.color = color
-
- # Data
-
- def getImageData(self):
- """Returns the data and information corresponding to the cut plane.
-
- The returned data is not interpolated,
- it is a slice of the 3D scalar field.
-
- Image data axes are so that plane normal is towards the point of view.
-
- :return: An object containing the 2D data slice and information
- """
- return _CutPlaneImage(self)
-
- # Interpolation
-
- def getInterpolation(self):
- """Returns the interpolation used to display to cut plane.
-
- :return: 'nearest' or 'linear'
- :rtype: str
- """
- return self._dataPlane.interpolation
-
- def setInterpolation(self, interpolation):
- """Set the interpolation used to display to cut plane
-
- The default interpolation is 'linear'
-
- :param str interpolation: 'nearest' or 'linear'
- """
- if interpolation != self.getInterpolation():
- self._dataPlane.interpolation = interpolation
- self.sigInterpolationChanged.emit(interpolation)
-
- # Colormap
-
- # def getAlpha(self):
- # """Returns the transparency of the plane as a float in [0., 1.]"""
- # return self._plane.alpha
-
- # def setAlpha(self, alpha):
- # """Set the plane transparency.
- #
- # :param float alpha: Transparency in [0., 1]
- # """
- # self._plane.alpha = alpha
-
- def getDisplayValuesBelowMin(self):
- """Return whether values <= colormap min are displayed or not.
-
- :rtype: bool
- """
- return self._dataPlane.colormap.displayValuesBelowMin
-
- def setDisplayValuesBelowMin(self, display):
- """Set whether to display values <= colormap min.
-
- :param bool display: True to show values below min,
- False to discard them
- """
- display = bool(display)
- if display != self.getDisplayValuesBelowMin():
- self._dataPlane.colormap.displayValuesBelowMin = display
- self.sigTransparencyChanged.emit()
-
- def getColormap(self):
- """Returns the colormap set by :meth:`setColormap`.
-
- :return: The colormap
- :rtype: ~silx.gui.colors.Colormap
- """
- return self._colormap
-
- def setColormap(self,
- name='gray',
- norm=None,
- vmin=None,
- vmax=None):
- """Set the colormap to use.
-
- By either providing a :class:`Colormap` object or
- its name, normalization and range.
-
- :param name: Name of the colormap in
- 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
- Or Colormap object.
- :type name: str or ~silx.gui.colors.Colormap
- :param str norm: Colormap mapping: 'linear' or 'log'.
- :param float vmin: The minimum value of the range or None for autoscale
- :param float vmax: The maximum value of the range or None for autoscale
- """
- _logger.debug('setColormap %s %s (%s, %s)',
- name, str(norm), str(vmin), str(vmax))
-
- self._colormap.sigChanged.disconnect(self._colormapChanged)
-
- if isinstance(name, Colormap): # Use it as it is
- assert (norm, vmin, vmax) == (None, None, None)
- self._colormap = name
- else:
- if norm is None:
- norm = 'linear'
- self._colormap = Colormap(
- name=name, normalization=norm, vmin=vmin, vmax=vmax)
-
- self._colormap.sigChanged.connect(self._colormapChanged)
- self._colormapChanged()
-
- def getColormapEffectiveRange(self):
- """Returns the currently used range of the colormap.
-
- This range is computed from the data set if colormap is in autoscale.
- Range is clipped to positive values when using log scale.
-
- :return: 2-tuple of float
- """
- return self._dataPlane.colormap.range_
-
- def _updateSceneColormap(self):
- """Synchronizes scene's colormap with Colormap object"""
- colormap = self.getColormap()
- sceneCMap = self._dataPlane.colormap
-
- sceneCMap.colormap = colormap.getNColors()
-
- sceneCMap.norm = colormap.getNormalization()
- range_ = colormap.getColormapRange(data=self._dataRange)
- sceneCMap.range_ = range_
-
- def _colormapChanged(self):
- """Handle update of Colormap object"""
- self._updateSceneColormap()
- # Forward colormap changed event
- self.sigColormapChanged.emit(self.getColormap())
-
-
-class _CutPlaneImage(object):
- """Object representing the data sliced by a cut plane
-
- :param CutPlane cutPlane: The CutPlane from which to generate image info
- """
-
- def __init__(self, cutPlane):
- # Init attributes with default values
- self._isValid = False
- self._data = numpy.zeros((0, 0), dtype=numpy.float32)
- self._index = 0
- self._xLabel = ''
- self._yLabel = ''
- self._normalLabel = ''
- self._scale = float('nan'), float('nan')
- self._translation = float('nan'), float('nan')
- self._position = float('nan')
-
- sfView = cutPlane.parent()
- if not sfView or not cutPlane.isValid():
- _logger.info("No plane available")
- return
-
- data = sfView.getData(copy=False)
- if data is None:
- _logger.info("No data available")
- return
-
- normal = cutPlane.getNormal(coordinates='array')
- point = cutPlane.getPoint(coordinates='array')
-
- if numpy.linalg.norm(numpy.cross(normal, (1., 0., 0.))) < 0.0017:
- if not 0 <= point[0] <= data.shape[2]:
- _logger.info("Plane outside dataset")
- return
- index = max(0, min(int(point[0]), data.shape[2] - 1))
- slice_ = data[:, :, index]
- xAxisIndex, yAxisIndex, normalAxisIndex = 1, 2, 0 # y, z, x
-
- elif numpy.linalg.norm(numpy.cross(normal, (0., 1., 0.))) < 0.0017:
- if not 0 <= point[1] <= data.shape[1]:
- _logger.info("Plane outside dataset")
- return
- index = max(0, min(int(point[1]), data.shape[1] - 1))
- slice_ = numpy.transpose(data[:, index, :])
- xAxisIndex, yAxisIndex, normalAxisIndex = 2, 0, 1 # z, x, y
-
- elif numpy.linalg.norm(numpy.cross(normal, (0., 0., 1.))) < 0.0017:
- if not 0 <= point[2] <= data.shape[0]:
- _logger.info("Plane outside dataset")
- return
- index = max(0, min(int(point[2]), data.shape[0] - 1))
- slice_ = data[index, :, :]
- xAxisIndex, yAxisIndex, normalAxisIndex = 0, 1, 2 # x, y, z
- else:
- _logger.warning('Unsupported normal: (%f, %f, %f)',
- normal[0], normal[1], normal[2])
- return
-
- # Store cut plane image info
-
- self._isValid = True
- self._data = numpy.array(slice_, copy=True)
- self._index = index
-
- # Only store extra information when no transform matrix is set
- # Otherwise this information can be meaningless
- if numpy.all(numpy.equal(sfView.getTransformMatrix(),
- numpy.identity(3, dtype=numpy.float32))):
- labels = sfView.getAxesLabels()
- self._xLabel = labels[xAxisIndex]
- self._yLabel = labels[yAxisIndex]
- self._normalLabel = labels[normalAxisIndex]
-
- scale = sfView.getScale()
- self._scale = scale[xAxisIndex], scale[yAxisIndex]
-
- translation = sfView.getTranslation()
- self._translation = translation[xAxisIndex], translation[yAxisIndex]
-
- self._position = float(index * scale[normalAxisIndex] +
- translation[normalAxisIndex])
-
- def isValid(self):
- """Returns True if the cut plane image is defined (bool)"""
- return self._isValid
-
- def getData(self, copy=True):
- """Returns the image data sliced by the cut plane.
-
- :param bool copy: True to get a copy, False otherwise
- :return: The 2D image data corresponding to the cut plane
- :rtype: numpy.ndarray
- """
- return numpy.array(self._data, copy=copy)
-
- def getXLabel(self):
- """Returns the label associated to the X axis of the image (str)"""
- return self._xLabel
-
- def getYLabel(self):
- """Returns the label associated to the Y axis of the image (str)"""
- return self._yLabel
-
- def getNormalLabel(self):
- """Returns the label of the 3D axis of the plane normal (str)"""
- return self._normalLabel
-
- def getScale(self):
- """Returns the scales of the data as a 2-tuple of float (sx, sy)"""
- return self._scale
-
- def getTranslation(self):
- """Returns the offset of the data as a 2-tuple of float (ox, oy)"""
- return self._translation
-
- def getIndex(self):
- """Returns the index in the data array of the cut plane (int)"""
- return self._index
-
- def getPosition(self):
- """Returns the cut plane position along the normal axis (flaot)"""
- return self._position
-
-
-class ScalarFieldView(Plot3DWindow):
- """Widget computing and displaying an iso-surface from a 3D scalar dataset.
-
- Limitation: Currently, iso-surfaces are generated with higher values
- than the iso-level 'inside' the surface.
-
- :param parent: See :class:`QMainWindow`
- """
-
- sigDataChanged = qt.Signal()
- """Signal emitted when the scalar data field has changed."""
-
- sigTransformChanged = qt.Signal()
- """Signal emitted when the transformation has changed.
-
- It is emitted by :meth:`setTranslation`, :meth:`setTransformMatrix`,
- :meth:`setScale`.
- """
-
- sigSelectedRegionChanged = qt.Signal(object)
- """Signal emitted when the selected region has changed.
-
- This signal provides the new selected region.
- """
-
- def __init__(self, parent=None):
- super(ScalarFieldView, self).__init__(parent)
- self._colormap = Colormap(
- name='gray', normalization='linear', vmin=None, vmax=None)
- self._selectedRange = None
-
- # Store iso-surfaces
- self._isosurfaces = []
-
- # Transformations
- self._dataScale = transform.Scale()
- self._dataTranslate = transform.Translate()
- self._dataTransform = transform.Matrix() # default to identity
-
- self._foregroundColor = 1., 1., 1., 1.
- self._highlightColor = 0.7, 0.7, 0., 1.
-
- self._data = None
- self._dataRange = None
-
- self._group = primitives.BoundedGroup()
- self._group.transforms = [
- self._dataTranslate, self._dataTransform, self._dataScale]
-
- self._bbox = axes.LabelledAxes()
- self._bbox.children = [self._group]
- self._outerScale = transform.Scale(1., 1., 1.)
- self._bbox.transforms = [self._outerScale]
- self.getPlot3DWidget().viewport.scene.children.append(self._bbox)
-
- self._selectionBox = primitives.Box()
- self._selectionBox.strokeSmooth = False
- self._selectionBox.strokeWidth = 1.
- # self._selectionBox.fillColor = 1., 1., 1., 0.3
- # self._selectionBox.fillCulling = 'back'
- self._selectionBox.visible = False
- self._group.children.append(self._selectionBox)
-
- self._cutPlane = CutPlane(sfView=self)
- self._cutPlane.sigVisibilityChanged.connect(
- self._planeVisibilityChanged)
- planeStroke, dataPlane = self._cutPlane._get3DPrimitives()
- self._bbox.children.append(planeStroke)
- self._group.children.append(dataPlane)
-
- self._isogroup = primitives.GroupDepthOffset()
- self._isogroup.transforms = [
- # Convert from z, y, x from marching cubes to x, y, z
- transform.Matrix((
- (0., 0., 1., 0.),
- (0., 1., 0., 0.),
- (1., 0., 0., 0.),
- (0., 0., 0., 1.))),
- # Offset to match cutting plane coords
- transform.Translate(0.5, 0.5, 0.5)
- ]
- self._group.children.append(self._isogroup)
-
- self._initPanPlaneAction()
-
- self._updateColors()
-
- self.getPlot3DWidget().viewport.light.shininess = 32
-
- def saveConfig(self, ioDevice):
- """
- Saves this view state. Only isosurfaces at the moment. Does not save
- the isosurface's function.
-
- :param qt.QIODevice ioDevice: A `qt.QIODevice`.
- """
-
- stream = qt.QDataStream(ioDevice)
-
- stream.writeString('<ScalarFieldView>')
-
- isoSurfaces = self.getIsosurfaces()
-
- nIsoSurfaces = len(isoSurfaces)
-
- # TODO : delegate the serialization to the serialized items
- # isosurfaces
- if nIsoSurfaces:
- tagIn = '<IsoSurfaces nIso={0}>'.format(nIsoSurfaces)
- stream.writeString(tagIn)
-
- for surface in isoSurfaces:
- color = surface.getColor()
- level = surface.getLevel()
- visible = surface.isVisible()
- stream << color
- stream.writeDouble(level)
- stream.writeBool(visible)
-
- stream.writeString('</IsoSurfaces>')
-
- stream.writeString('<Style>')
- background = self.getBackgroundColor()
- foreground = self.getForegroundColor()
- highlight = self.getHighlightColor()
- stream << background << foreground << highlight
- stream.writeString('</Style>')
-
- stream.writeString('</ScalarFieldView>')
-
- def loadConfig(self, ioDevice):
- """
- Loads this view state.
- See ScalarFieldView.saveView to know what is supported at the moment.
-
- :param qt.QIODevice ioDevice: A `qt.QIODevice`.
- """
-
- tagStack = deque()
-
- tagInRegex = re.compile('<(?P<itemId>[^ /]*) *'
- '(?P<args>.*)>')
-
- tagOutRegex = re.compile('</(?P<itemId>[^ ]*)>')
-
- tagRootInRegex = re.compile('<ScalarFieldView>')
-
- isoSurfaceArgsRegex = re.compile('nIso=(?P<nIso>[0-9]*)')
-
- stream = qt.QDataStream(ioDevice)
-
- tag = stream.readString()
- tagMatch = tagRootInRegex.match(tag)
-
- if tagMatch is None:
- # TODO : explicit error
- raise ValueError('Unknown data.')
-
- itemId = 'ScalarFieldView'
-
- tagStack.append(itemId)
-
- while True:
-
- tag = stream.readString()
-
- tagMatch = tagOutRegex.match(tag)
- if tagMatch:
- closeId = tagMatch.groupdict()['itemId']
- if closeId != itemId:
- # TODO : explicit error
- raise ValueError('Unexpected closing tag {0} '
- '(expected {1})'
- ''.format(closeId, itemId))
-
- if itemId == 'ScalarFieldView':
- # reached end
- break
- else:
- itemId = tagStack.pop()
- # fetching next tag
- continue
-
- tagMatch = tagInRegex.match(tag)
-
- if tagMatch is None:
- # TODO : explicit error
- raise ValueError('Unknown data.')
-
- tagStack.append(itemId)
-
- matchDict = tagMatch.groupdict()
-
- itemId = matchDict['itemId']
-
- # TODO : delegate the deserialization to the serialized items
- if itemId == 'IsoSurfaces':
- argsMatch = isoSurfaceArgsRegex.match(matchDict['args'])
- if not argsMatch:
- # TODO : explicit error
- raise ValueError('Failed to parse args "{0}".'
- ''.format(matchDict['args']))
- argsDict = argsMatch.groupdict()
- nIso = int(argsDict['nIso'])
- if nIso:
- for surface in self.getIsosurfaces():
- self.removeIsosurface(surface)
- for isoIdx in range(nIso):
- color = qt.QColor()
- stream >> color
- level = stream.readDouble()
- visible = stream.readBool()
- surface = self.addIsosurface(level, color=color)
- surface.setVisible(visible)
- elif itemId == 'Style':
- background = qt.QColor()
- foreground = qt.QColor()
- highlight = qt.QColor()
- stream >> background >> foreground >> highlight
- self.setBackgroundColor(background)
- self.setForegroundColor(foreground)
- self.setHighlightColor(highlight)
- else:
- raise ValueError('Unknown entry tag {0}.'
- ''.format(itemId))
-
- def _initPanPlaneAction(self):
- """Creates and init the pan plane action"""
- self._panPlaneAction = qt.QAction(self)
- self._panPlaneAction.setIcon(icons.getQIcon('3d-plane-pan'))
- self._panPlaneAction.setText('Pan plane')
- self._panPlaneAction.setCheckable(True)
- self._panPlaneAction.setToolTip(
- 'Pan the cutting plane. Press <b>Ctrl</b> to rotate the scene.')
- self._panPlaneAction.setEnabled(False)
-
- self._panPlaneAction.triggered[bool].connect(self._planeActionTriggered)
- self.getPlot3DWidget().sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
-
- toolbar = self.findChild(InteractiveModeToolBar)
- if toolbar is not None:
- toolbar.addAction(self._panPlaneAction)
-
- def _planeActionTriggered(self, checked=False):
- self._panPlaneAction.setChecked(True)
- self.setInteractiveMode('plane')
-
- def _interactiveModeChanged(self):
- self._panPlaneAction.setChecked(self.getInteractiveMode() == 'plane')
- self._updateColors()
-
- def _planeVisibilityChanged(self, visible):
- """Handle visibility events from the plane"""
- if visible != self._panPlaneAction.isEnabled():
- self._panPlaneAction.setEnabled(visible)
- if visible:
- self.setInteractiveMode('plane')
- elif self._panPlaneAction.isChecked():
- self.setInteractiveMode('rotate')
-
- def setInteractiveMode(self, mode):
- """Choose the current interaction.
-
- :param str mode: Either rotate, pan or plane
- """
- if mode == self.getInteractiveMode():
- return
-
- sceneScale = self.getPlot3DWidget().viewport.scene.transforms[0]
- if mode == 'plane':
- mode = interaction.PanPlaneZoomOnWheelControl(
- self.getPlot3DWidget().viewport,
- self._cutPlane._get3DPrimitives()[0],
- mode='position',
- orbitAroundCenter=False,
- scaleTransform=sceneScale)
-
- self.getPlot3DWidget().setInteractiveMode(mode)
- self._updateColors()
-
- def getInteractiveMode(self):
- """Returns the current interaction mode, see :meth:`setInteractiveMode`
- """
- if isinstance(self.getPlot3DWidget().eventHandler,
- interaction.PanPlaneZoomOnWheelControl):
- return 'plane'
- else:
- return self.getPlot3DWidget().getInteractiveMode()
-
- # Handle scalar field
-
- def setData(self, data, copy=True):
- """Set the 3D scalar data set to use for building the iso-surface.
-
- Dataset order is zyx (i.e., first dimension is z).
-
- :param data: scalar field from which to extract the iso-surface
- :type data: 3D numpy.ndarray of float32 with shape at least (2, 2, 2)
- :param bool copy:
- True (default) to make a copy,
- False to avoid copy (DO NOT MODIFY data afterwards)
- """
- if data is None:
- self._data = None
- self._dataRange = None
- self.setSelectedRegion(zrange=None, yrange=None, xrange_=None)
- self._group.shape = None
- self.centerScene()
-
- else:
- data = numpy.array(data, copy=copy, dtype=numpy.float32, order='C')
- assert data.ndim == 3
- assert min(data.shape) >= 2
-
- wasData = self._data is not None
- previousSelectedRegion = self.getSelectedRegion()
-
- self._data = data
-
- # Store data range info
- dataRange = min_max(self._data, min_positive=True, finite=True)
- if dataRange.minimum is None: # Only non-finite data
- dataRange = None
-
- if dataRange is not None:
- min_positive = dataRange.min_positive
- if min_positive is None:
- min_positive = float('nan')
- dataRange = dataRange.minimum, min_positive, dataRange.maximum
- self._dataRange = dataRange
-
- if previousSelectedRegion is not None:
- # Update selected region to ensure it is clipped to array range
- self.setSelectedRegion(*previousSelectedRegion.getArrayRange())
-
- self._group.shape = self._data.shape
-
- if not wasData:
- self.centerScene() # Reset viewpoint the first time only
-
- # Update iso-surfaces
- for isosurface in self.getIsosurfaces():
- isosurface._setData(self._data, copy=False)
-
- self.sigDataChanged.emit()
-
- def getData(self, copy=True):
- """Get the 3D scalar data currently used to build the iso-surface.
-
- :param bool copy:
- True (default) to get a copy,
- False to get the internal data (DO NOT modify!)
- :return: The data set (or None if not set)
- """
- if self._data is None:
- return None
- else:
- return numpy.array(self._data, copy=copy)
-
- def getDataRange(self):
- """Return the range of the data as a 3-tuple of values.
-
- positive min is NaN if no data is positive.
-
- :return: (min, positive min, max) or None.
- """
- return self._dataRange
-
- # Transformations
-
- def setOuterScale(self, sx=1., sy=1., sz=1.):
- """Set the scale to apply to the whole scene including the axes.
-
- This is useful when axis lengths in data space are really different.
-
- :param float sx: Scale factor along the X axis
- :param float sy: Scale factor along the Y axis
- :param float sz: Scale factor along the Z axis
- """
- self._outerScale.setScale(sx, sy, sz)
- self.centerScene()
-
- def getOuterScale(self):
- """Returns the scales provided by :meth:`setOuterScale`.
-
- :rtype: numpy.ndarray
- """
- return self._outerScale.scale
-
- def setScale(self, sx=1., sy=1., sz=1.):
- """Set the scale of the 3D scalar field (i.e., size of a voxel).
-
- :param float sx: Scale factor along the X axis
- :param float sy: Scale factor along the Y axis
- :param float sz: Scale factor along the Z axis
- """
- scale = numpy.array((sx, sy, sz), dtype=numpy.float32)
- if not numpy.all(numpy.equal(scale, self.getScale())):
- self._dataScale.scale = scale
- self.sigTransformChanged.emit()
- self.centerScene() # Reset viewpoint
-
- def getScale(self):
- """Returns the scales provided by :meth:`setScale` as a numpy.ndarray.
- """
- return self._dataScale.scale
-
- def setTranslation(self, x=0., y=0., z=0.):
- """Set the translation of the origin of the data array in data coordinates.
-
- :param float x: Offset of the data origin on the X axis
- :param float y: Offset of the data origin on the Y axis
- :param float z: Offset of the data origin on the Z axis
- """
- translation = numpy.array((x, y, z), dtype=numpy.float32)
- if not numpy.all(numpy.equal(translation, self.getTranslation())):
- self._dataTranslate.translation = translation
- self.sigTransformChanged.emit()
- self.centerScene() # Reset viewpoint
-
- def getTranslation(self):
- """Returns the offset set by :meth:`setTranslation` as a numpy.ndarray.
- """
- return self._dataTranslate.translation
-
- def setTransformMatrix(self, matrix3x3):
- """Set the transform matrix applied to the data.
-
- :param numpy.ndarray matrix: 3x3 transform matrix
- """
- matrix3x3 = numpy.array(matrix3x3, copy=True, dtype=numpy.float32)
- if not numpy.all(numpy.equal(matrix3x3, self.getTransformMatrix())):
- matrix = numpy.identity(4, dtype=numpy.float32)
- matrix[:3, :3] = matrix3x3
- self._dataTransform.setMatrix(matrix)
- self.sigTransformChanged.emit()
- self.centerScene() # Reset viewpoint
-
- def getTransformMatrix(self):
- """Returns the transform matrix applied to the data.
-
- See :meth:`setTransformMatrix`.
-
- :rtype: numpy.ndarray
- """
- return self._dataTransform.getMatrix()[:3, :3]
-
- # Axes labels
-
- def isBoundingBoxVisible(self):
- """Returns axes labels, grid and bounding box visibility.
-
- :rtype: bool
- """
- return self._bbox.boxVisible
-
- def setBoundingBoxVisible(self, visible):
- """Set axes labels, grid and bounding box visibility.
-
- :param bool visible: True to show axes, False to hide
- """
- visible = bool(visible)
- self._bbox.boxVisible = visible
-
- def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None):
- """Set the text labels of the axes.
-
- :param str xlabel: Label of the X axis, None to leave unchanged.
- :param str ylabel: Label of the Y axis, None to leave unchanged.
- :param str zlabel: Label of the Z axis, None to leave unchanged.
- """
- if xlabel is not None:
- self._bbox.xlabel = xlabel
-
- if ylabel is not None:
- self._bbox.ylabel = ylabel
-
- if zlabel is not None:
- self._bbox.zlabel = zlabel
-
- class _Labels(tuple):
- """Return type of :meth:`getAxesLabels`"""
-
- def getXLabel(self):
- """Label of the X axis (str)"""
- return self[0]
-
- def getYLabel(self):
- """Label of the Y axis (str)"""
- return self[1]
-
- def getZLabel(self):
- """Label of the Z axis (str)"""
- return self[2]
-
- def getAxesLabels(self):
- """Returns the text labels of the axes
-
- >>> widget = ScalarFieldView()
- >>> widget.setAxesLabels(xlabel='X')
-
- You can get the labels either as a 3-tuple:
-
- >>> xlabel, ylabel, zlabel = widget.getAxesLabels()
-
- Or as an object with methods getXLabel, getYLabel and getZLabel:
-
- >>> labels = widget.getAxesLabels()
- >>> labels.getXLabel()
- ... 'X'
-
- :return: object describing the labels
- """
- return self._Labels((self._bbox.xlabel,
- self._bbox.ylabel,
- self._bbox.zlabel))
-
- # Colors
-
- def _updateColors(self):
- """Update item depending on foreground/highlight color"""
- self._bbox.tickColor = self._foregroundColor
- self._selectionBox.strokeColor = self._foregroundColor
- if self.getInteractiveMode() == 'plane':
- self._cutPlane.setStrokeColor(self._highlightColor)
- self._bbox.color = self._foregroundColor
- else:
- self._cutPlane.setStrokeColor(self._foregroundColor)
- self._bbox.color = self._highlightColor
-
- def getForegroundColor(self):
- """Return color used for text and bounding box (QColor)"""
- return qt.QColor.fromRgbF(*self._foregroundColor)
-
- def setForegroundColor(self, color):
- """Set the foreground color.
-
- :param color: RGB color: name, #RRGGBB or RGB values
- :type color:
- QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
- """
- color = rgba(color)
- if color != self._foregroundColor:
- self._foregroundColor = color
- self._updateColors()
-
- def getHighlightColor(self):
- """Return color used for highlighted item bounding box (QColor)"""
- return qt.QColor.fromRgbF(*self._highlightColor)
-
- def setHighlightColor(self, color):
- """Set hightlighted item color.
-
- :param color: RGB color: name, #RRGGBB or RGB values
- :type color:
- QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
- """
- color = rgba(color)
- if color != self._highlightColor:
- self._highlightColor = color
- self._updateColors()
-
- # Cut Plane
-
- def getCutPlanes(self):
- """Return an iterable of all cut planes of the view.
-
- This includes hidden cut planes.
-
- For now, there is always one cut plane.
- """
- return (self._cutPlane,)
-
- # Selection
-
- def setSelectedRegion(self, zrange=None, yrange=None, xrange_=None):
- """Set the 3D selected region aligned with the axes.
-
- Provided range are array indices range.
- The provided ranges are clipped to the data.
- If a range is None, the range of the array on this dimension is used.
-
- :param zrange: (zmin, zmax) range of the selection
- :param yrange: (ymin, ymax) range of the selection
- :param xrange_: (xmin, xmax) range of the selection
- """
- # No range given: unset selection
- if zrange is None and yrange is None and xrange_ is None:
- selectedRange = None
-
- else:
- # Handle default ranges
- if self._data is not None:
- if zrange is None:
- zrange = 0, self._data.shape[0]
- if yrange is None:
- yrange = 0, self._data.shape[1]
- if xrange_ is None:
- xrange_ = 0, self._data.shape[2]
-
- elif None in (xrange_, yrange, zrange):
- # One of the range is None and no data available
- raise RuntimeError(
- 'Data is not set, cannot get default range from it.')
-
- # Clip selected region to data shape and make sure min <= max
- selectedRange = numpy.array((
- (max(0, min(*zrange)),
- min(self._data.shape[0], max(*zrange))),
- (max(0, min(*yrange)),
- min(self._data.shape[1], max(*yrange))),
- (max(0, min(*xrange_)),
- min(self._data.shape[2], max(*xrange_))),
- ), dtype=numpy.int64)
-
- # numpy.equal supports None
- if not numpy.all(numpy.equal(selectedRange, self._selectedRange)):
- self._selectedRange = selectedRange
-
- # Update scene accordingly
- if self._selectedRange is None:
- self._selectionBox.visible = False
- else:
- self._selectionBox.visible = True
- scales = self._selectedRange[:, 1] - self._selectedRange[:, 0]
- self._selectionBox.size = scales[::-1]
- self._selectionBox.transforms = [
- transform.Translate(*self._selectedRange[::-1, 0])]
-
- self.sigSelectedRegionChanged.emit(self.getSelectedRegion())
-
- def getSelectedRegion(self):
- """Returns the currently selected region or None."""
- if self._selectedRange is None:
- return None
- else:
- dataBBox = self._group.transforms.transformBounds(
- self._selectedRange[::-1].T).T
- return SelectedRegion(self._selectedRange, dataBBox,
- translation=self.getTranslation(),
- scale=self.getScale())
-
- # Handle iso-surfaces
-
- sigIsosurfaceAdded = qt.Signal(object)
- """Signal emitted when a new iso-surface is added to the view.
-
- The newly added iso-surface is provided by this signal
- """
-
- sigIsosurfaceRemoved = qt.Signal(object)
- """Signal emitted when an iso-surface is removed from the view
-
- The removed iso-surface is provided by this signal.
- """
-
- def addIsosurface(self, level, color):
- """Add an iso-surface to the view.
-
- :param level:
- The value at which to build the iso-surface or a callable
- (e.g., a function) taking a 3D numpy.ndarray as input and
- returning a float.
- Example: numpy.mean(data) + numpy.std(data)
- :type level: float or callable
- :param color: RGBA color of the isosurface
- :type color: str or array-like of 4 float in [0., 1.]
- :return: Isosurface object describing this isosurface
- """
- isosurface = Isosurface(parent=self)
- isosurface.setColor(color)
- if callable(level):
- isosurface.setAutoLevelFunction(level)
- else:
- isosurface.setLevel(level)
- isosurface._setData(self._data, copy=False)
- isosurface.sigLevelChanged.connect(self._updateIsosurfaces)
-
- self._isosurfaces.append(isosurface)
-
- self._updateIsosurfaces()
-
- self.sigIsosurfaceAdded.emit(isosurface)
- return isosurface
-
- def getIsosurfaces(self):
- """Return an iterable of all iso-surfaces of the view"""
- return tuple(self._isosurfaces)
-
- def removeIsosurface(self, isosurface):
- """Remove an iso-surface from the view.
-
- :param isosurface: The isosurface object to remove"""
- if isosurface not in self.getIsosurfaces():
- _logger.warning(
- "Try to remove isosurface that is not in the list: %s",
- str(isosurface))
- else:
- isosurface.sigLevelChanged.disconnect(self._updateIsosurfaces)
- self._isosurfaces.remove(isosurface)
- self._updateIsosurfaces()
- self.sigIsosurfaceRemoved.emit(isosurface)
-
- def clearIsosurfaces(self):
- """Remove all iso-surfaces from the view."""
- for isosurface in self.getIsosurfaces():
- self.removeIsosurface(isosurface)
-
- def _updateIsosurfaces(self, level=None):
- """Handle updates of iso-surfaces level and add/remove"""
- # Sorting using minus, this supposes data 'object' to be max values
- sortedIso = sorted(self.getIsosurfaces(),
- key=lambda iso: - iso.getLevel())
- self._isogroup.children = [iso._get3DPrimitive() for iso in sortedIso]
diff --git a/silx/gui/plot3d/SceneWidget.py b/silx/gui/plot3d/SceneWidget.py
deleted file mode 100644
index 883f5e7..0000000
--- a/silx/gui/plot3d/SceneWidget.py
+++ /dev/null
@@ -1,687 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-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.
-#
-# ###########################################################################*/
-"""This module provides a widget to view data sets in 3D."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-import enum
-import weakref
-
-import numpy
-
-from .. import qt
-from ..colors import rgba
-
-from .Plot3DWidget import Plot3DWidget
-from . import items
-from .items.core import RootGroupWithAxesItem
-from .scene import interaction
-from ._model import SceneModel, visitQAbstractItemModel
-from ._model.items import Item3DRow
-
-__all__ = ['items', 'SceneWidget']
-
-
-class _SceneSelectionHighlightManager(object):
- """Class controlling the highlight of the selection in a SceneWidget
-
- :param ~silx.gui.plot3d.SceneWidget.SceneSelection:
- """
-
- def __init__(self, selection):
- assert isinstance(selection, SceneSelection)
- self._sceneWidget = weakref.ref(selection.parent())
-
- self._enabled = True
- self._previousBBoxState = None
-
- self.__selectItem(selection.getCurrentItem())
- selection.sigCurrentChanged.connect(self.__currentChanged)
-
- def isEnabled(self):
- """Returns True if highlight of selection in enabled.
-
- :rtype: bool
- """
- return self._enabled
-
- def setEnabled(self, enabled=True):
- """Activate/deactivate selection highlighting
-
- :param bool enabled: True (default) to enable selection highlighting
- """
- enabled = bool(enabled)
- if enabled != self._enabled:
- self._enabled = enabled
-
- sceneWidget = self.getSceneWidget()
- if sceneWidget is not None:
- selection = sceneWidget.selection()
- current = selection.getCurrentItem()
-
- if enabled:
- self.__selectItem(current)
- selection.sigCurrentChanged.connect(self.__currentChanged)
-
- else: # disabled
- self.__unselectItem(current)
- selection.sigCurrentChanged.disconnect(
- self.__currentChanged)
-
- def getSceneWidget(self):
- """Returns the SceneWidget this class controls highlight for.
-
- :rtype: ~silx.gui.plot3d.SceneWidget.SceneWidget
- """
- return self._sceneWidget()
-
- def __selectItem(self, current):
- """Highlight given item.
-
- :param ~silx.gui.plot3d.items.Item3D current: New current or None
- """
- if current is None:
- return
-
- sceneWidget = self.getSceneWidget()
- if sceneWidget is None:
- return
-
- if isinstance(current, items.DataItem3D):
- self._previousBBoxState = current.isBoundingBoxVisible()
- current.setBoundingBoxVisible(True)
- current._setForegroundColor(sceneWidget.getHighlightColor())
- current.sigItemChanged.connect(self.__selectedChanged)
-
- def __unselectItem(self, current):
- """Remove highlight of given item.
-
- :param ~silx.gui.plot3d.items.Item3D current:
- Currently highlighted item
- """
- if current is None:
- return
-
- sceneWidget = self.getSceneWidget()
- if sceneWidget is None:
- return
-
- # Restore bbox visibility and color
- current.sigItemChanged.disconnect(self.__selectedChanged)
- if (self._previousBBoxState is not None and
- isinstance(current, items.DataItem3D)):
- current.setBoundingBoxVisible(self._previousBBoxState)
- current._setForegroundColor(sceneWidget.getForegroundColor())
-
- def __currentChanged(self, current, previous):
- """Handle change of current item in the selection
-
- :param ~silx.gui.plot3d.items.Item3D current: New current or None
- :param ~silx.gui.plot3d.items.Item3D previous: Previous current or None
- """
- self.__unselectItem(previous)
- self.__selectItem(current)
-
- def __selectedChanged(self, event):
- """Handle updates of selected item bbox.
-
- If bbox gets changed while selected, do not restore state.
-
- :param event:
- """
- if event == items.Item3DChangedType.BOUNDING_BOX_VISIBLE:
- self._previousBBoxState = None
-
-
-@enum.unique
-class HighlightMode(enum.Enum):
- """:class:`SceneSelection` highlight modes"""
-
- NONE = 'noHighlight'
- """Do not highlight selected item"""
-
- BOUNDING_BOX = 'boundingBox'
- """Highlight selected item bounding box"""
-
-
-class SceneSelection(qt.QObject):
- """Object managing a :class:`SceneWidget` selection
-
- :param SceneWidget parent:
- """
-
- NO_SELECTION = 0
- """Flag for no item selected"""
-
- sigCurrentChanged = qt.Signal(object, object)
- """This signal is emitted whenever the current item changes.
-
- It provides the current and previous items.
- Either of those can be :attr:`NO_SELECTION`.
- """
-
- def __init__(self, parent=None):
- super(SceneSelection, self).__init__(parent)
- self.__current = None # Store weakref to current item
- self.__selectionModel = None # Store sync selection model
- self.__syncInProgress = False # True during model synchronization
-
- self.__highlightManager = _SceneSelectionHighlightManager(self)
-
- def getHighlightMode(self):
- """Returns current selection highlight mode.
-
- Either NONE or BOUNDING_BOX.
-
- :rtype: HighlightMode
- """
- if self.__highlightManager.isEnabled():
- return HighlightMode.BOUNDING_BOX
- else:
- return HighlightMode.NONE
-
- def setHighlightMode(self, mode):
- """Set selection highlighting mode
-
- :param HighlightMode mode: The mode to use
- """
- assert isinstance(mode, HighlightMode)
- self.__highlightManager.setEnabled(mode == HighlightMode.BOUNDING_BOX)
-
- def getCurrentItem(self):
- """Returns the current item in the scene or None.
-
- :rtype: Union[~silx.gui.plot3d.items.Item3D, None]
- """
- return None if self.__current is None else self.__current()
-
- def setCurrentItem(self, item):
- """Set the current item in the scene.
-
- :param Union[Item3D, None] item:
- The new item to select or None to clear the selection.
- :raise ValueError: If the item is not the widget's scene
- """
- previous = self.getCurrentItem()
- if item is previous:
- return # Fast path, nothing to do
-
- if previous is not None:
- previous.sigItemChanged.disconnect(self.__currentChanged)
-
- if item is None:
- self.__current = None
-
- elif isinstance(item, items.Item3D):
- parent = self.parent()
- assert isinstance(parent, SceneWidget)
-
- sceneGroup = parent.getSceneGroup()
- if item is sceneGroup or item.root() is sceneGroup:
- item.sigItemChanged.connect(self.__currentChanged)
- self.__current = weakref.ref(item)
- else:
- raise ValueError(
- 'Item is not in this SceneWidget: %s' % str(item))
-
- else:
- raise ValueError(
- 'Not an Item3D: %s' % str(item))
-
- current = self.getCurrentItem()
- self.sigCurrentChanged.emit(current, previous)
- self.__updateSelectionModel()
-
- def __currentChanged(self, event):
- """Handle updates of the selected item"""
- if event == items.Item3DChangedType.ROOT_ITEM:
- item = self.sender()
-
- parent = self.parent()
- assert isinstance(parent, SceneWidget)
-
- if item.root() != parent.getSceneGroup():
- self.setCurrentItem(None)
-
- # Synchronization with QItemSelectionModel
-
- def _getSyncSelectionModel(self):
- """Returns the QItemSelectionModel this selection is synchronized with.
-
- :rtype: Union[QItemSelectionModel, None]
- """
- return self.__selectionModel
-
- def _setSyncSelectionModel(self, selectionModel):
- """Synchronizes this selection object with a selection model.
-
- :param Union[QItemSelectionModel, None] selectionModel:
- :raise ValueError: If the selection model does not correspond
- to the same :class:`SceneWidget`
- """
- if (not isinstance(selectionModel, qt.QItemSelectionModel) or
- not isinstance(selectionModel.model(), SceneModel) or
- selectionModel.model().sceneWidget() is not self.parent()):
- raise ValueError("Expecting a QItemSelectionModel "
- "attached to the same SceneWidget")
-
- # Disconnect from previous selection model
- previousSelectionModel = self._getSyncSelectionModel()
- if previousSelectionModel is not None:
- previousSelectionModel.selectionChanged.disconnect(
- self.__selectionModelSelectionChanged)
-
- self.__selectionModel = selectionModel
-
- if selectionModel is not None:
- # Connect to new selection model
- selectionModel.selectionChanged.connect(
- self.__selectionModelSelectionChanged)
- self.__updateSelectionModel()
-
- def __selectionModelSelectionChanged(self, selected, deselected):
- """Handle QItemSelectionModel selection updates.
-
- :param QItemSelection selected:
- :param QItemSelection deselected:
- """
- if self.__syncInProgress:
- return
-
- indices = selected.indexes()
- if not indices:
- item = None
-
- else: # Select the first selected item
- index = indices[0]
- itemRow = index.internalPointer()
- if isinstance(itemRow, Item3DRow):
- item = itemRow.item()
- else:
- item = None
-
- self.setCurrentItem(item)
-
- def __updateSelectionModel(self):
- """Sync selection model when current item has been updated"""
- selectionModel = self._getSyncSelectionModel()
- if selectionModel is None:
- return
-
- currentItem = self.getCurrentItem()
-
- if currentItem is None:
- selectionModel.clear()
-
- else:
- # visit the model to find selectable index corresponding to item
- model = selectionModel.model()
- for index in visitQAbstractItemModel(model):
- itemRow = index.internalPointer()
- if (isinstance(itemRow, Item3DRow) and
- itemRow.item() is currentItem and
- index.flags() & qt.Qt.ItemIsSelectable):
- # This is the item we are looking for: select it in the model
- self.__syncInProgress = True
- selectionModel.select(
- index, qt.QItemSelectionModel.Clear |
- qt.QItemSelectionModel.Select |
- qt.QItemSelectionModel.Current)
- self.__syncInProgress = False
- break
-
-
-class SceneWidget(Plot3DWidget):
- """Widget displaying data sets in 3D"""
-
- def __init__(self, parent=None):
- super(SceneWidget, self).__init__(parent)
- self._model = None # Store lazy-loaded model
- self._selection = None # Store lazy-loaded SceneSelection
- self._items = []
-
- self._textColor = 1., 1., 1., 1.
- self._foregroundColor = 1., 1., 1., 1.
- self._highlightColor = 0.7, 0.7, 0., 1.
-
- self._sceneGroup = RootGroupWithAxesItem(parent=self)
- self._sceneGroup.setLabel('Data')
-
- self.viewport.scene.children.append(
- self._sceneGroup._getScenePrimitive())
-
- def model(self):
- """Returns the model corresponding the scene of this widget
-
- :rtype: SceneModel
- """
- if self._model is None:
- # Lazy-loading of the model
- self._model = SceneModel(parent=self)
- return self._model
-
- def selection(self):
- """Returns the object managing selection in the scene
-
- :rtype: SceneSelection
- """
- if self._selection is None:
- # Lazy-loading of the SceneSelection
- self._selection = SceneSelection(parent=self)
- return self._selection
-
- def getSceneGroup(self):
- """Returns the root group of the scene
-
- :rtype: GroupItem
- """
- return self._sceneGroup
-
- def pickItems(self, x, y, condition=None):
- """Iterator over picked items in the scene at given position.
-
- Each picked item yield a
- :class:`~silx.gui.plot3d.items._pick.PickingResult` object
- holding the picking information.
-
- It traverses the scene tree in a left-to-right top-down way.
-
- :param int x: X widget coordinate
- :param int y: Y widget coordinate
- :param callable condition: Optional test called for each item
- checking whether to process it or not.
- """
- if not self.isValid() or not self.isVisible():
- return # Empty iterator
-
- devicePixelRatio = self.getDevicePixelRatio()
- for result in self.getSceneGroup().pickItems(
- x * devicePixelRatio, y * devicePixelRatio, condition):
- yield result
-
- # Interactive modes
-
- def _handleSelectionChanged(self, current, previous):
- """Handle change of selection to update interactive mode"""
- if self.getInteractiveMode() == 'panSelectedPlane':
- if isinstance(current, items.PlaneMixIn):
- # Update pan plane to use new selected plane
- self.setInteractiveMode('panSelectedPlane')
-
- else: # Switch to rotate scene if new selection is not a plane
- self.setInteractiveMode('rotate')
-
- def setInteractiveMode(self, mode):
- """Set the interactive mode.
-
- 'panSelectedPlane' mode set plane panning if a plane is selected,
- otherwise it fall backs to 'rotate'.
-
- :param str mode:
- The interactive mode: 'rotate', 'pan', 'panSelectedPlane' or None
- """
- if self.getInteractiveMode() == 'panSelectedPlane':
- self.selection().sigCurrentChanged.disconnect(
- self._handleSelectionChanged)
-
- if mode == 'panSelectedPlane':
- selected = self.selection().getCurrentItem()
-
- if isinstance(selected, items.PlaneMixIn):
- mode = interaction.PanPlaneZoomOnWheelControl(
- self.viewport,
- selected._getPlane(),
- mode='position',
- orbitAroundCenter=False,
- scaleTransform=self._sceneScale)
-
- self.selection().sigCurrentChanged.connect(
- self._handleSelectionChanged)
-
- else: # No selected plane, fallback to rotate scene
- mode = 'rotate'
-
- super(SceneWidget, self).setInteractiveMode(mode)
-
- def getInteractiveMode(self):
- """Returns the interactive mode in use.
-
- :rtype: str
- """
- if isinstance(self.eventHandler, interaction.PanPlaneZoomOnWheelControl):
- return 'panSelectedPlane'
- else:
- return super(SceneWidget, self).getInteractiveMode()
-
- # Add/remove items
-
- def addVolume(self, data, copy=True, index=None):
- """Add 3D data volume of scalar or complex to :class:`SceneWidget` content.
-
- Dataset order is zyx (i.e., first dimension is z).
-
- :param data: 3D array of complex with shape at least (2, 2, 2)
- :type data: numpy.ndarray[Union[numpy.complex64,numpy.float32]]
- :param bool copy:
- True (default) to make a copy,
- False to avoid copy (DO NOT MODIFY data afterwards)
- :param int index: The index at which to place the item.
- By default it is appended to the end of the list.
- :return: The newly created 3D volume item
- :rtype: Union[ScalarField3D,ComplexField3D]
-
- """
- if data is not None:
- data = numpy.array(data, copy=False)
-
- if numpy.iscomplexobj(data):
- volume = items.ComplexField3D()
- else:
- volume = items.ScalarField3D()
- volume.setData(data, copy=copy)
- self.addItem(volume, index)
- return volume
-
- def add3DScalarField(self, data, copy=True, index=None):
- # TODO deprecate in the future
- return self.addVolume(data, copy=copy, index=index)
-
- def add3DScatter(self, x, y, z, value, copy=True, index=None):
- """Add 3D scatter data to :class:`SceneWidget` content.
-
- :param numpy.ndarray x: Array of X coordinates (single value not accepted)
- :param y: Points Y coordinate (array-like or single value)
- :param z: Points Z coordinate (array-like or single value)
- :param value: Points values (array-like or single value)
- :param bool copy:
- True (default) to copy the data,
- False to use provided data (do not modify!)
- :param int index: The index at which to place the item.
- By default it is appended to the end of the list.
- :return: The newly created 3D scatter item
- :rtype: ~silx.gui.plot3d.items.scatter.Scatter3D
- """
- scatter3d = items.Scatter3D()
- scatter3d.setData(x=x, y=y, z=z, value=value, copy=copy)
- self.addItem(scatter3d, index)
- return scatter3d
-
- def add2DScatter(self, x, y, value, copy=True, index=None):
- """Add 2D scatter data to :class:`SceneWidget` content.
-
- Provided arrays must have the same length.
-
- :param numpy.ndarray x: X coordinates (array-like)
- :param numpy.ndarray y: Y coordinates (array-like)
- :param value: Points value: array-like or single scalar
- :param bool copy: True (default) to copy the data,
- False to use as is (do not modify!).
- :param int index: The index at which to place the item.
- By default it is appended to the end of the list.
- :return: The newly created 2D scatter item
- :rtype: ~silx.gui.plot3d.items.scatter.Scatter2D
- """
- scatter2d = items.Scatter2D()
- scatter2d.setData(x=x, y=y, value=value, copy=copy)
- self.addItem(scatter2d, index)
- return scatter2d
-
- def addImage(self, data, copy=True, index=None):
- """Add a 2D data or RGB(A) image to :class:`SceneWidget` content.
-
- 2D data is casted to float32.
- RGBA supported formats are: float32 in [0, 1] and uint8.
-
- :param numpy.ndarray data: Image as a 2D data array or
- RGBA image as a 3D array (height, width, channels)
- :param bool copy: True (default) to copy the data,
- False to use as is (do not modify!).
- :param int index: The index at which to place the item.
- By default it is appended to the end of the list.
- :return: The newly created image item
- :rtype: ~silx.gui.plot3d.items.image.ImageData or ~silx.gui.plot3d.items.image.ImageRgba
- :raise ValueError: For arrays of unsupported dimensions
- """
- data = numpy.array(data, copy=False)
- if data.ndim == 2:
- image = items.ImageData()
- elif data.ndim == 3:
- image = items.ImageRgba()
- else:
- raise ValueError("Unsupported array dimensions: %d" % data.ndim)
- image.setData(data, copy=copy)
- self.addItem(image, index)
- return image
-
- def addItem(self, item, index=None):
- """Add an item to :class:`SceneWidget` content
-
- :param Item3D item: The item to add
- :param int index: The index at which to place the item.
- By default it is appended to the end of the list.
- :raise ValueError: If the item is already in the :class:`SceneWidget`.
- """
- return self.getSceneGroup().addItem(item, index)
-
- def removeItem(self, item):
- """Remove an item from :class:`SceneWidget` content.
-
- :param Item3D item: The item to remove from the scene
- :raises ValueError: If the item does not belong to the group
- """
- return self.getSceneGroup().removeItem(item)
-
- def getItems(self):
- """Returns the list of :class:`SceneWidget` items.
-
- Only items in the top-level group are returned.
-
- :rtype: tuple
- """
- return self.getSceneGroup().getItems()
-
- def clearItems(self):
- """Remove all item from :class:`SceneWidget`."""
- return self.getSceneGroup().clearItems()
-
- # Colors
-
- def getTextColor(self):
- """Return color used for text
-
- :rtype: QColor"""
- return qt.QColor.fromRgbF(*self._textColor)
-
- def setTextColor(self, color):
- """Set the text color.
-
- :param color: RGB color: name, #RRGGBB or RGB values
- :type color:
- QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
- """
- color = rgba(color)
- if color != self._textColor:
- self._textColor = color
-
- # Update text color
- # TODO make entry point in Item3D for this
- bbox = self._sceneGroup._getScenePrimitive()
- bbox.tickColor = color
-
- self.sigStyleChanged.emit('textColor')
-
- def getForegroundColor(self):
- """Return color used for bounding box
-
- :rtype: QColor
- """
- return qt.QColor.fromRgbF(*self._foregroundColor)
-
- def setForegroundColor(self, color):
- """Set the foreground color.
-
- :param color: RGB color: name, #RRGGBB or RGB values
- :type color:
- QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
- """
- color = rgba(color)
- if color != self._foregroundColor:
- self._foregroundColor = color
-
- # Update scene items
- selected = self.selection().getCurrentItem()
- for item in self.getSceneGroup().visit(included=True):
- if item is not selected:
- item._setForegroundColor(color)
-
- self.sigStyleChanged.emit('foregroundColor')
-
- def getHighlightColor(self):
- """Return color used for highlighted item bounding box
-
- :rtype: QColor
- """
- return qt.QColor.fromRgbF(*self._highlightColor)
-
- def setHighlightColor(self, color):
- """Set highlighted item color.
-
- :param color: RGB color: name, #RRGGBB or RGB values
- :type color:
- QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
- """
- color = rgba(color)
- if color != self._highlightColor:
- self._highlightColor = color
-
- selected = self.selection().getCurrentItem()
- if selected is not None:
- selected._setForegroundColor(color)
-
- self.sigStyleChanged.emit('highlightColor')
diff --git a/silx/gui/plot3d/SceneWindow.py b/silx/gui/plot3d/SceneWindow.py
deleted file mode 100644
index 052a4dc..0000000
--- a/silx/gui/plot3d/SceneWindow.py
+++ /dev/null
@@ -1,219 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-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.
-#
-# ###########################################################################*/
-"""This module provides a QMainWindow with a 3D SceneWidget and toolbars.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "29/11/2017"
-
-
-from ...gui import qt, icons
-from ...gui.widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
-
-from .actions.mode import InteractiveModeAction
-from .SceneWidget import SceneWidget
-from .tools import OutputToolBar, InteractiveModeToolBar, ViewpointToolBar
-from .tools.GroupPropertiesWidget import GroupPropertiesWidget
-from .tools.PositionInfoWidget import PositionInfoWidget
-
-from .ParamTreeView import ParamTreeView
-
-# Imported here for convenience
-from . import items # noqa
-
-
-__all__ = ['items', 'SceneWidget', 'SceneWindow']
-
-
-class _PanPlaneAction(InteractiveModeAction):
- """QAction to set plane pan interaction on a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(_PanPlaneAction, self).__init__(
- parent, 'panSelectedPlane', plot3d)
- self.setIcon(icons.getQIcon('3d-plane-pan'))
- self.setText('Pan plane')
- self.setCheckable(True)
- self.setToolTip(
- 'Pan selected plane. Press <b>Ctrl</b> to rotate the scene.')
-
- def _planeChanged(self, event):
- """Handle plane updates"""
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.POSITION):
- plane = self.sender()
-
- isPlaneInteractive = \
- plane._getPlane().plane.isPlane and plane.isVisible()
-
- if isPlaneInteractive != self.isEnabled():
- self.setEnabled(isPlaneInteractive)
- mode = 'panSelectedPlane' if isPlaneInteractive else 'rotate'
- self.getPlot3DWidget().setInteractiveMode(mode)
-
- def _selectionChanged(self, current, previous):
- """Handle selected object change"""
- if isinstance(previous, items.PlaneMixIn):
- previous.sigItemChanged.disconnect(self._planeChanged)
-
- if isinstance(current, items.PlaneMixIn):
- current.sigItemChanged.connect(self._planeChanged)
- self.setEnabled(True)
- self.getPlot3DWidget().setInteractiveMode('panSelectedPlane')
- else:
- self.setEnabled(False)
-
- def setPlot3DWidget(self, widget):
- previous = self.getPlot3DWidget()
- if isinstance(previous, SceneWidget):
- previous.selection().sigCurrentChanged.disconnect(
- self._selectionChanged)
- self._selectionChanged(
- None, previous.selection().getCurrentItem())
-
- super(_PanPlaneAction, self).setPlot3DWidget(widget)
-
- if isinstance(widget, SceneWidget):
- self._selectionChanged(widget.selection().getCurrentItem(), None)
- widget.selection().sigCurrentChanged.connect(
- self._selectionChanged)
-
-
-class SceneWindow(qt.QMainWindow):
- """OpenGL 3D scene widget with toolbars."""
-
- def __init__(self, parent=None):
- super(SceneWindow, self).__init__(parent)
- if parent is not None:
- # behave as a widget
- self.setWindowFlags(qt.Qt.Widget)
-
- self._sceneWidget = SceneWidget()
- self.setCentralWidget(self._sceneWidget)
-
- # Add PositionInfoWidget to display picking info
- self._positionInfo = PositionInfoWidget()
- self._positionInfo.setSceneWidget(self._sceneWidget)
-
- dock = BoxLayoutDockWidget()
- dock.setWindowTitle("Selection Info")
- dock.setWidget(self._positionInfo)
- self.addDockWidget(qt.Qt.BottomDockWidgetArea, dock)
-
- self._interactiveModeToolBar = InteractiveModeToolBar(parent=self)
- panPlaneAction = _PanPlaneAction(self, plot3d=self._sceneWidget)
- self._interactiveModeToolBar.addAction(
- self._positionInfo.toggleAction())
- self._interactiveModeToolBar.addAction(panPlaneAction)
-
- self._viewpointToolBar = ViewpointToolBar(parent=self)
- self._outputToolBar = OutputToolBar(parent=self)
-
- for toolbar in (self._interactiveModeToolBar,
- self._viewpointToolBar,
- self._outputToolBar):
- toolbar.setPlot3DWidget(self._sceneWidget)
- self.addToolBar(toolbar)
- self.addActions(toolbar.actions())
-
- self._paramTreeView = ParamTreeView()
- self._paramTreeView.setModel(self._sceneWidget.model())
-
- selectionModel = self._paramTreeView.selectionModel()
- self._sceneWidget.selection()._setSyncSelectionModel(
- selectionModel)
-
- paramDock = qt.QDockWidget()
- paramDock.setWindowTitle('Object parameters')
- paramDock.setWidget(self._paramTreeView)
- self.addDockWidget(qt.Qt.RightDockWidgetArea, paramDock)
-
- self._sceneGroupResetWidget = GroupPropertiesWidget()
- self._sceneGroupResetWidget.setGroup(
- self._sceneWidget.getSceneGroup())
-
- resetDock = qt.QDockWidget()
- resetDock.setWindowTitle('Global parameters')
- resetDock.setWidget(self._sceneGroupResetWidget)
- self.addDockWidget(qt.Qt.RightDockWidgetArea, resetDock)
- self.tabifyDockWidget(paramDock, resetDock)
-
- paramDock.raise_()
-
- def getSceneWidget(self):
- """Returns the SceneWidget of this window.
-
- :rtype: ~silx.gui.plot3d.SceneWidget.SceneWidget
- """
- return self._sceneWidget
-
- def getGroupResetWidget(self):
- """Returns the :class:`GroupPropertiesWidget` of this window.
-
- :rtype: GroupPropertiesWidget
- """
- return self._sceneGroupResetWidget
-
- def getParamTreeView(self):
- """Returns the :class:`ParamTreeView` of this window.
-
- :rtype: ParamTreeView
- """
- return self._paramTreeView
-
- def getInteractiveModeToolBar(self):
- """Returns the interactive mode toolbar.
-
- :rtype: ~silx.gui.plot3d.tools.InteractiveModeToolBar
- """
- return self._interactiveModeToolBar
-
- def getViewpointToolBar(self):
- """Returns the viewpoint toolbar.
-
- :rtype: ~silx.gui.plot3d.tools.ViewpointToolBar
- """
- return self._viewpointToolBar
-
- def getOutputToolBar(self):
- """Returns the output toolbar.
-
- :rtype: ~silx.gui.plot3d.tools.OutputToolBar
- """
- return self._outputToolBar
-
- def getPositionInfoWidget(self):
- """Returns the widget displaying selected position information.
-
- :rtype: ~silx.gui.plot3d.tools.PositionInfoWidget.PositionInfoWidget
- """
- return self._positionInfo
diff --git a/silx/gui/plot3d/__init__.py b/silx/gui/plot3d/__init__.py
deleted file mode 100644
index af74613..0000000
--- a/silx/gui/plot3d/__init__.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-"""
-This package provides widgets displaying 3D content based on OpenGL.
-
-It depends on PyOpenGL and PyQtx.QtOpenGL or PyQt>=5.4.
-"""
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "18/01/2017"
-
-
-try:
- import OpenGL as _OpenGL
-except ImportError:
- raise ImportError('PyOpenGL is not installed')
diff --git a/silx/gui/plot3d/_model/__init__.py b/silx/gui/plot3d/_model/__init__.py
deleted file mode 100644
index 4b16e32..0000000
--- a/silx/gui/plot3d/_model/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""
-This package provides :class:`SceneWidget` content and parameters model.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "11/01/2018"
-
-from .model import SceneModel, visitQAbstractItemModel # noqa
diff --git a/silx/gui/plot3d/_model/core.py b/silx/gui/plot3d/_model/core.py
deleted file mode 100644
index e8e0820..0000000
--- a/silx/gui/plot3d/_model/core.py
+++ /dev/null
@@ -1,372 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""
-This module provides base classes to implement models for 3D scene content.
-"""
-
-from __future__ import absolute_import, division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "11/01/2018"
-
-
-import collections
-import weakref
-
-from ....utils.weakref import WeakMethodProxy
-from ... import qt
-
-
-class BaseRow(qt.QObject):
- """Base class for rows of the tree model.
-
- The root node parent MUST be set to the QAbstractItemModel it belongs to.
- By default item is enabled.
-
- :param children: Iterable of BaseRow to start with (not signaled)
- """
-
- def __init__(self, children=()):
- self.__modelRef = None
- self.__parentRef = None
- super(BaseRow, self).__init__()
- self.__children = []
- for row in children:
- assert isinstance(row, BaseRow)
- row.setParent(self)
- self.__children.append(row)
- self.__flags = collections.defaultdict(lambda: qt.Qt.ItemIsEnabled)
- self.__tooltip = None
-
- def setParent(self, parent):
- """Override :meth:`QObject.setParent` to cache model and parent"""
- self.__parentRef = None if parent is None else weakref.ref(parent)
-
- if isinstance(parent, qt.QAbstractItemModel):
- model = parent
- elif isinstance(parent, BaseRow):
- model = parent.model()
- else:
- model = None
-
- self._updateModel(model)
-
- super(BaseRow, self).setParent(parent)
-
- def parent(self):
- """Override :meth:`QObject.setParent` to use cached parent
-
- :rtype: Union[QObject, None]"""
- return self.__parentRef() if self.__parentRef is not None else None
-
- def _updateModel(self, model):
- """Update the model this row belongs to"""
- if model != self.model():
- self.__modelRef = weakref.ref(model) if model is not None else None
- for child in self.children():
- child._updateModel(model)
-
- def model(self):
- """Return the model this node belongs to or None if not in a model.
-
- :rtype: Union[QAbstractItemModel, None]
- """
- return self.__modelRef() if self.__modelRef is not None else None
-
- def index(self, column=0):
- """Return corresponding index in the model or None if not in a model.
-
- :param int column: The column to make the index for
- :rtype: Union[QModelIndex, None]
- """
- parent = self.parent()
- model = self.model()
-
- if model is None: # Not in a model
- return None
- elif parent is model: # Root node
- return qt.QModelIndex()
- else:
- index = parent.index()
- row = parent.children().index(self)
- return model.index(row, column, index)
-
- def columnCount(self):
- """Returns number of columns (default: 2)
-
- :rtype: int
- """
- return 2
-
- def children(self):
- """Returns the list of children nodes
-
- :rtype: tuple of Node
- """
- return tuple(self.__children)
-
- def rowCount(self):
- """Returns number of rows
-
- :rtype: int
- """
- return len(self.__children)
-
- def addRow(self, row, index=None):
- """Add a node to the children
-
- :param BaseRow row: The node to add
- :param int index: The index at which to insert it or
- None to append
- """
- if index is None:
- index = self.rowCount()
- assert index <= self.rowCount()
-
- model = self.model()
-
- if model is not None:
- parent = self.index()
- model.beginInsertRows(parent, index, index)
-
- self.__children.insert(index, row)
- row.setParent(self)
-
- if model is not None:
- model.endInsertRows()
-
- def removeRow(self, row):
- """Remove a row from the children list.
-
- It removes either a node or a row index.
-
- :param row: BaseRow object or index of row to remove
- :type row: Union[BaseRow, int]
- """
- if isinstance(row, BaseRow):
- row = self.__children.index(row)
- else:
- row = int(row)
- assert row < self.rowCount()
-
- model = self.model()
-
- if model is not None:
- index = self.index()
- model.beginRemoveRows(index, row, row)
-
- node = self.__children.pop(row)
- node.setParent(None)
-
- if model is not None:
- model.endRemoveRows()
-
- def data(self, column, role):
- """Returns data for given column and role
-
- :param int column: Column index for this row
- :param int role: The role to get
- :return: Corresponding data (Default: None)
- """
- if role == qt.Qt.ToolTipRole and self.__tooltip is not None:
- return self.__tooltip
- else:
- return None
-
- def setData(self, column, value, role):
- """Set data for given column and role
-
- :param int column: Column index for this row
- :param value: The data to set
- :param int role: The role to set
- :return: True on success, False on failure
- :rtype: bool
- """
- return False
-
- def setToolTip(self, tooltip):
- """Set the tooltip of the whole row.
-
- If None there is no tooltip.
-
- :param Union[str, None] tooltip:
- """
- self.__tooltip = tooltip
-
- def setFlags(self, flags, column=None):
- """Set the static flags to return.
-
- Default is ItemIsEnabled for all columns.
-
- :param int column: The column for which to set the flags
- :param flags: Item flags
- """
- if column is None:
- self.__flags = collections.defaultdict(lambda: flags)
- else:
- self.__flags[column] = flags
-
- def flags(self, column):
- """Returns flags for given column
-
- :rtype: int
- """
- return self.__flags[column]
-
-
-class StaticRow(BaseRow):
- """Row with static data.
-
- :param tuple display: List of data for DisplayRole for each column
- :param dict roles: Optional mapping of roles to list of data.
- :param children: Iterable of BaseRow to start with (not signaled)
- """
-
- def __init__(self, display=('', None), roles=None, children=()):
- super(StaticRow, self).__init__(children)
- self._dataByRoles = {} if roles is None else roles
- self._dataByRoles[qt.Qt.DisplayRole] = display
-
- def data(self, column, role):
- if role in self._dataByRoles:
- data = self._dataByRoles[role]
- if column < len(data):
- return data[column]
- return super(StaticRow, self).data(column, role)
-
- def columnCount(self):
- return len(self._dataByRoles[qt.Qt.DisplayRole])
-
-
-class ProxyRow(BaseRow):
- """Provides a node to proxy a data accessible through functions.
-
- Warning: Only weak reference are kept on fget and fset.
-
- :param str name: The name of this node
- :param callable fget: A callable returning the data
- :param callable fset:
- An optional callable setting the data with data as a single argument.
- :param notify:
- An optional signal emitted when data has changed.
- :param callable toModelData:
- An optional callable to convert from fget
- callable to data returned by the model.
- :param callable fromModelData:
- An optional callable converting data provided to the model to
- data for fset.
- :param editorHint: Data to provide as UserRole for editor selection/setup
- """
-
- def __init__(self,
- name='',
- fget=None,
- fset=None,
- notify=None,
- toModelData=None,
- fromModelData=None,
- editorHint=None):
-
- super(ProxyRow, self).__init__()
- self.__name = name
- self.__editorHint = editorHint
-
- assert fget is not None
- self._fget = WeakMethodProxy(fget)
- self._fset = WeakMethodProxy(fset) if fset is not None else None
- if fset is not None:
- self.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsEditable, 1)
- self._toModelData = toModelData
- self._fromModelData = fromModelData
-
- if notify is not None:
- notify.connect(self._notified) # TODO support sigItemChanged flags
-
- def _notified(self, *args, **kwargs):
- """Send update to the model upon signal notifications"""
- index = self.index(column=1)
- model = self.model()
- if model is not None:
- model.dataChanged.emit(index, index)
-
- def data(self, column, role):
- if column == 0:
- if role == qt.Qt.DisplayRole:
- return self.__name
-
- elif column == 1:
- if role == qt.Qt.UserRole: # EditorHint
- return self.__editorHint
- elif role == qt.Qt.DisplayRole or (role == qt.Qt.EditRole and
- self._fset is not None):
- data = self._fget()
- if self._toModelData is not None:
- data = self._toModelData(data)
- return data
-
- return super(ProxyRow, self).data(column, role)
-
- def setData(self, column, value, role):
- if role == qt.Qt.EditRole and self._fset is not None:
- if self._fromModelData is not None:
- value = self._fromModelData(value)
- self._fset(value)
- return True
-
- return super(ProxyRow, self).setData(column, value, role)
-
-
-class ColorProxyRow(ProxyRow):
- """Provides a proxy to a QColor property.
-
- The color is returned through the decorative role.
-
- See :class:`ProxyRow`
- """
-
- def data(self, column, role):
- if column == 1: # Show color as decoration, not text
- if role == qt.Qt.DisplayRole:
- return None
- if role == qt.Qt.DecorationRole:
- role = qt.Qt.DisplayRole
- return super(ColorProxyRow, self).data(column, role)
-
-
-class AngleDegreeRow(ProxyRow):
- """ProxyRow patching display of column 1 to add degree symbol
-
- See :class:`ProxyRow`
- """
-
- def __init__(self, *args, **kwargs):
- super(AngleDegreeRow, self).__init__(*args, **kwargs)
-
- def data(self, column, role):
- if column == 1 and role == qt.Qt.DisplayRole:
- return u'%g°' % super(AngleDegreeRow, self).data(column, role)
- else:
- return super(AngleDegreeRow, self).data(column, role)
diff --git a/silx/gui/plot3d/_model/items.py b/silx/gui/plot3d/_model/items.py
deleted file mode 100644
index be51663..0000000
--- a/silx/gui/plot3d/_model/items.py
+++ /dev/null
@@ -1,1760 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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.
-#
-# ###########################################################################*/
-"""
-This module provides base classes to implement models for 3D scene content
-"""
-
-from __future__ import absolute_import, division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-from collections import OrderedDict
-import functools
-import logging
-import weakref
-
-import numpy
-import six
-
-from ...utils.image import convertArrayToQImage
-from ...colors import preferredColormaps
-from ... import qt, icons
-from .. import items
-from ..items.volume import Isosurface, CutPlane, ComplexIsosurface
-from ..Plot3DWidget import Plot3DWidget
-
-
-from .core import AngleDegreeRow, BaseRow, ColorProxyRow, ProxyRow, StaticRow
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ItemProxyRow(ProxyRow):
- """Provides a node to proxy a data accessible through functions.
-
- It listens on sigItemChanged to trigger the update.
-
- Warning: Only weak reference are kept on fget and fset.
-
- :param Item3D item: The item to
- :param str name: The name of this node
- :param callable fget: A callable returning the data
- :param callable fset:
- An optional callable setting the data with data as a single argument.
- :param events:
- An optional event kind or list of event kinds to react upon.
- :param callable toModelData:
- An optional callable to convert from fget
- callable to data returned by the model.
- :param callable fromModelData:
- An optional callable converting data provided to the model to
- data for fset.
- :param editorHint: Data to provide as UserRole for editor selection/setup
- """
-
- def __init__(self,
- item,
- name='',
- fget=None,
- fset=None,
- events=None,
- toModelData=None,
- fromModelData=None,
- editorHint=None):
- super(ItemProxyRow, self).__init__(
- name=name,
- fget=fget,
- fset=fset,
- notify=None,
- toModelData=toModelData,
- fromModelData=fromModelData,
- editorHint=editorHint)
-
- if isinstance(events, (items.ItemChangedType,
- items.Item3DChangedType)):
- events = (events,)
- self.__events = events
- item.sigItemChanged.connect(self.__itemChanged)
-
- def __itemChanged(self, event):
- """Handle item changed
-
- :param Union[ItemChangedType,Item3DChangedType] event:
- """
- if self.__events is None or event in self.__events:
- self._notified()
-
-
-class ItemColorProxyRow(ColorProxyRow, ItemProxyRow):
- """Combines :class:`ColorProxyRow` and :class:`ItemProxyRow`"""
-
- def __init__(self, *args, **kwargs):
- ItemProxyRow.__init__(self, *args, **kwargs)
-
-
-class ItemAngleDegreeRow(AngleDegreeRow, ItemProxyRow):
- """Combines :class:`AngleDegreeRow` and :class:`ItemProxyRow`"""
-
- def __init__(self, *args, **kwargs):
- ItemProxyRow.__init__(self, *args, **kwargs)
-
-
-class _DirectionalLightProxy(qt.QObject):
- """Proxy to handle directional light with angles rather than vector.
- """
-
- sigAzimuthAngleChanged = qt.Signal()
- """Signal sent when the azimuth angle has changed."""
-
- sigAltitudeAngleChanged = qt.Signal()
- """Signal sent when altitude angle has changed."""
-
- def __init__(self, light):
- super(_DirectionalLightProxy, self).__init__()
- self._light = light
- light.addListener(self._directionUpdated)
- self._azimuth = 0
- self._altitude = 0
-
- def getAzimuthAngle(self):
- """Returns the signed angle in the horizontal plane.
-
- Unit: degrees.
- The 0 angle corresponds to the axis perpendicular to the screen.
-
- :rtype: int
- """
- return self._azimuth
-
- def getAltitudeAngle(self):
- """Returns the signed vertical angle from the horizontal plane.
-
- Unit: degrees.
- Range: [-90, +90]
-
- :rtype: int
- """
- return self._altitude
-
- def setAzimuthAngle(self, angle):
- """Set the horizontal angle.
-
- :param int angle: Angle from -z axis in zx plane in degrees.
- """
- angle = int(round(angle))
- if angle != self._azimuth:
- self._azimuth = angle
- self._updateLight()
- self.sigAzimuthAngleChanged.emit()
-
- def setAltitudeAngle(self, angle):
- """Set the horizontal angle.
-
- :param int angle: Angle from -z axis in zy plane in degrees.
- """
- angle = int(round(angle))
- if angle != self._altitude:
- self._altitude = angle
- self._updateLight()
- self.sigAltitudeAngleChanged.emit()
-
- def _directionUpdated(self, *args, **kwargs):
- """Handle light direction update in the scene"""
- # Invert direction to manipulate the 'source' pointing to
- # the center of the viewport
- x, y, z = - self._light.direction
-
- # Horizontal plane is plane xz
- azimuth = int(round(numpy.degrees(numpy.arctan2(x, z))))
- altitude = int(round(numpy.degrees(numpy.pi/2. - numpy.arccos(y))))
-
- if azimuth != self.getAzimuthAngle():
- self.setAzimuthAngle(azimuth)
-
- if altitude != self.getAltitudeAngle():
- self.setAltitudeAngle(altitude)
-
- def _updateLight(self):
- """Update light direction in the scene"""
- azimuth = numpy.radians(self._azimuth)
- delta = numpy.pi/2. - numpy.radians(self._altitude)
- if delta == 0.: # Avoids zenith position
- delta = 0.0001
- z = - numpy.sin(delta) * numpy.cos(azimuth)
- x = - numpy.sin(delta) * numpy.sin(azimuth)
- y = - numpy.cos(delta)
- self._light.direction = x, y, z
-
-
-class Settings(StaticRow):
- """Subtree for :class:`SceneWidget` style parameters.
-
- :param SceneWidget sceneWidget: The widget to control
- """
-
- def __init__(self, sceneWidget):
- background = ColorProxyRow(
- name='Background',
- fget=sceneWidget.getBackgroundColor,
- fset=sceneWidget.setBackgroundColor,
- notify=sceneWidget.sigStyleChanged)
-
- foreground = ColorProxyRow(
- name='Foreground',
- fget=sceneWidget.getForegroundColor,
- fset=sceneWidget.setForegroundColor,
- notify=sceneWidget.sigStyleChanged)
-
- text = ColorProxyRow(
- name='Text',
- fget=sceneWidget.getTextColor,
- fset=sceneWidget.setTextColor,
- notify=sceneWidget.sigStyleChanged)
-
- highlight = ColorProxyRow(
- name='Highlight',
- fget=sceneWidget.getHighlightColor,
- fset=sceneWidget.setHighlightColor,
- notify=sceneWidget.sigStyleChanged)
-
- axesIndicator = ProxyRow(
- name='Axes Indicator',
- fget=sceneWidget.isOrientationIndicatorVisible,
- fset=sceneWidget.setOrientationIndicatorVisible,
- notify=sceneWidget.sigStyleChanged)
-
- # Light direction
-
- self._lightProxy = _DirectionalLightProxy(sceneWidget.viewport.light)
-
- azimuthNode = ProxyRow(
- name='Azimuth',
- fget=self._lightProxy.getAzimuthAngle,
- fset=self._lightProxy.setAzimuthAngle,
- notify=self._lightProxy.sigAzimuthAngleChanged,
- editorHint=(-90, 90))
-
- altitudeNode = ProxyRow(
- name='Altitude',
- fget=self._lightProxy.getAltitudeAngle,
- fset=self._lightProxy.setAltitudeAngle,
- notify=self._lightProxy.sigAltitudeAngleChanged,
- editorHint=(-90, 90))
-
- lightDirection = StaticRow(('Light Direction', None),
- children=(azimuthNode, altitudeNode))
-
- # Fog
- fog = ProxyRow(
- name='Fog',
- fget=sceneWidget.getFogMode,
- fset=sceneWidget.setFogMode,
- notify=sceneWidget.sigStyleChanged,
- toModelData=lambda mode: mode is Plot3DWidget.FogMode.LINEAR,
- fromModelData=lambda mode: Plot3DWidget.FogMode.LINEAR if mode else Plot3DWidget.FogMode.NONE)
-
- # Settings row
- children = (background, foreground, text, highlight,
- axesIndicator, lightDirection, fog)
- super(Settings, self).__init__(('Settings', None), children=children)
-
-
-class Item3DRow(BaseRow):
- """Represents an :class:`Item3D` with checkable visibility
-
- :param Item3D item: The scene item to represent.
- :param str name: The optional name of the item
- """
-
- _EVENTS = items.ItemChangedType.VISIBLE, items.Item3DChangedType.LABEL
- """Events for which to update the first column in the tree"""
-
- def __init__(self, item, name=None):
- self.__name = None if name is None else six.text_type(name)
- super(Item3DRow, self).__init__()
-
- self.setFlags(
- self.flags(0) | qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsSelectable,
- 0)
- self.setFlags(self.flags(1) | qt.Qt.ItemIsSelectable, 1)
-
- self._item = weakref.ref(item)
- item.sigItemChanged.connect(self._itemChanged)
-
- def _itemChanged(self, event):
- """Handle model update upon change"""
- if event in self._EVENTS:
- model = self.model()
- if model is not None:
- index = self.index(column=0)
- model.dataChanged.emit(index, index)
-
- def item(self):
- """Returns the :class:`Item3D` item or None"""
- return self._item()
-
- def data(self, column, role):
- if column == 0:
- if role == qt.Qt.CheckStateRole:
- item = self.item()
- if item is not None and item.isVisible():
- return qt.Qt.Checked
- else:
- return qt.Qt.Unchecked
-
- elif role == qt.Qt.DecorationRole:
- return icons.getQIcon('item-3dim')
-
- elif role == qt.Qt.DisplayRole:
- if self.__name is None:
- item = self.item()
- return '' if item is None else item.getLabel()
- else:
- return self.__name
-
- return super(Item3DRow, self).data(column, role)
-
- def setData(self, column, value, role):
- if column == 0 and role == qt.Qt.CheckStateRole:
- item = self.item()
- if item is not None:
- item.setVisible(value == qt.Qt.Checked)
- return True
- else:
- return False
- return super(Item3DRow, self).setData(column, value, role)
-
- def columnCount(self):
- return 2
-
-
-class DataItem3DBoundingBoxRow(ItemProxyRow):
- """Represents :class:`DataItem3D` bounding box visibility
-
- :param DataItem3D item: The item for which to display/control bounding box
- """
-
- def __init__(self, item):
- super(DataItem3DBoundingBoxRow, self).__init__(
- item=item,
- name='Bounding box',
- fget=item.isBoundingBoxVisible,
- fset=item.setBoundingBoxVisible,
- events=items.Item3DChangedType.BOUNDING_BOX_VISIBLE)
-
-
-class MatrixProxyRow(ItemProxyRow):
- """Proxy for a row of a DataItem3D 3x3 matrix transform
-
- :param DataItem3D item:
- :param int index: Matrix row index
- """
-
- def __init__(self, item, index):
- self._item = weakref.ref(item)
- self._index = index
-
- super(MatrixProxyRow, self).__init__(
- item=item,
- name='',
- fget=self._getMatrixRow,
- fset=self._setMatrixRow,
- events=items.Item3DChangedType.TRANSFORM)
-
- def _getMatrixRow(self):
- """Returns the matrix row.
-
- :rtype: QVector3D
- """
- item = self._item()
- if item is not None:
- matrix = item.getMatrix()
- return qt.QVector3D(*matrix[self._index, :])
- else:
- return None
-
- def _setMatrixRow(self, row):
- """Set the row of the matrix
-
- :param QVector3D row: Row values to set
- """
- item = self._item()
- if item is not None:
- matrix = item.getMatrix()
- matrix[self._index, :] = row.x(), row.y(), row.z()
- item.setMatrix(matrix)
-
- def data(self, column, role):
- data = super(MatrixProxyRow, self).data(column, role)
-
- if column == 1 and role == qt.Qt.DisplayRole:
- # Convert QVector3D to text
- data = "%g; %g; %g" % (data.x(), data.y(), data.z())
-
- return data
-
-
-class DataItem3DTransformRow(StaticRow):
- """Represents :class:`DataItem3D` transform parameters
-
- :param DataItem3D item: The item for which to display/control transform
- """
-
- _ROTATION_CENTER_OPTIONS = 'Origin', 'Lower', 'Center', 'Upper'
-
- def __init__(self, item):
- super(DataItem3DTransformRow, self).__init__(('Transform', None))
- self._item = weakref.ref(item)
-
- translation = ItemProxyRow(
- item=item,
- name='Translation',
- fget=item.getTranslation,
- fset=self._setTranslation,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: qt.QVector3D(*data))
- self.addRow(translation)
-
- # Here to keep a reference
- self._xSetCenter = functools.partial(self._setCenter, index=0)
- self._ySetCenter = functools.partial(self._setCenter, index=1)
- self._zSetCenter = functools.partial(self._setCenter, index=2)
-
- rotateCenter = StaticRow(
- ('Center', None),
- children=(
- ItemProxyRow(item=item,
- name='X axis',
- fget=item.getRotationCenter,
- fset=self._xSetCenter,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=functools.partial(
- self._centerToModelData, index=0),
- editorHint=self._ROTATION_CENTER_OPTIONS),
- ItemProxyRow(item=item,
- name='Y axis',
- fget=item.getRotationCenter,
- fset=self._ySetCenter,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=functools.partial(
- self._centerToModelData, index=1),
- editorHint=self._ROTATION_CENTER_OPTIONS),
- ItemProxyRow(item=item,
- name='Z axis',
- fget=item.getRotationCenter,
- fset=self._zSetCenter,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=functools.partial(
- self._centerToModelData, index=2),
- editorHint=self._ROTATION_CENTER_OPTIONS),
- ))
-
- rotate = StaticRow(
- ('Rotation', None),
- children=(
- ItemAngleDegreeRow(
- item=item,
- name='Angle',
- fget=item.getRotation,
- fset=self._setAngle,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: data[0]),
- ItemProxyRow(
- item=item,
- name='Axis',
- fget=item.getRotation,
- fset=self._setAxis,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: qt.QVector3D(*data[1])),
- rotateCenter
- ))
- self.addRow(rotate)
-
- scale = ItemProxyRow(
- item=item,
- name='Scale',
- fget=item.getScale,
- fset=self._setScale,
- events=items.Item3DChangedType.TRANSFORM,
- toModelData=lambda data: qt.QVector3D(*data))
- self.addRow(scale)
-
- matrix = StaticRow(
- ('Matrix', None),
- children=(MatrixProxyRow(item, 0),
- MatrixProxyRow(item, 1),
- MatrixProxyRow(item, 2)))
- self.addRow(matrix)
-
- def item(self):
- """Returns the :class:`Item3D` item or None"""
- return self._item()
-
- @staticmethod
- def _centerToModelData(center, index):
- """Convert rotation center information from scene to model.
-
- :param center: The center info from the scene
- :param int index: dimension to convert
- """
- value = center[index]
- if isinstance(value, six.string_types):
- return value.title()
- elif value == 0.:
- return 'Origin'
- else:
- return six.text_type(value)
-
- def _setCenter(self, value, index):
- """Set one dimension of the rotation center.
-
- :param value: Value received through the model.
- :param int index: dimension to set
- """
- item = self.item()
- if item is not None:
- if value == 'Origin':
- value = 0.
- elif value not in self._ROTATION_CENTER_OPTIONS:
- value = float(value)
- else:
- value = value.lower()
-
- center = list(item.getRotationCenter())
- center[index] = value
- item.setRotationCenter(*center)
-
- def _setAngle(self, angle):
- """Set rotation angle.
-
- :param float angle:
- """
- item = self.item()
- if item is not None:
- _, axis = item.getRotation()
- item.setRotation(angle, axis)
-
- def _setAxis(self, axis):
- """Set rotation axis.
-
- :param QVector3D axis:
- """
- item = self.item()
- if item is not None:
- angle, _ = item.getRotation()
- item.setRotation(angle, (axis.x(), axis.y(), axis.z()))
-
- def _setTranslation(self, translation):
- """Set translation transform.
-
- :param QVector3D translation:
- """
- item = self.item()
- if item is not None:
- item.setTranslation(translation.x(), translation.y(), translation.z())
-
- def _setScale(self, scale):
- """Set scale transform.
-
- :param QVector3D scale:
- """
- item = self.item()
- if item is not None:
- sx, sy, sz = scale.x(), scale.y(), scale.z()
- if sx == 0. or sy == 0. or sz == 0.:
- _logger.warning('Cannot set scale to 0: ignored')
- else:
- item.setScale(scale.x(), scale.y(), scale.z())
-
-
-class GroupItemRow(Item3DRow):
- """Represents a :class:`GroupItem` with transforms and children
-
- :param GroupItem item: The scene group to represent.
- :param str name: The optional name of the group
- """
-
- _CHILDREN_ROW_OFFSET = 2
- """Number of rows for group parameters. Children are added after"""
-
- def __init__(self, item, name=None):
- super(GroupItemRow, self).__init__(item, name)
- self.addRow(DataItem3DBoundingBoxRow(item))
- self.addRow(DataItem3DTransformRow(item))
-
- item.sigItemAdded.connect(self._itemAdded)
- item.sigItemRemoved.connect(self._itemRemoved)
-
- for child in item.getItems():
- self.addRow(nodeFromItem(child))
-
- def _itemAdded(self, item):
- """Handle item addition to the group and add it to the model.
-
- :param Item3D item: added item
- """
- group = self.item()
- if group is None:
- return
-
- row = group.getItems().index(item)
- self.addRow(nodeFromItem(item), row + self._CHILDREN_ROW_OFFSET)
-
- def _itemRemoved(self, item):
- """Handle item removal from the group and remove it from the model.
-
- :param Item3D item: removed item
- """
- group = self.item()
- if group is None:
- return
-
- # Find item
- for row in self.children():
- if isinstance(row, Item3DRow) and row.item() is item:
- self.removeRow(row)
- break # Got it
- else:
- raise RuntimeError("Model does not correspond to scene content")
-
-
-class InterpolationRow(ItemProxyRow):
- """Represents :class:`InterpolationMixIn` property.
-
- :param Item3D item: Scene item with interpolation property
- """
-
- def __init__(self, item):
- modes = [mode.title() for mode in item.INTERPOLATION_MODES]
- super(InterpolationRow, self).__init__(
- item=item,
- name='Interpolation',
- fget=item.getInterpolation,
- fset=item.setInterpolation,
- events=items.Item3DChangedType.INTERPOLATION,
- toModelData=lambda mode: mode.title(),
- fromModelData=lambda mode: mode.lower(),
- editorHint=modes)
-
-
-class _ColormapBaseProxyRow(ProxyRow):
- """Base class for colormap model row
-
- This class handle synchronization and signals from the item and the colormap
- """
-
- _sigColormapChanged = qt.Signal()
- """Signal used internally to notify colormap (or data) update"""
-
- def __init__(self, item, *args, **kwargs):
- self._item = weakref.ref(item)
- self._colormap = item.getColormap()
-
- ProxyRow.__init__(self, *args, **kwargs)
-
- self._colormap.sigChanged.connect(self._colormapChanged)
- item.sigItemChanged.connect(self._itemChanged)
- self._sigColormapChanged.connect(self._modelUpdated)
-
- def item(self):
- """Returns the :class:`ColormapMixIn` item or None"""
- return self._item()
-
- def _getColormapRange(self):
- """Returns the range of the colormap for the current data.
-
- :return: Colormap range (min, max)
- """
- item = self.item()
- if item is not None and self._colormap is not None:
- return self._colormap.getColormapRange(item)
- else:
- return 1, 100 # Fallback
-
- def _modelUpdated(self, *args, **kwargs):
- """Emit dataChanged in the model"""
- topLeft = self.index(column=0)
- bottomRight = self.index(column=1)
- model = self.model()
- if model is not None:
- model.dataChanged.emit(topLeft, bottomRight)
-
- def _colormapChanged(self):
- self._sigColormapChanged.emit()
-
- def _itemChanged(self, event):
- """Handle change of colormap or data in the item.
-
- :param ItemChangedType event:
- """
- if event == items.ItemChangedType.COLORMAP:
- self._sigColormapChanged.emit()
- if self._colormap is not None:
- self._colormap.sigChanged.disconnect(self._colormapChanged)
-
- item = self.item()
- if item is not None:
- self._colormap = item.getColormap()
- self._colormap.sigChanged.connect(self._colormapChanged)
- else:
- self._colormap = None
-
- elif event == items.ItemChangedType.DATA:
- self._sigColormapChanged.emit()
-
-
-class _ColormapBoundRow(_ColormapBaseProxyRow):
- """ProxyRow for colormap min or max
-
- :param ColormapMixIn item: The item to handle
- :param str name: Name of the raw
- :param int index: 0 for Min and 1 of Max
- """
-
- def __init__(self, item, name, index):
- self._index = index
- _ColormapBaseProxyRow.__init__(
- self,
- item,
- name=name,
- fget=self._getBound,
- fset=self._setBound)
-
- self.setToolTip('Colormap %s bound:\n'
- 'Check to set bound manually, '
- 'uncheck for autoscale' % name.lower())
-
- def _getRawBound(self):
- """Proxy to get raw colormap bound
-
- :rtype: float or None
- """
- if self._colormap is None:
- return None
- elif self._index == 0:
- return self._colormap.getVMin()
- else: # self._index == 1
- return self._colormap.getVMax()
-
- def _getBound(self):
- """Proxy to get colormap effective bound value
-
- :rtype: float
- """
- if self._colormap is not None:
- bound = self._getRawBound()
-
- if bound is None:
- bound = self._getColormapRange()[self._index]
- return bound
- else:
- return 1. # Fallback
-
- def _setBound(self, value):
- """Proxy to set colormap bound.
-
- :param float value:
- """
- if self._colormap is not None:
- if self._index == 0:
- min_ = value
- max_ = self._colormap.getVMax()
- else: # self._index == 1
- min_ = self._colormap.getVMin()
- max_ = value
-
- if max_ is not None and min_ is not None and min_ > max_:
- min_, max_ = max_, min_
- self._colormap.setVRange(min_, max_)
-
- def flags(self, column):
- if column == 0:
- return qt.Qt.ItemIsEnabled | qt.Qt.ItemIsUserCheckable
-
- elif column == 1:
- if self._getRawBound() is not None:
- flags = qt.Qt.ItemIsEditable | qt.Qt.ItemIsEnabled
- else:
- flags = qt.Qt.NoItemFlags # Disabled if autoscale
- return flags
-
- else: # Never event
- return super(_ColormapBoundRow, self).flags(column)
-
- def data(self, column, role):
- if column == 0 and role == qt.Qt.CheckStateRole:
- if self._getRawBound() is None:
- return qt.Qt.Unchecked
- else:
- return qt.Qt.Checked
-
- else:
- return super(_ColormapBoundRow, self).data(column, role)
-
- def setData(self, column, value, role):
- if column == 0 and role == qt.Qt.CheckStateRole:
- if self._colormap is not None:
- bound = self._getBound() if value == qt.Qt.Checked else None
- self._setBound(bound)
- return True
- else:
- return False
-
- return super(_ColormapBoundRow, self).setData(column, value, role)
-
-
-class _ColormapGammaRow(_ColormapBaseProxyRow):
- """ProxyRow for colormap gamma normalization parameter
-
- :param ColormapMixIn item: The item to handle
- :param str name: Name of the raw
- """
-
- def __init__(self, item):
- _ColormapBaseProxyRow.__init__(
- self,
- item,
- name="Gamma",
- fget=self._getGammaNormalizationParameter,
- fset=self._setGammaNormalizationParameter)
-
- self.setToolTip('Colormap gamma correction parameter:\n'
- 'Only meaningful for gamma normalization.')
-
- def _getGammaNormalizationParameter(self):
- """Proxy for :meth:`Colormap.getGammaNormalizationParameter`"""
- if self._colormap is not None:
- return self._colormap.getGammaNormalizationParameter()
- else:
- return 0.0
-
- def _setGammaNormalizationParameter(self, gamma):
- """Proxy for :meth:`Colormap.setGammaNormalizationParameter`"""
- if self._colormap is not None:
- return self._colormap.setGammaNormalizationParameter(gamma)
-
- def _getNormalization(self):
- """Proxy for :meth:`Colormap.getNormalization`"""
- if self._colormap is not None:
- return self._colormap.getNormalization()
- else:
- return ''
-
- def flags(self, column):
- if column in (0, 1):
- if self._getNormalization() == 'gamma':
- flags = qt.Qt.ItemIsEditable | qt.Qt.ItemIsEnabled
- else:
- flags = qt.Qt.NoItemFlags # Disabled if not gamma correction
- return flags
-
- else: # Never event
- return super(_ColormapGammaRow, self).flags(column)
-
-
-class ColormapRow(_ColormapBaseProxyRow):
- """Represents :class:`ColormapMixIn` property.
-
- :param Item3D item: Scene item with colormap property
- """
-
- def __init__(self, item):
- super(ColormapRow, self).__init__(
- item,
- name='Colormap',
- fget=self._get)
-
- self._colormapImage = None
-
- self._colormapsMapping = {}
- for cmap in preferredColormaps():
- self._colormapsMapping[cmap.title()] = cmap
-
- self.addRow(ProxyRow(
- name='Name',
- fget=self._getName,
- fset=self._setName,
- notify=self._sigColormapChanged,
- editorHint=list(self._colormapsMapping.keys())))
-
- norms = [norm.title() for norm in self._colormap.NORMALIZATIONS]
- self.addRow(ProxyRow(
- name='Normalization',
- fget=self._getNormalization,
- fset=self._setNormalization,
- notify=self._sigColormapChanged,
- editorHint=norms))
-
- self.addRow(_ColormapGammaRow(item))
-
- modes = [mode.title() for mode in self._colormap.AUTOSCALE_MODES]
- self.addRow(ProxyRow(
- name='Autoscale Mode',
- fget=self._getAutoscaleMode,
- fset=self._setAutoscaleMode,
- notify=self._sigColormapChanged,
- editorHint=modes))
-
- self.addRow(_ColormapBoundRow(item, name='Min.', index=0))
- self.addRow(_ColormapBoundRow(item, name='Max.', index=1))
-
- self._sigColormapChanged.connect(self._updateColormapImage)
-
- def getColormapImage(self):
- """Returns image representing the colormap or None
-
- :rtype: Union[QImage,None]
- """
- if self._colormapImage is None and self._colormap is not None:
- image = numpy.zeros((16, 130, 3), dtype=numpy.uint8)
- image[1:-1, 1:-1] = self._colormap.getNColors(image.shape[1] - 2)[:, :3]
- self._colormapImage = convertArrayToQImage(image)
- return self._colormapImage
-
- def _get(self):
- """Getter for ProxyRow subclass"""
- return None
-
- def _getName(self):
- """Proxy for :meth:`Colormap.getName`"""
- if self._colormap is not None and self._colormap.getName() is not None:
- return self._colormap.getName().title()
- else:
- return ''
-
- def _setName(self, name):
- """Proxy for :meth:`Colormap.setName`"""
- # Convert back from titled to name if possible
- if self._colormap is not None:
- name = self._colormapsMapping.get(name, name)
- self._colormap.setName(name)
-
- def _getNormalization(self):
- """Proxy for :meth:`Colormap.getNormalization`"""
- if self._colormap is not None:
- return self._colormap.getNormalization().title()
- else:
- return ''
-
- def _setNormalization(self, normalization):
- """Proxy for :meth:`Colormap.setNormalization`"""
- if self._colormap is not None:
- return self._colormap.setNormalization(normalization.lower())
-
- def _getAutoscaleMode(self):
- """Proxy for :meth:`Colormap.getAutoscaleMode`"""
- if self._colormap is not None:
- return self._colormap.getAutoscaleMode().title()
- else:
- return ''
-
- def _setAutoscaleMode(self, mode):
- """Proxy for :meth:`Colormap.setAutoscaleMode`"""
- if self._colormap is not None:
- return self._colormap.setAutoscaleMode(mode.lower())
-
- def _updateColormapImage(self, *args, **kwargs):
- """Notify colormap update to update the image in the tree"""
- if self._colormapImage is not None:
- self._colormapImage = None
- model = self.model()
- if model is not None:
- index = self.index(column=1)
- model.dataChanged.emit(index, index)
-
- def data(self, column, role):
- if column == 1 and role == qt.Qt.DecorationRole:
- return self.getColormapImage()
- else:
- return super(ColormapRow, self).data(column, role)
-
-
-class SymbolRow(ItemProxyRow):
- """Represents :class:`SymbolMixIn` symbol property.
-
- :param Item3D item: Scene item with symbol property
- """
-
- def __init__(self, item):
- names = [item.getSymbolName(s) for s in item.getSupportedSymbols()]
- super(SymbolRow, self).__init__(
- item=item,
- name='Marker',
- fget=item.getSymbolName,
- fset=item.setSymbol,
- events=items.ItemChangedType.SYMBOL,
- editorHint=names)
-
-
-class SymbolSizeRow(ItemProxyRow):
- """Represents :class:`SymbolMixIn` symbol size property.
-
- :param Item3D item: Scene item with symbol size property
- """
-
- def __init__(self, item):
- super(SymbolSizeRow, self).__init__(
- item=item,
- name='Marker size',
- fget=item.getSymbolSize,
- fset=item.setSymbolSize,
- events=items.ItemChangedType.SYMBOL_SIZE,
- editorHint=(1, 20)) # TODO link with OpenGL max point size
-
-
-class PlaneEquationRow(ItemProxyRow):
- """Represents :class:`PlaneMixIn` as plane equation.
-
- :param Item3D item: Scene item with plane equation property
- """
-
- def __init__(self, item):
- super(PlaneEquationRow, self).__init__(
- item=item,
- name='Equation',
- fget=item.getParameters,
- fset=item.setParameters,
- events=items.ItemChangedType.POSITION,
- toModelData=lambda data: qt.QVector4D(*data),
- fromModelData=lambda data: (data.x(), data.y(), data.z(), data.w()))
- self._item = weakref.ref(item)
-
- def data(self, column, role):
- if column == 1 and role == qt.Qt.DisplayRole:
- item = self._item()
- if item is not None:
- params = item.getParameters()
- return ('%gx %+gy %+gz %+g = 0' %
- (params[0], params[1], params[2], params[3]))
- return super(PlaneEquationRow, self).data(column, role)
-
-
-class PlaneRow(ItemProxyRow):
- """Represents :class:`PlaneMixIn` property.
-
- :param Item3D item: Scene item with plane equation property
- """
-
- _PLANES = OrderedDict((('Plane 0', (1., 0., 0.)),
- ('Plane 1', (0., 1., 0.)),
- ('Plane 2', (0., 0., 1.)),
- ('-', None)))
- """Mapping of plane names to normals"""
-
- _PLANE_ICONS = {'Plane 0': '3d-plane-normal-x',
- 'Plane 1': '3d-plane-normal-y',
- 'Plane 2': '3d-plane-normal-z',
- '-': '3d-plane'}
- """Mapping of plane names to normals"""
-
- def __init__(self, item):
- super(PlaneRow, self).__init__(
- item=item,
- name='Plane',
- fget=self.__getPlaneName,
- fset=self.__setPlaneName,
- events=items.ItemChangedType.POSITION,
- editorHint=tuple(self._PLANES.keys()))
- self._item = weakref.ref(item)
- self._lastName = None
-
- self.addRow(PlaneEquationRow(item))
-
- def _notified(self, *args, **kwargs):
- """Handle notification of modification
-
- Here only send if plane name actually changed
- """
- if self._lastName != self.__getPlaneName():
- super(PlaneRow, self)._notified()
-
- def __getPlaneName(self):
- """Returns name of plane // to axes or '-'
-
- :rtype: str
- """
- item = self._item()
- planeNormal = item.getNormal() if item is not None else None
-
- for name, normal in self._PLANES.items():
- if numpy.array_equal(planeNormal, normal):
- return name
- return '-'
-
- def __setPlaneName(self, data):
- """Set plane normal according to given plane name
-
- :param str data: Selected plane name
- """
- item = self._item()
- if item is not None:
- for name, normal in self._PLANES.items():
- if data == name and normal is not None:
- item.setNormal(normal)
-
- def data(self, column, role):
- if column == 1 and role == qt.Qt.DecorationRole:
- return icons.getQIcon(self._PLANE_ICONS[self.__getPlaneName()])
- data = super(PlaneRow, self).data(column, role)
- if column == 1 and role == qt.Qt.DisplayRole:
- self._lastName = data
- return data
-
-
-class ComplexModeRow(ItemProxyRow):
- """Represents :class:`items.ComplexMixIn` symbol property.
-
- :param Item3D item: Scene item with symbol property
- """
-
- def __init__(self, item, name='Mode'):
- names = [m.value.replace('_', ' ').title()
- for m in item.supportedComplexModes()]
- super(ComplexModeRow, self).__init__(
- item=item,
- name=name,
- fget=item.getComplexMode,
- fset=item.setComplexMode,
- events=items.ItemChangedType.COMPLEX_MODE,
- toModelData=lambda data: data.value.replace('_', ' ').title(),
- fromModelData=lambda data: data.lower().replace(' ', '_'),
- editorHint=names)
-
-
-class RemoveIsosurfaceRow(BaseRow):
- """Class for Isosurface Delete button
-
- :param Isosurface isosurface: The isosurface item to attach the button to.
- """
-
- def __init__(self, isosurface):
- super(RemoveIsosurfaceRow, self).__init__()
- self._isosurface = weakref.ref(isosurface)
-
- def createEditor(self):
- """Specific editor factory provided to the model"""
- editor = qt.QWidget()
- layout = qt.QHBoxLayout(editor)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- removeBtn = qt.QToolButton()
- removeBtn.setText('Delete')
- removeBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
- layout.addWidget(removeBtn)
- removeBtn.clicked.connect(self._removeClicked)
-
- layout.addStretch(1)
- return editor
-
- def isosurface(self):
- """Returns the controlled isosurface
-
- :rtype: Isosurface
- """
- return self._isosurface()
-
- def data(self, column, role):
- if column == 0 and role == qt.Qt.UserRole: # editor hint
- return self.createEditor
-
- return super(RemoveIsosurfaceRow, self).data(column, role)
-
- def flags(self, column):
- flags = super(RemoveIsosurfaceRow, self).flags(column)
- if column == 0:
- flags |= qt.Qt.ItemIsEditable
- return flags
-
- def _removeClicked(self):
- """Handle Delete button clicked"""
- isosurface = self.isosurface()
- if isosurface is not None:
- volume = isosurface.parent()
- if volume is not None:
- volume.removeIsosurface(isosurface)
-
-
-class IsosurfaceRow(Item3DRow):
- """Represents an :class:`Isosurface` item.
-
- :param Isosurface item: Isosurface item
- """
-
- _LEVEL_SLIDER_RANGE = 0, 1000
- """Range given as editor hint"""
-
- _EVENTS = items.ItemChangedType.VISIBLE, items.ItemChangedType.COLOR
- """Events for which to update the first column in the tree"""
-
- def __init__(self, item):
- super(IsosurfaceRow, self).__init__(item, name=item.getLevel())
-
- self.setFlags(self.flags(1) | qt.Qt.ItemIsEditable, 1)
-
- item.sigItemChanged.connect(self._levelChanged)
-
- self.addRow(ItemProxyRow(
- item=item,
- name='Level',
- fget=self._getValueForLevelSlider,
- fset=self._setLevelFromSliderValue,
- events=items.Item3DChangedType.ISO_LEVEL,
- editorHint=self._LEVEL_SLIDER_RANGE))
-
- self.addRow(ItemColorProxyRow(
- item=item,
- name='Color',
- fget=self._rgbColor,
- fset=self._setRgbColor,
- events=items.ItemChangedType.COLOR))
-
- self.addRow(ItemProxyRow(
- item=item,
- name='Opacity',
- fget=self._opacity,
- fset=self._setOpacity,
- events=items.ItemChangedType.COLOR,
- editorHint=(0, 255)))
-
- self.addRow(RemoveIsosurfaceRow(item))
-
- def _getValueForLevelSlider(self):
- """Convert iso level to slider value.
-
- :rtype: int
- """
- item = self.item()
- if item is not None:
- volume = item.parent()
- if volume is not None:
- dataRange = volume.getDataRange()
- if dataRange is not None:
- dataMin, dataMax = dataRange[0], dataRange[-1]
- if dataMax != dataMin:
- offset = (item.getLevel() - dataMin) / (dataMax - dataMin)
- else:
- offset = 0.
-
- sliderMin, sliderMax = self._LEVEL_SLIDER_RANGE
- value = sliderMin + (sliderMax - sliderMin) * offset
- return value
- return 0
-
- def _setLevelFromSliderValue(self, value):
- """Convert slider value to isolevel.
-
- :param int value:
- """
- item = self.item()
- if item is not None:
- volume = item.parent()
- if volume is not None:
- dataRange = volume.getDataRange()
- if dataRange is not None:
- sliderMin, sliderMax = self._LEVEL_SLIDER_RANGE
- offset = (value - sliderMin) / (sliderMax - sliderMin)
-
- dataMin, dataMax = dataRange[0], dataRange[-1]
- level = dataMin + (dataMax - dataMin) * offset
- item.setLevel(level)
-
- def _rgbColor(self):
- """Proxy to get the isosurface's RGB color without transparency
-
- :rtype: QColor
- """
- item = self.item()
- if item is None:
- return None
- else:
- color = item.getColor()
- color.setAlpha(255)
- return color
-
- def _setRgbColor(self, color):
- """Proxy to set the isosurface's RGB color without transparency
-
- :param QColor color:
- """
- item = self.item()
- if item is not None:
- color.setAlpha(item.getColor().alpha())
- item.setColor(color)
-
- def _opacity(self):
- """Proxy to get the isosurface's transparency
-
- :rtype: int
- """
- item = self.item()
- return 255 if item is None else item.getColor().alpha()
-
- def _setOpacity(self, opacity):
- """Proxy to set the isosurface's transparency.
-
- :param int opacity:
- """
- item = self.item()
- if item is not None:
- color = item.getColor()
- color.setAlpha(opacity)
- item.setColor(color)
-
- def _levelChanged(self, event):
- """Handle isosurface level changed and notify model
-
- :param ItemChangedType event:
- """
- if event == items.Item3DChangedType.ISO_LEVEL:
- model = self.model()
- if model is not None:
- index = self.index(column=1)
- model.dataChanged.emit(index, index)
-
- def data(self, column, role):
- if column == 0: # Show color as decoration, not text
- if role == qt.Qt.DisplayRole:
- return None
- elif role == qt.Qt.DecorationRole:
- return self._rgbColor()
-
- elif column == 1 and role in (qt.Qt.DisplayRole, qt.Qt.EditRole):
- item = self.item()
- return None if item is None else item.getLevel()
-
- return super(IsosurfaceRow, self).data(column, role)
-
- def setData(self, column, value, role):
- if column == 1 and role == qt.Qt.EditRole:
- item = self.item()
- if item is not None:
- item.setLevel(value)
- return True
-
- return super(IsosurfaceRow, self).setData(column, value, role)
-
-
-class ComplexIsosurfaceRow(IsosurfaceRow):
- """Represents an :class:`ComplexIsosurface` item.
-
- :param ComplexIsosurface item:
- """
-
- _EVENTS = (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.COLOR,
- items.ItemChangedType.COMPLEX_MODE)
- """Events for which to update the first column in the tree"""
-
- def __init__(self, item):
- super(ComplexIsosurfaceRow, self).__init__(item)
-
- self.addRow(ComplexModeRow(item, "Color Complex Mode"), index=1)
- for row in self.children():
- if isinstance(row, ColorProxyRow):
- self._colorRow = row
- break
- else:
- raise RuntimeError("Cannot retrieve Color tree row")
- self._colormapRow = ColormapRow(item)
-
- self.__updateRowsForItem(item)
- item.sigItemChanged.connect(self.__itemChanged)
-
- def __itemChanged(self, event):
- """Update enabled/disabled rows"""
- if event == items.ItemChangedType.COMPLEX_MODE:
- item = self.sender()
- self.__updateRowsForItem(item)
-
- def __updateRowsForItem(self, item):
- """Update rows for item
-
- :param item:
- """
- if not isinstance(item, ComplexIsosurface):
- return
-
- if item.getComplexMode() == items.ComplexMixIn.ComplexMode.NONE:
- removed = self._colormapRow
- added = self._colorRow
- else:
- removed = self._colorRow
- added = self._colormapRow
-
- # Remove unwanted rows
- if removed in self.children():
- self.removeRow(removed)
-
- # Add required rows
- if added not in self.children():
- self.addRow(added, index=2)
-
- def data(self, column, role):
- if column == 0 and role == qt.Qt.DecorationRole:
- item = self.item()
- if (item is not None and
- item.getComplexMode() != items.ComplexMixIn.ComplexMode.NONE):
- return self._colormapRow.getColormapImage()
-
- return super(ComplexIsosurfaceRow, self).data(column, role)
-
-
-class AddIsosurfaceRow(BaseRow):
- """Class for Isosurface create button
-
- :param Union[ScalarField3D,ComplexField3D] volume:
- The volume item to attach the button to.
- """
-
- def __init__(self, volume):
- super(AddIsosurfaceRow, self).__init__()
- self._volume = weakref.ref(volume)
-
- def createEditor(self):
- """Specific editor factory provided to the model"""
- editor = qt.QWidget()
- layout = qt.QHBoxLayout(editor)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- addBtn = qt.QToolButton()
- addBtn.setText('+')
- addBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly)
- layout.addWidget(addBtn)
- addBtn.clicked.connect(self._addClicked)
-
- layout.addStretch(1)
- return editor
-
- def volume(self):
- """Returns the controlled volume item
-
- :rtype: Union[ScalarField3D,ComplexField3D]
- """
- return self._volume()
-
- def data(self, column, role):
- if column == 0 and role == qt.Qt.UserRole: # editor hint
- return self.createEditor
-
- return super(AddIsosurfaceRow, self).data(column, role)
-
- def flags(self, column):
- flags = super(AddIsosurfaceRow, self).flags(column)
- if column == 0:
- flags |= qt.Qt.ItemIsEditable
- return flags
-
- def _addClicked(self):
- """Handle Delete button clicked"""
- volume = self.volume()
- if volume is not None:
- dataRange = volume.getDataRange()
- if dataRange is None:
- dataRange = 0., 1.
-
- volume.addIsosurface(
- numpy.mean((dataRange[0], dataRange[-1])),
- '#0000FF')
-
-
-class VolumeIsoSurfacesRow(StaticRow):
- """Represents :class:`ScalarFieldView`'s isosurfaces
-
- :param Union[ScalarField3D,ComplexField3D] volume:
- Volume item to control
- """
-
- def __init__(self, volume):
- super(VolumeIsoSurfacesRow, self).__init__(
- ('Isosurfaces', None))
- self._volume = weakref.ref(volume)
-
- volume.sigIsosurfaceAdded.connect(self._isosurfaceAdded)
- volume.sigIsosurfaceRemoved.connect(self._isosurfaceRemoved)
-
- if isinstance(volume, items.ComplexMixIn):
- self.addRow(ComplexModeRow(volume, "Complex Mode"))
-
- for item in volume.getIsosurfaces():
- self.addRow(nodeFromItem(item))
-
- self.addRow(AddIsosurfaceRow(volume))
-
- def volume(self):
- """Returns the controlled volume item
-
- :rtype: Union[ScalarField3D,ComplexField3D]
- """
- return self._volume()
-
- def _isosurfaceAdded(self, item):
- """Handle isosurface addition
-
- :param Isosurface item: added isosurface
- """
- volume = self.volume()
- if volume is None:
- return
-
- row = volume.getIsosurfaces().index(item)
- if isinstance(volume, items.ComplexMixIn):
- row += 1 # Offset for the ComplexModeRow
- self.addRow(nodeFromItem(item), row)
-
- def _isosurfaceRemoved(self, item):
- """Handle isosurface removal
-
- :param Isosurface item: removed isosurface
- """
- volume = self.volume()
- if volume is None:
- return
-
- # Find item
- for row in self.children():
- if isinstance(row, IsosurfaceRow) and row.item() is item:
- self.removeRow(row)
- break # Got it
- else:
- raise RuntimeError("Model does not correspond to scene content")
-
-
-class Scatter2DPropertyMixInRow(object):
- """Mix-in class that enable/disable row according to Scatter2D mode.
-
- :param Scatter2D item:
- :param str propertyName: Name of the Scatter2D property of this row
- """
-
- def __init__(self, item, propertyName):
- assert propertyName in ('lineWidth', 'symbol', 'symbolSize')
- self.__propertyName = propertyName
-
- self.__isEnabled = item.isPropertyEnabled(propertyName)
- self.__updateFlags()
-
- item.sigItemChanged.connect(self.__itemChanged)
-
- def data(self, column, role):
- if column == 1 and not self.__isEnabled:
- # Discard data and editorHint if disabled
- return None
- else:
- return super(Scatter2DPropertyMixInRow, self).data(column, role)
-
- def __updateFlags(self):
- """Update model flags"""
- if self.__isEnabled:
- self.setFlags(qt.Qt.ItemIsEnabled, 0)
- self.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsEditable, 1)
- else:
- self.setFlags(qt.Qt.NoItemFlags)
-
- def __itemChanged(self, event):
- """Set flags to enable/disable the row"""
- if event == items.ItemChangedType.VISUALIZATION_MODE:
- item = self.sender()
- if item is not None: # This occurs with PySide/python2.7
- self.__isEnabled = item.isPropertyEnabled(self.__propertyName)
- self.__updateFlags()
-
- # Notify model
- model = self.model()
- if model is not None:
- begin = self.index(column=0)
- end = self.index(column=1)
- model.dataChanged.emit(begin, end)
-
-
-class Scatter2DSymbolRow(Scatter2DPropertyMixInRow, SymbolRow):
- """Specific class for Scatter2D symbol.
-
- It is enabled/disabled according to visualization mode.
-
- :param Scatter2D item:
- """
-
- def __init__(self, item):
- SymbolRow.__init__(self, item)
- Scatter2DPropertyMixInRow.__init__(self, item, 'symbol')
-
-
-class Scatter2DSymbolSizeRow(Scatter2DPropertyMixInRow, SymbolSizeRow):
- """Specific class for Scatter2D symbol size.
-
- It is enabled/disabled according to visualization mode.
-
- :param Scatter2D item:
- """
-
- def __init__(self, item):
- SymbolSizeRow.__init__(self, item)
- Scatter2DPropertyMixInRow.__init__(self, item, 'symbolSize')
-
-
-class Scatter2DLineWidth(Scatter2DPropertyMixInRow, ItemProxyRow):
- """Specific class for Scatter2D symbol size.
-
- It is enabled/disabled according to visualization mode.
-
- :param Scatter2D item:
- """
-
- def __init__(self, item):
- # TODO link editorHint with OpenGL max line width
- ItemProxyRow.__init__(self,
- item=item,
- name='Line width',
- fget=item.getLineWidth,
- fset=item.setLineWidth,
- events=items.ItemChangedType.LINE_WIDTH,
- editorHint=(1, 10))
- Scatter2DPropertyMixInRow.__init__(self, item, 'lineWidth')
-
-
-def initScatter2DNode(node, item):
- """Specific node init for Scatter2D to set order of parameters
-
- :param Item3DRow node: The model node to setup
- :param Scatter2D item: The Scatter2D the node is representing
- """
- node.addRow(ItemProxyRow(
- item=item,
- name='Mode',
- fget=item.getVisualization,
- fset=item.setVisualization,
- events=items.ItemChangedType.VISUALIZATION_MODE,
- editorHint=[m.value.title() for m in item.supportedVisualizations()],
- toModelData=lambda data: data.value.title(),
- fromModelData=lambda data: data.lower()))
-
- node.addRow(ItemProxyRow(
- item=item,
- name='Height map',
- fget=item.isHeightMap,
- fset=item.setHeightMap,
- events=items.Item3DChangedType.HEIGHT_MAP))
-
- node.addRow(ColormapRow(item))
-
- node.addRow(Scatter2DSymbolRow(item))
- node.addRow(Scatter2DSymbolSizeRow(item))
-
- node.addRow(Scatter2DLineWidth(item))
-
-
-def initVolumeNode(node, item):
- """Specific node init for volume items
-
- :param Item3DRow node: The model node to setup
- :param Union[ScalarField3D,ComplexField3D] item:
- The volume item represented by the node
- """
- node.addRow(nodeFromItem(item.getCutPlanes()[0])) # Add cut plane
- node.addRow(VolumeIsoSurfacesRow(item))
-
-
-def initVolumeCutPlaneNode(node, item):
- """Specific node init for volume CutPlane
-
- :param Item3DRow node: The model node to setup
- :param CutPlane item: The CutPlane the node is representing
- """
- if isinstance(item, items.ComplexMixIn):
- node.addRow(ComplexModeRow(item))
-
- node.addRow(PlaneRow(item))
-
- node.addRow(ColormapRow(item))
-
- node.addRow(ItemProxyRow(
- item=item,
- name='Show <=Min',
- fget=item.getDisplayValuesBelowMin,
- fset=item.setDisplayValuesBelowMin,
- events=items.ItemChangedType.ALPHA))
-
- node.addRow(InterpolationRow(item))
-
-
-NODE_SPECIFIC_INIT = [ # class, init(node, item)
- (items.Scatter2D, initScatter2DNode),
- (items.ScalarField3D, initVolumeNode),
- (CutPlane, initVolumeCutPlaneNode),
-]
-"""List of specific node init for different item class"""
-
-
-def nodeFromItem(item):
- """Create :class:`Item3DRow` subclass corresponding to item
-
- :param Item3D item: The item fow which to create the node
- :rtype: Item3DRow
- """
- assert isinstance(item, items.Item3D)
-
- # Item with specific model row class
- if isinstance(item, (items.GroupItem, items.GroupWithAxesItem)):
- return GroupItemRow(item)
- elif isinstance(item, ComplexIsosurface):
- return ComplexIsosurfaceRow(item)
- elif isinstance(item, Isosurface):
- return IsosurfaceRow(item)
-
- # Create Item3DRow and populate it
- node = Item3DRow(item)
-
- if isinstance(item, items.DataItem3D):
- node.addRow(DataItem3DBoundingBoxRow(item))
- node.addRow(DataItem3DTransformRow(item))
-
- # Specific extra init
- for cls, specificInit in NODE_SPECIFIC_INIT:
- if isinstance(item, cls):
- specificInit(node, item)
- break
-
- else: # Generic case: handle mixins
- for cls in item.__class__.__mro__:
- if cls is items.ColormapMixIn:
- node.addRow(ColormapRow(item))
-
- elif cls is items.InterpolationMixIn:
- node.addRow(InterpolationRow(item))
-
- elif cls is items.SymbolMixIn:
- node.addRow(SymbolRow(item))
- node.addRow(SymbolSizeRow(item))
-
- elif cls is items.PlaneMixIn:
- node.addRow(PlaneRow(item))
-
- return node
diff --git a/silx/gui/plot3d/_model/model.py b/silx/gui/plot3d/_model/model.py
deleted file mode 100644
index 186838f..0000000
--- a/silx/gui/plot3d/_model/model.py
+++ /dev/null
@@ -1,184 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""
-This module provides the :class:`SceneWidget` content and parameters model.
-"""
-
-from __future__ import absolute_import, division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "11/01/2018"
-
-
-import weakref
-
-from ... import qt
-
-from .core import BaseRow
-from .items import Settings, nodeFromItem
-
-
-def visitQAbstractItemModel(model, parent=qt.QModelIndex()):
- """Iterate over indices in the model starting from parent
-
- It iterates column by column and row by row
- (i.e., from left to right and from top to bottom).
- Parent are returned before their children.
- It only iterates through the children for the first column of a row.
-
- :param QAbstractItemModel model: The model to visit
- :param QModelIndex parent:
- Index from which to start visiting the model.
- Default: start from the root
- """
- assert isinstance(model, qt.QAbstractItemModel)
- assert isinstance(parent, qt.QModelIndex)
- assert parent.model() is model or not parent.isValid()
-
- for row in range(model.rowCount(parent)):
- for column in range(model.columnCount(parent)):
- index = model.index(row, column, parent)
- yield index
-
- index = model.index(row, 0, parent)
- for index in visitQAbstractItemModel(model, index):
- yield index
-
-
-class Root(BaseRow):
- """Root node of :class:`SceneWidget` parameters.
-
- It has two children:
- - Settings
- - Scene group
- """
-
- def __init__(self, model, sceneWidget):
- super(Root, self).__init__()
- self._sceneWidget = weakref.ref(sceneWidget)
- self.setParent(model) # Needed for Root
-
- def children(self):
- sceneWidget = self._sceneWidget()
- if sceneWidget is None:
- return ()
- else:
- return super(Root, self).children()
-
-
-class SceneModel(qt.QAbstractItemModel):
- """Model of a :class:`SceneWidget`.
-
- :param SceneWidget parent: The SceneWidget this model represents.
- """
-
- def __init__(self, parent):
- self._sceneWidget = weakref.ref(parent)
-
- super(SceneModel, self).__init__(parent)
- self._root = Root(self, parent)
- self._root.addRow(Settings(parent))
- self._root.addRow(nodeFromItem(parent.getSceneGroup()))
-
- def sceneWidget(self):
- """Returns the :class:`SceneWidget` this model represents.
-
- In case the widget has already been deleted, it returns None
-
- :rtype: SceneWidget
- """
- return self._sceneWidget()
-
- def _itemFromIndex(self, index):
- """Returns the corresponding :class:`Node` or :class:`Item3D`.
-
- :param QModelIndex index:
- :rtype: Node or Item3D
- """
- return index.internalPointer() if index.isValid() else self._root
-
- def index(self, row, column, parent=qt.QModelIndex()):
- """See :meth:`QAbstractItemModel.index`"""
- if column >= self.columnCount(parent) or row >= self.rowCount(parent):
- return qt.QModelIndex()
-
- item = self._itemFromIndex(parent)
- return self.createIndex(row, column, item.children()[row])
-
- def parent(self, index):
- """See :meth:`QAbstractItemModel.parent`"""
- if not index.isValid():
- return qt.QModelIndex()
-
- item = self._itemFromIndex(index)
- parent = item.parent()
-
- ancestor = parent.parent()
-
- if ancestor is not self: # root node
- children = ancestor.children()
- row = children.index(parent)
- return self.createIndex(row, 0, parent)
-
- return qt.QModelIndex()
-
- def rowCount(self, parent=qt.QModelIndex()):
- """See :meth:`QAbstractItemModel.rowCount`"""
- item = self._itemFromIndex(parent)
- return item.rowCount()
-
- def columnCount(self, parent=qt.QModelIndex()):
- """See :meth:`QAbstractItemModel.columnCount`"""
- item = self._itemFromIndex(parent)
- return item.columnCount()
-
- def data(self, index, role=qt.Qt.DisplayRole):
- """See :meth:`QAbstractItemModel.data`"""
- item = self._itemFromIndex(index)
- column = index.column()
- return item.data(column, role)
-
- def setData(self, index, value, role=qt.Qt.EditRole):
- """See :meth:`QAbstractItemModel.setData`"""
- item = self._itemFromIndex(index)
- column = index.column()
- if item.setData(column, value, role):
- self.dataChanged.emit(index, index)
- return True
- return False
-
- def flags(self, index):
- """See :meth:`QAbstractItemModel.flags`"""
- item = self._itemFromIndex(index)
- column = index.column()
- return item.flags(column)
-
- def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
- """See :meth:`QAbstractItemModel.headerData`"""
- if orientation == qt.Qt.Horizontal and role == qt.Qt.DisplayRole:
- return 'Item' if section == 0 else 'Value'
- else:
- return None
diff --git a/silx/gui/plot3d/actions/Plot3DAction.py b/silx/gui/plot3d/actions/Plot3DAction.py
deleted file mode 100644
index 94b9572..0000000
--- a/silx/gui/plot3d/actions/Plot3DAction.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""Base class for QAction attached to a Plot3DWidget."""
-
-from __future__ import absolute_import, division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/09/2017"
-
-
-import logging
-import weakref
-
-from silx.gui import qt
-
-
-_logger = logging.getLogger(__name__)
-
-
-class Plot3DAction(qt.QAction):
- """QAction associated to a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- def __init__(self, parent, plot3d=None):
- super(Plot3DAction, self).__init__(parent)
- self._plot3d = None
- self.setPlot3DWidget(plot3d)
-
- def setPlot3DWidget(self, widget):
- """Set the Plot3DWidget this action is associated with
-
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget widget:
- The Plot3DWidget to use
- """
- self._plot3d = None if widget is None else weakref.ref(widget)
-
- def getPlot3DWidget(self):
- """Return the Plot3DWidget associated to this action.
-
- If no widget is associated, it returns None.
-
- :rtype: QWidget
- """
- return None if self._plot3d is None else self._plot3d()
diff --git a/silx/gui/plot3d/actions/__init__.py b/silx/gui/plot3d/actions/__init__.py
deleted file mode 100644
index 26243cf..0000000
--- a/silx/gui/plot3d/actions/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides QAction that can be attached to a plot3DWidget."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/09/2017"
-
-from .Plot3DAction import Plot3DAction # noqa
-from . import viewpoint # noqa
-from . import io # noqa
-from . import mode # noqa
diff --git a/silx/gui/plot3d/actions/io.py b/silx/gui/plot3d/actions/io.py
deleted file mode 100644
index 4020d6f..0000000
--- a/silx/gui/plot3d/actions/io.py
+++ /dev/null
@@ -1,336 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""This module provides Plot3DAction related to input/output.
-
-It provides QAction to copy, save (snapshot and video), print a Plot3DWidget.
-"""
-
-from __future__ import absolute_import, division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/09/2017"
-
-
-import logging
-import os
-
-import numpy
-
-from silx.gui import qt, printer
-from silx.gui.icons import getQIcon
-from .Plot3DAction import Plot3DAction
-from ..utils import mng
-from ...utils.image import convertQImageToArray
-
-
-_logger = logging.getLogger(__name__)
-
-
-class CopyAction(Plot3DAction):
- """QAction to provide copy of a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- def __init__(self, parent, plot3d=None):
- super(CopyAction, self).__init__(parent, plot3d)
-
- self.setIcon(getQIcon('edit-copy'))
- self.setText('Copy')
- self.setToolTip('Copy a snapshot of the 3D scene to the clipboard')
- self.setCheckable(False)
- self.setShortcut(qt.QKeySequence.Copy)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
- self.triggered[bool].connect(self._triggered)
-
- def _triggered(self, checked=False):
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.error('Cannot copy widget, no associated Plot3DWidget')
- else:
- image = plot3d.grabGL()
- qt.QApplication.clipboard().setImage(image)
-
-
-class SaveAction(Plot3DAction):
- """QAction to provide save snapshot of a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- def __init__(self, parent, plot3d=None):
- super(SaveAction, self).__init__(parent, plot3d)
-
- self.setIcon(getQIcon('document-save'))
- self.setText('Save...')
- self.setToolTip('Save a snapshot of the 3D scene')
- self.setCheckable(False)
- self.setShortcut(qt.QKeySequence.Save)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
- self.triggered[bool].connect(self._triggered)
-
- def _triggered(self, checked=False):
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.error('Cannot save widget, no associated Plot3DWidget')
- else:
- dialog = qt.QFileDialog(self.parent())
- dialog.setWindowTitle('Save snapshot as')
- dialog.setModal(True)
- dialog.setNameFilters(('Plot3D Snapshot PNG (*.png)',
- 'Plot3D Snapshot JPEG (*.jpg)'))
-
- dialog.setFileMode(qt.QFileDialog.AnyFile)
- dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
-
- if not dialog.exec_():
- return
-
- nameFilter = dialog.selectedNameFilter()
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- # Forces the filename extension to match the chosen filter
- extension = nameFilter.split()[-1][2:-1]
- if (len(filename) <= len(extension) or
- filename[-len(extension):].lower() != extension.lower()):
- filename += extension
-
- image = plot3d.grabGL()
- if not image.save(filename):
- _logger.error('Failed to save image as %s', filename)
- qt.QMessageBox.critical(
- self.parent(),
- 'Save snapshot as',
- 'Failed to save snapshot')
-
-
-class PrintAction(Plot3DAction):
- """QAction to provide printing of a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- def __init__(self, parent, plot3d=None):
- super(PrintAction, self).__init__(parent, plot3d)
-
- self.setIcon(getQIcon('document-print'))
- self.setText('Print...')
- self.setToolTip('Print a snapshot of the 3D scene')
- self.setCheckable(False)
- self.setShortcut(qt.QKeySequence.Print)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
- self.triggered[bool].connect(self._triggered)
-
- def getPrinter(self):
- """Return the QPrinter instance used for printing.
-
- :rtype: QPrinter
- """
- return printer.getDefaultPrinter()
-
- def _triggered(self, checked=False):
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.error('Cannot print widget, no associated Plot3DWidget')
- else:
- printer = self.getPrinter()
- dialog = qt.QPrintDialog(printer, plot3d)
- dialog.setWindowTitle('Print Plot3D snapshot')
- if not dialog.exec_():
- return
-
- image = plot3d.grabGL()
-
- # Draw pixmap with painter
- painter = qt.QPainter()
- if not painter.begin(printer):
- return
-
- if (printer.pageRect().width() < image.width() or
- printer.pageRect().height() < image.height()):
- # Downscale to page
- xScale = printer.pageRect().width() / image.width()
- yScale = printer.pageRect().height() / image.height()
- scale = min(xScale, yScale)
- else:
- scale = 1.
-
- rect = qt.QRectF(0,
- 0,
- scale * image.width(),
- scale * image.height())
- painter.drawImage(rect, image)
- painter.end()
-
-
-class VideoAction(Plot3DAction):
- """This action triggers the recording of a video of the scene.
-
- The scene is rotated 360 degrees around a vertical axis.
-
- :param parent: Action parent see :class:`QAction`.
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- PNG_SERIE_FILTER = 'Serie of PNG files (*.png)'
- MNG_FILTER = 'Multiple-image Network Graphics file (*.mng)'
-
- def __init__(self, parent, plot3d=None):
- super(VideoAction, self).__init__(parent, plot3d)
- self.setText('Record video..')
- self.setIcon(getQIcon('camera'))
- self.setToolTip(
- 'Record a video of a 360 degrees rotation of the 3D scene.')
- self.setCheckable(False)
- self.triggered[bool].connect(self._triggered)
-
- def _triggered(self, checked=False):
- """Action triggered callback"""
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.warning(
- 'Ignoring action triggered without Plot3DWidget set')
- return
-
- dialog = qt.QFileDialog(parent=plot3d)
- dialog.setWindowTitle('Save video as...')
- dialog.setModal(True)
- dialog.setNameFilters([self.PNG_SERIE_FILTER,
- self.MNG_FILTER])
- dialog.setFileMode(dialog.AnyFile)
- dialog.setAcceptMode(dialog.AcceptSave)
-
- if not dialog.exec_():
- return
-
- nameFilter = dialog.selectedNameFilter()
- filename = dialog.selectedFiles()[0]
-
- # Forces the filename extension to match the chosen filter
- extension = nameFilter.split()[-1][2:-1]
- if (len(filename) <= len(extension) or
- filename[-len(extension):].lower() != extension.lower()):
- filename += extension
-
- nbFrames = int(4. * 25) # 4 seconds, 25 fps
-
- if nameFilter == self.PNG_SERIE_FILTER:
- self._saveAsPNGSerie(filename, nbFrames)
- elif nameFilter == self.MNG_FILTER:
- self._saveAsMNG(filename, nbFrames)
- else:
- _logger.error('Unsupported file filter: %s', nameFilter)
-
- def _saveAsPNGSerie(self, filename, nbFrames):
- """Save video as serie of PNG files.
-
- It adds a counter to the provided filename before the extension.
-
- :param str filename: filename to use as template
- :param int nbFrames: Number of frames to generate
- """
- plot3d = self.getPlot3DWidget()
- assert plot3d is not None
-
- # Define filename template
- nbDigits = int(numpy.log10(nbFrames)) + 1
- indexFormat = '%%0%dd' % nbDigits
- extensionIndex = filename.rfind('.')
- filenameFormat = \
- filename[:extensionIndex] + indexFormat + filename[extensionIndex:]
-
- try:
- for index, image in enumerate(self._video360(nbFrames)):
- image.save(filenameFormat % index)
- except GeneratorExit:
- pass
-
- def _saveAsMNG(self, filename, nbFrames):
- """Save video as MNG file.
-
- :param str filename: filename to use
- :param int nbFrames: Number of frames to generate
- """
- plot3d = self.getPlot3DWidget()
- assert plot3d is not None
-
- frames = (convertQImageToArray(im) for im in self._video360(nbFrames))
- try:
- with open(filename, 'wb') as file_:
- for chunk in mng.convert(frames, nb_images=nbFrames):
- file_.write(chunk)
- except GeneratorExit:
- os.remove(filename) # Saving aborted, delete file
-
- def _video360(self, nbFrames):
- """Run the video and provides the images
-
- :param int nbFrames: The number of frames to generate for
- :return: Iterator of QImage of the video sequence
- """
- plot3d = self.getPlot3DWidget()
- assert plot3d is not None
-
- angleStep = 360. / nbFrames
-
- # Create progress bar dialog
- dialog = qt.QDialog(plot3d)
- dialog.setWindowTitle('Record Video')
- layout = qt.QVBoxLayout(dialog)
- progress = qt.QProgressBar()
- progress.setRange(0, nbFrames)
- layout.addWidget(progress)
-
- btnBox = qt.QDialogButtonBox(qt.QDialogButtonBox.Abort)
- btnBox.rejected.connect(dialog.reject)
- layout.addWidget(btnBox)
-
- dialog.setModal(True)
- dialog.show()
-
- qapp = qt.QApplication.instance()
-
- for frame in range(nbFrames):
- progress.setValue(frame)
- image = plot3d.grabGL()
- yield image
- plot3d.viewport.orbitCamera('left', angleStep)
- qapp.processEvents()
- if not dialog.isVisible():
- break # It as been rejected by the abort button
- else:
- dialog.accept()
-
- if dialog.result() == qt.QDialog.Rejected:
- raise GeneratorExit('Aborted')
diff --git a/silx/gui/plot3d/actions/mode.py b/silx/gui/plot3d/actions/mode.py
deleted file mode 100644
index ce09b4c..0000000
--- a/silx/gui/plot3d/actions/mode.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-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.
-#
-# ###########################################################################*/
-"""This module provides Plot3DAction related to interaction modes.
-
-It provides QAction to rotate or pan a Plot3DWidget
-as well as toggle a picking mode.
-"""
-
-from __future__ import absolute_import, division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/09/2017"
-
-
-import logging
-
-from ....utils.proxy import docstring
-from ... import qt
-from ...icons import getQIcon
-from .Plot3DAction import Plot3DAction
-
-
-_logger = logging.getLogger(__name__)
-
-
-class InteractiveModeAction(Plot3DAction):
- """Base class for QAction changing interactive mode of a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param str interaction: The interactive mode this action controls
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- def __init__(self, parent, interaction, plot3d=None):
- self._interaction = interaction
-
- super(InteractiveModeAction, self).__init__(parent, plot3d)
- self.setCheckable(True)
- self.triggered[bool].connect(self._triggered)
-
- def _triggered(self, checked=False):
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.error(
- 'Cannot set %s interaction, no associated Plot3DWidget' %
- self._interaction)
- else:
- plot3d.setInteractiveMode(self._interaction)
- self.setChecked(True)
-
- @docstring(Plot3DAction)
- def setPlot3DWidget(self, widget):
- # Disconnect from previous Plot3DWidget
- plot3d = self.getPlot3DWidget()
- if plot3d is not None:
- plot3d.sigInteractiveModeChanged.disconnect(
- self._interactiveModeChanged)
-
- super(InteractiveModeAction, self).setPlot3DWidget(widget)
-
- # Connect to new Plot3DWidget
- if widget is None:
- self.setChecked(False)
- else:
- self.setChecked(widget.getInteractiveMode() == self._interaction)
- widget.sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
-
- def _interactiveModeChanged(self):
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.error('Received a signal while there is no widget')
- else:
- self.setChecked(plot3d.getInteractiveMode() == self._interaction)
-
-
-class RotateArcballAction(InteractiveModeAction):
- """QAction to set arcball rotation interaction on a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- def __init__(self, parent, plot3d=None):
- super(RotateArcballAction, self).__init__(parent, 'rotate', plot3d)
-
- self.setIcon(getQIcon('rotate-3d'))
- self.setText('Rotate')
- self.setToolTip('Rotate the view. Press <b>Ctrl</b> to pan.')
-
-
-class PanAction(InteractiveModeAction):
- """QAction to set pan interaction on a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- def __init__(self, parent, plot3d=None):
- super(PanAction, self).__init__(parent, 'pan', plot3d)
-
- self.setIcon(getQIcon('pan'))
- self.setText('Pan')
- self.setToolTip('Pan the view. Press <b>Ctrl</b> to rotate.')
-
-
-class PickingModeAction(Plot3DAction):
- """QAction to toggle picking moe on a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- sigSceneClicked = qt.Signal(float, float)
- """Signal emitted when the scene is clicked with the left mouse button.
-
- This signal is only emitted when the action is checked.
-
- It provides the (x, y) clicked mouse position
- """
-
- def __init__(self, parent, plot3d=None):
- super(PickingModeAction, self).__init__(parent, plot3d)
- self.setIcon(getQIcon('pointing-hand'))
- self.setText('Picking')
- self.setToolTip('Toggle picking with left button click')
- self.setCheckable(True)
- self.triggered[bool].connect(self._triggered)
-
- def _triggered(self, checked=False):
- plot3d = self.getPlot3DWidget()
- if plot3d is not None:
- if checked:
- plot3d.sigSceneClicked.connect(self.sigSceneClicked)
- else:
- plot3d.sigSceneClicked.disconnect(self.sigSceneClicked)
-
- @docstring(Plot3DAction)
- def setPlot3DWidget(self, widget):
- # Disconnect from previous Plot3DWidget
- plot3d = self.getPlot3DWidget()
- if plot3d is not None and self.isChecked():
- plot3d.sigSceneClicked.disconnect(self.sigSceneClicked)
-
- super(PickingModeAction, self).setPlot3DWidget(widget)
-
- # Connect to new Plot3DWidget
- if widget is None:
- self.setChecked(False)
- elif self.isChecked():
- widget.sigSceneClicked.connect(self.sigSceneClicked)
diff --git a/silx/gui/plot3d/actions/viewpoint.py b/silx/gui/plot3d/actions/viewpoint.py
deleted file mode 100644
index d764c40..0000000
--- a/silx/gui/plot3d/actions/viewpoint.py
+++ /dev/null
@@ -1,231 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides Plot3DAction controlling the viewpoint.
-
-It provides QAction to rotate or pan a Plot3DWidget.
-"""
-
-from __future__ import absolute_import, division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/10/2017"
-
-
-import time
-import logging
-
-from silx.gui import qt
-from silx.gui.icons import getQIcon
-from .Plot3DAction import Plot3DAction
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _SetViewpointAction(Plot3DAction):
- """Base class for actions setting a Plot3DWidget viewpoint
-
- :param parent: See :class:`QAction`
- :param str face: The name of the predefined viewpoint
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, face, plot3d=None):
- super(_SetViewpointAction, self).__init__(parent, plot3d)
- assert face in ('side', 'front', 'back', 'left', 'right', 'top', 'bottom')
- self._face = face
-
- self.setIconVisibleInMenu(True)
- self.setCheckable(False)
- self.triggered[bool].connect(self._triggered)
-
- def _triggered(self, checked=False):
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.error(
- 'Cannot start/stop rotation, no associated Plot3DWidget')
- else:
- plot3d.viewport.camera.extrinsic.reset(face=self._face)
- plot3d.centerScene()
-
-
-class FrontViewpointAction(_SetViewpointAction):
- """QAction to set Plot3DWidget viewpoint to look from the front
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(FrontViewpointAction, self).__init__(parent, 'front', plot3d)
-
- self.setIcon(getQIcon('cube-front'))
- self.setText('Front')
- self.setToolTip('View along the -Z axis')
-
-
-class BackViewpointAction(_SetViewpointAction):
- """QAction to set Plot3DWidget viewpoint to look from the back
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(BackViewpointAction, self).__init__(parent, 'back', plot3d)
-
- self.setIcon(getQIcon('cube-back'))
- self.setText('Back')
- self.setToolTip('View along the +Z axis')
-
-
-class LeftViewpointAction(_SetViewpointAction):
- """QAction to set Plot3DWidget viewpoint to look from the left
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(LeftViewpointAction, self).__init__(parent, 'left', plot3d)
-
- self.setIcon(getQIcon('cube-left'))
- self.setText('Left')
- self.setToolTip('View along the +X axis')
-
-
-class RightViewpointAction(_SetViewpointAction):
- """QAction to set Plot3DWidget viewpoint to look from the right
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(RightViewpointAction, self).__init__(parent, 'right', plot3d)
-
- self.setIcon(getQIcon('cube-right'))
- self.setText('Right')
- self.setToolTip('View along the -X axis')
-
-
-class TopViewpointAction(_SetViewpointAction):
- """QAction to set Plot3DWidget viewpoint to look from the top
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(TopViewpointAction, self).__init__(parent, 'top', plot3d)
-
- self.setIcon(getQIcon('cube-top'))
- self.setText('Top')
- self.setToolTip('View along the -Y axis')
-
-
-class BottomViewpointAction(_SetViewpointAction):
- """QAction to set Plot3DWidget viewpoint to look from the bottom
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(BottomViewpointAction, self).__init__(parent, 'bottom', plot3d)
-
- self.setIcon(getQIcon('cube-bottom'))
- self.setText('Bottom')
- self.setToolTip('View along the +Y axis')
-
-
-class SideViewpointAction(_SetViewpointAction):
- """QAction to set Plot3DWidget viewpoint to look from the side
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
- def __init__(self, parent, plot3d=None):
- super(SideViewpointAction, self).__init__(parent, 'side', plot3d)
-
- self.setIcon(getQIcon('cube'))
- self.setText('Side')
- self.setToolTip('Side view')
-
-
-class RotateViewpoint(Plot3DAction):
- """QAction to rotate the scene of a Plot3DWidget
-
- :param parent: See :class:`QAction`
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget plot3d:
- Plot3DWidget the action is associated with
- """
-
- _TIMEOUT_MS = 50
- """Time interval between to frames (in milliseconds)"""
-
- _DEGREE_PER_SECONDS = 360. / 5.
- """Rotation speed of the animation"""
-
- def __init__(self, parent, plot3d=None):
- super(RotateViewpoint, self).__init__(parent, plot3d)
-
- self._previousTime = None
-
- self._timer = qt.QTimer(self)
- self._timer.setInterval(self._TIMEOUT_MS) # 20fps
- self._timer.timeout.connect(self._rotate)
-
- self.setIcon(getQIcon('cube-rotate'))
- self.setText('Rotate scene')
- self.setToolTip('Rotate the 3D scene around the vertical axis')
- self.setCheckable(True)
- self.triggered[bool].connect(self._triggered)
-
-
- def _triggered(self, checked=False):
- plot3d = self.getPlot3DWidget()
- if plot3d is None:
- _logger.error(
- 'Cannot start/stop rotation, no associated Plot3DWidget')
- elif checked:
- self._previousTime = time.time()
- self._timer.start()
- else:
- self._timer.stop()
- self._previousTime = None
-
- def _rotate(self):
- """Perform a step of the rotation"""
- if self._previousTime is None:
- _logger.error('Previous time not set!')
- angleStep = 0.
- else:
- angleStep = self._DEGREE_PER_SECONDS * (time.time() - self._previousTime)
-
- self.getPlot3DWidget().viewport.orbitCamera('left', angleStep)
- self._previousTime = time.time()
diff --git a/silx/gui/plot3d/items/__init__.py b/silx/gui/plot3d/items/__init__.py
deleted file mode 100644
index e7c4af1..0000000
--- a/silx/gui/plot3d/items/__init__.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-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.
-#
-# ###########################################################################*/
-"""This package provides classes that describes :class:`.SceneWidget` content.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/11/2017"
-
-
-from .core import DataItem3D, Item3D, GroupItem, GroupWithAxesItem # noqa
-from .core import ItemChangedType, Item3DChangedType # noqa
-from .mixins import (ColormapMixIn, ComplexMixIn, InterpolationMixIn, # noqa
- PlaneMixIn, SymbolMixIn) # noqa
-from .clipplane import ClipPlane # noqa
-from .image import ImageData, ImageRgba, HeightMapData, HeightMapRGBA # noqa
-from .mesh import Mesh, ColormapMesh, Box, Cylinder, Hexagon # noqa
-from .scatter import Scatter2D, Scatter3D # noqa
-from .volume import ComplexField3D, ScalarField3D # noqa
diff --git a/silx/gui/plot3d/items/_pick.py b/silx/gui/plot3d/items/_pick.py
deleted file mode 100644
index 0d6a495..0000000
--- a/silx/gui/plot3d/items/_pick.py
+++ /dev/null
@@ -1,265 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 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.
-#
-# ###########################################################################*/
-"""This module provides classes supporting item picking.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/09/2018"
-
-import logging
-import numpy
-
-from ...plot.items._pick import PickingResult as _PickingResult
-from ..scene import Viewport, Base
-
-
-_logger = logging.getLogger(__name__)
-
-
-class PickContext(object):
- """Store information related to current picking
-
- :param int x: Widget coordinate
- :param int y: Widget coordinate
- :param ~silx.gui.plot3d.scene.Viewport viewport:
- Viewport where picking occurs
- :param Union[None,callable] condition:
- Test whether each item needs to be picked or not.
- """
-
- def __init__(self, x, y, viewport, condition):
- self._widgetPosition = x, y
- assert isinstance(viewport, Viewport)
- self._viewport = viewport
- self._ndcZRange = -1., 1.
- self._enabled = True
- self._condition = condition
-
- def copy(self):
- """Returns a copy
-
- :rtype: PickContent
- """
- x, y = self.getWidgetPosition()
- context = PickContext(x, y, self.getViewport(), self._condition)
- context.setNDCZRange(*self._ndcZRange)
- context.setEnabled(self.isEnabled())
- return context
-
- def isItemPickable(self, item):
- """Check condition for the given item.
-
- :param Item3D item:
- :return: Whether to process the item (True) or to skip it (False)
- :rtype: bool
- """
- return self._condition is None or self._condition(item)
-
- def getViewport(self):
- """Returns viewport where picking occurs
-
- :rtype: ~silx.gui.plot3d.scene.Viewport
- """
- return self._viewport
-
- def getWidgetPosition(self):
- """Returns (x, y) position in pixel in the widget
-
- Origin is at the top-left corner of the widget,
- X from left to right, Y goes downward.
-
- :rtype: List[int]
- """
- return self._widgetPosition
-
- def setEnabled(self, enabled):
- """Set whether picking is enabled or not
-
- :param bool enabled: True to enable picking, False otherwise
- """
- self._enabled = bool(enabled)
-
- def isEnabled(self):
- """Returns True if picking is currently enabled, False otherwise.
-
- :rtype: bool
- """
- return self._enabled
-
- def setNDCZRange(self, near=-1., far=1.):
- """Set near and far Z value in normalized device coordinates
-
- This allows to clip the ray to a subset of the NDC range
-
- :param float near: Near segment end point Z coordinate
- :param float far: Far segment end point Z coordinate
- """
- self._ndcZRange = near, far
-
- def getNDCPosition(self):
- """Return Normalized device coordinates of picked point.
-
- :return: (x, y) in NDC coordinates or None if outside viewport.
- :rtype: Union[None,List[float]]
- """
- if not self.isEnabled():
- return None
-
- # Convert x, y from window to NDC
- x, y = self.getWidgetPosition()
- return self.getViewport().windowToNdc(x, y, checkInside=True)
-
- def getPickingSegment(self, frame):
- """Returns picking segment in requested coordinate frame.
-
- :param Union[str,Base] frame:
- The frame in which to get the picking segment,
- either a keyword: 'ndc', 'camera', 'scene' or a scene
- :class:`~silx.gui.plot3d.scene.Base` object.
- :return: Near and far points of the segment as (x, y, z, w)
- or None if picked point is outside viewport
- :rtype: Union[None,numpy.ndarray]
- """
- assert frame in ('ndc', 'camera', 'scene') or isinstance(frame, Base)
-
- positionNdc = self.getNDCPosition()
- if positionNdc is None:
- return None
-
- near, far = self._ndcZRange
- rayNdc = numpy.array((positionNdc + (near, 1.),
- positionNdc + (far, 1.)),
- dtype=numpy.float64)
- if frame == 'ndc':
- return rayNdc
-
- viewport = self.getViewport()
-
- rayCamera = viewport.camera.intrinsic.transformPoints(
- rayNdc,
- direct=False,
- perspectiveDivide=True)
- if frame == 'camera':
- return rayCamera
-
- rayScene = viewport.camera.extrinsic.transformPoints(
- rayCamera, direct=False)
- if frame == 'scene':
- return rayScene
-
- # frame is a scene Base object
- rayObject = frame.objectToSceneTransform.transformPoints(
- rayScene, direct=False)
- return rayObject
-
-
-class PickingResult(_PickingResult):
- """Class to access picking information in a 3D scene."""
-
- def __init__(self, item, positions, indices=None, fetchdata=None):
- """Init
-
- :param ~silx.gui.plot3d.items.Item3D item: The picked item
- :param numpy.ndarray positions:
- Nx3 array-like of picked positions (x, y, z) in item coordinates.
- :param numpy.ndarray indices: Array-like of indices of picked data.
- Either 1D or 2D with dim0: data dimension and dim1: indices.
- No copy is made.
- :param callable fetchdata: Optional function with a bool copy argument
- to provide an alternative function to access item data.
- Default is to use `item.getData`.
- """
- super(PickingResult, self).__init__(item, indices)
-
- self._objectPositions = numpy.array(
- positions, copy=False, dtype=numpy.float64)
-
- # Store matrices to generate positions on demand
- primitive = item._getScenePrimitive()
- self._objectToSceneTransform = primitive.objectToSceneTransform
- self._objectToNDCTransform = primitive.objectToNDCTransform
- self._scenePositions = None
- self._ndcPositions = None
-
- self._fetchdata = fetchdata
-
- def getData(self, copy=True):
- """Returns picked data values
-
- :param bool copy: True (default) to get a copy,
- False to return internal arrays
- :rtype: Union[None,numpy.ndarray]
- """
-
- indices = self.getIndices(copy=False)
- if indices is None or len(indices) == 0:
- return None
-
- item = self.getItem()
- if self._fetchdata is None:
- if hasattr(item, 'getData'):
- data = item.getData(copy=False)
- else:
- return None
- else:
- data = self._fetchdata(copy=False)
-
- return numpy.array(data[indices], copy=copy)
-
- def getPositions(self, frame='scene', copy=True):
- """Returns picking positions in item coordinates.
-
- :param str frame: The frame in which the positions are returned
- Either 'scene' for world space,
- 'ndc' for normalized device coordinates or 'object' for item frame.
- :param bool copy: True (default) to get a copy,
- False to return internal arrays
- :return: Nx3 array of (x, y, z) coordinates
- :rtype: numpy.ndarray
- """
- if frame == 'ndc':
- if self._ndcPositions is None: # Lazy-loading
- self._ndcPositions = self._objectToNDCTransform.transformPoints(
- self._objectPositions, perspectiveDivide=True)
-
- positions = self._ndcPositions
-
- elif frame == 'scene':
- if self._scenePositions is None: # Lazy-loading
- self._scenePositions = self._objectToSceneTransform.transformPoints(
- self._objectPositions)
-
- positions = self._scenePositions
-
- elif frame == 'object':
- positions = self._objectPositions
-
- else:
- raise ValueError('Unsupported frame argument: %s' % str(frame))
-
- return numpy.array(positions, copy=copy)
diff --git a/silx/gui/plot3d/items/clipplane.py b/silx/gui/plot3d/items/clipplane.py
deleted file mode 100644
index 3e819d0..0000000
--- a/silx/gui/plot3d/items/clipplane.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides a scene clip plane class.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/11/2017"
-
-
-import numpy
-
-from ..scene import primitives, utils
-
-from ._pick import PickingResult
-from .core import Item3D
-from .mixins import PlaneMixIn
-
-
-class ClipPlane(Item3D, PlaneMixIn):
- """Represents a clipping plane that clips following items within the group.
-
- For now only on clip plane is allowed at once in a scene.
- """
-
- def __init__(self, parent=None):
- plane = primitives.ClipPlane()
- Item3D.__init__(self, parent=parent, primitive=plane)
- PlaneMixIn.__init__(self, plane=plane)
-
- def __pickPreProcessing(self, context):
- """Common processing for :meth:`_pickPostProcess` and :meth:`_pickFull`
-
- :param PickContext context: Current picking context
- :return None or (bounds, intersection points, rayObject)
- """
- plane = self._getPlane()
- planeParent = plane.parent
- if planeParent is None:
- return None
-
- rayObject = context.getPickingSegment(frame=plane)
- if rayObject is None:
- return None
-
- bounds = planeParent.bounds(dataBounds=True)
- rayClip = utils.clipSegmentToBounds(rayObject[:, :3], bounds)
- if rayClip is None:
- return None # Ray is outside parent's bounding box
-
- points = utils.segmentPlaneIntersect(
- rayObject[0, :3],
- rayObject[1, :3],
- planeNorm=self.getNormal(),
- planePt=self.getPoint())
-
- # A single intersection inside bounding box
- picked = (len(points) == 1 and
- numpy.all(bounds[0] <= points[0]) and
- numpy.all(points[0] <= bounds[1]))
-
- return picked, points, rayObject
-
- def _pick(self, context):
- # Perform picking before modifying context
- result = super(ClipPlane, self)._pick(context)
-
- # Modify context if needed
- if self.isVisible() and context.isEnabled():
- info = self.__pickPreProcessing(context)
- if info is not None:
- picked, points, rayObject = info
- plane = self._getPlane()
-
- if picked: # A single intersection inside bounding box
- # Clip NDC z range for following brother items
- ndcIntersect = plane.objectToNDCTransform.transformPoint(
- points[0], perspectiveDivide=True)
- ndcNormal = plane.objectToNDCTransform.transformNormal(
- self.getNormal())
- if ndcNormal[2] < 0:
- context.setNDCZRange(-1., ndcIntersect[2])
- else:
- context.setNDCZRange(ndcIntersect[2], 1.)
-
- else:
- # TODO check this might not be correct
- rayObject[:, 3] = 1. # Make sure 4h coordinate is one
- if numpy.sum(rayObject[0] * self.getParameters()) < 0.:
- # Disable picking for remaining brothers
- context.setEnabled(False)
-
- return result
-
- def _pickFastCheck(self, context):
- return True
-
- def _pickFull(self, context):
- """Perform picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- info = self.__pickPreProcessing(context)
- if info is not None:
- picked, points, _ = info
-
- if picked:
- return PickingResult(self, positions=[points[0]])
-
- return None
diff --git a/silx/gui/plot3d/items/core.py b/silx/gui/plot3d/items/core.py
deleted file mode 100644
index ab2ceb6..0000000
--- a/silx/gui/plot3d/items/core.py
+++ /dev/null
@@ -1,779 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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.
-#
-# ###########################################################################*/
-"""This module provides the base class for items of the :class:`.SceneWidget`.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/11/2017"
-
-from collections import defaultdict
-import enum
-
-import numpy
-import six
-
-from ... import qt
-from ...plot.items import ItemChangedType
-from .. import scene
-from ..scene import axes, primitives, transform
-from ._pick import PickContext
-
-
-@enum.unique
-class Item3DChangedType(enum.Enum):
- """Type of modification provided by :attr:`Item3D.sigItemChanged` signal."""
-
- INTERPOLATION = 'interpolationChanged'
- """Item3D image interpolation changed flag."""
-
- TRANSFORM = 'transformChanged'
- """Item3D transform changed flag."""
-
- HEIGHT_MAP = 'heightMapChanged'
- """Item3D height map changed flag."""
-
- ISO_LEVEL = 'isoLevelChanged'
- """Isosurface level changed flag."""
-
- LABEL = 'labelChanged'
- """Item's label changed flag."""
-
- BOUNDING_BOX_VISIBLE = 'boundingBoxVisibleChanged'
- """Item's bounding box visibility changed"""
-
- ROOT_ITEM = 'rootItemChanged'
- """Item's root changed flag."""
-
-
-class Item3D(qt.QObject):
- """Base class representing an item in the scene.
-
- :param parent: The View widget this item belongs to.
- :param primitive: An optional primitive to use as scene primitive
- """
-
- _LABEL_INDICES = defaultdict(int)
- """Store per class label indices"""
-
- sigItemChanged = qt.Signal(object)
- """Signal emitted when an item's property has changed.
-
- It provides a flag describing which property of the item has changed.
- See :class:`ItemChangedType` and :class:`Item3DChangedType`
- for flags description.
- """
-
- def __init__(self, parent, primitive=None):
- qt.QObject.__init__(self, parent)
-
- if primitive is None:
- primitive = scene.Group()
-
- self._primitive = primitive
-
- self.__syncForegroundColor()
-
- labelIndex = self._LABEL_INDICES[self.__class__]
- self._label = six.text_type(self.__class__.__name__)
- if labelIndex != 0:
- self._label += u' %d' % labelIndex
- self._LABEL_INDICES[self.__class__] += 1
-
- if isinstance(parent, Item3D):
- parent.sigItemChanged.connect(self.__parentItemChanged)
-
- def setParent(self, parent):
- """Override set parent to handle root item change"""
- previousParent = self.parent()
- if isinstance(previousParent, Item3D):
- previousParent.sigItemChanged.disconnect(self.__parentItemChanged)
-
- super(Item3D, self).setParent(parent)
-
- if isinstance(parent, Item3D):
- parent.sigItemChanged.connect(self.__parentItemChanged)
-
- self._updated(Item3DChangedType.ROOT_ITEM)
-
- def __parentItemChanged(self, event):
- """Handle updates of the parent if it is an Item3D
-
- :param Item3DChangedType event:
- """
- if event == Item3DChangedType.ROOT_ITEM:
- self._updated(Item3DChangedType.ROOT_ITEM)
-
- def root(self):
- """Returns the root of the scene this item belongs to.
-
- The root is the up-most Item3D in the scene tree hierarchy.
-
- :rtype: Union[Item3D, None]
- """
- root = None
- ancestor = self.parent()
- while isinstance(ancestor, Item3D):
- root = ancestor
- ancestor = ancestor.parent()
-
- return root
-
- def _getScenePrimitive(self):
- """Return the group containing the item rendering"""
- return self._primitive
-
- def _updated(self, event=None):
- """Handle MixIn class updates.
-
- :param event: The event to send to :attr:`sigItemChanged` signal.
- """
- if event == Item3DChangedType.ROOT_ITEM:
- self.__syncForegroundColor()
-
- if event is not None:
- self.sigItemChanged.emit(event)
-
- # Label
-
- def getLabel(self):
- """Returns the label associated to this item.
-
- :rtype: str
- """
- return self._label
-
- def setLabel(self, label):
- """Set the label associated to this item.
-
- :param str label:
- """
- label = six.text_type(label)
- if label != self._label:
- self._label = label
- self._updated(Item3DChangedType.LABEL)
-
- # Visibility
-
- def isVisible(self):
- """Returns True if item is visible, else False
-
- :rtype: bool
- """
- return self._getScenePrimitive().visible
-
- def setVisible(self, visible=True):
- """Set the visibility of the item in the scene.
-
- :param bool visible: True (default) to show the item, False to hide
- """
- visible = bool(visible)
- primitive = self._getScenePrimitive()
- if visible != primitive.visible:
- primitive.visible = visible
- self._updated(ItemChangedType.VISIBLE)
-
- # Foreground color
-
- def _setForegroundColor(self, color):
- """Set the foreground color of the item.
-
- The default implementation does nothing, override it in subclass.
-
- :param color: RGBA color
- :type color: tuple of 4 float in [0., 1.]
- """
- if hasattr(super(Item3D, self), '_setForegroundColor'):
- super(Item3D, self)._setForegroundColor(color)
-
- def __syncForegroundColor(self):
- """Retrieve foreground color from parent and update this item"""
- # Look-up for SceneWidget to get its foreground color
- root = self.root()
- if root is not None:
- widget = root.parent()
- if isinstance(widget, qt.QWidget):
- self._setForegroundColor(
- widget.getForegroundColor().getRgbF())
-
- # picking
-
- def _pick(self, context):
- """Implement picking on this item.
-
- :param PickContext context: Current picking context
- :return: Data indices at picked position or None
- :rtype: Union[None,PickingResult]
- """
- if (self.isVisible() and
- context.isEnabled() and
- context.isItemPickable(self) and
- self._pickFastCheck(context)):
- return self._pickFull(context)
- return None
-
- def _pickFastCheck(self, context):
- """Approximate item pick test (e.g., bounding box-based picking).
-
- :param PickContext context: Current picking context
- :return: True if item might be picked
- :rtype: bool
- """
- primitive = self._getScenePrimitive()
-
- positionNdc = context.getNDCPosition()
- if positionNdc is None: # No picking outside viewport
- return False
-
- bounds = primitive.bounds(transformed=False, dataBounds=False)
- if bounds is None: # primitive has no bounds
- return False
-
- bounds = primitive.objectToNDCTransform.transformBounds(bounds)
-
- return (bounds[0, 0] <= positionNdc[0] <= bounds[1, 0] and
- bounds[0, 1] <= positionNdc[1] <= bounds[1, 1])
-
- def _pickFull(self, context):
- """Perform precise picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- return None
-
-
-class DataItem3D(Item3D):
- """Base class representing a data item with transform in the scene.
-
- :param parent: The View widget this item belongs to.
- :param Union[GroupBBox, None] group:
- The scene group to use for rendering
- """
-
- def __init__(self, parent, group=None):
- if group is None:
- group = primitives.GroupBBox()
-
- # Set-up bounding box
- group.boxVisible = False
- group.axesVisible = False
- else:
- assert isinstance(group, primitives.GroupBBox)
-
- Item3D.__init__(self, parent=parent, primitive=group)
-
- # Transformations
- self._translate = transform.Translate()
- self._rotateForwardTranslation = transform.Translate()
- self._rotate = transform.Rotate()
- self._rotateBackwardTranslation = transform.Translate()
- self._translateFromRotationCenter = transform.Translate()
- self._matrix = transform.Matrix()
- self._scale = transform.Scale()
- # Group transforms to do to data before rotation
- # This is useful to handle rotation center relative to bbox
- self._transformObjectToRotate = transform.TransformList(
- [self._matrix, self._scale])
- self._transformObjectToRotate.addListener(self._updateRotationCenter)
-
- self._rotationCenter = 0., 0., 0.
-
- self.__transforms = transform.TransformList([
- self._translate,
- self._rotateForwardTranslation,
- self._rotate,
- self._rotateBackwardTranslation,
- self._transformObjectToRotate])
-
- self._getScenePrimitive().transforms = self.__transforms
-
- def _updated(self, event=None):
- """Handle MixIn class updates.
-
- :param event: The event to send to :attr:`sigItemChanged` signal.
- """
- if event == ItemChangedType.DATA:
- self._updateRotationCenter()
- super(DataItem3D, self)._updated(event)
-
- # Transformations
-
- def _getSceneTransforms(self):
- """Return TransformList corresponding to current transforms
-
- :rtype: TransformList
- """
- return self.__transforms
-
- def setScale(self, sx=1., sy=1., sz=1.):
- """Set the scale of the item in the scene.
-
- :param float sx: Scale factor along the X axis
- :param float sy: Scale factor along the Y axis
- :param float sz: Scale factor along the Z axis
- """
- scale = numpy.array((sx, sy, sz), dtype=numpy.float32)
- if not numpy.all(numpy.equal(scale, self.getScale())):
- self._scale.scale = scale
- self._updated(Item3DChangedType.TRANSFORM)
-
- def getScale(self):
- """Returns the scales provided by :meth:`setScale`.
-
- :rtype: numpy.ndarray
- """
- return self._scale.scale
-
- def setTranslation(self, x=0., y=0., z=0.):
- """Set the translation of the origin of the item in the scene.
-
- :param float x: Offset of the data origin on the X axis
- :param float y: Offset of the data origin on the Y axis
- :param float z: Offset of the data origin on the Z axis
- """
- translation = numpy.array((x, y, z), dtype=numpy.float32)
- if not numpy.all(numpy.equal(translation, self.getTranslation())):
- self._translate.translation = translation
- self._updated(Item3DChangedType.TRANSFORM)
-
- def getTranslation(self):
- """Returns the offset set by :meth:`setTranslation`.
-
- :rtype: numpy.ndarray
- """
- return self._translate.translation
-
- _ROTATION_CENTER_TAGS = 'lower', 'center', 'upper'
-
- def _updateRotationCenter(self, *args, **kwargs):
- """Update rotation center relative to bounding box"""
- center = []
- for index, position in enumerate(self.getRotationCenter()):
- # Patch position relative to bounding box
- if position in self._ROTATION_CENTER_TAGS:
- bounds = self._getScenePrimitive().bounds(
- transformed=False, dataBounds=True)
- bounds = self._transformObjectToRotate.transformBounds(bounds)
-
- if bounds is None:
- position = 0.
- elif position == 'lower':
- position = bounds[0, index]
- elif position == 'center':
- position = 0.5 * (bounds[0, index] + bounds[1, index])
- elif position == 'upper':
- position = bounds[1, index]
-
- center.append(position)
-
- if not numpy.all(numpy.equal(
- center, self._rotateForwardTranslation.translation)):
- self._rotateForwardTranslation.translation = center
- self._rotateBackwardTranslation.translation = \
- - self._rotateForwardTranslation.translation
- self._updated(Item3DChangedType.TRANSFORM)
-
- def setRotationCenter(self, x=0., y=0., z=0.):
- """Set the center of rotation of the item.
-
- Position of the rotation center is either a float
- for an absolute position or one of the following
- string to define a position relative to the item's bounding box:
- 'lower', 'center', 'upper'
-
- :param x: rotation center position on the X axis
- :rtype: float or str
- :param y: rotation center position on the Y axis
- :rtype: float or str
- :param z: rotation center position on the Z axis
- :rtype: float or str
- """
- center = []
- for position in (x, y, z):
- if isinstance(position, six.string_types):
- assert position in self._ROTATION_CENTER_TAGS
- else:
- position = float(position)
- center.append(position)
- center = tuple(center)
-
- if center != self._rotationCenter:
- self._rotationCenter = center
- self._updateRotationCenter()
-
- def getRotationCenter(self):
- """Returns the rotation center set by :meth:`setRotationCenter`.
-
- :rtype: 3-tuple of float or str
- """
- return self._rotationCenter
-
- def setRotation(self, angle=0., axis=(0., 0., 1.)):
- """Set the rotation of the item in the scene
-
- :param float angle: The rotation angle in degrees.
- :param axis: The (x, y, z) coordinates of the rotation axis.
- """
- axis = numpy.array(axis, dtype=numpy.float32)
- assert axis.ndim == 1
- assert axis.size == 3
- if (self._rotate.angle != angle or
- not numpy.all(numpy.equal(axis, self._rotate.axis))):
- self._rotate.setAngleAxis(angle, axis)
- self._updated(Item3DChangedType.TRANSFORM)
-
- def getRotation(self):
- """Returns the rotation set by :meth:`setRotation`.
-
- :return: (angle, axis)
- :rtype: 2-tuple (float, numpy.ndarray)
- """
- return self._rotate.angle, self._rotate.axis
-
- def setMatrix(self, matrix=None):
- """Set the transform matrix
-
- :param numpy.ndarray matrix: 3x3 transform matrix
- """
- matrix4x4 = numpy.identity(4, dtype=numpy.float32)
-
- if matrix is not None:
- matrix = numpy.array(matrix, dtype=numpy.float32)
- assert matrix.shape == (3, 3)
- matrix4x4[:3, :3] = matrix
-
- if not numpy.all(numpy.equal(matrix4x4, self._matrix.getMatrix())):
- self._matrix.setMatrix(matrix4x4)
- self._updated(Item3DChangedType.TRANSFORM)
-
- def getMatrix(self):
- """Returns the matrix set by :meth:`setMatrix`
-
- :return: 3x3 matrix
- :rtype: numpy.ndarray"""
- return self._matrix.getMatrix(copy=True)[:3, :3]
-
- # Bounding box
-
- def _setForegroundColor(self, color):
- """Set the color of the bounding box
-
- :param color: RGBA color as 4 floats in [0, 1]
- """
- self._getScenePrimitive().color = color
- super(DataItem3D, self)._setForegroundColor(color)
-
- def isBoundingBoxVisible(self):
- """Returns item's bounding box visibility.
-
- :rtype: bool
- """
- return self._getScenePrimitive().boxVisible
-
- def setBoundingBoxVisible(self, visible):
- """Set item's bounding box visibility.
-
- :param bool visible:
- True to show the bounding box, False (default) to hide it
- """
- visible = bool(visible)
- primitive = self._getScenePrimitive()
- if visible != primitive.boxVisible:
- primitive.boxVisible = visible
- self._updated(Item3DChangedType.BOUNDING_BOX_VISIBLE)
-
-
-class BaseNodeItem(DataItem3D):
- """Base class for data item having children (e.g., group, 3d volume)."""
-
- def __init__(self, parent=None, group=None):
- """Base class representing a group of items in the scene.
-
- :param parent: The View widget this item belongs to.
- :param Union[GroupBBox, None] group:
- The scene group to use for rendering
- """
- DataItem3D.__init__(self, parent=parent, group=group)
-
- def getItems(self):
- """Returns the list of items currently present in the group.
-
- :rtype: tuple
- """
- raise NotImplementedError('getItems must be implemented in subclass')
-
- def visit(self, included=True):
- """Generator visiting the group content.
-
- It traverses the group sub-tree in a top-down left-to-right way.
-
- :param bool included: True (default) to include self in visit
- """
- if included:
- yield self
- for child in self.getItems():
- yield child
- if hasattr(child, 'visit'):
- for item in child.visit(included=False):
- yield item
-
- def pickItems(self, x, y, condition=None):
- """Iterator over picked items in the group at given position.
-
- Each picked item yield a :class:`PickingResult` object
- holding the picking information.
-
- It traverses the group sub-tree in a left-to-right top-down way.
-
- :param int x: X widget device pixel coordinate
- :param int y: Y widget device pixel coordinate
- :param callable condition: Optional test called for each item
- checking whether to process it or not.
- """
- viewport = self._getScenePrimitive().viewport
- if viewport is None:
- raise RuntimeError(
- 'Cannot perform picking: Item not attached to a widget')
-
- context = PickContext(x, y, viewport, condition)
- for result in self._pickItems(context):
- yield result
-
- def _pickItems(self, context):
- """Implement :meth:`pickItems`
-
- :param PickContext context: Current picking context
- """
- if not self.isVisible() or not context.isEnabled():
- return # empty iterator
-
- # Use a copy to discard context changes once this returns
- context = context.copy()
-
- if not self._pickFastCheck(context):
- return # empty iterator
-
- result = self._pick(context)
- if result is not None:
- yield result
-
- for child in self.getItems():
- if isinstance(child, BaseNodeItem):
- for result in child._pickItems(context):
- yield result # Flatten result
-
- else:
- result = child._pick(context)
- if result is not None:
- yield result
-
-
-class _BaseGroupItem(BaseNodeItem):
- """Base class for group of items sharing a common transform."""
-
- sigItemAdded = qt.Signal(object)
- """Signal emitted when a new item is added to the group.
-
- The newly added item is provided by this signal
- """
-
- sigItemRemoved = qt.Signal(object)
- """Signal emitted when an item is removed from the group.
-
- The removed item is provided by this signal.
- """
-
- def __init__(self, parent=None, group=None):
- """Base class representing a group of items in the scene.
-
- :param parent: The View widget this item belongs to.
- :param Union[GroupBBox, None] group:
- The scene group to use for rendering
- """
- BaseNodeItem.__init__(self, parent=parent, group=group)
- self._items = []
-
- def _getGroupPrimitive(self):
- """Returns the group for which to handle children.
-
- This allows this group to be different from the primitive.
- """
- return self._getScenePrimitive()
-
- def addItem(self, item, index=None):
- """Add an item to the group
-
- :param Item3D item: The item to add
- :param int index: The index at which to place the item.
- By default it is appended to the end of the list.
- :raise ValueError: If the item is already in the group.
- """
- assert isinstance(item, Item3D)
- assert item.parent() in (None, self)
-
- if item in self.getItems():
- raise ValueError("Item3D already in group: %s" % item)
-
- item.setParent(self)
- if index is None:
- self._getGroupPrimitive().children.append(
- item._getScenePrimitive())
- self._items.append(item)
- else:
- self._getGroupPrimitive().children.insert(
- index, item._getScenePrimitive())
- self._items.insert(index, item)
- self.sigItemAdded.emit(item)
-
- def getItems(self):
- """Returns the list of items currently present in the group.
-
- :rtype: tuple
- """
- return tuple(self._items)
-
- def removeItem(self, item):
- """Remove an item from the scene.
-
- :param Item3D item: The item to remove from the scene
- :raises ValueError: If the item does not belong to the group
- """
- if item not in self.getItems():
- raise ValueError("Item3D not in group: %s" % str(item))
-
- self._getGroupPrimitive().children.remove(item._getScenePrimitive())
- self._items.remove(item)
- item.setParent(None)
- self.sigItemRemoved.emit(item)
-
- def clearItems(self):
- """Remove all item from the group."""
- for item in self.getItems():
- self.removeItem(item)
-
-
-class GroupItem(_BaseGroupItem):
- """Group of items sharing a common transform."""
-
- def __init__(self, parent=None):
- super(GroupItem, self).__init__(parent=parent)
-
-
-class GroupWithAxesItem(_BaseGroupItem):
- """
- Group of items sharing a common transform surrounded with labelled axes.
- """
-
- def __init__(self, parent=None):
- """Class representing a group of items in the scene with labelled axes.
-
- :param parent: The View widget this item belongs to.
- """
- super(GroupWithAxesItem, self).__init__(parent=parent,
- group=axes.LabelledAxes())
-
- # Axes labels
-
- def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None):
- """Set the text labels of the axes.
-
- :param str xlabel: Label of the X axis, None to leave unchanged.
- :param str ylabel: Label of the Y axis, None to leave unchanged.
- :param str zlabel: Label of the Z axis, None to leave unchanged.
- """
- labelledAxes = self._getScenePrimitive()
- if xlabel is not None:
- labelledAxes.xlabel = xlabel
-
- if ylabel is not None:
- labelledAxes.ylabel = ylabel
-
- if zlabel is not None:
- labelledAxes.zlabel = zlabel
-
- class _Labels(tuple):
- """Return type of :meth:`getAxesLabels`"""
-
- def getXLabel(self):
- """Label of the X axis (str)"""
- return self[0]
-
- def getYLabel(self):
- """Label of the Y axis (str)"""
- return self[1]
-
- def getZLabel(self):
- """Label of the Z axis (str)"""
- return self[2]
-
- def getAxesLabels(self):
- """Returns the text labels of the axes
-
- >>> group = GroupWithAxesItem()
- >>> group.setAxesLabels(xlabel='X')
-
- You can get the labels either as a 3-tuple:
-
- >>> xlabel, ylabel, zlabel = group.getAxesLabels()
-
- Or as an object with methods getXLabel, getYLabel and getZLabel:
-
- >>> labels = group.getAxesLabels()
- >>> labels.getXLabel()
- ... 'X'
-
- :return: object describing the labels
- """
- labelledAxes = self._getScenePrimitive()
- return self._Labels((labelledAxes.xlabel,
- labelledAxes.ylabel,
- labelledAxes.zlabel))
-
-
-class RootGroupWithAxesItem(GroupWithAxesItem):
- """Special group with axes item for root of the scene.
-
- Uses 2 groups so that axes take transforms into account.
- """
-
- def __init__(self, parent=None):
- super(RootGroupWithAxesItem, self).__init__(parent)
- self.__group = scene.Group()
- self.__group.transforms = self._getSceneTransforms()
-
- groupWithAxes = self._getScenePrimitive()
- groupWithAxes.transforms = [] # Do not apply transforms here
- groupWithAxes.children.append(self.__group)
-
- def _getGroupPrimitive(self):
- """Returns the group for which to handle children.
-
- This allows this group to be different from the primitive.
- """
- return self.__group
diff --git a/silx/gui/plot3d/items/image.py b/silx/gui/plot3d/items/image.py
deleted file mode 100644
index 4e2b396..0000000
--- a/silx/gui/plot3d/items/image.py
+++ /dev/null
@@ -1,425 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-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.
-#
-# ###########################################################################*/
-"""This module provides 2D data and RGB(A) image item class.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/11/2017"
-
-import numpy
-
-from ..scene import primitives, utils
-from .core import DataItem3D, ItemChangedType
-from .mixins import ColormapMixIn, InterpolationMixIn
-from ._pick import PickingResult
-
-
-class _Image(DataItem3D, InterpolationMixIn):
- """Base class for images
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- DataItem3D.__init__(self, parent=parent)
- InterpolationMixIn.__init__(self)
-
- def _setPrimitive(self, primitive):
- InterpolationMixIn._setPrimitive(self, primitive)
-
- def getData(self, copy=True):
- raise NotImplementedError()
-
- def _pickFull(self, context):
- """Perform picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
- if rayObject is None:
- return None
-
- points = utils.segmentPlaneIntersect(
- rayObject[0, :3],
- rayObject[1, :3],
- planeNorm=numpy.array((0., 0., 1.), dtype=numpy.float64),
- planePt=numpy.array((0., 0., 0.), dtype=numpy.float64))
-
- if len(points) == 1: # Single intersection
- if points[0][0] < 0. or points[0][1] < 0.:
- return None # Outside image
- row, column = int(points[0][1]), int(points[0][0])
- data = self.getData(copy=False)
- height, width = data.shape[:2]
- if row < height and column < width:
- return PickingResult(
- self,
- positions=[(points[0][0], points[0][1], 0.)],
- indices=([row], [column]))
- else:
- return None # Outside image
- else: # Either no intersection or segment and image are coplanar
- return None
-
-
-class ImageData(_Image, ColormapMixIn):
- """Description of a 2D image data.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- _Image.__init__(self, parent=parent)
- ColormapMixIn.__init__(self)
-
- self._data = numpy.zeros((0, 0), dtype=numpy.float32)
-
- self._image = primitives.ImageData(self._data)
- self._getScenePrimitive().children.append(self._image)
-
- # Connect scene primitive to mix-in class
- ColormapMixIn._setSceneColormap(self, self._image.colormap)
- _Image._setPrimitive(self, self._image)
-
- def setData(self, data, copy=True):
- """Set the image data to display.
-
- The data will be casted to float32.
-
- :param numpy.ndarray data: The image data
- :param bool copy: True (default) to copy the data,
- False to use as is (do not modify!).
- """
- self._image.setData(data, copy=copy)
- self._setColormappedData(self.getData(copy=False), copy=False)
- self._updated(ItemChangedType.DATA)
-
- def getData(self, copy=True):
- """Get the image data.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :rtype: numpy.ndarray
- :return: The image data
- """
- return self._image.getData(copy=copy)
-
-
-class ImageRgba(_Image, InterpolationMixIn):
- """Description of a 2D data RGB(A) image.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- _Image.__init__(self, parent=parent)
- InterpolationMixIn.__init__(self)
-
- self._data = numpy.zeros((0, 0, 3), dtype=numpy.float32)
-
- self._image = primitives.ImageRgba(self._data)
- self._getScenePrimitive().children.append(self._image)
-
- # Connect scene primitive to mix-in class
- _Image._setPrimitive(self, self._image)
-
- def setData(self, data, copy=True):
- """Set the RGB(A) image data to display.
-
- Supported array format: float32 in [0, 1], uint8.
-
- :param numpy.ndarray data:
- The RGBA image data as an array of shape (H, W, Channels)
- :param bool copy: True (default) to copy the data,
- False to use as is (do not modify!).
- """
- self._image.setData(data, copy=copy)
- self._updated(ItemChangedType.DATA)
-
- def getData(self, copy=True):
- """Get the image data.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :rtype: numpy.ndarray
- :return: The image data
- """
- return self._image.getData(copy=copy)
-
-
-class _HeightMap(DataItem3D):
- """Base class for 2D data array displayed as a height field.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- DataItem3D.__init__(self, parent=parent)
- self.__data = numpy.zeros((0, 0), dtype=numpy.float32)
-
- def _pickFull(self, context, threshold=0., sort='depth'):
- """Perform picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :param float threshold: Picking threshold in pixel.
- Perform picking in a square of size threshold x threshold.
- :param str sort: How returned indices are sorted:
-
- - 'index' (default): sort by the value of the indices
- - 'depth': Sort by the depth of the points from the current
- camera point of view.
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- assert sort in ('index', 'depth')
-
- rayNdc = context.getPickingSegment(frame='ndc')
- if rayNdc is None: # No picking outside viewport
- return None
-
- # TODO no colormapped or color data
- # Project data to NDC
- heightData = self.getData(copy=False)
- if heightData.size == 0:
- return # Nothing displayed
-
- height, width = heightData.shape
- z = numpy.ravel(heightData)
- y, x = numpy.mgrid[0:height, 0:width]
- dataPoints = numpy.transpose((numpy.ravel(x),
- numpy.ravel(y),
- z,
- numpy.ones_like(z)))
-
- primitive = self._getScenePrimitive()
-
- pointsNdc = primitive.objectToNDCTransform.transformPoints(
- dataPoints, perspectiveDivide=True)
-
- # Perform picking
- distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
- # TODO issue with symbol size: using pixel instead of points
- threshold += 1. # symbol size
- thresholdNdc = 2. * threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
- numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
-
- if sort == 'depth':
- # Sort picked points from front to back
- picked = picked[numpy.argsort(pointsNdc[picked, 2])]
-
- if picked.size > 0:
- # Convert indices from 1D to 2D
- return PickingResult(self,
- positions=dataPoints[picked, :3],
- indices=(picked // width, picked % width),
- fetchdata=self.getData)
- else:
- return None
-
- def setData(self, data, copy: bool=True):
- """Set the height field data.
-
- :param data:
- :param copy: True (default) to copy the data,
- False to use as is (do not modify!).
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 2
-
- self.__data = data
- self._updated(ItemChangedType.DATA)
-
- def getData(self, copy: bool=True) -> numpy.ndarray:
- """Get the height field 2D data.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- """
- return numpy.array(self.__data, copy=copy)
-
-
-class HeightMapData(_HeightMap, ColormapMixIn):
- """Description of a 2D height field associated to a colormapped dataset.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- _HeightMap.__init__(self, parent=parent)
- ColormapMixIn.__init__(self)
-
- self.__data = numpy.zeros((0, 0), dtype=numpy.float32)
-
- def _updated(self, event=None):
- if event == ItemChangedType.DATA:
- self.__updateScene()
- super()._updated(event=event)
-
- def __updateScene(self):
- """Update display primitive to use"""
- self._getScenePrimitive().children = [] # Remove previous primitives
- ColormapMixIn._setSceneColormap(self, None)
-
- if not self.isVisible():
- return # Update when visible
-
- data = self.getColormappedData(copy=False)
- heightData = self.getData(copy=False)
-
- if data.size == 0 or heightData.size == 0:
- return # Nothing to display
-
- # Display as a set of points
- height, width = heightData.shape
- # Generates coordinates
- y, x = numpy.mgrid[0:height, 0:width]
-
- if data.shape != heightData.shape: # data and height size miss-match
- # Colormapped data is interpolated (nearest-neighbour) to match the height field
- data = data[numpy.floor(y * data.shape[0] / height).astype(numpy.int),
- numpy.floor(x * data.shape[1] / height).astype(numpy.int)]
-
- x = numpy.ravel(x)
- y = numpy.ravel(y)
-
- primitive = primitives.Points(
- x=x,
- y=y,
- z=numpy.ravel(heightData),
- value=numpy.ravel(data),
- size=1)
- primitive.marker = 's'
- ColormapMixIn._setSceneColormap(self, primitive.colormap)
- self._getScenePrimitive().children = [primitive]
-
- def setColormappedData(self, data, copy: bool=True):
- """Set the 2D data used to compute colors.
-
- :param data: 2D array of data
- :param copy: True (default) to copy the data,
- False to use as is (do not modify!).
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 2
-
- self.__data = data
- self._updated(ItemChangedType.DATA)
-
- def getColormappedData(self, copy: bool=True) -> numpy.ndarray:
- """Returns the 2D data used to compute colors.
-
- :param copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- """
- return numpy.array(self.__data, copy=copy)
-
-
-class HeightMapRGBA(_HeightMap):
- """Description of a 2D height field associated to a RGB(A) image.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- _HeightMap.__init__(self, parent=parent)
-
- self.__rgba = numpy.zeros((0, 0, 3), dtype=numpy.float32)
-
- def _updated(self, event=None):
- if event == ItemChangedType.DATA:
- self.__updateScene()
- super()._updated(event=event)
-
- def __updateScene(self):
- """Update display primitive to use"""
- self._getScenePrimitive().children = [] # Remove previous primitives
-
- if not self.isVisible():
- return # Update when visible
-
- rgba = self.getColorData(copy=False)
- heightData = self.getData(copy=False)
- if rgba.size == 0 or heightData.size == 0:
- return # Nothing to display
-
- # Display as a set of points
- height, width = heightData.shape
- # Generates coordinates
- y, x = numpy.mgrid[0:height, 0:width]
-
- if rgba.shape[:2] != heightData.shape: # image and height size miss-match
- # RGBA data is interpolated (nearest-neighbour) to match the height field
- rgba = rgba[numpy.floor(y * rgba.shape[0] / height).astype(numpy.int),
- numpy.floor(x * rgba.shape[1] / height).astype(numpy.int)]
-
- x = numpy.ravel(x)
- y = numpy.ravel(y)
-
- primitive = primitives.ColorPoints(
- x=x,
- y=y,
- z=numpy.ravel(heightData),
- color=rgba.reshape(-1, rgba.shape[-1]),
- size=1)
- primitive.marker = 's'
- self._getScenePrimitive().children = [primitive]
-
- def setColorData(self, data, copy: bool=True):
- """Set the RGB(A) image to use.
-
- Supported array format: float32 in [0, 1], uint8.
-
- :param data:
- The RGBA image data as an array of shape (H, W, Channels)
- :param copy: True (default) to copy the data,
- False to use as is (do not modify!).
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 3
- assert data.shape[-1] in (3, 4)
- # TODO check type
-
- self.__rgba = data
- self._updated(ItemChangedType.DATA)
-
- def getColorData(self, copy: bool=True) -> numpy.ndarray:
- """Get the RGB(A) image data.
-
- :param copy: True (default) to get a copy,
- False to get internal representation (do not modify!).
- """
- return numpy.array(self.__rgba, copy=copy)
diff --git a/silx/gui/plot3d/items/mesh.py b/silx/gui/plot3d/items/mesh.py
deleted file mode 100644
index 4e19939..0000000
--- a/silx/gui/plot3d/items/mesh.py
+++ /dev/null
@@ -1,792 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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.
-#
-# ###########################################################################*/
-"""This module provides regular mesh item class.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/07/2018"
-
-
-import logging
-import numpy
-
-from ... import _glutils as glu
-from ..scene import primitives, utils, function
-from ..scene.transform import Rotate
-from .core import DataItem3D, ItemChangedType
-from .mixins import ColormapMixIn
-from ._pick import PickingResult
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _MeshBase(DataItem3D):
- """Base class for :class:`Mesh' and :class:`ColormapMesh`.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- DataItem3D.__init__(self, parent=parent)
- self._mesh = None
-
- def _setMesh(self, mesh):
- """Set mesh primitive
-
- :param Union[None,Geometry] mesh: The scene primitive
- """
- self._getScenePrimitive().children = [] # Remove any previous mesh
-
- self._mesh = mesh
- if self._mesh is not None:
- self._getScenePrimitive().children.append(self._mesh)
-
- self._updated(ItemChangedType.DATA)
-
- def _getMesh(self):
- """Returns the underlying Mesh scene primitive"""
- return self._mesh
-
- def getPositionData(self, copy=True):
- """Get the mesh vertex positions.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: The (x, y, z) positions as a (N, 3) array
- :rtype: numpy.ndarray
- """
- if self._getMesh() is None:
- return numpy.empty((0, 3), dtype=numpy.float32)
- else:
- return self._getMesh().getAttribute('position', copy=copy)
-
- def getNormalData(self, copy=True):
- """Get the mesh vertex normals.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: The normals as a (N, 3) array, a single normal or None
- :rtype: Union[numpy.ndarray,None]
- """
- if self._getMesh() is None:
- return None
- else:
- return self._getMesh().getAttribute('normal', copy=copy)
-
- def getIndices(self, copy=True):
- """Get the vertex indices.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: The vertex indices as an array or None.
- :rtype: Union[numpy.ndarray,None]
- """
- if self._getMesh() is None:
- return None
- else:
- return self._getMesh().getIndices(copy=copy)
-
- def getDrawMode(self):
- """Get mesh rendering mode.
-
- :return: The drawing mode of this primitive
- :rtype: str
- """
- return self._getMesh().drawMode
-
- def _pickFull(self, context):
- """Perform precise picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
- if rayObject is None: # No picking outside viewport
- return None
- rayObject = rayObject[:, :3]
-
- positions = self.getPositionData(copy=False)
- if positions.size == 0:
- return None
-
- mode = self.getDrawMode()
-
- vertexIndices = self.getIndices(copy=False)
- if vertexIndices is not None: # Expand indices
- positions = utils.unindexArrays(mode, vertexIndices, positions)[0]
- triangles = positions.reshape(-1, 3, 3)
- else:
- if mode == 'triangles':
- triangles = positions.reshape(-1, 3, 3)
-
- elif mode == 'triangle_strip':
- # Expand strip
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
- triangles[:, 0] = positions[:-2]
- triangles[:, 1] = positions[1:-1]
- triangles[:, 2] = positions[2:]
-
- elif mode == 'fan':
- # Expand fan
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
- triangles[:, 0] = positions[0]
- triangles[:, 1] = positions[1:-1]
- triangles[:, 2] = positions[2:]
-
- else:
- _logger.warning("Unsupported draw mode: %s" % mode)
- return None
-
- trianglesIndices, t, barycentric = glu.segmentTrianglesIntersection(
- rayObject, triangles)
-
- if len(trianglesIndices) == 0:
- return None
-
- points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
-
- # Get vertex index from triangle index and closest point in triangle
- closest = numpy.argmax(barycentric, axis=1)
-
- if mode == 'triangles':
- indices = trianglesIndices * 3 + closest
-
- elif mode == 'triangle_strip':
- indices = trianglesIndices + closest
-
- elif mode == 'fan':
- indices = trianglesIndices + closest # For corners 1 and 2
- indices[closest == 0] = 0 # For first corner (common)
-
- if vertexIndices is not None:
- # Convert from indices in expanded triangles to input vertices
- indices = vertexIndices[indices]
-
- return PickingResult(self,
- positions=points,
- indices=indices,
- fetchdata=self.getPositionData)
-
-
-class Mesh(_MeshBase):
- """Description of mesh.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- _MeshBase.__init__(self, parent=parent)
-
- def setData(self,
- position,
- color,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
- """Set mesh geometry data.
-
- Supported drawing modes are: 'triangles', 'triangle_strip', 'fan'
-
- :param numpy.ndarray position:
- Position (x, y, z) of each vertex as a (N, 3) array
- :param numpy.ndarray color: Colors for each point or a single color
- :param Union[numpy.ndarray,None] normal: Normals for each point or None (default)
- :param str mode: The drawing mode.
- :param Union[List[int],None] indices:
- Array of vertex indices or None to use arrays directly.
- :param bool copy: True (default) to copy the data,
- False to use as is (do not modify!).
- """
- assert mode in ('triangles', 'triangle_strip', 'fan')
- if position is None or len(position) == 0:
- mesh = None
- else:
- mesh = primitives.Mesh3D(
- position, color, normal, mode=mode, indices=indices, copy=copy)
- self._setMesh(mesh)
-
- def getData(self, copy=True):
- """Get the mesh geometry.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: The positions, colors, normals and mode
- :rtype: tuple of numpy.ndarray
- """
- return (self.getPositionData(copy=copy),
- self.getColorData(copy=copy),
- self.getNormalData(copy=copy),
- self.getDrawMode())
-
- def getColorData(self, copy=True):
- """Get the mesh vertex colors.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: The RGBA colors as a (N, 4) array or a single color
- :rtype: numpy.ndarray
- """
- if self._getMesh() is None:
- return numpy.empty((0, 4), dtype=numpy.float32)
- else:
- return self._getMesh().getAttribute('color', copy=copy)
-
-
-class ColormapMesh(_MeshBase, ColormapMixIn):
- """Description of mesh which color is defined by scalar and a colormap.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- _MeshBase.__init__(self, parent=parent)
- ColormapMixIn.__init__(self, function.Colormap())
-
- def setData(self,
- position,
- value,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
- """Set mesh geometry data.
-
- Supported drawing modes are: 'triangles', 'triangle_strip', 'fan'
-
- :param numpy.ndarray position:
- Position (x, y, z) of each vertex as a (N, 3) array
- :param numpy.ndarray value: Data value for each vertex.
- :param Union[numpy.ndarray,None] normal: Normals for each point or None (default)
- :param str mode: The drawing mode.
- :param Union[List[int],None] indices:
- Array of vertex indices or None to use arrays directly.
- :param bool copy: True (default) to copy the data,
- False to use as is (do not modify!).
- """
- assert mode in ('triangles', 'triangle_strip', 'fan')
- if position is None or len(position) == 0:
- mesh = None
- else:
- mesh = primitives.ColormapMesh3D(
- position=position,
- value=numpy.array(value, copy=False).reshape(-1, 1), # Make it a 2D array
- colormap=self._getSceneColormap(),
- normal=normal,
- mode=mode,
- indices=indices,
- copy=copy)
- self._setMesh(mesh)
-
- self._setColormappedData(self.getValueData(copy=False), copy=False)
-
- def getData(self, copy=True):
- """Get the mesh geometry.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: The positions, values, normals and mode
- :rtype: tuple of numpy.ndarray
- """
- return (self.getPositionData(copy=copy),
- self.getValueData(copy=copy),
- self.getNormalData(copy=copy),
- self.getDrawMode())
-
- def getValueData(self, copy=True):
- """Get the mesh vertex values.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: Array of data values
- :rtype: numpy.ndarray
- """
- if self._getMesh() is None:
- return numpy.empty((0,), dtype=numpy.float32)
- else:
- return self._getMesh().getAttribute('value', copy=copy)
-
-
-class _CylindricalVolume(DataItem3D):
- """Class that represents a volume with a rotational symmetry along z
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- DataItem3D.__init__(self, parent=parent)
- self._mesh = None
- self._nbFaces = 0
-
- def getPosition(self, copy=True):
- """Get primitive positions.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: Position of the primitives as a (N, 3) array.
- :rtype: numpy.ndarray
- """
- raise NotImplementedError("Must be implemented in subclass")
-
- def _setData(self, position, radius, height, angles, color, flatFaces,
- rotation):
- """Set volume geometry data.
-
- :param numpy.ndarray position:
- Center position (x, y, z) of each volume as (N, 3) array.
- :param float radius: External radius ot the volume.
- :param float height: Height of the volume(s).
- :param numpy.ndarray angles: Angles of the edges.
- :param numpy.array color: RGB color of the volume(s).
- :param bool flatFaces:
- If the volume as flat faces or not. Used for normals calculation.
- """
-
- self._getScenePrimitive().children = [] # Remove any previous mesh
-
- if position is None or len(position) == 0:
- self._mesh = None
- self._nbFaces = 0
- else:
- self._nbFaces = len(angles) - 1
-
- volume = numpy.empty(shape=(len(angles) - 1, 12, 3),
- dtype=numpy.float32)
- normal = numpy.empty(shape=(len(angles) - 1, 12, 3),
- dtype=numpy.float32)
-
- for i in range(0, len(angles) - 1):
- # c6
- # /\
- # / \
- # / \
- # c4|------|c5
- # | \ |
- # | \ |
- # | \ |
- # | \ |
- # c2|------|c3
- # \ /
- # \ /
- # \/
- # c1
- c1 = numpy.array([0, 0, -height/2])
- c1 = rotation.transformPoint(c1)
- c2 = numpy.array([radius * numpy.cos(angles[i]),
- radius * numpy.sin(angles[i]),
- -height/2])
- c2 = rotation.transformPoint(c2)
- c3 = numpy.array([radius * numpy.cos(angles[i+1]),
- radius * numpy.sin(angles[i+1]),
- -height/2])
- c3 = rotation.transformPoint(c3)
- c4 = numpy.array([radius * numpy.cos(angles[i]),
- radius * numpy.sin(angles[i]),
- height/2])
- c4 = rotation.transformPoint(c4)
- c5 = numpy.array([radius * numpy.cos(angles[i+1]),
- radius * numpy.sin(angles[i+1]),
- height/2])
- c5 = rotation.transformPoint(c5)
- c6 = numpy.array([0, 0, height/2])
- c6 = rotation.transformPoint(c6)
-
- volume[i] = numpy.array([c1, c3, c2,
- c2, c3, c4,
- c3, c5, c4,
- c4, c5, c6])
- if flatFaces:
- normal[i] = numpy.array([numpy.cross(c3-c1, c2-c1), # c1
- numpy.cross(c2-c3, c1-c3), # c3
- numpy.cross(c1-c2, c3-c2), # c2
- numpy.cross(c3-c2, c4-c2), # c2
- numpy.cross(c4-c3, c2-c3), # c3
- numpy.cross(c2-c4, c3-c4), # c4
- numpy.cross(c5-c3, c4-c3), # c3
- numpy.cross(c4-c5, c3-c5), # c5
- numpy.cross(c3-c4, c5-c4), # c4
- numpy.cross(c5-c4, c6-c4), # c4
- numpy.cross(c6-c5, c5-c5), # c5
- numpy.cross(c4-c6, c5-c6)]) # c6
- else:
- normal[i] = numpy.array([numpy.cross(c3-c1, c2-c1),
- numpy.cross(c2-c3, c1-c3),
- numpy.cross(c1-c2, c3-c2),
- c2-c1, c3-c1, c4-c6, # c2 c2 c4
- c3-c1, c5-c6, c4-c6, # c3 c5 c4
- numpy.cross(c5-c4, c6-c4),
- numpy.cross(c6-c5, c5-c5),
- numpy.cross(c4-c6, c5-c6)])
-
- # Multiplication according to the number of positions
- vertices = numpy.tile(volume.reshape(-1, 3), (len(position), 1))\
- .reshape((-1, 3))
- normals = numpy.tile(normal.reshape(-1, 3), (len(position), 1))\
- .reshape((-1, 3))
-
- # Translations
- numpy.add(vertices, numpy.tile(position, (1, (len(angles)-1) * 12))
- .reshape((-1, 3)), out=vertices)
-
- # Colors
- if numpy.ndim(color) == 2:
- color = numpy.tile(color, (1, 12 * (len(angles) - 1)))\
- .reshape(-1, 3)
-
- self._mesh = primitives.Mesh3D(
- vertices, color, normals, mode='triangles', copy=False)
- self._getScenePrimitive().children.append(self._mesh)
-
- self._updated(ItemChangedType.DATA)
-
- def _pickFull(self, context):
- """Perform precise picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- if self._mesh is None or self._nbFaces == 0:
- return None
-
- rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
- if rayObject is None: # No picking outside viewport
- return None
- rayObject = rayObject[:, :3]
-
- positions = self._mesh.getAttribute('position', copy=False)
- triangles = positions.reshape(-1, 3, 3) # 'triangle' draw mode
-
- trianglesIndices, t = glu.segmentTrianglesIntersection(
- rayObject, triangles)[:2]
-
- if len(trianglesIndices) == 0:
- return None
-
- # Get object index from triangle index
- indices = trianglesIndices // (4 * self._nbFaces)
-
- # Select closest intersection point for each primitive
- indices, firstIndices = numpy.unique(indices, return_index=True)
- t = t[firstIndices]
-
- # Resort along t as result of numpy.unique is not sorted by t
- sortedIndices = numpy.argsort(t)
- t = t[sortedIndices]
- indices = indices[sortedIndices]
-
- points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
-
- return PickingResult(self,
- positions=points,
- indices=indices,
- fetchdata=self.getPosition)
-
-
-class Box(_CylindricalVolume):
- """Description of a box.
-
- Can be used to draw one box or many similar boxes.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- super(Box, self).__init__(parent)
- self.position = None
- self.size = None
- self.color = None
- self.rotation = None
- self.setData()
-
- def setData(self, size=(1, 1, 1), color=(1, 1, 1),
- position=(0, 0, 0), rotation=(0, (0, 0, 0))):
- """
- Set Box geometry data.
-
- :param numpy.array size: Size (dx, dy, dz) of the box(es).
- :param numpy.array color: RGB color of the box(es).
- :param numpy.ndarray position:
- Center position (x, y, z) of each box as a (N, 3) array.
- :param tuple(float, array) rotation:
- Angle (in degrees) and axis of rotation.
- If (0, (0, 0, 0)) (default), the hexagonal faces are on
- xy plane and a side face is aligned with x axis.
- """
- self.position = numpy.atleast_2d(numpy.array(position, copy=True))
- self.size = numpy.array(size, copy=True)
- self.color = numpy.array(color, copy=True)
- self.rotation = Rotate(rotation[0],
- rotation[1][0], rotation[1][1], rotation[1][2])
-
- assert (numpy.ndim(self.color) == 1 or
- len(self.color) == len(self.position))
-
- diagonal = numpy.sqrt(self.size[0]**2 + self.size[1]**2)
- alpha = 2 * numpy.arcsin(self.size[1] / diagonal)
- beta = 2 * numpy.arcsin(self.size[0] / diagonal)
- angles = numpy.array([0,
- alpha,
- alpha + beta,
- alpha + beta + alpha,
- 2 * numpy.pi])
- numpy.subtract(angles, 0.5 * alpha, out=angles)
- self._setData(self.position,
- numpy.sqrt(self.size[0]**2 + self.size[1]**2)/2,
- self.size[2],
- angles,
- self.color,
- True,
- self.rotation)
-
- def getPosition(self, copy=True):
- """Get box(es) position(s).
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: Position of the box(es) as a (N, 3) array.
- :rtype: numpy.ndarray
- """
- return numpy.array(self.position, copy=copy)
-
- def getSize(self):
- """Get box(es) size.
-
- :return: Size (dx, dy, dz) of the box(es).
- :rtype: numpy.ndarray
- """
- return numpy.array(self.size, copy=True)
-
- def getColor(self, copy=True):
- """Get box(es) color.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: RGB color of the box(es).
- :rtype: numpy.ndarray
- """
- return numpy.array(self.color, copy=copy)
-
-
-class Cylinder(_CylindricalVolume):
- """Description of a cylinder.
-
- Can be used to draw one cylinder or many similar cylinders.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- super(Cylinder, self).__init__(parent)
- self.position = None
- self.radius = None
- self.height = None
- self.color = None
- self.nbFaces = 0
- self.rotation = None
- self.setData()
-
- def setData(self, radius=1, height=1, color=(1, 1, 1), nbFaces=20,
- position=(0, 0, 0), rotation=(0, (0, 0, 0))):
- """
- Set the cylinder geometry data
-
- :param float radius: Radius of the cylinder(s).
- :param float height: Height of the cylinder(s).
- :param numpy.array color: RGB color of the cylinder(s).
- :param int nbFaces:
- Number of faces for cylinder approximation (default 20).
- :param numpy.ndarray position:
- Center position (x, y, z) of each cylinder as a (N, 3) array.
- :param tuple(float, array) rotation:
- Angle (in degrees) and axis of rotation.
- If (0, (0, 0, 0)) (default), the hexagonal faces are on
- xy plane and a side face is aligned with x axis.
- """
- self.position = numpy.atleast_2d(numpy.array(position, copy=True))
- self.radius = float(radius)
- self.height = float(height)
- self.color = numpy.array(color, copy=True)
- self.nbFaces = int(nbFaces)
- self.rotation = Rotate(rotation[0],
- rotation[1][0], rotation[1][1], rotation[1][2])
-
- assert (numpy.ndim(self.color) == 1 or
- len(self.color) == len(self.position))
-
- angles = numpy.linspace(0, 2*numpy.pi, self.nbFaces + 1)
- self._setData(self.position,
- self.radius,
- self.height,
- angles,
- self.color,
- False,
- self.rotation)
-
- def getPosition(self, copy=True):
- """Get cylinder(s) position(s).
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: Position(s) of the cylinder(s) as a (N, 3) array.
- :rtype: numpy.ndarray
- """
- return numpy.array(self.position, copy=copy)
-
- def getRadius(self):
- """Get cylinder(s) radius.
-
- :return: Radius of the cylinder(s).
- :rtype: float
- """
- return self.radius
-
- def getHeight(self):
- """Get cylinder(s) height.
-
- :return: Height of the cylinder(s).
- :rtype: float
- """
- return self.height
-
- def getColor(self, copy=True):
- """Get cylinder(s) color.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: RGB color of the cylinder(s).
- :rtype: numpy.ndarray
- """
- return numpy.array(self.color, copy=copy)
-
-
-class Hexagon(_CylindricalVolume):
- """Description of a uniform hexagonal prism.
-
- Can be used to draw one hexagonal prim or many similar hexagonal
- prisms.
-
- :param parent: The View widget this item belongs to.
- """
-
- def __init__(self, parent=None):
- super(Hexagon, self).__init__(parent)
- self.position = None
- self.radius = 0
- self.height = 0
- self.color = None
- self.rotation = None
- self.setData()
-
- def setData(self, radius=1, height=1, color=(1, 1, 1),
- position=(0, 0, 0), rotation=(0, (0, 0, 0))):
- """
- Set the uniform hexagonal prism geometry data
-
- :param float radius: External radius of the hexagonal prism
- :param float height: Height of the hexagonal prism
- :param numpy.array color: RGB color of the prism(s)
- :param numpy.ndarray position:
- Center position (x, y, z) of each prism as a (N, 3) array
- :param tuple(float, array) rotation:
- Angle (in degrees) and axis of rotation.
- If (0, (0, 0, 0)) (default), the hexagonal faces are on
- xy plane and a side face is aligned with x axis.
- """
- self.position = numpy.atleast_2d(numpy.array(position, copy=True))
- self.radius = float(radius)
- self.height = float(height)
- self.color = numpy.array(color, copy=True)
- self.rotation = Rotate(rotation[0], rotation[1][0], rotation[1][1],
- rotation[1][2])
-
- assert (numpy.ndim(self.color) == 1 or
- len(self.color) == len(self.position))
-
- angles = numpy.linspace(0, 2*numpy.pi, 7)
- self._setData(self.position,
- self.radius,
- self.height,
- angles,
- self.color,
- True,
- self.rotation)
-
- def getPosition(self, copy=True):
- """Get hexagonal prim(s) position(s).
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: Position(s) of hexagonal prism(s) as a (N, 3) array.
- :rtype: numpy.ndarray
- """
- return numpy.array(self.position, copy=copy)
-
- def getRadius(self):
- """Get hexagonal prism(s) radius.
-
- :return: Radius of hexagon(s).
- :rtype: float
- """
- return self.radius
-
- def getHeight(self):
- """Get hexagonal prism(s) height.
-
- :return: Height of hexagonal prism(s).
- :rtype: float
- """
- return self.height
-
- def getColor(self, copy=True):
- """Get hexagonal prism(s) color.
-
- :param bool copy:
- True (default) to get a copy,
- False to get internal representation (do not modify!).
- :return: RGB color of the hexagonal prism(s).
- :rtype: numpy.ndarray
- """
- return numpy.array(self.color, copy=copy)
diff --git a/silx/gui/plot3d/items/mixins.py b/silx/gui/plot3d/items/mixins.py
deleted file mode 100644
index f512365..0000000
--- a/silx/gui/plot3d/items/mixins.py
+++ /dev/null
@@ -1,288 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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.
-#
-# ###########################################################################*/
-"""This module provides mix-in classes for :class:`Item3D`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import collections
-import numpy
-
-from silx.math.combo import min_max
-
-from ...plot.items.core import ItemMixInBase
-from ...plot.items.core import ColormapMixIn as _ColormapMixIn
-from ...plot.items.core import SymbolMixIn as _SymbolMixIn
-from ...plot.items.core import ComplexMixIn as _ComplexMixIn
-from ...colors import rgba
-
-from ..scene import primitives
-from .core import Item3DChangedType, ItemChangedType
-
-
-class InterpolationMixIn(ItemMixInBase):
- """Mix-in class for image interpolation mode
-
- :param str mode: 'linear' (default) or 'nearest'
- :param primitive:
- scene object for which to sync interpolation mode.
- This object MUST have an interpolation property that is updated.
- """
-
- NEAREST_INTERPOLATION = 'nearest'
- """Nearest interpolation mode (see :meth:`setInterpolation`)"""
-
- LINEAR_INTERPOLATION = 'linear'
- """Linear interpolation mode (see :meth:`setInterpolation`)"""
-
- INTERPOLATION_MODES = NEAREST_INTERPOLATION, LINEAR_INTERPOLATION
- """Supported interpolation modes for :meth:`setInterpolation`"""
-
- def __init__(self, mode=NEAREST_INTERPOLATION, primitive=None):
- self.__primitive = primitive
- self._syncPrimitiveInterpolation()
-
- self.__interpolationMode = None
- self.setInterpolation(mode)
-
- def _setPrimitive(self, primitive):
-
- """Set the scene object for which to sync interpolation"""
- self.__primitive = primitive
- self._syncPrimitiveInterpolation()
-
- def _syncPrimitiveInterpolation(self):
- """Synchronize scene object's interpolation"""
- if self.__primitive is not None:
- self.__primitive.interpolation = self.getInterpolation()
-
- def setInterpolation(self, mode):
- """Set image interpolation mode
-
- :param str mode: 'nearest' or 'linear'
- """
- mode = str(mode)
- assert mode in self.INTERPOLATION_MODES
- if mode != self.__interpolationMode:
- self.__interpolationMode = mode
- self._syncPrimitiveInterpolation()
- self._updated(Item3DChangedType.INTERPOLATION)
-
- def getInterpolation(self):
- """Returns the interpolation mode set by :meth:`setInterpolation`
-
- :rtype: str
- """
- return self.__interpolationMode
-
-
-class ColormapMixIn(_ColormapMixIn):
- """Mix-in class for Item3D object with a colormap
-
- :param sceneColormap:
- The plot3d scene colormap to sync with Colormap object.
- """
-
- def __init__(self, sceneColormap=None):
- super(ColormapMixIn, self).__init__()
-
- self.__sceneColormap = sceneColormap
- self._syncSceneColormap()
-
- def _colormapChanged(self):
- """Handle colormap updates"""
- self._syncSceneColormap()
- super(ColormapMixIn, self)._colormapChanged()
-
- def _setSceneColormap(self, sceneColormap):
- """Set the scene colormap to sync with Colormap object.
-
- :param sceneColormap:
- The plot3d scene colormap to sync with Colormap object.
- """
- self.__sceneColormap = sceneColormap
- self._syncSceneColormap()
-
- def _getSceneColormap(self):
- """Returns scene colormap that is sync"""
- return self.__sceneColormap
-
- def _syncSceneColormap(self):
- """Synchronizes scene's colormap with Colormap object"""
- if self.__sceneColormap is not None:
- colormap = self.getColormap()
-
- self.__sceneColormap.colormap = colormap.getNColors()
- self.__sceneColormap.norm = colormap.getNormalization()
- self.__sceneColormap.gamma = colormap.getGammaNormalizationParameter()
- self.__sceneColormap.range_ = colormap.getColormapRange(self)
- self.__sceneColormap.nancolor = rgba(colormap.getNaNColor())
-
-
-class ComplexMixIn(_ComplexMixIn):
- __doc__ = _ComplexMixIn.__doc__ # Reuse docstring
-
- _SUPPORTED_COMPLEX_MODES = (
- _ComplexMixIn.ComplexMode.REAL,
- _ComplexMixIn.ComplexMode.IMAGINARY,
- _ComplexMixIn.ComplexMode.ABSOLUTE,
- _ComplexMixIn.ComplexMode.PHASE,
- _ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
- """Overrides supported ComplexMode"""
-
-
-class SymbolMixIn(_SymbolMixIn):
- """Mix-in class for symbol and symbolSize properties for Item3D"""
-
- _SUPPORTED_SYMBOLS = collections.OrderedDict((
- ('o', 'Circle'),
- ('d', 'Diamond'),
- ('s', 'Square'),
- ('+', 'Plus'),
- ('x', 'Cross'),
- ('*', 'Star'),
- ('|', 'Vertical Line'),
- ('_', 'Horizontal Line'),
- ('.', 'Point'),
- (',', 'Pixel')))
-
- def _getSceneSymbol(self):
- """Returns a symbol name and size suitable for scene primitives.
-
- :return: (symbol, size)
- """
- symbol = self.getSymbol()
- size = self.getSymbolSize()
- if symbol == ',': # pixel
- return 's', 1.
- elif symbol == '.': # point
- # Size as in plot OpenGL backend, mimic matplotlib
- return 'o', numpy.ceil(0.5 * size) + 1.
- else:
- return symbol, size
-
-
-class PlaneMixIn(ItemMixInBase):
- """Mix-in class for plane items (based on PlaneInGroup primitive)"""
-
- def __init__(self, plane):
- assert isinstance(plane, primitives.PlaneInGroup)
- self.__plane = plane
- self.__plane.alpha = 1.
- self.__plane.addListener(self._planeChanged)
- self.__plane.plane.addListener(self._planePositionChanged)
-
- def _getPlane(self):
- """Returns plane primitive
-
- :rtype: primitives.PlaneInGroup
- """
- return self.__plane
-
- def _planeChanged(self, source, *args, **kwargs):
- """Handle events from the plane primitive"""
- # Sync visibility
- if source.visible != self.isVisible():
- self.setVisible(source.visible)
-
- def _planePositionChanged(self, source, *args, **kwargs):
- """Handle update of cut plane position and normal"""
- if self.__plane.visible: # TODO send even if hidden? or send also when showing if moved while hidden
- self._updated(ItemChangedType.POSITION)
-
- # Plane position
-
- def moveToCenter(self):
- """Move cut plane to center of data set"""
- self.__plane.moveToCenter()
-
- def isValid(self):
- """Returns whether the cut plane is defined or not (bool)"""
- return self.__plane.isValid
-
- def getNormal(self):
- """Returns the normal of the plane (as a unit vector)
-
- :return: Normal (nx, ny, nz), vector is 0 if no plane is defined
- :rtype: numpy.ndarray
- """
- return self.__plane.plane.normal
-
- def setNormal(self, normal):
- """Set the normal of the plane
-
- :param normal: 3-tuple of float: nx, ny, nz
- """
- self.__plane.plane.normal = normal
-
- def getPoint(self):
- """Returns a point on the plane
-
- :return: (x, y, z)
- :rtype: numpy.ndarray
- """
- return self.__plane.plane.point
-
- def setPoint(self, point):
- """Set a point contained in the plane.
-
- Warning: The plane might not intersect the bounding box of the data.
-
- :param point: (x, y, z) position
- :type point: 3-tuple of float
- """
- self.__plane.plane.point = point # TODO rework according to PR #1303
-
- def getParameters(self):
- """Returns the plane equation parameters: a*x + b*y + c*z + d = 0
-
- :return: Plane equation parameters: (a, b, c, d)
- :rtype: numpy.ndarray
- """
- return self.__plane.plane.parameters
-
- def setParameters(self, parameters):
- """Set the plane equation parameters: a*x + b*y + c*z + d = 0
-
- Warning: The plane might not intersect the bounding box of the data.
- The given parameters will be normalized.
-
- :param parameters: (a, b, c, d) equation parameters
- """
- self.__plane.plane.parameters = parameters
-
- # Border stroke
-
- def _setForegroundColor(self, color):
- """Set the color of the plane border.
-
- :param color: RGBA color as 4 floats in [0, 1]
- """
- self.__plane.color = rgba(color)
- if hasattr(super(PlaneMixIn, self), '_setForegroundColor'):
- super(PlaneMixIn, self)._setForegroundColor(color)
diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py
deleted file mode 100644
index 24abaa5..0000000
--- a/silx/gui/plot3d/items/scatter.py
+++ /dev/null
@@ -1,617 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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.
-#
-# ###########################################################################*/
-"""This module provides 2D and 3D scatter data item class.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/11/2017"
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-import logging
-import numpy
-
-from ....utils.deprecation import deprecated
-from ... import _glutils as glu
-from ...plot._utils.delaunay import delaunay
-from ..scene import function, primitives, utils
-
-from ...plot.items import ScatterVisualizationMixIn
-from .core import DataItem3D, Item3DChangedType, ItemChangedType
-from .mixins import ColormapMixIn, SymbolMixIn
-from ._pick import PickingResult
-
-
-_logger = logging.getLogger(__name__)
-
-
-class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
- """Description of a 3D scatter plot.
-
- :param parent: The View widget this item belongs to.
- """
-
- # TODO supports different size for each point
-
- def __init__(self, parent=None):
- DataItem3D.__init__(self, parent=parent)
- ColormapMixIn.__init__(self)
- SymbolMixIn.__init__(self)
-
- noData = numpy.zeros((0, 1), dtype=numpy.float32)
- symbol, size = self._getSceneSymbol()
- self._scatter = primitives.Points(
- x=noData, y=noData, z=noData, value=noData, size=size)
- self._scatter.marker = symbol
- self._getScenePrimitive().children.append(self._scatter)
-
- # Connect scene primitive to mix-in class
- ColormapMixIn._setSceneColormap(self, self._scatter.colormap)
-
- def _updated(self, event=None):
- """Handle mix-in class updates"""
- if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE):
- symbol, size = self._getSceneSymbol()
- self._scatter.marker = symbol
- self._scatter.setAttribute('size', size, copy=True)
-
- super(Scatter3D, self)._updated(event)
-
- def setData(self, x, y, z, value, copy=True):
- """Set the data of the scatter plot
-
- :param numpy.ndarray x: Array of X coordinates (single value not accepted)
- :param y: Points Y coordinate (array-like or single value)
- :param z: Points Z coordinate (array-like or single value)
- :param value: Points values (array-like or single value)
- :param bool copy:
- True (default) to copy the data,
- False to use provided data (do not modify!)
- """
- self._scatter.setAttribute('x', x, copy=copy)
- self._scatter.setAttribute('y', y, copy=copy)
- self._scatter.setAttribute('z', z, copy=copy)
- self._scatter.setAttribute('value', value, copy=copy)
-
- self._setColormappedData(self.getValueData(copy=False), copy=False)
- self._updated(ItemChangedType.DATA)
-
- def getData(self, copy=True):
- """Returns data as provided to :meth:`setData`.
-
- :param bool copy: True to get a copy,
- False to return internal data (do not modify!)
- :return: (x, y, z, value)
- """
- return (self.getXData(copy),
- self.getYData(copy),
- self.getZData(copy),
- self.getValueData(copy))
-
- def getXData(self, copy=True):
- """Returns X data coordinates.
-
- :param bool copy: True to get a copy,
- False to return internal array (do not modify!)
- :return: X coordinates
- :rtype: numpy.ndarray
- """
- return self._scatter.getAttribute('x', copy=copy).reshape(-1)
-
- def getYData(self, copy=True):
- """Returns Y data coordinates.
-
- :param bool copy: True to get a copy,
- False to return internal array (do not modify!)
- :return: Y coordinates
- :rtype: numpy.ndarray
- """
- return self._scatter.getAttribute('y', copy=copy).reshape(-1)
-
- def getZData(self, copy=True):
- """Returns Z data coordinates.
-
- :param bool copy: True to get a copy,
- False to return internal array (do not modify!)
- :return: Z coordinates
- :rtype: numpy.ndarray
- """
- return self._scatter.getAttribute('z', copy=copy).reshape(-1)
-
- def getValueData(self, copy=True):
- """Returns data values.
-
- :param bool copy: True to get a copy,
- False to return internal array (do not modify!)
- :return: data values
- :rtype: numpy.ndarray
- """
- return self._scatter.getAttribute('value', copy=copy).reshape(-1)
-
- @deprecated(reason="Consistency with PlotWidget items",
- replacement="getValueData", since_version="0.10.0")
- def getValues(self, copy=True):
- return self.getValueData(copy)
-
- def _pickFull(self, context, threshold=0., sort='depth'):
- """Perform picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :param float threshold: Picking threshold in pixel.
- Perform picking in a square of size threshold x threshold.
- :param str sort: How returned indices are sorted:
-
- - 'index' (default): sort by the value of the indices
- - 'depth': Sort by the depth of the points from the current
- camera point of view.
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- assert sort in ('index', 'depth')
-
- rayNdc = context.getPickingSegment(frame='ndc')
- if rayNdc is None: # No picking outside viewport
- return None
-
- # Project data to NDC
- xData = self.getXData(copy=False)
- if len(xData) == 0: # No data in the scatter
- return None
-
- primitive = self._getScenePrimitive()
-
- dataPoints = numpy.transpose((xData,
- self.getYData(copy=False),
- self.getZData(copy=False),
- numpy.ones_like(xData)))
-
- pointsNdc = primitive.objectToNDCTransform.transformPoints(
- dataPoints, perspectiveDivide=True)
-
- # Perform picking
- distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
- # TODO issue with symbol size: using pixel instead of points
- threshold += self.getSymbolSize()
- thresholdNdc = 2. * threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
- numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
-
- if sort == 'depth':
- # Sort picked points from front to back
- picked = picked[numpy.argsort(pointsNdc[picked, 2])]
-
- if picked.size > 0:
- return PickingResult(self,
- positions=dataPoints[picked, :3],
- indices=picked,
- fetchdata=self.getValueData)
- else:
- return None
-
-
-class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
- ScatterVisualizationMixIn):
- """2D scatter data with settable visualization mode.
-
- :param parent: The View widget this item belongs to.
- """
-
- _VISUALIZATION_PROPERTIES = {
- ScatterVisualizationMixIn.Visualization.POINTS:
- ('symbol', 'symbolSize'),
- ScatterVisualizationMixIn.Visualization.LINES:
- ('lineWidth',),
- ScatterVisualizationMixIn.Visualization.SOLID: (),
- }
- """Dict {visualization mode: property names used in this mode}"""
-
- _SUPPORTED_SCATTER_VISUALIZATION = tuple(_VISUALIZATION_PROPERTIES.keys())
- """Overrides supported Visualizations"""
-
- def __init__(self, parent=None):
- DataItem3D.__init__(self, parent=parent)
- ColormapMixIn.__init__(self)
- SymbolMixIn.__init__(self)
- ScatterVisualizationMixIn.__init__(self)
-
- self._heightMap = False
- self._lineWidth = 1.
-
- self._x = numpy.zeros((0,), dtype=numpy.float32)
- self._y = numpy.zeros((0,), dtype=numpy.float32)
- self._value = numpy.zeros((0,), dtype=numpy.float32)
-
- self._cachedLinesIndices = None
- self._cachedTrianglesIndices = None
-
- # Connect scene primitive to mix-in class
- ColormapMixIn._setSceneColormap(self, function.Colormap())
-
- def _updated(self, event=None):
- """Handle mix-in class updates"""
- if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE):
- symbol, size = self._getSceneSymbol()
- for child in self._getScenePrimitive().children:
- if isinstance(child, primitives.Points):
- child.marker = symbol
- child.setAttribute('size', size, copy=True)
-
- elif event is ItemChangedType.VISIBLE:
- # TODO smart update?, need dirty flags
- self._updateScene()
-
- elif event is ItemChangedType.VISUALIZATION_MODE:
- self._updateScene()
-
- super(Scatter2D, self)._updated(event)
-
- def isPropertyEnabled(self, name, visualization=None):
- """Returns true if the property is used with visualization mode.
-
- :param str name: The name of the property to check, in:
- 'lineWidth', 'symbol', 'symbolSize'
- :param str visualization:
- The visualization mode for which to get the info.
- By default, it is the current visualization mode.
- :return:
- """
- assert name in ('lineWidth', 'symbol', 'symbolSize')
- if visualization is None:
- visualization = self.getVisualization()
- assert visualization in self.supportedVisualizations()
- return name in self._VISUALIZATION_PROPERTIES[visualization]
-
- def setHeightMap(self, heightMap):
- """Set whether to display the data has a height map or not.
-
- When displayed as a height map, the data values are used as
- z coordinates.
-
- :param bool heightMap:
- True to display a height map,
- False to display as 2D data with z=0
- """
- heightMap = bool(heightMap)
- if heightMap != self.isHeightMap():
- self._heightMap = heightMap
- self._updateScene()
- self._updated(Item3DChangedType.HEIGHT_MAP)
-
- def isHeightMap(self):
- """Returns True if data is displayed as a height map.
-
- :rtype: bool
- """
- return self._heightMap
-
- def getLineWidth(self):
- """Return the curve line width in pixels (float)"""
- return self._lineWidth
-
- def setLineWidth(self, width):
- """Set the width in pixel of the curve line
-
- See :meth:`getLineWidth`.
-
- :param float width: Width in pixels
- """
- width = float(width)
- assert width >= 1.
- if width != self._lineWidth:
- self._lineWidth = width
- for child in self._getScenePrimitive().children:
- if hasattr(child, 'lineWidth'):
- child.lineWidth = width
- self._updated(ItemChangedType.LINE_WIDTH)
-
- def setData(self, x, y, value, copy=True):
- """Set the data represented by this item.
-
- Provided arrays must have the same length.
-
- :param numpy.ndarray x: X coordinates (array-like)
- :param numpy.ndarray y: Y coordinates (array-like)
- :param value: Points value: array-like or single scalar
- :param bool copy:
- True (default) to make a copy of the data,
- False to avoid copy if possible (do not modify the arrays).
- """
- x = numpy.array(
- x, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
- y = numpy.array(
- y, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
- assert len(x) == len(y)
-
- if isinstance(value, abc.Iterable):
- value = numpy.array(
- value, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
- assert len(value) == len(x)
- else: # Single scalar
- value = numpy.array((float(value),), dtype=numpy.float32)
-
- self._x = x
- self._y = y
- self._value = value
-
- # Reset cache
- self._cachedLinesIndices = None
- self._cachedTrianglesIndices = None
-
- self._setColormappedData(self.getValueData(copy=False), copy=False)
-
- self._updateScene()
-
- self._updated(ItemChangedType.DATA)
-
- def getData(self, copy=True):
- """Returns data as provided to :meth:`setData`.
-
- :param bool copy: True to get a copy,
- False to return internal data (do not modify!)
- :return: (x, y, value)
- """
- return (self.getXData(copy=copy),
- self.getYData(copy=copy),
- self.getValueData(copy=copy))
-
- def getXData(self, copy=True):
- """Returns X data coordinates.
-
- :param bool copy: True to get a copy,
- False to return internal array (do not modify!)
- :return: X coordinates
- :rtype: numpy.ndarray
- """
- return numpy.array(self._x, copy=copy)
-
- def getYData(self, copy=True):
- """Returns Y data coordinates.
-
- :param bool copy: True to get a copy,
- False to return internal array (do not modify!)
- :return: Y coordinates
- :rtype: numpy.ndarray
- """
- return numpy.array(self._y, copy=copy)
-
- def getValueData(self, copy=True):
- """Returns data values.
-
- :param bool copy: True to get a copy,
- False to return internal array (do not modify!)
- :return: data values
- :rtype: numpy.ndarray
- """
- return numpy.array(self._value, copy=copy)
-
- @deprecated(reason="Consistency with PlotWidget items",
- replacement="getValueData", since_version="0.10.0")
- def getValues(self, copy=True):
- return self.getValueData(copy)
-
- def _pickPoints(self, context, points, threshold=1., sort='depth'):
- """Perform picking while in 'points' visualization mode
-
- :param PickContext context: Current picking context
- :param float threshold: Picking threshold in pixel.
- Perform picking in a square of size threshold x threshold.
- :param str sort: How returned indices are sorted:
-
- - 'index' (default): sort by the value of the indices
- - 'depth': Sort by the depth of the points from the current
- camera point of view.
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- assert sort in ('index', 'depth')
-
- rayNdc = context.getPickingSegment(frame='ndc')
- if rayNdc is None: # No picking outside viewport
- return None
-
- # Project data to NDC
- primitive = self._getScenePrimitive()
- pointsNdc = primitive.objectToNDCTransform.transformPoints(
- points, perspectiveDivide=True)
-
- # Perform picking
- distancesNdc = numpy.abs(pointsNdc[:, :2] - rayNdc[0, :2])
- thresholdNdc = threshold / numpy.array(primitive.viewport.size)
- picked = numpy.where(numpy.logical_and(
- numpy.all(distancesNdc < thresholdNdc, axis=1),
- numpy.logical_and(rayNdc[0, 2] <= pointsNdc[:, 2],
- pointsNdc[:, 2] <= rayNdc[1, 2])))[0]
-
- if sort == 'depth':
- # Sort picked points from front to back
- picked = picked[numpy.argsort(pointsNdc[picked, 2])]
-
- if picked.size > 0:
- return PickingResult(self,
- positions=points[picked, :3],
- indices=picked,
- fetchdata=self.getValueData)
- else:
- return None
-
- def _pickSolid(self, context, points):
- """Perform picking while in 'solid' visualization mode
-
- :param PickContext context: Current picking context
- """
- if self._cachedTrianglesIndices is None:
- _logger.info("Picking on Scatter2D before rendering")
- return None
-
- rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
- if rayObject is None: # No picking outside viewport
- return None
- rayObject = rayObject[:, :3]
-
- trianglesIndices = self._cachedTrianglesIndices.reshape(-1, 3)
- triangles = points[trianglesIndices, :3]
- selectedIndices, t, barycentric = glu.segmentTrianglesIntersection(
- rayObject, triangles)
- closest = numpy.argmax(barycentric, axis=1)
-
- indices = trianglesIndices.reshape(-1, 3)[selectedIndices, closest]
-
- if len(indices) == 0: # No point is picked
- return None
-
- # Compute intersection points and get closest data point
- positions = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
-
- return PickingResult(self,
- positions=positions,
- indices=indices,
- fetchdata=self.getValueData)
-
- def _pickFull(self, context):
- """Perform picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- xData = self.getXData(copy=False)
- if len(xData) == 0: # No data in the scatter
- return None
-
- if self.isHeightMap():
- zData = self.getValueData(copy=False)
- else:
- zData = numpy.zeros_like(xData)
-
- points = numpy.transpose((xData,
- self.getYData(copy=False),
- zData,
- numpy.ones_like(xData)))
-
- mode = self.getVisualization()
- if mode is self.Visualization.POINTS:
- # TODO issue with symbol size: using pixel instead of points
- # Get "corrected" symbol size
- _, threshold = self._getSceneSymbol()
- return self._pickPoints(
- context, points, threshold=max(3., threshold))
-
- elif mode is self.Visualization.LINES:
- # Picking only at point
- return self._pickPoints(context, points, threshold=5.)
-
- else: # mode == 'solid'
- return self._pickSolid(context, points)
-
- def _updateScene(self):
- self._getScenePrimitive().children = [] # Remove previous primitives
-
- if not self.isVisible():
- return # Update when visible
-
- x, y, value = self.getData(copy=False)
- if len(x) == 0:
- return # Nothing to display
-
- mode = self.getVisualization()
- heightMap = self.isHeightMap()
-
- if mode is self.Visualization.POINTS:
- z = value if heightMap else 0.
- symbol, size = self._getSceneSymbol()
- primitive = primitives.Points(
- x=x, y=y, z=z, value=value,
- size=size,
- colormap=self._getSceneColormap())
- primitive.marker = symbol
-
- else:
- # TODO run delaunay in a thread
- # Compute lines/triangles indices if not cached
- if self._cachedTrianglesIndices is None:
- triangulation = delaunay(x, y)
- if triangulation is None:
- return None
- self._cachedTrianglesIndices = numpy.ravel(
- triangulation.simplices.astype(numpy.uint32))
-
- if (mode is self.Visualization.LINES and
- self._cachedLinesIndices is None):
- # Compute line indices
- self._cachedLinesIndices = utils.triangleToLineIndices(
- self._cachedTrianglesIndices, unicity=True)
-
- if mode is self.Visualization.LINES:
- indices = self._cachedLinesIndices
- renderMode = 'lines'
- else:
- indices = self._cachedTrianglesIndices
- renderMode = 'triangles'
-
- # TODO supports x, y instead of copy
- if heightMap:
- if len(value) == 1:
- value = numpy.ones_like(x) * value
- coordinates = numpy.array((x, y, value), dtype=numpy.float32).T
- else:
- coordinates = numpy.array((x, y), dtype=numpy.float32).T
-
- # TODO option to enable/disable light, cache normals
- # TODO smooth surface
- if mode is self.Visualization.SOLID:
- if heightMap:
- coordinates = coordinates[indices]
- if len(value) > 1:
- value = value[indices]
- triangleNormals = utils.trianglesNormal(coordinates)
- normal = numpy.empty((len(triangleNormals) * 3, 3),
- dtype=numpy.float32)
- normal[0::3, :] = triangleNormals
- normal[1::3, :] = triangleNormals
- normal[2::3, :] = triangleNormals
- indices = None
- else:
- normal = (0., 0., 1.)
- else:
- normal = None
-
- primitive = primitives.ColormapMesh3D(
- coordinates,
- value.reshape(-1, 1), # Makes it a 2D array
- normal=normal,
- colormap=self._getSceneColormap(),
- indices=indices,
- mode=renderMode)
- primitive.lineWidth = self.getLineWidth()
- primitive.lineSmooth = False
-
- self._getScenePrimitive().children = [primitive]
diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py
deleted file mode 100644
index f80fea2..0000000
--- a/silx/gui/plot3d/items/volume.py
+++ /dev/null
@@ -1,886 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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.
-#
-# ###########################################################################*/
-"""This module provides 3D array item class and its sub-items.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-import logging
-import time
-import numpy
-
-from silx.math.combo import min_max
-from silx.math.marchingcubes import MarchingCubes
-from silx.math.interpolate import interp3d
-
-from ....utils.proxy import docstring
-from ... import _glutils as glu
-from ... import qt
-from ...colors import rgba
-
-from ..scene import cutplane, function, primitives, transform, utils
-
-from .core import BaseNodeItem, Item3D, ItemChangedType, Item3DChangedType
-from .mixins import ColormapMixIn, ComplexMixIn, InterpolationMixIn, PlaneMixIn
-from ._pick import PickingResult
-
-
-_logger = logging.getLogger(__name__)
-
-
-class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
- """Class representing a cutting plane in a :class:`ScalarField3D` item.
-
- :param parent: 3D Data set in which the cut plane is applied.
- """
-
- def __init__(self, parent):
- plane = cutplane.CutPlane(normal=(0, 1, 0))
-
- Item3D.__init__(self, parent=None)
- ColormapMixIn.__init__(self)
- InterpolationMixIn.__init__(self)
- PlaneMixIn.__init__(self, plane=plane)
-
- self._dataRange = None
- self._data = None
-
- self._getScenePrimitive().children = [plane]
-
- # Connect scene primitive to mix-in class
- ColormapMixIn._setSceneColormap(self, plane.colormap)
- InterpolationMixIn._setPrimitive(self, plane)
-
- self.setParent(parent)
-
- def _updateData(self, data, range_):
- """Update used dataset
-
- No copy is made.
-
- :param Union[numpy.ndarray[float],None] data: The dataset
- :param Union[List[float],None] range_:
- (min, min positive, max) values
- """
- self._data = None if data is None else numpy.array(data, copy=False)
- self._getPlane().setData(self._data, copy=False)
-
- # Store data range info as 3-tuple of values
- self._dataRange = range_
- if range_ is None:
- range_ = None, None, None
- self._setColormappedData(self._data, copy=False,
- min_=range_[0],
- minPositive=range_[1],
- max_=range_[2])
-
- self._updated(ItemChangedType.DATA)
-
- def _syncDataWithParent(self):
- """Synchronize this instance data with that of its parent"""
- parent = self.parent()
- if parent is None:
- data, range_ = None, None
- else:
- data = parent.getData(copy=False)
- range_ = parent.getDataRange()
- self._updateData(data, range_)
-
- def _parentChanged(self, event):
- """Handle data change in the parent this plane belongs to"""
- if event == ItemChangedType.DATA:
- self._syncDataWithParent()
-
- def setParent(self, parent):
- oldParent = self.parent()
- if isinstance(oldParent, Item3D):
- oldParent.sigItemChanged.disconnect(self._parentChanged)
-
- super(CutPlane, self).setParent(parent)
-
- if isinstance(parent, Item3D):
- parent.sigItemChanged.connect(self._parentChanged)
-
- self._syncDataWithParent()
-
- # Colormap
-
- def getDisplayValuesBelowMin(self):
- """Return whether values <= colormap min are displayed or not.
-
- :rtype: bool
- """
- return self._getPlane().colormap.displayValuesBelowMin
-
- def setDisplayValuesBelowMin(self, display):
- """Set whether to display values <= colormap min.
-
- :param bool display: True to show values below min,
- False to discard them
- """
- display = bool(display)
- if display != self.getDisplayValuesBelowMin():
- self._getPlane().colormap.displayValuesBelowMin = display
- self._updated(ItemChangedType.ALPHA)
-
- def getDataRange(self):
- """Return the range of the data as a 3-tuple of values.
-
- positive min is NaN if no data is positive.
-
- :return: (min, positive min, max) or None.
- :rtype: Union[List[float],None]
- """
- return None if self._dataRange is None else tuple(self._dataRange)
-
- def getData(self, copy=True):
- """Return 3D dataset.
-
- :param bool copy:
- True (default) to get a copy,
- False to get the internal data (DO NOT modify!)
- :return: The data set (or None if not set)
- """
- if self._data is None:
- return None
- else:
- return numpy.array(self._data, copy=copy)
-
- def _pickFull(self, context):
- """Perform picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
- if rayObject is None:
- return None
-
- points = utils.segmentPlaneIntersect(
- rayObject[0, :3],
- rayObject[1, :3],
- planeNorm=self.getNormal(),
- planePt=self.getPoint())
-
- if len(points) == 1: # Single intersection
- if numpy.any(points[0] < 0.):
- return None # Outside volume
- z, y, x = int(points[0][2]), int(points[0][1]), int(points[0][0])
-
- data = self.getData(copy=False)
- if data is None:
- return None # No dataset
-
- depth, height, width = data.shape
- if z < depth and y < height and x < width:
- return PickingResult(self,
- positions=[points[0]],
- indices=([z], [y], [x]))
- else:
- return None # Outside image
- else: # Either no intersection or segment and image are coplanar
- return None
-
-
-class Isosurface(Item3D):
- """Class representing an iso-surface in a :class:`ScalarField3D` item.
-
- :param parent: The DataItem3D this iso-surface belongs to
- """
-
- def __init__(self, parent):
- Item3D.__init__(self, parent=None)
- self._data = None
- self._level = float('nan')
- self._autoLevelFunction = None
- self._color = rgba('#FFD700FF')
- self.setParent(parent)
-
- def _syncDataWithParent(self):
- """Synchronize this instance data with that of its parent"""
- parent = self.parent()
- if parent is None:
- self._data = None
- else:
- self._data = parent.getData(copy=False)
- self._updateScenePrimitive()
-
- def _parentChanged(self, event):
- """Handle data change in the parent this isosurface belongs to"""
- if event == ItemChangedType.DATA:
- self._syncDataWithParent()
-
- def setParent(self, parent):
- oldParent = self.parent()
- if isinstance(oldParent, Item3D):
- oldParent.sigItemChanged.disconnect(self._parentChanged)
-
- super(Isosurface, self).setParent(parent)
-
- if isinstance(parent, Item3D):
- parent.sigItemChanged.connect(self._parentChanged)
-
- self._syncDataWithParent()
-
- def getData(self, copy=True):
- """Return 3D dataset.
-
- :param bool copy:
- True (default) to get a copy,
- False to get the internal data (DO NOT modify!)
- :return: The data set (or None if not set)
- """
- if self._data is None:
- return None
- else:
- return numpy.array(self._data, copy=copy)
-
- def getLevel(self):
- """Return the level of this iso-surface (float)"""
- return self._level
-
- def setLevel(self, level):
- """Set the value at which to build the iso-surface.
-
- Setting this value reset auto-level function
-
- :param float level: The value at which to build the iso-surface
- """
- self._autoLevelFunction = None
- level = float(level)
- if level != self._level:
- self._level = level
- self._updateScenePrimitive()
- self._updated(Item3DChangedType.ISO_LEVEL)
-
- def isAutoLevel(self):
- """True if iso-level is rebuild for each data set."""
- return self.getAutoLevelFunction() is not None
-
- def getAutoLevelFunction(self):
- """Return the function computing the iso-level (callable or None)"""
- return self._autoLevelFunction
-
- def setAutoLevelFunction(self, autoLevel):
- """Set the function used to compute the iso-level.
-
- WARNING: The function might get called in a thread.
-
- :param callable autoLevel:
- A function taking a 3D numpy.ndarray of float32 and returning
- a float used as iso-level.
- Example: numpy.mean(data) + numpy.std(data)
- """
- assert callable(autoLevel)
- self._autoLevelFunction = autoLevel
- self._updateScenePrimitive()
-
- def getColor(self):
- """Return the color of this iso-surface (QColor)"""
- return qt.QColor.fromRgbF(*self._color)
-
- def _updateColor(self, color):
- """Handle update of color
-
- :param List[float] color: RGBA channels in [0, 1]
- """
- primitive = self._getScenePrimitive()
- if len(primitive.children) != 0:
- primitive.children[0].setAttribute('color', color)
-
- def setColor(self, color):
- """Set the color of the iso-surface
-
- :param color: RGBA color of the isosurface
- :type color: QColor, str or array-like of 4 float in [0., 1.]
- """
- color = rgba(color)
- if color != self._color:
- self._color = color
- self._updateColor(self._color)
- self._updated(ItemChangedType.COLOR)
-
- def _computeIsosurface(self):
- """Compute isosurface for current state.
-
- :return: (vertices, normals, indices) arrays
- :rtype: List[Union[None,numpy.ndarray]]
- """
- data = self.getData(copy=False)
-
- if data is None:
- if self.isAutoLevel():
- self._level = float('nan')
-
- else:
- if self.isAutoLevel():
- st = time.time()
- try:
- level = float(self.getAutoLevelFunction()(data))
-
- except Exception:
- module_ = self.getAutoLevelFunction().__module__
- name = self.getAutoLevelFunction().__name__
- _logger.error(
- "Error while executing iso level function %s.%s",
- module_,
- name,
- exc_info=True)
- level = float('nan')
-
- else:
- _logger.info(
- 'Computed iso-level in %f s.', time.time() - st)
-
- if level != self._level:
- self._level = level
- self._updated(Item3DChangedType.ISO_LEVEL)
-
- if numpy.isfinite(self._level):
- st = time.time()
- vertices, normals, indices = MarchingCubes(
- data,
- isolevel=self._level)
- _logger.info('Computed iso-surface in %f s.', time.time() - st)
-
- if len(vertices) != 0:
- return vertices, normals, indices
-
- return None, None, None
-
- def _updateScenePrimitive(self):
- """Update underlying mesh"""
- self._getScenePrimitive().children = []
-
- vertices, normals, indices = self._computeIsosurface()
- if vertices is not None:
- mesh = primitives.Mesh3D(vertices,
- colors=self._color,
- normals=normals,
- mode='triangles',
- indices=indices,
- copy=False)
- self._getScenePrimitive().children = [mesh]
-
- def _pickFull(self, context):
- """Perform picking in this item at given widget position.
-
- :param PickContext context: Current picking context
- :return: Object holding the results or None
- :rtype: Union[None,PickingResult]
- """
- rayObject = context.getPickingSegment(frame=self._getScenePrimitive())
- if rayObject is None:
- return None
- rayObject = rayObject[:, :3]
-
- data = self.getData(copy=False)
- bins = utils.segmentVolumeIntersect(
- rayObject, numpy.array(data.shape) - 1)
- if bins is None:
- return None
-
- # gather bin data
- offsets = [(i, j, k) for i in (0, 1) for j in (0, 1) for k in (0, 1)]
- indices = bins[:, numpy.newaxis, :] + offsets
- binsData = data[indices[:, :, 0], indices[:, :, 1], indices[:, :, 2]]
- # binsData.shape = nbins, 8
- # TODO up-to this point everything can be done once for all isosurfaces
-
- # check bin candidates
- level = self.getLevel()
- mask = numpy.logical_and(numpy.nanmin(binsData, axis=1) <= level,
- level <= numpy.nanmax(binsData, axis=1))
- bins = bins[mask]
- binsData = binsData[mask]
-
- if len(bins) == 0:
- return None # No bin candidate
-
- # do picking on candidates
- intersections = []
- depths = []
- for currentBin, data in zip(bins, binsData):
- mc = MarchingCubes(data.reshape(2, 2, 2), isolevel=level)
- points = mc.get_vertices() + currentBin
- triangles = points[mc.get_indices()]
- t = glu.segmentTrianglesIntersection(rayObject, triangles)[1]
- t = numpy.unique(t) # Duplicates happen on triangle edges
- if len(t) != 0:
- # Compute intersection points and get closest data point
- points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
- # Get closest data points by rounding to int
- intersections.extend(points)
- depths.extend(t)
-
- if len(intersections) == 0:
- return None # No intersected triangles
-
- intersections = numpy.array(intersections)[numpy.argsort(depths)]
- indices = numpy.transpose(numpy.round(intersections).astype(numpy.int64))
- return PickingResult(self, positions=intersections, indices=indices)
-
-
-class ScalarField3D(BaseNodeItem):
- """3D scalar field on a regular grid.
-
- :param parent: The View widget this item belongs to.
- """
-
- _CutPlane = CutPlane
- """CutPlane class associated to this class"""
-
- _Isosurface = Isosurface
- """Isosurface classe associated to this class"""
-
- def __init__(self, parent=None):
- BaseNodeItem.__init__(self, parent=parent)
-
- # Gives this item the shape of the data, no matter
- # of the isosurface/cut plane size
- self._boundedGroup = primitives.BoundedGroup()
-
- # Store iso-surfaces
- self._isosurfaces = []
-
- self._data = None
- self._dataRange = None
-
- self._cutPlane = self._CutPlane(parent=self)
- self._cutPlane.setVisible(False)
-
- self._isogroup = primitives.GroupDepthOffset()
- self._isogroup.transforms = [
- # Convert from z, y, x from marching cubes to x, y, z
- transform.Matrix((
- (0., 0., 1., 0.),
- (0., 1., 0., 0.),
- (1., 0., 0., 0.),
- (0., 0., 0., 1.))),
- # Offset to match cutting plane coords
- transform.Translate(0.5, 0.5, 0.5)
- ]
-
- self._getScenePrimitive().children = [
- self._boundedGroup,
- self._cutPlane._getScenePrimitive(),
- self._isogroup]
-
- @staticmethod
- def _computeRangeFromData(data):
- """Compute range info (min, min positive, max) from data
-
- :param Union[numpy.ndarray,None] data:
- :return: Union[List[float],None]
- """
- if data is None:
- return None
-
- dataRange = min_max(data, min_positive=True, finite=True)
- if dataRange.minimum is None: # Only non-finite data
- return None
-
- if dataRange is not None:
- min_positive = dataRange.min_positive
- if min_positive is None:
- min_positive = float('nan')
- return dataRange.minimum, min_positive, dataRange.maximum
-
- def setData(self, data, copy=True):
- """Set the 3D scalar data represented by this item.
-
- Dataset order is zyx (i.e., first dimension is z).
-
- :param data: 3D array
- :type data: 3D numpy.ndarray of float32 with shape at least (2, 2, 2)
- :param bool copy:
- True (default) to make a copy,
- False to avoid copy (DO NOT MODIFY data afterwards)
- """
- if data is None:
- self._data = None
- self._boundedGroup.shape = None
-
- else:
- data = numpy.array(data, copy=copy, dtype=numpy.float32, order='C')
- assert data.ndim == 3
- assert min(data.shape) >= 2
-
- self._data = data
- self._boundedGroup.shape = self._data.shape
-
- self._dataRange = self._computeRangeFromData(self._data)
- self._updated(ItemChangedType.DATA)
-
- def getData(self, copy=True):
- """Return 3D dataset.
-
- :param bool copy:
- True (default) to get a copy,
- False to get the internal data (DO NOT modify!)
- :return: The data set (or None if not set)
- """
- if self._data is None:
- return None
- else:
- return numpy.array(self._data, copy=copy)
-
- def getDataRange(self):
- """Return the range of the data as a 3-tuple of values.
-
- positive min is NaN if no data is positive.
-
- :return: (min, positive min, max) or None.
- """
- return self._dataRange
-
- # Cut Plane
-
- def getCutPlanes(self):
- """Return an iterable of all :class:`CutPlane` of this item.
-
- This includes hidden cut planes.
-
- For now, there is always one cut plane.
- """
- return (self._cutPlane,)
-
- # Handle iso-surfaces
-
- # TODO rename to sigItemAdded|Removed?
- sigIsosurfaceAdded = qt.Signal(object)
- """Signal emitted when a new iso-surface is added to the view.
-
- The newly added iso-surface is provided by this signal
- """
-
- sigIsosurfaceRemoved = qt.Signal(object)
- """Signal emitted when an iso-surface is removed from the view
-
- The removed iso-surface is provided by this signal.
- """
-
- def addIsosurface(self, level, color):
- """Add an isosurface to this item.
-
- :param level:
- The value at which to build the iso-surface or a callable
- (e.g., a function) taking a 3D numpy.ndarray as input and
- returning a float.
- Example: numpy.mean(data) + numpy.std(data)
- :type level: float or callable
- :param color: RGBA color of the isosurface
- :type color: str or array-like of 4 float in [0., 1.]
- :return: isosurface object
- :rtype: ~silx.gui.plot3d.items.volume.Isosurface
- """
- isosurface = self._Isosurface(parent=self)
- isosurface.setColor(color)
- if callable(level):
- isosurface.setAutoLevelFunction(level)
- else:
- isosurface.setLevel(level)
- isosurface.sigItemChanged.connect(self._isosurfaceItemChanged)
-
- self._isosurfaces.append(isosurface)
-
- self._updateIsosurfaces()
-
- self.sigIsosurfaceAdded.emit(isosurface)
- return isosurface
-
- def getIsosurfaces(self):
- """Return an iterable of all :class:`.Isosurface` instance of this item"""
- return tuple(self._isosurfaces)
-
- def removeIsosurface(self, isosurface):
- """Remove an iso-surface from this item.
-
- :param ~silx.gui.plot3d.Plot3DWidget.Isosurface isosurface:
- The isosurface object to remove
- """
- if isosurface not in self.getIsosurfaces():
- _logger.warning(
- "Try to remove isosurface that is not in the list: %s",
- str(isosurface))
- else:
- isosurface.sigItemChanged.disconnect(self._isosurfaceItemChanged)
- self._isosurfaces.remove(isosurface)
- self._updateIsosurfaces()
- self.sigIsosurfaceRemoved.emit(isosurface)
-
- def clearIsosurfaces(self):
- """Remove all :class:`.Isosurface` instances from this item."""
- for isosurface in self.getIsosurfaces():
- self.removeIsosurface(isosurface)
-
- def _isosurfaceItemChanged(self, event):
- """Handle update of isosurfaces upon level changed"""
- if event == Item3DChangedType.ISO_LEVEL:
- self._updateIsosurfaces()
-
- def _updateIsosurfaces(self):
- """Handle updates of iso-surfaces level and add/remove"""
- # Sorting using minus, this supposes data 'object' to be max values
- sortedIso = sorted(self.getIsosurfaces(),
- key=lambda isosurface: - isosurface.getLevel())
- self._isogroup.children = [iso._getScenePrimitive() for iso in sortedIso]
-
- # BaseNodeItem
-
- def getItems(self):
- """Returns the list of items currently present in this item.
-
- :rtype: tuple
- """
- return self.getCutPlanes() + self.getIsosurfaces()
-
-
-##################
-# ComplexField3D #
-##################
-
-class ComplexCutPlane(CutPlane, ComplexMixIn):
- """Class representing a cutting plane in a :class:`ComplexField3D` item.
-
- :param parent: 3D Data set in which the cut plane is applied.
- """
-
- def __init__(self, parent):
- ComplexMixIn.__init__(self)
- CutPlane.__init__(self, parent=parent)
-
- def _syncDataWithParent(self):
- """Synchronize this instance data with that of its parent"""
- parent = self.parent()
- if parent is None:
- data, range_ = None, None
- else:
- mode = self.getComplexMode()
- data = parent.getData(mode=mode, copy=False)
- range_ = parent.getDataRange(mode=mode)
- self._updateData(data, range_)
-
- def _updated(self, event=None):
- """Handle update of the cut plane (and take care of mode change
-
- :param Union[None,ItemChangedType] event: The kind of update
- """
- if event == ItemChangedType.COMPLEX_MODE:
- self._syncDataWithParent()
- super(ComplexCutPlane, self)._updated(event)
-
-
-class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
- """Class representing an iso-surface in a :class:`ComplexField3D` item.
-
- :param parent: The DataItem3D this iso-surface belongs to
- """
-
- _SUPPORTED_COMPLEX_MODES = \
- (ComplexMixIn.ComplexMode.NONE,) + ComplexMixIn._SUPPORTED_COMPLEX_MODES
- """Overrides supported ComplexMode"""
-
- def __init__(self, parent):
- ComplexMixIn.__init__(self)
- ColormapMixIn.__init__(self, function.Colormap())
- Isosurface.__init__(self, parent=parent)
- self.setComplexMode(self.ComplexMode.NONE)
-
- def _updateColor(self, color):
- """Handle update of color
-
- :param List[float] color: RGBA channels in [0, 1]
- """
- primitive = self._getScenePrimitive()
- if (len(primitive.children) != 0 and
- isinstance(primitive.children[0], primitives.ColormapMesh3D)):
- primitive.children[0].alpha = self._color[3]
- else:
- super(ComplexIsosurface, self)._updateColor(color)
-
- def _syncDataWithParent(self):
- """Synchronize this instance data with that of its parent"""
- parent = self.parent()
- if parent is None:
- self._data = None
- else:
- self._data = parent.getData(
- mode=parent.getComplexMode(), copy=False)
-
- if parent is None or self.getComplexMode() == self.ComplexMode.NONE:
- self._setColormappedData(None, copy=False)
- else:
- self._setColormappedData(
- parent.getData(mode=self.getComplexMode(), copy=False),
- copy=False)
-
- self._updateScenePrimitive()
-
- def _parentChanged(self, event):
- """Handle data change in the parent this isosurface belongs to"""
- if event == ItemChangedType.COMPLEX_MODE:
- self._syncDataWithParent()
- super(ComplexIsosurface, self)._parentChanged(event)
-
- def _updated(self, event=None):
- """Handle update of the isosurface (and take care of mode change)
-
- :param ItemChangedType event: The kind of update
- """
- if event == ItemChangedType.COMPLEX_MODE:
- self._syncDataWithParent()
-
- elif event in (ItemChangedType.COLORMAP,
- Item3DChangedType.INTERPOLATION):
- self._updateScenePrimitive()
- super(ComplexIsosurface, self)._updated(event)
-
- def _updateScenePrimitive(self):
- """Update underlying mesh"""
- if self.getComplexMode() == self.ComplexMode.NONE:
- super(ComplexIsosurface, self)._updateScenePrimitive()
-
- else: # Specific display for colormapped isosurface
- self._getScenePrimitive().children = []
-
- values = self.getColormappedData(copy=False)
- if values is not None:
- vertices, normals, indices = self._computeIsosurface()
- if vertices is not None:
- values = interp3d(values, vertices, method='linear_omp')
- # TODO reuse isosurface when only color changes...
-
- mesh = primitives.ColormapMesh3D(
- vertices,
- value=values.reshape(-1, 1),
- colormap=self._getSceneColormap(),
- normal=normals,
- mode='triangles',
- indices=indices,
- copy=False)
- mesh.alpha = self._color[3]
- self._getScenePrimitive().children = [mesh]
-
-
-class ComplexField3D(ScalarField3D, ComplexMixIn):
- """3D complex field on a regular grid.
-
- :param parent: The View widget this item belongs to.
- """
-
- _CutPlane = ComplexCutPlane
- _Isosurface = ComplexIsosurface
-
- def __init__(self, parent=None):
- self._dataRangeCache = None
-
- ComplexMixIn.__init__(self)
- ScalarField3D.__init__(self, parent=parent)
-
- @docstring(ComplexMixIn)
- def setComplexMode(self, mode):
- mode = ComplexMixIn.ComplexMode.from_value(mode)
- if mode != self.getComplexMode():
- self.clearIsosurfaces() # Reset isosurfaces
- ComplexMixIn.setComplexMode(self, mode)
-
- def setData(self, data, copy=True):
- """Set the 3D complex data represented by this item.
-
- Dataset order is zyx (i.e., first dimension is z).
-
- :param data: 3D array
- :type data: 3D numpy.ndarray of float32 with shape at least (2, 2, 2)
- :param bool copy:
- True (default) to make a copy,
- False to avoid copy (DO NOT MODIFY data afterwards)
- """
- if data is None:
- self._data = None
- self._dataRangeCache = None
- self._boundedGroup.shape = None
-
- else:
- data = numpy.array(data, copy=copy, dtype=numpy.complex64, order='C')
- assert data.ndim == 3
- assert min(data.shape) >= 2
-
- self._data = data
- self._dataRangeCache = {}
- self._boundedGroup.shape = self._data.shape
-
- self._updated(ItemChangedType.DATA)
-
- def getData(self, copy=True, mode=None):
- """Return 3D dataset.
-
- This method does not cache data converted to a specific mode,
- it computes it for each request.
-
- :param bool copy:
- True (default) to get a copy,
- False to get the internal data (DO NOT modify!)
- :param Union[None,Mode] mode:
- The kind of data to retrieve.
- If None (the default), it returns the complex data,
- else it computes the requested scalar data.
- :return: The data set (or None if not set)
- :rtype: Union[numpy.ndarray,None]
- """
- if mode is None:
- return super(ComplexField3D, self).getData(copy=copy)
- else:
- return self._convertComplexData(self._data, mode)
-
- def getDataRange(self, mode=None):
- """Return the range of the requested data as a 3-tuple of values.
-
- Positive min is NaN if no data is positive.
-
- :param Union[None,Mode] mode:
- The kind of data for which to get the range information.
- If None (the default), it returns the data range for the current mode,
- else it returns the data range for the requested mode.
- :return: (min, positive min, max) or None.
- :rtype: Union[None,List[float]]
- """
- if self._dataRangeCache is None:
- return None
-
- if mode is None:
- mode = self.getComplexMode()
-
- if mode not in self._dataRangeCache:
- # Compute it and store it in cache
- data = self.getData(copy=False, mode=mode)
- self._dataRangeCache[mode] = self._computeRangeFromData(data)
-
- return self._dataRangeCache[mode]
diff --git a/silx/gui/plot3d/scene/__init__.py b/silx/gui/plot3d/scene/__init__.py
deleted file mode 100644
index 9671725..0000000
--- a/silx/gui/plot3d/scene/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-"""This module provides a 3D graphics scene graph structure."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/11/2016"
-
-
-from .core import Base, Elem, Group, PrivateGroup # noqa
-from .viewport import Viewport # noqa
-from .window import Window # noqa
diff --git a/silx/gui/plot3d/scene/axes.py b/silx/gui/plot3d/scene/axes.py
deleted file mode 100644
index e35e5e1..0000000
--- a/silx/gui/plot3d/scene/axes.py
+++ /dev/null
@@ -1,258 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""Primitive displaying a text field in the scene."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/10/2016"
-
-
-import logging
-import numpy
-
-from ...plot._utils import ticklayout
-
-from . import core, primitives, text, transform
-
-
-_logger = logging.getLogger(__name__)
-
-
-class LabelledAxes(primitives.GroupBBox):
- """A group displaying a bounding box with axes labels around its children.
- """
-
- def __init__(self):
- super(LabelledAxes, self).__init__()
- self._ticksForBounds = None
-
- self._font = text.Font()
-
- self._boxVisibility = True
-
- # TODO offset labels from anchor in pixels
-
- self._xlabel = text.Text2D(font=self._font)
- self._xlabel.align = 'center'
- self._xlabel.transforms = [self._boxTransforms,
- transform.Translate(tx=0.5)]
- self._children.insert(-1, self._xlabel)
-
- self._ylabel = text.Text2D(font=self._font)
- self._ylabel.align = 'center'
- self._ylabel.transforms = [self._boxTransforms,
- transform.Translate(ty=0.5)]
- self._children.insert(-1, self._ylabel)
-
- self._zlabel = text.Text2D(font=self._font)
- self._zlabel.align = 'center'
- self._zlabel.transforms = [self._boxTransforms,
- transform.Translate(tz=0.5)]
- self._children.insert(-1, self._zlabel)
-
- # Init tick lines with dummy pos
- self._tickLines = primitives.DashedLines(
- positions=((0., 0., 0.), (0., 0., 0.)))
- self._tickLines.dash = 5, 10
- self._tickLines.visible = False
- self._children.insert(-1, self._tickLines)
-
- self._tickLabels = core.Group()
- self._children.insert(-1, self._tickLabels)
-
- # Sync color
- self.tickColor = 1., 1., 1., 1.
-
- def _updateBoxAndAxes(self):
- """Update bbox and axes position and size according to children.
-
- Overridden from GroupBBox
- """
- super(LabelledAxes, self)._updateBoxAndAxes()
-
- bounds = self._group.bounds(dataBounds=True)
- if bounds is not None:
- tx, ty, tz = (bounds[1] - bounds[0]) / 2.
- else:
- tx, ty, tz = 0.5, 0.5, 0.5
-
- self._xlabel.transforms[-1].tx = tx
- self._ylabel.transforms[-1].ty = ty
- self._zlabel.transforms[-1].tz = tz
-
- @property
- def tickColor(self):
- """Color of ticks and text labels.
-
- This does NOT set bounding box color.
- Use :attr:`color` for the bounding box.
- """
- return self._xlabel.foreground
-
- @tickColor.setter
- def tickColor(self, color):
- self._xlabel.foreground = color
- self._ylabel.foreground = color
- self._zlabel.foreground = color
- transparentColor = color[0], color[1], color[2], color[3] * 0.6
- self._tickLines.setAttribute('color', transparentColor)
- for label in self._tickLabels.children:
- label.foreground = color
-
- @property
- def font(self):
- """Font of axes text labels (Font)"""
- return self._font
-
- @font.setter
- def font(self, font):
- self._font = font
- self._xlabel.font = font
- self._ylabel.font = font
- self._zlabel.font = font
- for label in self._tickLabels.children:
- label.font = font
-
- @property
- def xlabel(self):
- """Text label of the X axis (str)"""
- return self._xlabel.text
-
- @xlabel.setter
- def xlabel(self, text):
- self._xlabel.text = text
-
- @property
- def ylabel(self):
- """Text label of the Y axis (str)"""
- return self._ylabel.text
-
- @ylabel.setter
- def ylabel(self, text):
- self._ylabel.text = text
-
- @property
- def zlabel(self):
- """Text label of the Z axis (str)"""
- return self._zlabel.text
-
- @zlabel.setter
- def zlabel(self, text):
- self._zlabel.text = text
-
- @property
- def boxVisible(self):
- """Returns bounding box, axes labels and grid visibility."""
- return self._boxVisibility
-
- @boxVisible.setter
- def boxVisible(self, visible):
- self._boxVisibility = bool(visible)
- for child in self._children:
- if child == self._tickLines:
- if self._ticksForBounds is not None:
- child.visible = self._boxVisibility
- elif child != self._group:
- child.visible = self._boxVisibility
-
- def _updateTicks(self):
- """Check if ticks need update and update them if needed."""
- bounds = self._group.bounds(transformed=False, dataBounds=True)
- if bounds is None: # No content
- if self._ticksForBounds is not None:
- self._ticksForBounds = None
- self._tickLines.visible = False
- self._tickLabels.children = [] # Reset previous labels
-
- elif (self._ticksForBounds is None or
- not numpy.all(numpy.equal(bounds, self._ticksForBounds))):
- self._ticksForBounds = bounds
-
- # Update ticks
- ticklength = numpy.abs(bounds[1] - bounds[0])
-
- xticks, xlabels = ticklayout.ticks(*bounds[:, 0])
- yticks, ylabels = ticklayout.ticks(*bounds[:, 1])
- zticks, zlabels = ticklayout.ticks(*bounds[:, 2])
-
- # Update tick lines
- coords = numpy.empty(
- ((len(xticks) + len(yticks) + len(zticks)), 4, 3),
- dtype=numpy.float32)
- coords[:, :, :] = bounds[0, :] # account for offset from origin
-
- xcoords = coords[:len(xticks)]
- xcoords[:, :, 0] = numpy.asarray(xticks)[:, numpy.newaxis]
- xcoords[:, 1, 1] += ticklength[1] # X ticks on XY plane
- xcoords[:, 3, 2] += ticklength[2] # X ticks on XZ plane
-
- ycoords = coords[len(xticks):len(xticks) + len(yticks)]
- ycoords[:, :, 1] = numpy.asarray(yticks)[:, numpy.newaxis]
- ycoords[:, 1, 0] += ticklength[0] # Y ticks on XY plane
- ycoords[:, 3, 2] += ticklength[2] # Y ticks on YZ plane
-
- zcoords = coords[len(xticks) + len(yticks):]
- zcoords[:, :, 2] = numpy.asarray(zticks)[:, numpy.newaxis]
- zcoords[:, 1, 0] += ticklength[0] # Z ticks on XZ plane
- zcoords[:, 3, 1] += ticklength[1] # Z ticks on YZ plane
-
- self._tickLines.setPositions(coords.reshape(-1, 3))
- self._tickLines.visible = self._boxVisibility
-
- # Update labels
- color = self.tickColor
- offsets = bounds[0] - ticklength / 20.
- labels = []
- for tick, label in zip(xticks, xlabels):
- text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
- text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=tick, ty=offsets[1], tz=offsets[2])]
- labels.append(text2d)
-
- for tick, label in zip(yticks, ylabels):
- text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
- text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=offsets[0], ty=tick, tz=offsets[2])]
- labels.append(text2d)
-
- for tick, label in zip(zticks, zlabels):
- text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
- text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=offsets[0], ty=offsets[1], tz=tick)]
- labels.append(text2d)
-
- self._tickLabels.children = labels # Reset previous labels
-
- def prepareGL2(self, context):
- self._updateTicks()
- super(LabelledAxes, self).prepareGL2(context)
diff --git a/silx/gui/plot3d/scene/camera.py b/silx/gui/plot3d/scene/camera.py
deleted file mode 100644
index 90de7ed..0000000
--- a/silx/gui/plot3d/scene/camera.py
+++ /dev/null
@@ -1,353 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-"""This module provides classes to handle a perspective projection in 3D."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-import numpy
-
-from . import transform
-
-
-# CameraExtrinsic #############################################################
-
-class CameraExtrinsic(transform.Transform):
- """Transform matrix to handle camera position and orientation.
-
- :param position: Coordinates of the point of view.
- :type position: numpy.ndarray-like of 3 float32.
- :param direction: Sight direction vector.
- :type direction: numpy.ndarray-like of 3 float32.
- :param up: Vector pointing upward in the image plane.
- :type up: numpy.ndarray-like of 3 float32.
- """
-
- def __init__(self, position=(0., 0., 0.),
- direction=(0., 0., -1.),
- up=(0., 1., 0.)):
-
- super(CameraExtrinsic, self).__init__()
- self._position = None
- self.position = position # set _position
- self._side = 1., 0., 0.
- self._up = 0., 1., 0.
- self._direction = 0., 0., -1.
- self.setOrientation(direction=direction, up=up) # set _direction, _up
-
- def _makeMatrix(self):
- return transform.mat4LookAtDir(self._position,
- self._direction, self._up)
-
- def copy(self):
- """Return an independent copy"""
- return CameraExtrinsic(self.position, self.direction, self.up)
-
- def setOrientation(self, direction=None, up=None):
- """Set the rotation of the point of view.
-
- :param direction: Sight direction vector or
- None to keep the current one.
- :type direction: numpy.ndarray-like of 3 float32 or None.
- :param up: Vector pointing upward in the image plane or
- None to keep the current one.
- :type up: numpy.ndarray-like of 3 float32 or None.
- :raises RuntimeError: if the direction and up are parallel.
- """
- if direction is None: # Use current direction
- direction = self.direction
- else:
- assert len(direction) == 3
- direction = numpy.array(direction, copy=True, dtype=numpy.float32)
- direction /= numpy.linalg.norm(direction)
-
- if up is None: # Use current up
- up = self.up
- else:
- assert len(up) == 3
- up = numpy.array(up, copy=True, dtype=numpy.float32)
-
- # Update side and up to make sure they are perpendicular and normalized
- side = numpy.cross(direction, up)
- sidenormal = numpy.linalg.norm(side)
- if sidenormal == 0.:
- raise RuntimeError('direction and up vectors are parallel.')
- # Alternative: when one of the input parameter is None, it is
- # possible to guess correct vectors using previous direction and up
- side /= sidenormal
- up = numpy.cross(side, direction)
- up /= numpy.linalg.norm(up)
-
- self._side = side
- self._up = up
- self._direction = direction
- self.notify()
-
- @property
- def position(self):
- """Coordinates of the point of view as a numpy.ndarray of 3 float32."""
- return self._position.copy()
-
- @position.setter
- def position(self, position):
- assert len(position) == 3
- self._position = numpy.array(position, copy=True, dtype=numpy.float32)
- self.notify()
-
- @property
- def direction(self):
- """Sight direction (ndarray of 3 float32)."""
- return self._direction.copy()
-
- @direction.setter
- def direction(self, direction):
- self.setOrientation(direction=direction)
-
- @property
- def up(self):
- """Vector pointing upward in the image plane (ndarray of 3 float32).
- """
- return self._up.copy()
-
- @up.setter
- def up(self, up):
- self.setOrientation(up=up)
-
- @property
- def side(self):
- """Vector pointing towards the side of the image plane.
-
- ndarray of 3 float32"""
- return self._side.copy()
-
- def move(self, direction, step=1.):
- """Move the camera relative to the image plane.
-
- :param str direction: Direction relative to image plane.
- One of: 'up', 'down', 'left', 'right',
- 'forward', 'backward'.
- :param float step: The step of the pan to perform in the coordinate
- in which the camera position is defined.
- """
- if direction in ('up', 'down'):
- vector = self.up * (1. if direction == 'up' else -1.)
- elif direction in ('left', 'right'):
- vector = self.side * (1. if direction == 'right' else -1.)
- elif direction in ('forward', 'backward'):
- vector = self.direction * (1. if direction == 'forward' else -1.)
- else:
- raise ValueError('Unsupported direction: %s' % direction)
-
- self.position += step * vector
-
- def rotate(self, direction, angle=1.):
- """First-person rotation of the camera towards the direction.
-
- :param str direction: Direction of movement relative to image plane.
- In: 'up', 'down', 'left', 'right'.
- :param float angle: The angle in degrees of the rotation.
- """
- if direction in ('up', 'down'):
- axis = self.side * (1. if direction == 'up' else -1.)
- elif direction in ('left', 'right'):
- axis = self.up * (1. if direction == 'left' else -1.)
- else:
- raise ValueError('Unsupported direction: %s' % direction)
-
- matrix = transform.mat4RotateFromAngleAxis(numpy.radians(angle), *axis)
- newdir = numpy.dot(matrix[:3, :3], self.direction)
-
- if direction in ('up', 'down'):
- # Rotate up to avoid up and new direction to be (almost) co-linear
- newup = numpy.dot(matrix[:3, :3], self.up)
- self.setOrientation(newdir, newup)
- else:
- # No need to rotate up here as it is the rotation axis
- self.direction = newdir
-
- def orbit(self, direction, center=(0., 0., 0.), angle=1.):
- """Rotate the camera around a point.
-
- :param str direction: Direction of movement relative to image plane.
- In: 'up', 'down', 'left', 'right'.
- :param center: Position around which to rotate the point of view.
- :type center: numpy.ndarray-like of 3 float32.
- :param float angle: he angle in degrees of the rotation.
- """
- if direction in ('up', 'down'):
- axis = self.side * (1. if direction == 'down' else -1.)
- elif direction in ('left', 'right'):
- axis = self.up * (1. if direction == 'right' else -1.)
- else:
- raise ValueError('Unsupported direction: %s' % direction)
-
- # Rotate viewing direction
- rotmatrix = transform.mat4RotateFromAngleAxis(
- numpy.radians(angle), *axis)
- self.direction = numpy.dot(rotmatrix[:3, :3], self.direction)
-
- # Rotate position around center
- center = numpy.array(center, copy=False, dtype=numpy.float32)
- matrix = numpy.dot(transform.mat4Translate(*center), rotmatrix)
- matrix = numpy.dot(matrix, transform.mat4Translate(*(-center)))
- position = numpy.append(self.position, 1.)
- self.position = numpy.dot(matrix, position)[:3]
-
- _RESET_CAMERA_ORIENTATIONS = {
- 'side': ((-1., -1., -1.), (0., 1., 0.)),
- 'front': ((0., 0., -1.), (0., 1., 0.)),
- 'back': ((0., 0., 1.), (0., 1., 0.)),
- 'top': ((0., -1., 0.), (0., 0., -1.)),
- 'bottom': ((0., 1., 0.), (0., 0., 1.)),
- 'right': ((-1., 0., 0.), (0., 1., 0.)),
- 'left': ((1., 0., 0.), (0., 1., 0.))
- }
-
- def reset(self, face=None):
- """Reset the camera position to pre-defined orientations.
-
- :param str face: The direction of the camera in:
- side, front, back, top, bottom, right, left.
- """
- if face not in self._RESET_CAMERA_ORIENTATIONS:
- raise ValueError('Unsupported face: %s' % face)
-
- distance = numpy.linalg.norm(self.position)
- direction, up = self._RESET_CAMERA_ORIENTATIONS[face]
- self.setOrientation(direction, up)
- self.position = - self.direction * distance
-
-
-class Camera(transform.Transform):
- """Combination of camera projection and position.
-
- See :class:`Perspective` and :class:`CameraExtrinsic`.
-
- :param float fovy: Vertical field-of-view in degrees.
- :param float near: The near clipping plane Z coord (strictly positive).
- :param float far: The far clipping plane Z coord (> near).
- :param size:
- Viewport's size used to compute the aspect ratio (width, height).
- :type size: 2-tuple of float
- :param position: Coordinates of the point of view.
- :type position: numpy.ndarray-like of 3 float32.
- :param direction: Sight direction vector.
- :type direction: numpy.ndarray-like of 3 float32.
- :param up: Vector pointing upward in the image plane.
- :type up: numpy.ndarray-like of 3 float32.
- """
-
- def __init__(self, fovy=30., near=0.1, far=1., size=(1., 1.),
- position=(0., 0., 0.),
- direction=(0., 0., -1.), up=(0., 1., 0.)):
- super(Camera, self).__init__()
- self._intrinsic = transform.Perspective(fovy, near, far, size)
- self._intrinsic.addListener(self._transformChanged)
- self._extrinsic = CameraExtrinsic(position, direction, up)
- self._extrinsic.addListener(self._transformChanged)
-
- def _makeMatrix(self):
- return numpy.dot(self.intrinsic.matrix, self.extrinsic.matrix)
-
- def _transformChanged(self, source):
- """Listener of intrinsic and extrinsic camera parameters instances."""
- if source is not self:
- self.notify()
-
- def resetCamera(self, bounds):
- """Change camera to have the bounds in the viewing frustum.
-
- It updates the camera position and depth extent.
- Camera sight direction and up are not affected.
-
- :param bounds: The axes-aligned bounds to include.
- :type bounds: numpy.ndarray: ((xMin, yMin, zMin), (xMax, yMax, zMax))
- """
-
- center = 0.5 * (bounds[0] + bounds[1])
- radius = numpy.linalg.norm(0.5 * (bounds[1] - bounds[0]))
- if radius == 0.: # bounds are all collapsed
- radius = 1.
-
- if isinstance(self.intrinsic, transform.Perspective):
- # Get the viewpoint distance from the bounds center
- minfov = numpy.radians(self.intrinsic.fovy)
- width, height = self.intrinsic.size
- if width < height:
- minfov *= width / height
-
- offset = radius / numpy.sin(0.5 * minfov)
-
- # Update camera
- self.extrinsic.position = \
- center - offset * self.extrinsic.direction
- self.intrinsic.setDepthExtent(offset - radius, offset + radius)
-
- elif isinstance(self.intrinsic, transform.Orthographic):
- # Y goes up
- self.intrinsic.setClipping(
- left=center[0] - radius,
- right=center[0] + radius,
- bottom=center[1] - radius,
- top=center[1] + radius)
-
- # Update camera
- self.extrinsic.position = 0, 0, 0
- self.intrinsic.setDepthExtent(center[2] - radius,
- center[2] + radius)
- else:
- raise RuntimeError('Unsupported camera: %s' % self.intrinsic)
-
- @property
- def intrinsic(self):
- """Intrinsic camera parameters, i.e., projection matrix."""
- return self._intrinsic
-
- @intrinsic.setter
- def intrinsic(self, intrinsic):
- self._intrinsic.removeListener(self._transformChanged)
- self._intrinsic = intrinsic
- self._intrinsic.addListener(self._transformChanged)
-
- @property
- def extrinsic(self):
- """Extrinsic camera parameters, i.e., position and orientation."""
- return self._extrinsic
-
- def move(self, *args, **kwargs):
- """See :meth:`CameraExtrinsic.move`."""
- self.extrinsic.move(*args, **kwargs)
-
- def rotate(self, *args, **kwargs):
- """See :meth:`CameraExtrinsic.rotate`."""
- self.extrinsic.rotate(*args, **kwargs)
-
- def orbit(self, *args, **kwargs):
- """See :meth:`CameraExtrinsic.orbit`."""
- self.extrinsic.orbit(*args, **kwargs)
diff --git a/silx/gui/plot3d/scene/core.py b/silx/gui/plot3d/scene/core.py
deleted file mode 100644
index 43838fe..0000000
--- a/silx/gui/plot3d/scene/core.py
+++ /dev/null
@@ -1,343 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides the base scene structure.
-
-This module provides the classes for describing a tree structure with
-rendering and picking API.
-All nodes inherit from :class:`Base`.
-Nodes with children are provided with :class:`PrivateGroup` and
-:class:`Group` classes.
-Leaf rendering nodes should inherit from :class:`Elem`.
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-import itertools
-import weakref
-
-import numpy
-
-from . import event
-from . import transform
-
-from .viewport import Viewport
-
-
-# Nodes #######################################################################
-
-class Base(event.Notifier):
- """A scene node with common features."""
-
- def __init__(self):
- super(Base, self).__init__()
- self._visible = True
- self._pickable = False
-
- self._parentRef = None
-
- self._transforms = transform.TransformList()
- self._transforms.addListener(self._transformChanged)
-
- # notifying properties
-
- visible = event.notifyProperty('_visible',
- doc="Visibility flag of the node")
- pickable = event.notifyProperty('_pickable',
- doc="True to make node pickable")
-
- # Access to tree path
-
- @property
- def parent(self):
- """Parent or None if no parent"""
- return None if self._parentRef is None else self._parentRef()
-
- def _setParent(self, parent):
- """Set the parent of this node.
-
- For internal use.
-
- :param Base parent: The parent.
- """
- if parent is not None and self._parentRef is not None:
- raise RuntimeError('Trying to add a node at two places.')
- # Alternative: remove it from previous children list
- self._parentRef = None if parent is None else weakref.ref(parent)
-
- @property
- def path(self):
- """Tuple of scene nodes, from the tip of the tree down to this node.
-
- If this tree is attached to a :class:`Viewport`,
- then the :class:`Viewport` is the first element of path.
- """
- if self.parent is None:
- return self,
- elif isinstance(self.parent, Viewport):
- return self.parent, self
- else:
- return self.parent.path + (self, )
-
- @property
- def viewport(self):
- """The viewport this node is attached to or None."""
- root = self.path[0]
- return root if isinstance(root, Viewport) else None
-
- @property
- def root(self):
- """The root node of the scene.
-
- If attached to a :class:`Viewport`, this is the item right under it
- """
- path = self.path
- return path[1] if isinstance(path[0], Viewport) else path[0]
-
- @property
- def objectToNDCTransform(self):
- """Transform from object to normalized device coordinates.
-
- Do not forget perspective divide.
- """
- # Using the Viewport's transforms property to proxy the camera
- path = self.path
- assert isinstance(path[0], Viewport)
- return transform.StaticTransformList(elem.transforms for elem in path)
-
- @property
- def objectToSceneTransform(self):
- """Transform from object to scene.
-
- Combine transforms up to the Viewport (not including it).
- """
- path = self.path
- if isinstance(path[0], Viewport):
- path = path[1:] # Remove viewport to remove camera transforms
- return transform.StaticTransformList(elem.transforms for elem in path)
-
- # transform
-
- @property
- def transforms(self):
- """List of transforms defining the frame of this node relative
- to its parent."""
- return self._transforms
-
- @transforms.setter
- def transforms(self, iterable):
- self._transforms.removeListener(self._transformChanged)
- if isinstance(iterable, transform.TransformList):
- # If it is a TransformList, do not create one to enable sharing.
- self._transforms = iterable
- else:
- assert hasattr(iterable, '__iter__')
- self._transforms = transform.TransformList(iterable)
- self._transforms.addListener(self._transformChanged)
-
- def _transformChanged(self, source):
- self.notify() # Broadcast transform notification
-
- # Bounds
-
- _CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
- dtype=numpy.float32)
- """Unit cube corners used to transform bounds"""
-
- def _bounds(self, dataBounds=False):
- """Override in subclass to provide bounds in object coordinates"""
- return None
-
- def bounds(self, transformed=False, dataBounds=False):
- """Returns the bounds of this node aligned with the axis,
- with or without transform applied.
-
- :param bool transformed: False to give bounds in object coordinates
- (the default), True to apply this object's
- transforms.
- :param bool dataBounds: False to give bounds of vertices (the default),
- True to give bounds of the represented data.
- :return: The bounds: ((xMin, yMin, zMin), (xMax, yMax, zMax)) or None
- if no bounds.
- :rtype: numpy.ndarray of float
- """
- bounds = self._bounds(dataBounds)
-
- if transformed and bounds is not None:
- bounds = self.transforms.transformBounds(bounds)
-
- return bounds
-
- # Rendering
-
- def prepareGL2(self, ctx):
- """Called before the rendering to prepare OpenGL resources.
-
- Override in subclass.
- """
- pass
-
- def renderGL2(self, ctx):
- """Called to perform the OpenGL rendering.
-
- Override in subclass.
- """
- pass
-
- def render(self, ctx):
- """Called internally to perform rendering."""
- if self.visible:
- ctx.pushTransform(self.transforms)
- self.prepareGL2(ctx)
- self.renderGL2(ctx)
- ctx.popTransform()
-
- def postRender(self, ctx):
- """Hook called when parent's node render is finished.
-
- Called in the reverse of rendering order (i.e., last child first).
-
- Meant for nodes that modify the :class:`RenderContext` ctx to
- reset their modifications.
- """
- pass
-
- def pick(self, ctx, x, y, depth=None):
- """True/False picking, should be fast"""
- if self.pickable:
- pass
-
- def pickRay(self, ctx, ray):
- """Picking returning list of ray intersections."""
- if self.pickable:
- pass
-
-
-class Elem(Base):
- """A scene node that does some rendering."""
-
- def __init__(self):
- super(Elem, self).__init__()
- # self.showBBox = False # Here or outside scene graph?
- # self.clipPlane = None # This needs to be handled in the shader
-
-
-class PrivateGroup(Base):
- """A scene node that renders its (private) childern.
-
- :param iterable children: :class:`Base` nodes to add as children
- """
-
- class ChildrenList(event.NotifierList):
- """List of children with notification and children's parent update."""
-
- def _listWillChangeHook(self, methodName, *args, **kwargs):
- super(PrivateGroup.ChildrenList, self)._listWillChangeHook(
- methodName, *args, **kwargs)
- for item in self:
- item._setParent(None)
-
- def _listWasChangedHook(self, methodName, *args, **kwargs):
- for item in self:
- item._setParent(self._parentRef())
- super(PrivateGroup.ChildrenList, self)._listWasChangedHook(
- methodName, *args, **kwargs)
-
- def __init__(self, parent, children):
- self._parentRef = weakref.ref(parent)
- super(PrivateGroup.ChildrenList, self).__init__(children)
-
- def __init__(self, children=()):
- super(PrivateGroup, self).__init__()
- self.__children = PrivateGroup.ChildrenList(self, children)
- self.__children.addListener(self._updated)
-
- @property
- def _children(self):
- """List of children to be rendered.
-
- This private attribute is meant to be used by subclass.
- """
- return self.__children
-
- @_children.setter
- def _children(self, iterable):
- self.__children.removeListener(self._updated)
- for item in self.__children:
- item._setParent(None)
- del self.__children # This is needed
- self.__children = PrivateGroup.ChildrenList(self, iterable)
- self.__children.addListener(self._updated)
- self.notify()
-
- def _updated(self, source, *args, **kwargs):
- """Listen for updates"""
- if source is not self: # Avoid infinite recursion
- self.notify(*args, **kwargs)
-
- def _bounds(self, dataBounds=False):
- """Compute the bounds from transformed children bounds"""
- bounds = []
- for child in self._children:
- if child.visible:
- childBounds = child.bounds(
- transformed=True, dataBounds=dataBounds)
- if childBounds is not None:
- bounds.append(childBounds)
-
- if len(bounds) == 0:
- return None
- else:
- bounds = numpy.array(bounds, dtype=numpy.float32)
- return numpy.array((bounds[:, 0, :].min(axis=0),
- bounds[:, 1, :].max(axis=0)),
- dtype=numpy.float32)
-
- def prepareGL2(self, ctx):
- pass
-
- def renderGL2(self, ctx):
- """Render all children"""
- for child in self._children:
- child.render(ctx)
- for child in reversed(self._children):
- child.postRender(ctx)
-
-
-class Group(PrivateGroup):
- """A scene node that renders its (public) children."""
-
- @property
- def children(self):
- """List of children to be rendered."""
- return self._children
-
- @children.setter
- def children(self, iterable):
- self._children = iterable
diff --git a/silx/gui/plot3d/scene/cutplane.py b/silx/gui/plot3d/scene/cutplane.py
deleted file mode 100644
index 88147df..0000000
--- a/silx/gui/plot3d/scene/cutplane.py
+++ /dev/null
@@ -1,390 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 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.
-#
-# ###########################################################################*/
-"""A cut plane in a 3D texture: hackish implementation...
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "11/01/2018"
-
-import string
-import numpy
-
-from ... import _glutils
-from ..._glutils import gl
-
-from .function import Colormap
-from .primitives import Box, Geometry, PlaneInGroup
-from . import transform, utils
-
-
-class ColormapMesh3D(Geometry):
- """A 3D mesh with color from a 3D texture."""
-
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 normal;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- //uniform mat3 matrixInvTranspose;
- uniform vec3 dataScale;
- uniform vec3 texCoordsOffset;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec3 vTexCoords;
-
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- //vNormal = matrixInvTranspose * normalize(normal);
- vPosition = position;
- vTexCoords = dataScale * position + texCoordsOffset;
- vNormal = normal;
- gl_Position = matrix * vec4(position, 1.0);
- }
- """,
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec3 vTexCoords;
- uniform sampler3D data;
- uniform float alpha;
-
- $colormapDecl
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- float value = texture3D(data, vTexCoords).r;
- vec4 color = $colormapCall(value);
- color.a *= alpha;
-
- gl_FragColor = $lightingCall(color, vPosition, vNormal);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self, position, normal, data, copy=True,
- mode='triangles', indices=None, colormap=None):
- assert mode in self._TRIANGLE_MODES
- data = numpy.array(data, copy=copy, order='C')
- assert data.ndim == 3
- self._data = data
- self._texture = None
- self._update_texture = True
- self._update_texture_filter = False
- self._alpha = 1.
- self._colormap = colormap or Colormap() # Default colormap
- self._colormap.addListener(self._cmapChanged)
- self._interpolation = 'linear'
- super(ColormapMesh3D, self).__init__(mode,
- indices,
- position=position,
- normal=normal)
-
- self.isBackfaceVisible = True
- self.textureOffset = 0., 0., 0.
- """Offset to add to texture coordinates"""
-
- def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C')
- assert data.ndim == 3
- self._data = data
- self._update_texture = True
-
- def getData(self, copy=True):
- return numpy.array(self._data, copy=copy)
-
- @property
- def interpolation(self):
- """The texture interpolation mode: 'linear' or 'nearest'"""
- return self._interpolation
-
- @interpolation.setter
- def interpolation(self, interpolation):
- assert interpolation in ('linear', 'nearest')
- self._interpolation = interpolation
- self._update_texture_filter = True
- self.notify()
-
- @property
- def alpha(self):
- """Transparency of the plane, float in [0, 1]"""
- return self._alpha
-
- @alpha.setter
- def alpha(self, alpha):
- self._alpha = float(alpha)
-
- @property
- def colormap(self):
- """The colormap used by this primitive"""
- return self._colormap
-
- def _cmapChanged(self, source, *args, **kwargs):
- """Broadcast colormap changes"""
- self.notify(*args, **kwargs)
-
- def prepareGL2(self, ctx):
- if self._texture is None or self._update_texture:
- if self._texture is not None:
- self._texture.discard()
-
- if self.interpolation == 'nearest':
- filter_ = gl.GL_NEAREST
- else:
- filter_ = gl.GL_LINEAR
- self._update_texture = False
- self._update_texture_filter = False
- self._texture = _glutils.Texture(
- gl.GL_R32F, self._data, gl.GL_RED,
- minFilter=filter_,
- magFilter=filter_,
- wrap=gl.GL_CLAMP_TO_EDGE)
-
- if self._update_texture_filter:
- self._update_texture_filter = False
- if self.interpolation == 'nearest':
- filter_ = gl.GL_NEAREST
- else:
- filter_ = gl.GL_LINEAR
- self._texture.minFilter = filter_
- self._texture.magFilter = filter_
-
- super(ColormapMesh3D, self).prepareGL2(ctx)
-
- def renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall,
- colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call
- )
- program = ctx.glCtx.prog(self._shaders[0], fragment)
- program.use()
-
- ctx.viewport.light.setupProgram(ctx, program)
- self.colormap.setupProgram(ctx, program)
-
- if not self.isBackfaceVisible:
- gl.glCullFace(gl.GL_BACK)
- gl.glEnable(gl.GL_CULL_FACE)
-
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
-
- shape = self._data.shape
- scales = 1./shape[2], 1./shape[1], 1./shape[0]
- gl.glUniform3f(program.uniforms['dataScale'], *scales)
- gl.glUniform3f(program.uniforms['texCoordsOffset'], *self.textureOffset)
-
- gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
-
- ctx.setupProgram(program)
-
- self._texture.bind()
- self._draw(program)
-
- if not self.isBackfaceVisible:
- gl.glDisable(gl.GL_CULL_FACE)
-
-
-class CutPlane(PlaneInGroup):
- """A cutting plane in a 3D texture"""
-
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
- self._data = None
- self._mesh = None
- self._alpha = 1.
- self._interpolation = 'linear'
- self._colormap = Colormap()
- super(CutPlane, self).__init__(point, normal)
-
- def setData(self, data, copy=True):
- if data is None:
- self._data = None
- if self._mesh is not None:
- self._children.remove(self._mesh)
- self._mesh = None
-
- else:
- data = numpy.array(data, copy=copy, order='C')
- assert data.ndim == 3
- self._data = data
- if self._mesh is not None:
- self._mesh.setData(data, copy=False)
-
- def getData(self, copy=True):
- return None if self._mesh is None else self._mesh.getData(copy=copy)
-
- @property
- def alpha(self):
- return self._alpha
-
- @alpha.setter
- def alpha(self, alpha):
- self._alpha = float(alpha)
- if self._mesh is not None:
- self._mesh.alpha = alpha
-
- @property
- def colormap(self):
- return self._colormap
-
- @property
- def interpolation(self):
- """The texture interpolation mode: 'linear' (default) or 'nearest'"""
- return self._interpolation
-
- @interpolation.setter
- def interpolation(self, interpolation):
- assert interpolation in ('nearest', 'linear')
- if interpolation != self.interpolation:
- self._interpolation = interpolation
- if self._mesh is not None:
- self._mesh.interpolation = interpolation
- self.notify()
-
- def prepareGL2(self, ctx):
- if self.isValid:
-
- contourVertices = self.contourVertices
-
- if self._mesh is None and self._data is not None:
- self._mesh = ColormapMesh3D(contourVertices,
- normal=self.plane.normal,
- data=self._data,
- copy=False,
- mode='fan',
- colormap=self.colormap)
- self._mesh.alpha = self._alpha
- self._mesh.interpolation = self.interpolation
- self._children.insert(0, self._mesh)
-
- if self._mesh is not None:
- if (contourVertices is None or
- len(contourVertices) == 0):
- self._mesh.visible = False
- else:
- self._mesh.visible = True
- self._mesh.setAttribute('normal', self.plane.normal)
- self._mesh.setAttribute('position', contourVertices)
-
- needTextureOffset = False
- if self.interpolation == 'nearest':
- # If cut plane is co-linear with array bin edges add texture offset
- planePt = self.plane.point
- for index, normal in enumerate(((1., 0., 0.),
- (0., 1., 0.),
- (0., 0., 1.))):
- if (numpy.all(numpy.equal(self.plane.normal, normal)) and
- int(planePt[index]) == planePt[index]):
- needTextureOffset = True
- break
-
- if needTextureOffset:
- self._mesh.textureOffset = self.plane.normal * 1e-6
- else:
- self._mesh.textureOffset = 0., 0., 0.
-
- super(CutPlane, self).prepareGL2(ctx)
-
- def renderGL2(self, ctx):
- with self.viewport.light.turnOff():
- super(CutPlane, self).renderGL2(ctx)
-
- def _bounds(self, dataBounds=False):
- if not dataBounds:
- vertices = self.contourVertices
- if vertices is not None:
- return numpy.array(
- (vertices.min(axis=0), vertices.max(axis=0)),
- dtype=numpy.float32)
- else:
- return None # Plane in not slicing the data volume
- else:
- if self._data is None:
- return None
- else:
- depth, height, width = self._data.shape
- return numpy.array(((0., 0., 0.),
- (width, height, depth)),
- dtype=numpy.float32)
-
- @property
- def contourVertices(self):
- """The vertices of the contour of the plane/bounds intersection."""
- # TODO copy from PlaneInGroup, refactor all that!
- bounds = self.bounds(dataBounds=True)
- if bounds is None:
- return None # No bounds: no vertices
-
- # Check if cache is valid and return it
- cachebounds, cachevertices = self._cache
- if numpy.all(numpy.equal(bounds, cachebounds)):
- return cachevertices
-
- # Cache is not OK, rebuild it
- boxVertices = Box.getVertices(copy=True)
- boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
- lineIndices = Box.getLineIndices(copy=False)
- vertices = utils.boxPlaneIntersect(
- boxVertices, lineIndices, self.plane.normal, self.plane.point)
-
- self._cache = bounds, vertices if len(vertices) != 0 else None
-
- return self._cache[1]
-
- # Render transforms RW, TODO refactor this!
- @property
- def transforms(self):
- return self._transforms
-
- @transforms.setter
- def transforms(self, iterable):
- self._transforms.removeListener(self._transformChanged)
- if isinstance(iterable, transform.TransformList):
- # If it is a TransformList, do not create one to enable sharing.
- self._transforms = iterable
- else:
- assert hasattr(iterable, '__iter__')
- self._transforms = transform.TransformList(iterable)
- self._transforms.addListener(self._transformChanged)
diff --git a/silx/gui/plot3d/scene/event.py b/silx/gui/plot3d/scene/event.py
deleted file mode 100644
index 98f8f8b..0000000
--- a/silx/gui/plot3d/scene/event.py
+++ /dev/null
@@ -1,225 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-"""This module provides a simple generic notification system."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/07/2018"
-
-
-import logging
-
-from silx.utils.weakref import WeakList
-
-_logger = logging.getLogger(__name__)
-
-
-# Notifier ####################################################################
-
-class Notifier(object):
- """Base class for object with notification mechanism."""
-
- def __init__(self):
- self._listeners = WeakList()
-
- def addListener(self, listener):
- """Register a listener.
-
- Adding an already registered listener has no effect.
-
- :param callable listener: The function or method to register.
- """
- if listener not in self._listeners:
- self._listeners.append(listener)
- else:
- _logger.warning('Ignoring addition of an already registered listener')
-
- def removeListener(self, listener):
- """Remove a previously registered listener.
-
- :param callable listener: The function or method to unregister.
- """
- try:
- self._listeners.remove(listener)
- except ValueError:
- _logger.warning('Trying to remove a listener that is not registered')
-
- def notify(self, *args, **kwargs):
- """Notify all registered listeners with the given parameters.
-
- Listeners are called directly in this method.
- Listeners are called in the order they were registered.
- """
- for listener in self._listeners:
- listener(self, *args, **kwargs)
-
-
-def notifyProperty(attrName, copy=False, converter=None, doc=None):
- """Create a property that adds notification to an attribute.
-
- :param str attrName: The name of the attribute to wrap.
- :param bool copy: Whether to return a copy of the attribute
- or not (the default).
- :param converter: Function converting input value to appropriate type
- This function takes a single argument and return the
- converted value.
- It can be used to perform some asserts.
- :param str doc: The docstring of the property
- :return: A property with getter and setter
- """
- if copy:
- def getter(self):
- return getattr(self, attrName).copy()
- else:
- def getter(self):
- return getattr(self, attrName)
-
- if converter is None:
- def setter(self, value):
- if getattr(self, attrName) != value:
- setattr(self, attrName, value)
- self.notify()
-
- else:
- def setter(self, value):
- value = converter(value)
- if getattr(self, attrName) != value:
- setattr(self, attrName, value)
- self.notify()
-
- return property(getter, setter, doc=doc)
-
-
-class HookList(list):
- """List with hooks before and after modification."""
-
- def __init__(self, iterable):
- super(HookList, self).__init__(iterable)
-
- self._listWasChangedHook('__init__', iterable)
-
- def _listWillChangeHook(self, methodName, *args, **kwargs):
- """To override. Called before modifying the list.
-
- This method is called with the name of the method called to
- modify the list and its parameters.
- """
- pass
-
- def _listWasChangedHook(self, methodName, *args, **kwargs):
- """To override. Called after modifying the list.
-
- This method is called with the name of the method called to
- modify the list and its parameters.
- """
- pass
-
- # Wrapping methods that modify the list
-
- def _wrapper(self, methodName, *args, **kwargs):
- """Generic wrapper of list methods calling the hooks."""
- self._listWillChangeHook(methodName, *args, **kwargs)
- result = getattr(super(HookList, self),
- methodName)(*args, **kwargs)
- self._listWasChangedHook(methodName, *args, **kwargs)
- return result
-
- # Add methods
-
- def __iadd__(self, *args, **kwargs):
- return self._wrapper('__iadd__', *args, **kwargs)
-
- def __imul__(self, *args, **kwargs):
- return self._wrapper('__imul__', *args, **kwargs)
-
- def append(self, *args, **kwargs):
- return self._wrapper('append', *args, **kwargs)
-
- def extend(self, *args, **kwargs):
- return self._wrapper('extend', *args, **kwargs)
-
- def insert(self, *args, **kwargs):
- return self._wrapper('insert', *args, **kwargs)
-
- # Remove methods
-
- def __delitem__(self, *args, **kwargs):
- return self._wrapper('__delitem__', *args, **kwargs)
-
- def __delslice__(self, *args, **kwargs):
- return self._wrapper('__delslice__', *args, **kwargs)
-
- def remove(self, *args, **kwargs):
- return self._wrapper('remove', *args, **kwargs)
-
- def pop(self, *args, **kwargs):
- return self._wrapper('pop', *args, **kwargs)
-
- # Set methods
-
- def __setitem__(self, *args, **kwargs):
- return self._wrapper('__setitem__', *args, **kwargs)
-
- def __setslice__(self, *args, **kwargs):
- return self._wrapper('__setslice__', *args, **kwargs)
-
- # In place methods
-
- def sort(self, *args, **kwargs):
- return self._wrapper('sort', *args, **kwargs)
-
- def reverse(self, *args, **kwargs):
- return self._wrapper('reverse', *args, **kwargs)
-
-
-class NotifierList(HookList, Notifier):
- """List of Notifiers with notification mechanism.
-
- This class registers itself as a listener of the list items.
-
- The default listener method forward notification from list items
- to the listeners of the list.
- """
-
- def __init__(self, iterable=()):
- Notifier.__init__(self)
- HookList.__init__(self, iterable)
-
- def _listWillChangeHook(self, methodName, *args, **kwargs):
- for item in self:
- item.removeListener(self._notified)
-
- def _listWasChangedHook(self, methodName, *args, **kwargs):
- for item in self:
- item.addListener(self._notified)
- self.notify()
-
- def _notified(self, source, *args, **kwargs):
- """Default listener forwarding list item changes to its listeners."""
- # Avoid infinite recursion if the list is listening itself
- if source is not self:
- self.notify(*args, **kwargs)
diff --git a/silx/gui/plot3d/scene/function.py b/silx/gui/plot3d/scene/function.py
deleted file mode 100644
index 2deb785..0000000
--- a/silx/gui/plot3d/scene/function.py
+++ /dev/null
@@ -1,654 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2020 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.
-#
-# ###########################################################################*/
-"""This module provides functions to add to shaders."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/07/2018"
-
-
-import contextlib
-import logging
-import string
-import numpy
-
-from ... import _glutils
-from ..._glutils import gl
-
-from . import event
-from . import utils
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ProgramFunction(object):
- """Class providing a function to add to a GLProgram shaders.
- """
-
- def setupProgram(self, context, program):
- """Sets-up uniforms of a program using this shader function.
-
- :param RenderContext context: The current rendering context
- :param GLProgram program: The program to set-up.
- It MUST be in use and using this function.
- """
- pass
-
-
-class Fog(event.Notifier, ProgramFunction):
- """Linear fog over the whole scene content.
-
- The background of the viewport is used as fog color,
- otherwise it defaults to white.
- """
- # TODO: add more controls (set fog range), add more fog modes
-
- _fragDecl = """
- /* (1/(far - near) or 0, near) z in [0 (camera), -inf[ */
- uniform vec2 fogExtentInfo;
-
- /* Color to use as fog color */
- uniform vec3 fogColor;
-
- vec4 fog(vec4 color, vec4 cameraPosition) {
- /* d = (pos - near) / (far - near) */
- float distance = fogExtentInfo.x * (cameraPosition.z/cameraPosition.w - fogExtentInfo.y);
- float fogFactor = clamp(distance, 0.0, 1.0);
- vec3 rgb = mix(color.rgb, fogColor, fogFactor);
- return vec4(rgb.r, rgb.g, rgb.b, color.a);
- }
- """
-
- _fragDeclNoop = """
- vec4 fog(vec4 color, vec4 cameraPosition) {
- return color;
- }
- """
-
- def __init__(self):
- super(Fog, self).__init__()
- self._isOn = True
-
- @property
- def isOn(self):
- """True to enable fog, False to disable (bool)"""
- return self._isOn
-
- @isOn.setter
- def isOn(self, isOn):
- isOn = bool(isOn)
- if self._isOn != isOn:
- self._isOn = bool(isOn)
- self.notify()
-
- @property
- def fragDecl(self):
- return self._fragDecl if self.isOn else self._fragDeclNoop
-
- @property
- def fragCall(self):
- return "fog"
-
- @staticmethod
- def _zExtentCamera(viewport):
- """Return (far, near) planes Z in camera coordinates.
-
- :param Viewport viewport:
- :return: (far, near) position in camera coords (from 0 to -inf)
- """
- # Provide scene z extent in camera coords
- bounds = viewport.camera.extrinsic.transformBounds(
- viewport.scene.bounds(transformed=True, dataBounds=True))
- return bounds[:, 2]
-
- def setupProgram(self, context, program):
- if not self.isOn:
- return
-
- far, near = context.cache(key='zExtentCamera',
- factory=self._zExtentCamera,
- viewport=context.viewport)
- extent = far - near
- gl.glUniform2f(program.uniforms['fogExtentInfo'],
- 0.9/extent if extent != 0. else 0.,
- near)
-
- # Use background color as fog color
- bgColor = context.viewport.background
- if bgColor is None:
- bgColor = 1., 1., 1.
- gl.glUniform3f(program.uniforms['fogColor'], *bgColor[:3])
-
-
-class ClippingPlane(ProgramFunction):
- """Description of a clipping plane and rendering.
-
- Convention: Clipping is performed in camera/eye space.
-
- :param point: Local coordinates of a point on the plane.
- :type point: numpy.ndarray-like of 3 float32
- :param normal: Local coordinates of the plane normal.
- :type normal: numpy.ndarray-like of 3 float32
- """
-
- _fragDecl = """
- /* Clipping plane */
- /* as rx + gy + bz + a > 0, clipping all positive */
- uniform vec4 planeEq;
-
- /* Position is in camera/eye coordinates */
-
- bool isClipped(vec4 position) {
- vec4 tmp = planeEq * position;
- float value = tmp.x + tmp.y + tmp.z + planeEq.a;
- return (value < 0.0001);
- }
-
- void clipping(vec4 position) {
- if (isClipped(position)) {
- discard;
- }
- }
- /* End of clipping */
- """
-
- _fragDeclNoop = """
- bool isClipped(vec4 position)
- {
- return false;
- }
-
- void clipping(vec4 position) {}
- """
-
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 0.)):
- self._plane = utils.Plane(point, normal)
-
- @property
- def plane(self):
- """Plane parameters in camera space."""
- return self._plane
-
- # GL2
-
- @property
- def fragDecl(self):
- return self._fragDecl if self.plane.isPlane else self._fragDeclNoop
-
- @property
- def fragCall(self):
- return "clipping"
-
- def setupProgram(self, context, program):
- """Sets-up uniforms of a program using this shader function.
-
- :param RenderContext context: The current rendering context
- :param GLProgram program: The program to set-up.
- It MUST be in use and using this function.
- """
- if self.plane.isPlane:
- gl.glUniform4f(program.uniforms['planeEq'], *self.plane.parameters)
-
-
-class DirectionalLight(event.Notifier, ProgramFunction):
- """Description of a directional Phong light.
-
- :param direction: The direction of the light or None to disable light
- :type direction: ndarray of 3 floats or None
- :param ambient: RGB ambient light
- :type ambient: ndarray of 3 floats in [0, 1], default: (1., 1., 1.)
- :param diffuse: RGB diffuse light parameter
- :type diffuse: ndarray of 3 floats in [0, 1], default: (0., 0., 0.)
- :param specular: RGB specular light parameter
- :type specular: ndarray of 3 floats in [0, 1], default: (1., 1., 1.)
- :param int shininess: The shininess of the material for specular term,
- default: 0 which disables specular component.
- """
-
- fragmentShaderFunction = """
- /* Lighting */
- struct DLight {
- vec3 lightDir; // Direction of light in object space
- vec3 ambient;
- vec3 diffuse;
- vec3 specular;
- float shininess;
- vec3 viewPos; // Camera position in object space
- };
-
- uniform DLight dLight;
-
- vec4 lighting(vec4 color, vec3 position, vec3 normal)
- {
- normal = normalize(normal);
- // 1-sided
- float nDotL = max(0.0, dot(normal, - dLight.lightDir));
-
- // 2-sided
- //float nDotL = dot(normal, - dLight.lightDir);
- //if (nDotL < 0.) {
- // nDotL = - nDotL;
- // normal = - normal;
- //}
-
- float specFactor = 0.;
- if (dLight.shininess > 0. && nDotL > 0.) {
- vec3 reflection = reflect(dLight.lightDir, normal);
- vec3 viewDir = normalize(dLight.viewPos - position);
- specFactor = max(0.0, dot(reflection, viewDir));
- if (specFactor > 0.) {
- specFactor = pow(specFactor, dLight.shininess);
- }
- }
-
- vec3 enlightedColor = color.rgb * (dLight.ambient +
- dLight.diffuse * nDotL) +
- dLight.specular * specFactor;
-
- return vec4(enlightedColor.rgb, color.a);
- }
- /* End of Lighting */
- """
-
- fragmentShaderFunctionNoop = """
- vec4 lighting(vec4 color, vec3 position, vec3 normal)
- {
- return color;
- }
- """
-
- def __init__(self, direction=None,
- ambient=(1., 1., 1.), diffuse=(0., 0., 0.),
- specular=(1., 1., 1.), shininess=0):
- super(DirectionalLight, self).__init__()
- self._direction = None
- self.direction = direction # Set _direction
- self._isOn = True
- self._ambient = ambient
- self._diffuse = diffuse
- self._specular = specular
- self._shininess = shininess
-
- ambient = event.notifyProperty('_ambient')
- diffuse = event.notifyProperty('_diffuse')
- specular = event.notifyProperty('_specular')
- shininess = event.notifyProperty('_shininess')
-
- @property
- def isOn(self):
- """True if light is on, False otherwise."""
- return self._isOn and self._direction is not None
-
- @isOn.setter
- def isOn(self, isOn):
- self._isOn = bool(isOn)
-
- @contextlib.contextmanager
- def turnOff(self):
- """Context manager to temporary turn off lighting during rendering.
-
- >>> with light.turnOff():
- ... # Do some rendering without lighting
- """
- wason = self._isOn
- self._isOn = False
- yield
- self._isOn = wason
-
- @property
- def direction(self):
- """The direction of the light, or None if light is not on."""
- return self._direction
-
- @direction.setter
- def direction(self, direction):
- if direction is None:
- self._direction = None
- else:
- assert len(direction) == 3
- direction = numpy.array(direction, dtype=numpy.float32, copy=True)
- norm = numpy.linalg.norm(direction)
- assert norm != 0
- self._direction = direction / norm
- self.notify()
-
- # GL2
-
- @property
- def fragmentDef(self):
- """Definition to add to fragment shader"""
- if self.isOn:
- return self.fragmentShaderFunction
- else:
- return self.fragmentShaderFunctionNoop
-
- @property
- def fragmentCall(self):
- """Function name to call in fragment shader"""
- return "lighting"
-
- def setupProgram(self, context, program):
- """Sets-up uniforms of a program using this shader function.
-
- :param RenderContext context: The current rendering context
- :param GLProgram program: The program to set-up.
- It MUST be in use and using this function.
- """
- if self.isOn and self._direction is not None:
- # Transform light direction from camera space to object coords
- lightdir = context.objectToCamera.transformDir(
- self._direction, direct=False)
- lightdir /= numpy.linalg.norm(lightdir)
-
- gl.glUniform3f(program.uniforms['dLight.lightDir'], *lightdir)
-
- # Convert view position to object coords
- viewpos = context.objectToCamera.transformPoint(
- numpy.array((0., 0., 0., 1.), dtype=numpy.float32),
- direct=False,
- perspectiveDivide=True)[:3]
- gl.glUniform3f(program.uniforms['dLight.viewPos'], *viewpos)
-
- gl.glUniform3f(program.uniforms['dLight.ambient'], *self.ambient)
- gl.glUniform3f(program.uniforms['dLight.diffuse'], *self.diffuse)
- gl.glUniform3f(program.uniforms['dLight.specular'], *self.specular)
- gl.glUniform1f(program.uniforms['dLight.shininess'],
- self.shininess)
-
-
-class Colormap(event.Notifier, ProgramFunction):
-
- _declTemplate = string.Template("""
- uniform sampler2D cmap_texture;
- uniform int cmap_normalization;
- uniform float cmap_parameter;
- uniform float cmap_min;
- uniform float cmap_oneOverRange;
- uniform vec4 nancolor;
-
- const float oneOverLog10 = 0.43429448190325176;
-
- vec4 colormap(float value) {
- float data = value; /* Keep original input value for isnan test */
-
- if (cmap_normalization == 1) { /* Log10 mapping */
- if (value > 0.0) {
- value = clamp(cmap_oneOverRange *
- (oneOverLog10 * log(value) - cmap_min),
- 0.0, 1.0);
- } else {
- value = 0.0;
- }
- } else if (cmap_normalization == 2) { /* Sqrt mapping */
- if (value > 0.0) {
- value = clamp(cmap_oneOverRange * (sqrt(value) - cmap_min),
- 0.0, 1.0);
- } else {
- value = 0.0;
- }
- } else if (cmap_normalization == 3) { /*Gamma correction mapping*/
- value = pow(
- clamp(cmap_oneOverRange * (value - cmap_min), 0.0, 1.0),
- cmap_parameter);
- } else if (cmap_normalization == 4) { /* arcsinh mapping */
- /* asinh = log(x + sqrt(x*x + 1) for compatibility with GLSL 1.20 */
- value = clamp(cmap_oneOverRange * (log(value + sqrt(value*value + 1.0)) - cmap_min), 0.0, 1.0);
- } else { /* Linear mapping */
- value = clamp(cmap_oneOverRange * (value - cmap_min), 0.0, 1.0);
- }
-
- $discard
-
- vec4 color;
- if (data != data) { /* isnan alternative for compatibility with GLSL 1.20 */
- color = nancolor;
- } else {
- color = texture2D(cmap_texture, vec2(value, 0.5));
- }
- return color;
- }
- """)
-
- _discardCode = """
- if (value == 0.) {
- discard;
- }
- """
-
- call = "colormap"
-
- NORMS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'
- """Tuple of supported normalizations."""
-
- _COLORMAP_TEXTURE_UNIT = 1
- """Texture unit to use for storing the colormap"""
-
- def __init__(self, colormap=None, norm='linear', gamma=0., range_=(1., 10.)):
- """Shader function to apply a colormap to a value.
-
- :param colormap: RGB(A) color look-up table (default: gray)
- :param colormap: numpy.ndarray of numpy.uint8 of dimension Nx3 or Nx4
- :param str norm: Normalization to apply: see :attr:`NORMS`.
- :param float gamma: Gamma normalization parameter
- :param range_: Range of value to map to the colormap.
- :type range_: 2-tuple of float (begin, end).
- """
- super(Colormap, self).__init__()
-
- # Init privates to default
- self._colormap = None
- self._norm = 'linear'
- self._gamma = -1.
- self._range = 1., 10.
- self._displayValuesBelowMin = True
- self._nancolor = numpy.array((1., 1., 1., 0.), dtype=numpy.float32)
-
- self._texture = None
- self._textureToDiscard = None
-
- if colormap is None:
- # default colormap
- colormap = numpy.empty((256, 3), dtype=numpy.uint8)
- colormap[:] = numpy.arange(256,
- dtype=numpy.uint8)[:, numpy.newaxis]
-
- # Set to values through properties to perform asserts and updates
- self.colormap = colormap
- self.norm = norm
- self.gamma = gamma
- self.range_ = range_
-
- @property
- def decl(self):
- """Source code of the function declaration"""
- return self._declTemplate.substitute(
- discard="" if self.displayValuesBelowMin else self._discardCode)
-
- @property
- def colormap(self):
- """Color look-up table to use."""
- return numpy.array(self._colormap, copy=True)
-
- @colormap.setter
- def colormap(self, colormap):
- colormap = numpy.array(colormap, copy=True)
- assert colormap.ndim == 2
- assert colormap.shape[1] in (3, 4)
- self._colormap = colormap
-
- if self._texture is not None and self._texture.name is not None:
- self._textureToDiscard = self._texture
-
- data = numpy.empty(
- (16, self._colormap.shape[0], self._colormap.shape[1]),
- dtype=self._colormap.dtype)
- data[:] = self._colormap
-
- format_ = gl.GL_RGBA if data.shape[-1] == 4 else gl.GL_RGB
-
- self._texture = _glutils.Texture(
- format_, data, format_,
- texUnit=self._COLORMAP_TEXTURE_UNIT,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
-
- self.notify()
-
- @property
- def nancolor(self):
- """RGBA color to use for Not-A-Number values as 4 float in [0., 1.]"""
- return self._nancolor
-
- @nancolor.setter
- def nancolor(self, color):
- color = numpy.clip(numpy.array(color, dtype=numpy.float32), 0., 1.)
- assert color.ndim == 1
- assert len(color) == 4
- if not numpy.array_equal(self._nancolor, color):
- self._nancolor = color
- self.notify()
-
- @property
- def norm(self):
- """Normalization to use for colormap mapping.
-
- One of 'linear' (the default), 'log' for log10 mapping or 'sqrt'.
- Invalid values (e.g., negative values with 'log' or 'sqrt') are mapped to 0.
- """
- return self._norm
-
- @norm.setter
- def norm(self, norm):
- if norm != self._norm:
- assert norm in self.NORMS
- self._norm = norm
- if norm in ('log', 'sqrt'):
- self.range_ = self.range_ # To test for positive range_
- self.notify()
-
- @property
- def gamma(self):
- """Gamma correction normalization parameter (float >= 0.)"""
- return self._gamma
-
- @gamma.setter
- def gamma(self, gamma):
- if gamma != self._gamma:
- assert gamma >= 0.
- self._gamma = gamma
- self.notify()
-
- @property
- def range_(self):
- """Range of values to map to the colormap.
-
- 2-tuple of floats: (begin, end).
- The begin value is mapped to the origin of the colormap and the
- end value is mapped to the other end of the colormap.
- The colormap is reversed if begin > end.
- """
- return self._range
-
- @range_.setter
- def range_(self, range_):
- assert len(range_) == 2
- range_ = float(range_[0]), float(range_[1])
-
- if self.norm == 'log' and (range_[0] <= 0. or range_[1] <= 0.):
- _logger.warning(
- "Log normalization and negative range: updating range.")
- minPos = numpy.finfo(numpy.float32).tiny
- range_ = max(range_[0], minPos), max(range_[1], minPos)
- elif self.norm == 'sqrt' and (range_[0] < 0. or range_[1] < 0.):
- _logger.warning(
- "Sqrt normalization and negative range: updating range.")
- range_ = max(range_[0], 0.), max(range_[1], 0.)
-
- if range_ != self._range:
- self._range = range_
- self.notify()
-
- @property
- def displayValuesBelowMin(self):
- """True to display values below colormap min, False to discard them.
- """
- return self._displayValuesBelowMin
-
- @displayValuesBelowMin.setter
- def displayValuesBelowMin(self, displayValuesBelowMin):
- displayValuesBelowMin = bool(displayValuesBelowMin)
- if self._displayValuesBelowMin != displayValuesBelowMin:
- self._displayValuesBelowMin = displayValuesBelowMin
- self.notify()
-
- def setupProgram(self, context, program):
- """Sets-up uniforms of a program using this shader function.
-
- :param RenderContext context: The current rendering context
- :param GLProgram program: The program to set-up.
- It MUST be in use and using this function.
- """
- self.prepareGL2(context) # TODO see how to handle
-
- self._texture.bind()
-
- gl.glUniform1i(program.uniforms['cmap_texture'],
- self._texture.texUnit)
-
- min_, max_ = self.range_
- param = 0.
- if self._norm == 'log':
- min_, max_ = numpy.log10(min_), numpy.log10(max_)
- normID = 1
- elif self._norm == 'sqrt':
- min_, max_ = numpy.sqrt(min_), numpy.sqrt(max_)
- normID = 2
- elif self._norm == 'gamma':
- # Keep min_, max_ as is
- param = self._gamma
- normID = 3
- elif self._norm == 'arcsinh':
- min_, max_ = numpy.arcsinh(min_), numpy.arcsinh(max_)
- normID = 4
- else: # Linear
- normID = 0
-
- gl.glUniform1i(program.uniforms['cmap_normalization'], normID)
- gl.glUniform1f(program.uniforms['cmap_parameter'], param)
- gl.glUniform1f(program.uniforms['cmap_min'], min_)
- gl.glUniform1f(program.uniforms['cmap_oneOverRange'],
- (1. / (max_ - min_)) if max_ != min_ else 0.)
- gl.glUniform4f(program.uniforms['nancolor'], *self._nancolor)
-
- def prepareGL2(self, context):
- if self._textureToDiscard is not None:
- self._textureToDiscard.discard()
- self._textureToDiscard = None
-
- self._texture.prepare()
diff --git a/silx/gui/plot3d/scene/interaction.py b/silx/gui/plot3d/scene/interaction.py
deleted file mode 100644
index 14a54dc..0000000
--- a/silx/gui/plot3d/scene/interaction.py
+++ /dev/null
@@ -1,701 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides interaction to plug on the scene graph."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-import logging
-import numpy
-
-from silx.gui import qt
-from silx.gui.plot.Interaction import \
- StateMachine, State, LEFT_BTN, RIGHT_BTN # , MIDDLE_BTN
-
-from . import transform
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ClickOrDrag(StateMachine):
- """Click or drag interaction for a given button.
-
- """
- #TODO: merge this class with silx.gui.plot.Interaction.ClickOrDrag
-
- DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
-
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == self.machine.button:
- self.goto('clickOrDrag', x, y)
- return True
-
- class ClickOrDrag(State):
- def enterState(self, x, y):
- self.initPos = x, y
-
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
- def onMove(self, x, y):
- dx = (x - self.initPos[0]) ** 2
- dy = (y - self.initPos[1]) ** 2
- if (dx ** 2 + dy ** 2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('drag', self.initPos, (x, y))
-
- def onRelease(self, x, y, btn):
- if btn == self.machine.button:
- self.machine.click(x, y)
- self.goto('idle')
-
- class Drag(State):
- def enterState(self, initPos, curPos):
- self.initPos = initPos
- self.machine.beginDrag(*initPos)
- self.machine.drag(*curPos)
-
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
- def onMove(self, x, y):
- self.machine.drag(x, y)
-
- def onRelease(self, x, y, btn):
- if btn == self.machine.button:
- self.machine.endDrag(self.initPos, (x, y))
- self.goto('idle')
-
- def __init__(self, button=LEFT_BTN):
- self.button = button
- states = {
- 'idle': ClickOrDrag.Idle,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- super(ClickOrDrag, self).__init__(states, 'idle')
-
- def click(self, x, y):
- """Called upon a left or right button click.
- To override in a subclass.
- """
- pass
-
- def beginDrag(self, x, y):
- """Called at the beginning of a drag gesture with left button
- pressed.
- To override in a subclass.
- """
- pass
-
- def drag(self, x, y):
- """Called on mouse moved during a drag gesture.
- To override in a subclass.
- """
- pass
-
- def endDrag(self, x, y):
- """Called at the end of a drag gesture when the left button is
- released.
- To override in a subclass.
- """
- pass
-
-
-class CameraSelectRotate(ClickOrDrag):
- """Camera rotation using an arcball-like interaction."""
-
- def __init__(self, viewport, orbitAroundCenter=True, button=RIGHT_BTN,
- selectCB=None):
- self._viewport = viewport
- self._orbitAroundCenter = orbitAroundCenter
- self._selectCB = selectCB
- self._reset()
- super(CameraSelectRotate, self).__init__(button)
-
- def _reset(self):
- self._origin, self._center = None, None
- self._startExtrinsic = None
-
- def click(self, x, y):
- if self._selectCB is not None:
- ndcZ = self._viewport._pickNdcZGL(x, y)
- position = self._viewport._getXZYGL(x, y)
- # This assume no object lie on the far plane
- # Alternative, change the depth range so that far is < 1
- if ndcZ != 1. and position is not None:
- self._selectCB((x, y, ndcZ), position)
-
- def beginDrag(self, x, y):
- centerPos = None
- if not self._orbitAroundCenter:
- # Try to use picked object position as center of rotation
- ndcZ = self._viewport._pickNdcZGL(x, y)
- if ndcZ != 1.:
- # Hit an object, use picked point as center
- centerPos = self._viewport._getXZYGL(x, y) # Can return None
-
- if centerPos is None:
- # Not using picked position, use scene center
- bounds = self._viewport.scene.bounds(transformed=True)
- centerPos = 0.5 * (bounds[0] + bounds[1])
-
- self._center = transform.Translate(*centerPos)
- self._origin = x, y
- self._startExtrinsic = self._viewport.camera.extrinsic.copy()
-
- def drag(self, x, y):
- if self._center is None:
- return
-
- dx, dy = self._origin[0] - x, self._origin[1] - y
-
- if dx == 0 and dy == 0:
- direction = self._startExtrinsic.direction
- up = self._startExtrinsic.up
- position = self._startExtrinsic.position
- else:
- minsize = min(self._viewport.size)
- distance = numpy.sqrt(dx ** 2 + dy ** 2)
- angle = distance / minsize * numpy.pi
-
- # Take care of y inversion
- direction = dx * self._startExtrinsic.side - \
- dy * self._startExtrinsic.up
- direction /= numpy.linalg.norm(direction)
- axis = numpy.cross(direction, self._startExtrinsic.direction)
- axis /= numpy.linalg.norm(axis)
-
- # Orbit start camera with current angle and axis
- # Rotate viewing direction
- rotation = transform.Rotate(numpy.degrees(angle), *axis)
- direction = rotation.transformDir(self._startExtrinsic.direction)
- up = rotation.transformDir(self._startExtrinsic.up)
-
- # Rotate position around center
- trlist = transform.StaticTransformList((
- self._center,
- rotation,
- self._center.inverse()))
- position = trlist.transformPoint(self._startExtrinsic.position)
-
- camerapos = self._viewport.camera.extrinsic
- camerapos.setOrientation(direction, up)
- camerapos.position = position
-
- def endDrag(self, x, y):
- self._reset()
-
-
-class CameraSelectPan(ClickOrDrag):
- """Picking on click and pan camera on drag."""
-
- def __init__(self, viewport, button=LEFT_BTN, selectCB=None):
- self._viewport = viewport
- self._selectCB = selectCB
- self._lastPosNdc = None
- super(CameraSelectPan, self).__init__(button)
-
- def click(self, x, y):
- if self._selectCB is not None:
- ndcZ = self._viewport._pickNdcZGL(x, y)
- position = self._viewport._getXZYGL(x, y)
- # This assume no object lie on the far plane
- # Alternative, change the depth range so that far is < 1
- if ndcZ != 1. and position is not None:
- self._selectCB((x, y, ndcZ), position)
-
- def beginDrag(self, x, y):
- ndc = self._viewport.windowToNdc(x, y)
- ndcZ = self._viewport._pickNdcZGL(x, y)
- # ndcZ is the panning plane
- if ndc is not None and ndcZ is not None:
- self._lastPosNdc = numpy.array((ndc[0], ndc[1], ndcZ, 1.),
- dtype=numpy.float32)
- else:
- self._lastPosNdc = None
-
- def drag(self, x, y):
- if self._lastPosNdc is not None:
- ndc = self._viewport.windowToNdc(x, y)
- if ndc is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], self._lastPosNdc[2], 1.),
- dtype=numpy.float32)
-
- # Convert last and current NDC positions to scene coords
- scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
- lastScenePos = self._viewport.camera.transformPoint(
- self._lastPosNdc, direct=False, perspectiveDivide=True)
-
- # Get translation in scene coords
- translation = scenePos[:3] - lastScenePos[:3]
- self._viewport.camera.extrinsic.position -= translation
-
- # Store for next drag
- self._lastPosNdc = ndcPos
-
- def endDrag(self, x, y):
- self._lastPosNdc = None
-
-
-class CameraWheel(object):
- """StateMachine like class, just handling wheel events."""
-
- # TODO choose scale of motion? Translation or Scale?
- def __init__(self, viewport, mode='center', scaleTransform=None):
- assert mode in ('center', 'position', 'scale')
- self._viewport = viewport
- if mode == 'center':
- self._zoomTo = self._zoomToCenter
- elif mode == 'position':
- self._zoomTo = self._zoomToPosition
- elif mode == 'scale':
- self._zoomTo = self._zoomByScale
- self._scale = scaleTransform
- else:
- raise ValueError('Unsupported mode: %s' % mode)
-
- def handleEvent(self, eventName, *args, **kwargs):
- if eventName == 'wheel':
- return self._zoomTo(*args, **kwargs)
-
- def _zoomToCenter(self, x, y, angleInDegrees):
- """Zoom to center of display.
-
- Only works with perspective camera.
- """
- direction = 'forward' if angleInDegrees > 0 else 'backward'
- self._viewport.camera.move(direction)
- return True
-
- def _zoomToPositionAbsolute(self, x, y, angleInDegrees):
- """Zoom while keeping pixel under mouse invariant.
-
- Only works with perspective camera.
- """
- ndc = self._viewport.windowToNdc(x, y)
- if ndc is not None:
- near = numpy.array((ndc[0], ndc[1], -1., 1.), dtype=numpy.float32)
-
- nearscene = self._viewport.camera.transformPoint(
- near, direct=False, perspectiveDivide=True)
-
- far = numpy.array((ndc[0], ndc[1], 1., 1.), dtype=numpy.float32)
- farscene = self._viewport.camera.transformPoint(
- far, direct=False, perspectiveDivide=True)
-
- dirscene = farscene[:3] - nearscene[:3]
- dirscene /= numpy.linalg.norm(dirscene)
-
- if angleInDegrees < 0:
- dirscene *= -1.
-
- # TODO which scale
- self._viewport.camera.extrinsic.position += dirscene
- return True
-
- def _zoomToPosition(self, x, y, angleInDegrees):
- """Zoom while keeping pixel under mouse invariant."""
- projection = self._viewport.camera.intrinsic
- extrinsic = self._viewport.camera.extrinsic
-
- if isinstance(projection, transform.Perspective):
- # For perspective projection, move camera
- ndc = self._viewport.windowToNdc(x, y)
- if ndc is not None:
- ndcz = self._viewport._pickNdcZGL(x, y)
-
- position = numpy.array((ndc[0], ndc[1], ndcz),
- dtype=numpy.float32)
- positionscene = self._viewport.camera.transformPoint(
- position, direct=False, perspectiveDivide=True)
-
- camtopos = extrinsic.position - positionscene
-
- step = 0.2 * (1. if angleInDegrees < 0 else -1.)
- extrinsic.position += step * camtopos
-
- elif isinstance(projection, transform.Orthographic):
- # For orthographic projection, change projection borders
- ndcx, ndcy = self._viewport.windowToNdc(x, y, checkInside=False)
-
- step = 0.2 * (1. if angleInDegrees < 0 else -1.)
-
- dx = (ndcx + 1) / 2.
- stepwidth = step * (projection.right - projection.left)
- left = projection.left - dx * stepwidth
- right = projection.right + (1. - dx) * stepwidth
-
- dy = (ndcy + 1) / 2.
- stepheight = step * (projection.top - projection.bottom)
- bottom = projection.bottom - dy * stepheight
- top = projection.top + (1. - dy) * stepheight
-
- projection.setClipping(left, right, bottom, top)
-
- else:
- raise RuntimeError('Unsupported camera', projection)
- return True
-
- def _zoomByScale(self, x, y, angleInDegrees):
- """Zoom by scaling scene (do not keep pixel under mouse invariant)."""
- scalefactor = 1.1
- if angleInDegrees < 0.:
- scalefactor = 1. / scalefactor
- self._scale.scale = scalefactor * self._scale.scale
-
- self._viewport.adjustCameraDepthExtent()
- return True
-
-
-class FocusManager(StateMachine):
- """Manages focus across multiple event handlers
-
- On press an event handler can acquire focus.
- By default it looses focus when all buttons are released.
- """
- class Idle(State):
- def onPress(self, x, y, btn):
- for eventHandler in self.machine.currentEventHandler:
- requestFocus = eventHandler.handleEvent('press', x, y, btn)
- if requestFocus:
- self.goto('focus', eventHandler, btn)
- break
-
- def _processEvent(self, *args):
- for eventHandler in self.machine.currentEventHandler:
- consumeEvent = eventHandler.handleEvent(*args)
- if consumeEvent:
- break
-
- def onMove(self, x, y):
- self._processEvent('move', x, y)
-
- def onRelease(self, x, y, btn):
- self._processEvent('release', x, y, btn)
-
- def onWheel(self, x, y, angle):
- self._processEvent('wheel', x, y, angle)
-
- class Focus(State):
- def enterState(self, eventHandler, btn):
- self.eventHandler = eventHandler
- self.focusBtns = {btn} # Set
-
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
- def onPress(self, x, y, btn):
- self.focusBtns.add(btn)
- self.eventHandler.handleEvent('press', x, y, btn)
-
- def onMove(self, x, y):
- self.eventHandler.handleEvent('move', x, y)
-
- def onRelease(self, x, y, btn):
- self.focusBtns.discard(btn)
- requestfocus = self.eventHandler.handleEvent('release', x, y, btn)
- if len(self.focusBtns) == 0 and not requestfocus:
- self.goto('idle')
-
- def onWheel(self, x, y, angleInDegrees):
- self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
-
- def __init__(self, eventHandlers=(), ctrlEventHandlers=None):
- self.defaultEventHandlers = eventHandlers
- self.ctrlEventHandlers = ctrlEventHandlers
- self.currentEventHandler = self.defaultEventHandlers
-
- states = {
- 'idle': FocusManager.Idle,
- 'focus': FocusManager.Focus
- }
- super(FocusManager, self).__init__(states, 'idle')
-
- def onKeyPress(self, key):
- if key == qt.Qt.Key_Control and self.ctrlEventHandlers is not None:
- self.currentEventHandler = self.ctrlEventHandlers
-
- def onKeyRelease(self, key):
- if key == qt.Qt.Key_Control:
- self.currentEventHandler = self.defaultEventHandlers
-
- def cancel(self):
- for handler in self.currentEventHandler:
- handler.cancel()
-
-
-class RotateCameraControl(FocusManager):
- """Combine wheel and rotate state machine for left button
- and pan when ctrl is pressed
- """
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN, selectCB))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB))
- super(RotateCameraControl, self).__init__(handlers, ctrlHandlers)
-
-
-class PanCameraControl(FocusManager):
- """Combine wheel, selectPan and rotate state machine for left button
- and rotate when ctrl is pressed"""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN, selectCB))
- super(PanCameraControl, self).__init__(handlers, ctrlHandlers)
-
-
-class CameraControl(FocusManager):
- """Combine wheel, selectPan and rotate state machine."""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB),
- CameraSelectRotate(
- viewport, orbitAroundCenter, RIGHT_BTN, selectCB))
- super(CameraControl, self).__init__(handlers)
-
-
-class PlaneRotate(ClickOrDrag):
- """Plane rotation using arcball interaction.
-
- Arcball ref.:
- Ken Shoemake. ARCBALL: A user interface for specifying three-dimensional
- orientation using a mouse. In Proc. GI '92. (1992). pp. 151-156.
- """
-
- def __init__(self, viewport, plane, button=RIGHT_BTN):
- self._viewport = viewport
- self._plane = plane
- self._reset()
- super(PlaneRotate, self).__init__(button)
-
- def _reset(self):
- self._beginNormal, self._beginCenter = None, None
-
- def click(self, x, y):
- pass # No interaction
-
- @staticmethod
- def _sphereUnitVector(radius, center, position):
- """Returns the unit vector of the projection of position on a sphere.
-
- It assumes an orthographic projection.
- For perspective projection, it gives an approximation, but it
- simplifies computations and results in consistent arcball control
- in control space.
-
- All parameters must be in screen coordinate system
- (either pixels or normalized coordinates).
-
- :param float radius: The radius of the sphere.
- :param center: (x, y) coordinates of the center.
- :param position: (x, y) coordinates of the cursor position.
- :return: Unit vector.
- :rtype: numpy.ndarray of 3 floats.
- """
- center, position = numpy.array(center), numpy.array(position)
-
- # Normalize x and y on a unit circle
- spherecoords = (position - center) / float(radius)
- squarelength = numpy.sum(spherecoords ** 2)
-
- # Project on the unit sphere and compute z coordinates
- if squarelength > 1.0: # Outside sphere: project
- spherecoords /= numpy.sqrt(squarelength)
- zsphere = 0.0
- else: # In sphere: compute z
- zsphere = numpy.sqrt(1. - squarelength)
-
- spherecoords = numpy.append(spherecoords, zsphere)
- return spherecoords
-
- def beginDrag(self, x, y):
- # Makes sure the point defining the plane is at the center as
- # it will be the center of rotation (as rotation is applied to normal)
- self._plane.plane.point = self._plane.center
-
- # Store the plane normal
- self._beginNormal = self._plane.plane.normal
-
- _logger.debug(
- 'Begin arcball, plane center %s', str(self._plane.center))
-
- # Do the arcball on the screen
- radius = min(self._viewport.size)
- if self._plane.center is None:
- self._beginCenter = None
-
- else:
- center = self._plane.objectToNDCTransform.transformPoint(
- self._plane.center, perspectiveDivide=True)
- self._beginCenter = self._viewport.ndcToWindow(
- center[0], center[1], checkInside=False)
-
- self._startVector = self._sphereUnitVector(
- radius, self._beginCenter, (x, y))
-
- def drag(self, x, y):
- if self._beginCenter is None:
- return
-
- # Compute rotation: this is twice the rotation of the arcball
- radius = min(self._viewport.size)
- currentvector = self._sphereUnitVector(
- radius, self._beginCenter, (x, y))
- crossprod = numpy.cross(self._startVector, currentvector)
- dotprod = numpy.dot(self._startVector, currentvector)
-
- quaternion = numpy.append(crossprod, dotprod)
- # Rotation was computed with Y downward, but apply in NDC, invert Y
- quaternion[1] *= -1.
-
- rotation = transform.Rotate()
- rotation.quaternion = quaternion
-
- # Convert to NDC, rotate, convert back to object
- normal = self._plane.objectToNDCTransform.transformNormal(
- self._beginNormal)
- normal = rotation.transformNormal(normal)
- normal = self._plane.objectToNDCTransform.transformNormal(
- normal, direct=False)
- self._plane.plane.normal = normal
-
- def endDrag(self, x, y):
- self._reset()
-
-
-class PlanePan(ClickOrDrag):
- """Pan a plane along its normal on drag."""
-
- def __init__(self, viewport, plane, button=LEFT_BTN):
- self._plane = plane
- self._viewport = viewport
- self._beginPlanePoint = None
- self._beginPos = None
- self._dragNdcZ = 0.
- super(PlanePan, self).__init__(button)
-
- def click(self, x, y):
- pass
-
- def beginDrag(self, x, y):
- ndc = self._viewport.windowToNdc(x, y)
- ndcZ = self._viewport._pickNdcZGL(x, y)
- # ndcZ is the panning plane
- if ndc is not None and ndcZ is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], ndcZ, 1.),
- dtype=numpy.float32)
- scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
- self._beginPos = self._plane.objectToSceneTransform.transformPoint(
- scenePos, direct=False)
- self._dragNdcZ = ndcZ
- else:
- self._beginPos = None
- self._dragNdcZ = 0.
-
- self._beginPlanePoint = self._plane.plane.point
-
- def drag(self, x, y):
- if self._beginPos is not None:
- ndc = self._viewport.windowToNdc(x, y)
- if ndc is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], self._dragNdcZ, 1.),
- dtype=numpy.float32)
-
- # Convert last and current NDC positions to scene coords
- scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
- curPos = self._plane.objectToSceneTransform.transformPoint(
- scenePos, direct=False)
-
- # Get translation in scene coords
- translation = curPos[:3] - self._beginPos[:3]
-
- newPoint = self._beginPlanePoint + translation
-
- # Keep plane point in bounds
- bounds = self._plane.parent.bounds(dataBounds=True)
- if bounds is not None:
- newPoint = numpy.clip(
- newPoint, a_min=bounds[0], a_max=bounds[1])
-
- # Only update plane if it is in some bounds
- self._plane.plane.point = newPoint
-
- def endDrag(self, x, y):
- self._beginPlanePoint = None
-
-
-class PlaneControl(FocusManager):
- """Combine wheel, selectPan and rotate state machine for plane control."""
- def __init__(self, viewport, plane,
- mode='center', scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN),
- PlaneRotate(viewport, plane, RIGHT_BTN))
- super(PlaneControl, self).__init__(handlers)
-
-
-class PanPlaneRotateCameraControl(FocusManager):
- """Combine wheel, pan plane and camera rotate state machine."""
- def __init__(self, viewport, plane,
- mode='center', scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN),
- CameraSelectRotate(viewport,
- orbitAroundCenter=False,
- button=RIGHT_BTN))
- super(PanPlaneRotateCameraControl, self).__init__(handlers)
-
-
-class PanPlaneZoomOnWheelControl(FocusManager):
- """Combine zoom on wheel and pan plane state machines."""
- def __init__(self, viewport, plane,
- mode='center',
- orbitAroundCenter=False,
- scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN))
- super(PanPlaneZoomOnWheelControl, self).__init__(handlers, ctrlHandlers)
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
deleted file mode 100644
index 7f35c3c..0000000
--- a/silx/gui/plot3d/scene/primitives.py
+++ /dev/null
@@ -1,2524 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-import ctypes
-from functools import reduce
-import logging
-import string
-
-import numpy
-
-from silx.gui.colors import rgba
-
-from ... import _glutils
-from ..._glutils import gl
-
-from . import event
-from . import core
-from . import transform
-from . import utils
-from .function import Colormap
-
-_logger = logging.getLogger(__name__)
-
-
-# Geometry ####################################################################
-
-class Geometry(core.Elem):
- """Set of vertices with normals and colors.
-
- :param str mode: OpenGL drawing mode:
- lines, line_strip, loop, triangles, triangle_strip, fan
- :param indices: Array of vertex indices or None
- :param bool copy: True (default) to copy the data, False to use as is.
- :param str attrib0: Name of the attribute that MUST be an array.
- :param attributes: Provide list of attributes as extra parameters.
- """
-
- _ATTR_INFO = {
- 'position': {'dims': (1, 2), 'lastDim': (2, 3, 4)},
- 'normal': {'dims': (1, 2), 'lastDim': (3,)},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
- }
-
- _MODE_CHECKS = { # Min, Modulo
- 'lines': (2, 2), 'line_strip': (2, 0), 'loop': (2, 0),
- 'points': (1, 0),
- 'triangles': (3, 3), 'triangle_strip': (3, 0), 'fan': (3, 0)
- }
-
- _MODES = {
- 'lines': gl.GL_LINES,
- 'line_strip': gl.GL_LINE_STRIP,
- 'loop': gl.GL_LINE_LOOP,
-
- 'points': gl.GL_POINTS,
-
- 'triangles': gl.GL_TRIANGLES,
- 'triangle_strip': gl.GL_TRIANGLE_STRIP,
- 'fan': gl.GL_TRIANGLE_FAN
- }
-
- _LINE_MODES = 'lines', 'line_strip', 'loop'
-
- _TRIANGLE_MODES = 'triangles', 'triangle_strip', 'fan'
-
- def __init__(self,
- mode,
- indices=None,
- copy=True,
- attrib0='position',
- **attributes):
- super(Geometry, self).__init__()
-
- self._attrib0 = str(attrib0)
-
- self._vbos = {} # Store current vbos
- self._unsyncAttributes = [] # Store attributes to copy to vbos
- self.__bounds = None # Cache object's bounds
- # Attribute names defining the object bounds
- self.__boundsAttributeNames = (self._attrib0,)
-
- assert mode in self._MODES
- self._mode = mode
-
- # Set attributes
- self._attributes = {}
- for name, data in attributes.items():
- self.setAttribute(name, data, copy=copy)
-
- # Set indices
- self._indices = None
- self.setIndices(indices, copy=copy)
-
- # More consistency checks
- mincheck, modulocheck = self._MODE_CHECKS[self._mode]
- if self._indices is not None:
- nbvertices = len(self._indices)
- else:
- nbvertices = self.nbVertices
-
- if nbvertices != 0:
- assert nbvertices >= mincheck
- if modulocheck != 0:
- assert (nbvertices % modulocheck) == 0
-
- @property
- def drawMode(self):
- """Kind of primitive to render, in :attr:`_MODES` (str)"""
- return self._mode
-
- @staticmethod
- def _glReadyArray(array, copy=True):
- """Making a contiguous array, checking float types.
-
- :param iterable array: array-like data to prepare for attribute
- :param bool copy: True to make a copy of the array, False to use as is
- """
- # Convert single value (int, float, numpy types) to tuple
- if not isinstance(array, abc.Iterable):
- array = (array, )
-
- # Makes sure it is an array
- array = numpy.array(array, copy=False)
-
- dtype = None
- if array.dtype.kind == 'f' and array.dtype.itemsize != 4:
- # Cast to float32
- _logger.info('Cast array to float32')
- dtype = numpy.float32
- elif array.dtype.itemsize > 4:
- # Cast (u)int64 to (u)int32
- if array.dtype.kind == 'i':
- _logger.info('Cast array to int32')
- dtype = numpy.int32
- elif array.dtype.kind == 'u':
- _logger.info('Cast array to uint32')
- dtype = numpy.uint32
-
- return numpy.array(array, dtype=dtype, order='C', copy=copy)
-
- @property
- def nbVertices(self):
- """Returns the number of vertices of current attributes.
-
- It returns None if there is no attributes.
- """
- for array in self._attributes.values():
- if len(array.shape) == 2:
- return len(array)
- return None
-
- @property
- def attrib0(self):
- """Attribute name that MUST be an array (str)"""
- return self._attrib0
-
- def setAttribute(self, name, array, copy=True):
- """Set attribute with provided array.
-
- :param str name: The name of the attribute
- :param array: Array-like attribute data or None to remove attribute
- :param bool copy: True (default) to copy the data, False to use as is
- """
- # This triggers associated GL resources to be garbage collected
- self._vbos.pop(name, None)
-
- if array is None:
- self._attributes.pop(name, None)
-
- else:
- array = self._glReadyArray(array, copy=copy)
-
- if name not in self._ATTR_INFO:
- _logger.debug('Not checking attribute %s dimensions', name)
- else:
- checks = self._ATTR_INFO[name]
-
- if (array.ndim == 1 and checks['lastDim'] == (1,) and
- len(array) > 1):
- array = array.reshape((len(array), 1))
-
- # Checks
- assert array.ndim in checks['dims'], "Attr %s" % name
- assert array.shape[-1] in checks['lastDim'], "Attr %s" % name
-
- # Makes sure attrib0 is considered as an array of values
- if name == self.attrib0 and array.ndim == 1:
- array.shape = 1, -1
-
- # Check length against another attribute array
- # Causes problems when updating
- # nbVertices = self.nbVertices
- # if array.ndim == 2 and nbVertices is not None:
- # assert len(array) == nbVertices
-
- self._attributes[name] = array
- if array.ndim == 2: # Store this in a VBO
- self._unsyncAttributes.append(name)
-
- if name in self.boundsAttributeNames: # Reset bounds
- self.__bounds = None
-
- self.notify()
-
- def getAttribute(self, name, copy=True):
- """Returns the numpy.ndarray corresponding to the name attribute.
-
- :param str name: The name of the attribute to get.
- :param bool copy: True to get a copy (default),
- False to get internal array (DO NOT MODIFY)
- :return: The corresponding array or None if no corresponding attribute.
- :rtype: numpy.ndarray
- """
- attr = self._attributes.get(name, None)
- return None if attr is None else numpy.array(attr, copy=copy)
-
- def useAttribute(self, program, name=None):
- """Enable and bind attribute(s) for a specific program.
-
- This MUST be called with OpenGL context active and after prepareGL2
- has been called.
-
- :param GLProgram program: The program for which to set the attributes
- :param str name: The attribute name to set or None to set then all
- """
- if name is None:
- for name in program.attributes:
- self.useAttribute(program, name)
-
- else:
- attribute = program.attributes.get(name)
- if attribute is None:
- return
-
- vboattrib = self._vbos.get(name)
- if vboattrib is not None:
- gl.glEnableVertexAttribArray(attribute)
- vboattrib.setVertexAttrib(attribute)
-
- elif name not in self._attributes:
- gl.glDisableVertexAttribArray(attribute)
-
- else:
- array = self._attributes[name]
- assert array is not None
-
- if array.ndim == 1:
- assert len(array) in (1, 2, 3, 4)
- gl.glDisableVertexAttribArray(attribute)
- _glVertexAttribFunc = getattr(
- _glutils.gl, 'glVertexAttrib{}f'.format(len(array)))
- _glVertexAttribFunc(attribute, *array)
- else:
- # TODO As is this is a never event, remove?
- gl.glEnableVertexAttribArray(attribute)
- gl.glVertexAttribPointer(
- attribute,
- array.shape[-1],
- _glutils.numpyToGLType(array.dtype),
- gl.GL_FALSE,
- 0,
- array)
-
- def setIndices(self, indices, copy=True):
- """Set the primitive indices to use.
-
- :param indices: Array-like of uint primitive indices or None to unset
- :param bool copy: True (default) to copy the data, False to use as is
- """
- # Trigger garbage collection of previous indices VBO if any
- self._vbos.pop('__indices__', None)
-
- if indices is None:
- self._indices = None
- else:
- indices = self._glReadyArray(indices, copy=copy).ravel()
- assert indices.dtype.name in ('uint8', 'uint16', 'uint32')
- if _logger.getEffectiveLevel() <= logging.DEBUG:
- # This might be a costy check
- assert indices.max() < self.nbVertices
- self._indices = indices
- self.notify()
-
- def getIndices(self, copy=True):
- """Returns the numpy.ndarray corresponding to the indices.
-
- :param bool copy: True to get a copy (default),
- False to get internal array (DO NOT MODIFY)
- :return: The primitive indices array or None if not set.
- :rtype: numpy.ndarray or None
- """
- if self._indices is None:
- return None
- else:
- return numpy.array(self._indices, copy=copy)
-
- @property
- def boundsAttributeNames(self):
- """Tuple of attribute names defining the bounds of the object.
-
- Attributes name are taken in the given order to compute the
- (x, y, z) the bounding box, e.g.::
-
- geometry.boundsAttributeNames = 'position'
- geometry.boundsAttributeNames = 'x', 'y', 'z'
- """
- return self.__boundsAttributeNames
-
- @boundsAttributeNames.setter
- def boundsAttributeNames(self, names):
- self.__boundsAttributeNames = tuple(str(name) for name in names)
- self.__bounds = None
- self.notify()
-
- def _bounds(self, dataBounds=False):
- if self.__bounds is None:
- if len(self.boundsAttributeNames) == 0:
- return None # No bounds
-
- self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
-
- # Coordinates defined in one or more attributes
- index = 0
- for name in self.boundsAttributeNames:
- if index == 3:
- _logger.error("Too many attributes defining bounds")
- break
-
- attribute = self._attributes[name]
- assert attribute.ndim in (1, 2)
- if attribute.ndim == 1: # Single value
- min_ = attribute
- max_ = attribute
- elif len(attribute) > 0: # Array of values, compute min/max
- min_ = numpy.nanmin(attribute, axis=0)
- max_ = numpy.nanmax(attribute, axis=0)
- else:
- min_, max_ = numpy.zeros((2, attribute.shape[1]), dtype=numpy.float32)
-
- toCopy = min(len(min_), 3-index)
- if toCopy != len(min_):
- _logger.error("Attribute defining bounds"
- " has too many dimensions")
-
- self.__bounds[0, index:index+toCopy] = min_[:toCopy]
- self.__bounds[1, index:index+toCopy] = max_[:toCopy]
-
- index += toCopy
-
- self.__bounds[numpy.isnan(self.__bounds)] = 0. # Avoid NaNs
-
- return self.__bounds.copy()
-
- def prepareGL2(self, ctx):
- # TODO manage _vbo and multiple GL context + allow to share them !
- # TODO make one or multiple VBO depending on len(vertices),
- # TODO use a general common VBO for small amount of data
- for name in self._unsyncAttributes:
- array = self._attributes[name]
- self._vbos[name] = ctx.glCtx.makeVboAttrib(array)
- self._unsyncAttributes = []
-
- if self._indices is not None and '__indices__' not in self._vbos:
- vbo = ctx.glCtx.makeVbo(self._indices,
- usage=gl.GL_STATIC_DRAW,
- target=gl.GL_ELEMENT_ARRAY_BUFFER)
- self._vbos['__indices__'] = vbo
-
- def _draw(self, program=None, nbVertices=None):
- """Perform OpenGL draw calls.
-
- :param GLProgram program:
- If not None, call :meth:`useAttribute` for this program.
- :param int nbVertices:
- The number of vertices to render or None to render all vertices.
- """
- if program is not None:
- self.useAttribute(program)
-
- if self._indices is None:
- if nbVertices is None:
- nbVertices = self.nbVertices
- gl.glDrawArrays(self._MODES[self._mode], 0, nbVertices)
- else:
- if nbVertices is None:
- nbVertices = self._indices.size
- with self._vbos['__indices__']:
- gl.glDrawElements(self._MODES[self._mode],
- nbVertices,
- _glutils.numpyToGLType(self._indices.dtype),
- ctypes.c_void_p(0))
-
-
-# Lines #######################################################################
-
-class Lines(Geometry):
- """A set of segments"""
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 normal;
- attribute vec4 color;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- void main(void)
- {
- gl_Position = matrix * vec4(position, 1.0);
- vCameraPosition = transformMat * vec4(position, 1.0);
- vPosition = position;
- vNormal = normal;
- vColor = color;
- }
- """,
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
- gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self, positions, normals=None, colors=(1., 1., 1., 1.),
- indices=None, mode='lines', width=1.):
- if mode == 'strip':
- mode = 'line_strip'
- assert mode in self._LINE_MODES
-
- self._width = width
- self._smooth = True
-
- super(Lines, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors)
-
- width = event.notifyProperty('_width', converter=float,
- doc="Width of the line in pixels.")
-
- smooth = event.notifyProperty(
- '_smooth',
- converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
-
- def renderGL2(self, ctx):
- # Prepare program
- isnormals = 'normal' in self._attributes
- if isnormals:
- fraglightfunction = ctx.viewport.light.fragmentDef
- else:
- fraglightfunction = ctx.viewport.light.fragmentShaderFunctionNoop
-
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=fraglightfunction,
- lightingCall=ctx.viewport.light.fragmentCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- if isnormals:
- ctx.viewport.light.setupProgram(ctx, prog)
-
- gl.glLineWidth(self.width)
-
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- with gl.enabled(gl.GL_LINE_SMOOTH, self._smooth):
- self._draw(prog)
-
-
-class DashedLines(Lines):
- """Set of dashed lines
-
- This MUST be defined as a set of lines (no strip or loop).
- """
-
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 origin;
- attribute vec3 normal;
- attribute vec4 color;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- uniform vec2 viewportSize; /* Width, height of the viewport */
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
- varying vec2 vOriginFragCoord;
-
- void main(void)
- {
- gl_Position = matrix * vec4(position, 1.0);
- vCameraPosition = transformMat * vec4(position, 1.0);
- vPosition = position;
- vNormal = normal;
- vColor = color;
-
- vec4 clipOrigin = matrix * vec4(origin, 1.0);
- vec4 ndcOrigin = clipOrigin / clipOrigin.w; /* Perspective divide */
- /* Convert to same frame as gl_FragCoord: lower-left, pixel center at 0.5, 0.5 */
- vOriginFragCoord = (ndcOrigin.xy + vec2(1.0, 1.0)) * 0.5 * viewportSize + vec2(0.5, 0.5);
- }
- """, # noqa
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
- varying vec2 vOriginFragCoord;
-
- uniform vec2 dash;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- /* Discard off dash fragments */
- float lineDist = distance(vOriginFragCoord, gl_FragCoord.xy);
- if (mod(lineDist, dash.x + dash.y) > dash.x) {
- discard;
- }
- gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self, positions, colors=(1., 1., 1., 1.),
- indices=None, width=1.):
- self._dash = 1, 0
- super(DashedLines, self).__init__(positions=positions,
- colors=colors,
- indices=indices,
- mode='lines',
- width=width)
-
- @property
- def dash(self):
- """Dash of the line as a 2-tuple of lengths in pixels: (on, off)"""
- return self._dash
-
- @dash.setter
- def dash(self, dash):
- dash = float(dash[0]), float(dash[1])
- if dash != self._dash:
- self._dash = dash
- self.notify()
-
- def getPositions(self, copy=True):
- """Get coordinates of lines.
-
- :param bool copy: True to get a copy, False otherwise
- :returns: Coordinates of lines
- :rtype: numpy.ndarray of float32 of shape (N, 2, Ndim)
- """
- return self.getAttribute('position', copy=copy)
-
- def setPositions(self, positions, copy=True):
- """Set line coordinates.
-
- :param positions: Array of line coordinates
- :param bool copy: True to copy input array, False to use as is
- """
- self.setAttribute('position', positions, copy=copy)
- # Update line origins from given positions
- origins = numpy.array(positions, copy=True, order='C')
- origins[1::2] = origins[::2]
- self.setAttribute('origin', origins, copy=False)
-
- def renderGL2(self, context):
- # Prepare program
- isnormals = 'normal' in self._attributes
- if isnormals:
- fraglightfunction = context.viewport.light.fragmentDef
- else:
- fraglightfunction = \
- context.viewport.light.fragmentShaderFunctionNoop
-
- fragment = self._shaders[1].substitute(
- sceneDecl=context.fragDecl,
- scenePreCall=context.fragCallPre,
- scenePostCall=context.fragCallPost,
- lightingFunction=fraglightfunction,
- lightingCall=context.viewport.light.fragmentCall)
- program = context.glCtx.prog(self._shaders[0], fragment)
- program.use()
-
- if isnormals:
- context.viewport.light.setupProgram(context, program)
-
- gl.glLineWidth(self.width)
-
- program.setUniformMatrix('matrix', context.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- context.objectToCamera.matrix,
- safe=True)
-
- gl.glUniform2f(
- program.uniforms['viewportSize'], *context.viewport.size)
- gl.glUniform2f(program.uniforms['dash'], *self.dash)
-
- context.setupProgram(program)
-
- self._draw(program)
-
-
-class Box(core.PrivateGroup):
- """Rectangular box"""
-
- _lineIndices = numpy.array((
- (0, 1), (1, 2), (2, 3), (3, 0), # Lines with z=0
- (0, 4), (1, 5), (2, 6), (3, 7), # Lines from z=0 to z=1
- (4, 5), (5, 6), (6, 7), (7, 4)), # Lines with z=1
- dtype=numpy.uint8)
-
- _faceIndices = numpy.array(
- (0, 3, 1, 2, 5, 6, 4, 7, 7, 6, 6, 2, 7, 3, 4, 0, 5, 1),
- dtype=numpy.uint8)
-
- _vertices = numpy.array((
- # Corners with z=0
- (0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- def __init__(self, stroke=(1., 1., 1., 1.), fill=(1., 1., 1., 0.)):
- super(Box, self).__init__()
-
- self._fill = Mesh3D(self._vertices,
- colors=rgba(fill),
- mode='triangle_strip',
- indices=self._faceIndices)
- self._fill.visible = self.fillColor[-1] != 0.
-
- self._stroke = Lines(self._vertices,
- indices=self._lineIndices,
- colors=rgba(stroke),
- mode='lines')
- self._stroke.visible = self.strokeColor[-1] != 0.
- self.strokeWidth = 1.
-
- self._children = [self._stroke, self._fill]
-
- self._size = 1., 1., 1.
-
- @classmethod
- def getLineIndices(cls, copy=True):
- """Returns 2D array of Box lines indices
-
- :param copy: True (default) to get a copy,
- False to get internal array (Do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(cls._lineIndices, copy=copy)
-
- @classmethod
- def getVertices(cls, copy=True):
- """Returns 2D array of Box corner coordinates.
-
- :param copy: True (default) to get a copy,
- False to get internal array (Do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(cls._vertices, copy=copy)
-
- @property
- def size(self):
- """Size of the box (sx, sy, sz)"""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 3
- size = tuple(size)
- if size != self.size:
- self._size = size
- self._fill.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self._stroke.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self.notify()
-
- @property
- def strokeSmooth(self):
- """True to draw smooth stroke, False otherwise"""
- return self._stroke.smooth
-
- @strokeSmooth.setter
- def strokeSmooth(self, smooth):
- smooth = bool(smooth)
- if smooth != self._stroke.smooth:
- self._stroke.smooth = smooth
- self.notify()
-
- @property
- def strokeWidth(self):
- """Width of the stroke (float)"""
- return self._stroke.width
-
- @strokeWidth.setter
- def strokeWidth(self, width):
- width = float(width)
- if width != self.strokeWidth:
- self._stroke.width = width
- self.notify()
-
- @property
- def strokeColor(self):
- """RGBA color of the box lines (4-tuple of float in [0, 1])"""
- return tuple(self._stroke.getAttribute('color', copy=False))
-
- @strokeColor.setter
- def strokeColor(self, color):
- color = rgba(color)
- if color != self.strokeColor:
- self._stroke.setAttribute('color', color)
- # Fully transparent = hidden
- self._stroke.visible = color[-1] != 0.
- self.notify()
-
- @property
- def fillColor(self):
- """RGBA color of the box faces (4-tuple of float in [0, 1])"""
- return tuple(self._fill.getAttribute('color', copy=False))
-
- @fillColor.setter
- def fillColor(self, color):
- color = rgba(color)
- if color != self.fillColor:
- self._fill.setAttribute('color', color)
- # Fully transparent = hidden
- self._fill.visible = color[-1] != 0.
- self.notify()
-
- @property
- def fillCulling(self):
- return self._fill.culling
-
- @fillCulling.setter
- def fillCulling(self, culling):
- self._fill.culling = culling
-
-
-class Axes(Lines):
- """3D RGB orthogonal axes"""
- _vertices = numpy.array(((0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.)),
- dtype=numpy.float32)
-
- _colors = numpy.array(((255, 0, 0, 255), (255, 0, 0, 255),
- (0, 255, 0, 255), (0, 255, 0, 255),
- (0, 0, 255, 255), (0, 0, 255, 255)),
- dtype=numpy.uint8)
-
- def __init__(self):
- super(Axes, self).__init__(self._vertices,
- colors=self._colors,
- width=3.)
- self._size = 1., 1., 1.
-
- @property
- def size(self):
- """Size of the axes (sx, sy, sz)"""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 3
- size = tuple(size)
- if size != self.size:
- self._size = size
- self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self.notify()
-
-
-class BoxWithAxes(Lines):
- """Rectangular box with RGB OX, OY, OZ axes
-
- :param color: RGBA color of the box
- """
-
- _vertices = numpy.array((
- # Axes corners
- (0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.),
- # Box corners with z=0
- (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Box corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- _axesColors = numpy.array(((1., 0., 0., 1.), (1., 0., 0., 1.),
- (0., 1., 0., 1.), (0., 1., 0., 1.),
- (0., 0., 1., 1.), (0., 0., 1., 1.)),
- dtype=numpy.float32)
-
- _lineIndices = numpy.array((
- (0, 1), (2, 3), (4, 5), # Axes lines
- (6, 7), (7, 8), # Box lines with z=0
- (6, 10), (7, 11), (8, 12), # Box lines from z=0 to z=1
- (9, 10), (10, 11), (11, 12), (12, 9)), # Box lines with z=1
- dtype=numpy.uint8)
-
- def __init__(self, color=(1., 1., 1., 1.)):
- self._color = (1., 1., 1., 1.)
- colors = numpy.ones((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
-
- super(BoxWithAxes, self).__init__(self._vertices,
- indices=self._lineIndices,
- colors=colors,
- width=2.)
- self._size = 1., 1., 1.
- self.color = color
-
- @property
- def color(self):
- """The RGBA color to use for the box: 4 float in [0, 1]"""
- return self._color
-
- @color.setter
- def color(self, color):
- color = rgba(color)
- if color != self._color:
- self._color = color
- colors = numpy.empty((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
- colors[len(self._axesColors):, :] = self._color
- self.setAttribute('color', colors) # Do the notification
-
- @property
- def size(self):
- """Size of the axes (sx, sy, sz)"""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 3
- size = tuple(size)
- if size != self.size:
- self._size = size
- self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
- self.notify()
-
-
-class PlaneInGroup(core.PrivateGroup):
- """A plane using its parent bounds to display a contour.
-
- If plane is outside the bounds of its parent, it is not visible.
-
- Cannot set the transform attribute of this primitive.
- This primitive never has any bounds.
- """
- # TODO inherit from Lines directly?, make sure the plane remains visible?
-
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
- super(PlaneInGroup, self).__init__()
- self._cache = None, None # Store bounds, vertices
- self._outline = None
-
- self._color = None
- self.color = 1., 1., 1., 1. # Set _color
- self._width = 2.
- self._strokeVisible = True
-
- self._plane = utils.Plane(point, normal)
- self._plane.addListener(self._planeChanged)
-
- def moveToCenter(self):
- """Place the plane at the center of the data, not changing orientation.
- """
- if self.parent is not None:
- bounds = self.parent.bounds(dataBounds=True)
- if bounds is not None:
- center = (bounds[0] + bounds[1]) / 2.
- _logger.debug('Moving plane to center: %s', str(center))
- self.plane.point = center
-
- @property
- def color(self):
- """Plane outline color (array of 4 float in [0, 1])."""
- return self._color.copy()
-
- @color.setter
- def color(self, color):
- self._color = numpy.array(color, copy=True, dtype=numpy.float32)
- if self._outline is not None:
- self._outline.setAttribute('color', self._color)
- self.notify() # This is OK as Lines are rebuild for each rendering
-
- @property
- def width(self):
- """Width of the plane stroke in pixels"""
- return self._width
-
- @width.setter
- def width(self, width):
- self._width = float(width)
- if self._outline is not None:
- self._outline.width = self._width # Sync width
-
- @property
- def strokeVisible(self):
- """Whether surrounding stroke is visible or not (bool)."""
- return self._strokeVisible
-
- @strokeVisible.setter
- def strokeVisible(self, visible):
- self._strokeVisible = bool(visible)
- if self._outline is not None:
- self._outline.visible = self._strokeVisible
-
- # Plane access
-
- @property
- def plane(self):
- """The plane parameters in the frame of the object."""
- return self._plane
-
- def _planeChanged(self, source):
- """Listener of plane changes: clear cache and notify listeners."""
- self._cache = None, None
- self.notify()
-
- # Disable some scene features
-
- @property
- def transforms(self):
- # Ready-only transforms to prevent using it
- return self._transforms
-
- def _bounds(self, dataBounds=False):
- # This is bound less as it uses the bounds of its parent.
- return None
-
- @property
- def contourVertices(self):
- """The vertices of the contour of the plane/bounds intersection."""
- parent = self.parent
- if parent is None:
- return None # No parent: no vertices
-
- bounds = parent.bounds(dataBounds=True)
- if bounds is None:
- return None # No bounds: no vertices
-
- # Check if cache is valid and return it
- cachebounds, cachevertices = self._cache
- if numpy.all(numpy.equal(bounds, cachebounds)):
- return cachevertices
-
- # Cache is not OK, rebuild it
- boxVertices = Box.getVertices(copy=True)
- boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
- lineIndices = Box.getLineIndices(copy=False)
- vertices = utils.boxPlaneIntersect(
- boxVertices, lineIndices, self.plane.normal, self.plane.point)
-
- self._cache = bounds, vertices if len(vertices) != 0 else None
-
- return self._cache[1]
-
- @property
- def center(self):
- """The center of the plane/bounds intersection points."""
- if not self.isValid:
- return None
- else:
- return numpy.mean(self.contourVertices, axis=0)
-
- @property
- def isValid(self):
- """True if a contour is defined, False otherwise."""
- return self.plane.isPlane and self.contourVertices is not None
-
- def prepareGL2(self, ctx):
- if self.isValid:
- if self._outline is None: # Init outline
- self._outline = Lines(self.contourVertices,
- mode='loop',
- colors=self.color)
- self._outline.width = self._width
- self._outline.visible = self._strokeVisible
- self._children.append(self._outline)
-
- # Update vertices, TODO only when necessary
- self._outline.setAttribute('position', self.contourVertices)
-
- super(PlaneInGroup, self).prepareGL2(ctx)
-
- def renderGL2(self, ctx):
- if self.isValid:
- super(PlaneInGroup, self).renderGL2(ctx)
-
-
-class BoundedGroup(core.Group):
- """Group with data bounds"""
-
- _shape = None # To provide a default value without overriding __init__
-
- @property
- def shape(self):
- """Data shape (depth, height, width) of this group or None"""
- return self._shape
-
- @shape.setter
- def shape(self, shape):
- if shape is None:
- self._shape = None
- else:
- depth, height, width = shape
- self._shape = float(depth), float(height), float(width)
-
- @property
- def size(self):
- """Data size (width, height, depth) of this group or None"""
- shape = self.shape
- if shape is None:
- return None
- else:
- return shape[2], shape[1], shape[0]
-
- @size.setter
- def size(self, size):
- if size is None:
- self.shape = None
- else:
- self.shape = size[2], size[1], size[0]
-
- def _bounds(self, dataBounds=False):
- if dataBounds and self.size is not None:
- return numpy.array(((0., 0., 0.), self.size),
- dtype=numpy.float32)
- else:
- return super(BoundedGroup, self)._bounds(dataBounds)
-
-
-# Points ######################################################################
-
-class _Points(Geometry):
- """Base class to render a set of points."""
-
- DIAMOND = 'd'
- CIRCLE = 'o'
- SQUARE = 's'
- PLUS = '+'
- X_MARKER = 'x'
- ASTERISK = '*'
- H_LINE = '_'
- V_LINE = '|'
-
- SUPPORTED_MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS,
- X_MARKER, ASTERISK, H_LINE, V_LINE)
- """List of supported markers:
-
- - 'd' diamond
- - 'o' circle
- - 's' square
- - '+' cross
- - 'x' x-cross
- - '*' asterisk
- - '_' horizontal line
- - '|' vertical line
- """
-
- _MARKER_FUNCTIONS = {
- DIAMOND: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 centerCoord = abs(coord - vec2(0.5, 0.5));
- float f = centerCoord.x + centerCoord.y;
- return clamp(size * (0.5 - f), 0.0, 1.0);
- }
- """,
- CIRCLE: """
- float alphaSymbol(vec2 coord, float size) {
- float radius = 0.5;
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (radius - r), 0.0, 1.0);
- }
- """,
- SQUARE: """
- float alphaSymbol(vec2 coord, float size) {
- return 1.0;
- }
- """,
- PLUS: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 d = abs(size * (coord - vec2(0.5, 0.5)));
- if (min(d.x, d.y) < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- X_MARKER: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 pos = floor(size * coord) + 0.5;
- vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_x.x, d_x.y) <= 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- ASTERISK: """
- float alphaSymbol(vec2 coord, float size) {
- /* Combining +, x and circle */
- vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5)));
- vec2 pos = floor(size * coord) + 0.5;
- vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_plus.x, d_plus.y) < 0.5) {
- return 1.0;
- } else if (min(d_x.x, d_x.y) <= 0.5) {
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (0.5 - r), 0.0, 1.0);
- } else {
- return 0.0;
- }
- }
- """,
- H_LINE: """
- float alphaSymbol(vec2 coord, float size) {
- float dy = abs(size * (coord.y - 0.5));
- if (dy < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- V_LINE: """
- float alphaSymbol(vec2 coord, float size) {
- float dx = abs(size * (coord.x - 0.5));
- if (dx < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """
- }
-
- _shaders = (string.Template("""
- #version 120
-
- attribute float x;
- attribute float y;
- attribute float z;
- attribute $valueType value;
- attribute float size;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
-
- varying vec4 vCameraPosition;
- varying $valueType vValue;
- varying float vSize;
-
- void main(void)
- {
- vValue = value;
-
- vec4 positionVec4 = vec4(x, y, z, 1.0);
- gl_Position = matrix * positionVec4;
- vCameraPosition = transformMat * positionVec4;
-
- gl_PointSize = size;
- vSize = size;
- }
- """),
- string.Template("""
- #version 120
-
- varying vec4 vCameraPosition;
- varying float vSize;
- varying $valueType vValue;
-
- $valueToColorDecl
- $sceneDecl
- $alphaSymbolDecl
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- float alpha = alphaSymbol(gl_PointCoord, vSize);
-
- gl_FragColor = $valueToColorCall(vValue);
- gl_FragColor.a *= alpha;
- if (gl_FragColor.a == 0.0) {
- discard;
- }
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _ATTR_INFO = {
- 'x': {'dims': (1, 2), 'lastDim': (1,)},
- 'y': {'dims': (1, 2), 'lastDim': (1,)},
- 'z': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)},
- }
-
- def __init__(self, x, y, z, value, size=1., indices=None):
- super(_Points, self).__init__('points', indices,
- x=x,
- y=y,
- z=z,
- value=value,
- size=size,
- attrib0='x')
- self.boundsAttributeNames = 'x', 'y', 'z'
- self._marker = 'o'
-
- @property
- def marker(self):
- """The marker symbol used to display the scatter plot (str)
-
- See :attr:`SUPPORTED_MARKERS` for the list of supported marker string.
- """
- return self._marker
-
- @marker.setter
- def marker(self, marker):
- marker = str(marker)
- assert marker in self.SUPPORTED_MARKERS
- if marker != self._marker:
- self._marker = marker
- self.notify()
-
- def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- raise NotImplementedError(
- "This method must be implemented in subclass")
-
- def _renderGL2PreDrawHook(self, ctx, program):
- """Override in subclass to run code before calling gl draw"""
- pass
-
- def renderGL2(self, ctx):
- valueType, valueToColorDecl, valueToColorCall = \
- self._shaderValueDefinition()
- vertexShader = self._shaders[0].substitute(
- valueType=valueType)
- fragmentShader = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- valueType=valueType,
- valueToColorDecl=valueToColorDecl,
- valueToColorCall=valueToColorCall,
- alphaSymbolDecl=self._MARKER_FUNCTIONS[self.marker])
- program = ctx.glCtx.prog(vertexShader, fragmentShader,
- attrib0=self.attrib0)
- program.use()
-
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(program)
-
- self._renderGL2PreDrawHook(ctx, program)
-
- self._draw(program)
-
-
-class Points(_Points):
- """A set of data points with an associated value and size."""
-
- _ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (1,)}})
-
- def __init__(self, x, y, z, value=0., size=1.,
- indices=None, colormap=None):
- super(Points, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=value)
-
- self._colormap = colormap or Colormap() # Default colormap
- self._colormap.addListener(self._cmapChanged)
-
- @property
- def colormap(self):
- """The colormap used to render the image"""
- return self._colormap
-
- def _cmapChanged(self, source, *args, **kwargs):
- """Broadcast colormap changes"""
- self.notify(*args, **kwargs)
-
- def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'float', self.colormap.decl, self.colormap.call
-
- def _renderGL2PreDrawHook(self, ctx, program):
- """Set-up colormap before calling gl draw"""
- self.colormap.setupProgram(ctx, program)
-
-
-class ColorPoints(_Points):
- """A set of points with an associated color and size."""
-
- _ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (3, 4)}})
-
- def __init__(self, x, y, z, color=(1., 1., 1., 1.), size=1.,
- indices=None):
- super(ColorPoints, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=color)
-
- def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'vec4', '', ''
-
- def setColor(self, color, copy=True):
- """Set colors
-
- :param color: Single RGBA color or
- 2D array of color of length number of points
- :param bool copy: True to copy colors (default),
- False to use provided array (Do not modify!)
- """
- self.setAttribute('value', color, copy=copy)
-
- def getColor(self, copy=True):
- """Returns the color or array of colors of the points.
-
- :param copy: True to get a copy (default),
- False to return internal array (Do not modify!)
- :return: Color or array of colors
- :rtype: numpy.ndarray
- """
- return self.getAttribute('value', copy=copy)
-
-
-class GridPoints(Geometry):
- # GLSL 1.30 !
- """Data points on a regular grid with an associated value and size."""
- _shaders = ("""
- #version 130
-
- in float value;
- in float size;
-
- uniform ivec3 gridDims;
- uniform mat4 matrix;
- uniform mat4 transformMat;
- uniform vec2 valRange;
-
- out vec4 vCameraPosition;
- out float vNormValue;
-
- //ivec3 coordsFromIndex(int index, ivec3 shape)
- //{
- /*Assumes that data is stored as z-major, then y, contiguous on x
- */
- // int yxPlaneSize = shape.y * shape.x; /* nb of elem in 2d yx plane */
- // int z = index / yxPlaneSize;
- // int yxIndex = index - z * yxPlaneSize; /* index in 2d yx plane */
- // int y = yxIndex / shape.x;
- // int x = yxIndex - y * shape.x;
- // return ivec3(x, y, z);
- // }
-
- ivec3 coordsFromIndex(int index, ivec3 shape)
- {
- /*Assumes that data is stored as x-major, then y, contiguous on z
- */
- int yzPlaneSize = shape.y * shape.z; /* nb of elem in 2d yz plane */
- int x = index / yzPlaneSize;
- int yzIndex = index - x * yzPlaneSize; /* index in 2d yz plane */
- int y = yzIndex / shape.z;
- int z = yzIndex - y * shape.z;
- return ivec3(x, y, z);
- }
-
- void main(void)
- {
- vNormValue = clamp((value - valRange.x) / (valRange.y - valRange.x),
- 0.0, 1.0);
-
- bool isValueInRange = value >= valRange.x && value <= valRange.y;
- if (isValueInRange) {
- /* Retrieve 3D position from gridIndex */
- vec3 coords = vec3(coordsFromIndex(gl_VertexID, gridDims));
- vec3 position = coords / max(vec3(gridDims) - 1.0, 1.0);
- gl_Position = matrix * vec4(position, 1.0);
- vCameraPosition = transformMat * vec4(position, 1.0);
- } else {
- gl_Position = vec4(2.0, 0.0, 0.0, 1.0); /* Get clipped */
- vCameraPosition = vec4(0.0, 0.0, 0.0, 0.0);
- }
-
- gl_PointSize = size;
- }
- """,
- string.Template("""
- #version 130
-
- in vec4 vCameraPosition;
- in float vNormValue;
- out vec4 gl_FragColor;
-
- $sceneDecl
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- gl_FragColor = vec4(0.5 * vNormValue + 0.5, 0.0, 0.0, 1.0);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _ATTR_INFO = {
- 'value': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)}
- }
-
- # TODO Add colormap, shape?
- # TODO could also use a texture to store values
-
- def __init__(self, values=0., shape=None, sizes=1., indices=None,
- minValue=None, maxValue=None):
- if isinstance(values, abc.Iterable):
- values = numpy.array(values, copy=False)
-
- # Test if gl_VertexID will overflow
- assert values.size < numpy.iinfo(numpy.int32).max
-
- self._shape = values.shape
- values = values.ravel() # 1D to add as a 1D vertex attribute
-
- else:
- assert shape is not None
- self._shape = tuple(shape)
-
- assert len(self._shape) in (1, 2, 3)
-
- super(GridPoints, self).__init__('points', indices,
- value=values,
- size=sizes)
-
- data = self.getAttribute('value', copy=False)
- self._minValue = data.min() if minValue is None else minValue
- self._maxValue = data.max() if maxValue is None else maxValue
-
- minValue = event.notifyProperty('_minValue')
- maxValue = event.notifyProperty('_maxValue')
-
- def _bounds(self, dataBounds=False):
- # Get bounds from values shape
- bounds = numpy.zeros((2, 3), dtype=numpy.float32)
- bounds[1, :] = self._shape
- bounds[1, :] -= 1
- return bounds
-
- def renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- gl.glUniform3i(prog.uniforms['gridDims'],
- self._shape[2] if len(self._shape) == 3 else 1,
- self._shape[1] if len(self._shape) >= 2 else 1,
- self._shape[0])
-
- gl.glUniform2f(prog.uniforms['valRange'], self.minValue, self.maxValue)
-
- self._draw(prog, nbVertices=reduce(lambda a, b: a * b, self._shape))
-
-
-# Spheres #####################################################################
-
-class Spheres(Geometry):
- """A set of spheres.
-
- Spheres are rendered as circles using points.
- This brings some limitations:
- - Do not support non-uniform scaling.
- - Assume the projection keeps ratio.
- - Do not render distorion by perspective projection.
- - If the sphere center is clipped, the whole sphere is not displayed.
- """
- # TODO check those links
- # Accounting for perspective projection
- # http://iquilezles.org/www/articles/sphereproj/sphereproj.htm
-
- # Michael Mara and Morgan McGuire.
- # 2D Polyhedral Bounds of a Clipped, Perspective-Projected 3D Sphere
- # Journal of Computer Graphics Techniques, Vol. 2, No. 2, 2013.
- # http://jcgt.org/published/0002/02/05/paper.pdf
- # https://research.nvidia.com/publication/2d-polyhedral-bounds-clipped-perspective-projected-3d-sphere
-
- # TODO some issues with small scaling and regular grid or due to sampling
-
- _shaders = ("""
- #version 120
-
- attribute vec3 position;
- attribute vec4 color;
- attribute float radius;
-
- uniform mat4 transformMat;
- uniform mat4 projMat;
- uniform vec2 screenSize;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec4 vColor;
- varying float vViewDepth;
- varying float vViewRadius;
-
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- gl_Position = projMat * vCameraPosition;
-
- vPosition = gl_Position.xyz / gl_Position.w;
-
- /* From object space radius to view space diameter.
- * Do not support non-uniform scaling */
- vec4 viewSizeVector = transformMat * vec4(2.0 * radius, 0.0, 0.0, 0.0);
- float viewSize = length(viewSizeVector.xyz);
-
- /* Convert to pixel size at the xy center of the view space */
- vec4 projSize = projMat * vec4(0.5 * viewSize, 0.0,
- vCameraPosition.z, vCameraPosition.w);
- gl_PointSize = max(1.0, screenSize[0] * projSize.x / projSize.w);
-
- vColor = color;
- vViewRadius = 0.5 * viewSize;
- vViewDepth = vCameraPosition.z;
- }
- """,
- string.Template("""
- # version 120
-
- uniform mat4 projMat;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec4 vColor;
- varying float vViewDepth;
- varying float vViewRadius;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- /* Get normal from point coords */
- vec3 normal;
- normal.xy = 2.0 * gl_PointCoord - vec2(1.0);
- normal.y *= -1.0; /*Invert y to match NDC orientation*/
- float sqLength = dot(normal.xy, normal.xy);
- if (sqLength > 1.0) { /* Length -> out of sphere */
- discard;
- }
- normal.z = sqrt(1.0 - sqLength);
-
- /*Lighting performed in NDC*/
- /*TODO update this when lighting changed*/
- //XXX vec3 position = vPosition + vViewRadius * normal;
- gl_FragColor = $lightingCall(vColor, vPosition, normal);
-
- /*Offset depth*/
- float viewDepth = vViewDepth + vViewRadius * normal.z;
- vec2 clipZW = viewDepth * projMat[2].zw + projMat[3].zw;
- gl_FragDepth = 0.5 * (clipZW.x / clipZW.y) + 0.5;
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _ATTR_INFO = {
- 'position': {'dims': (2, ), 'lastDim': (2, 3, 4)},
- 'radius': {'dims': (1, 2), 'lastDim': (1, )},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
- }
-
- def __init__(self, positions, radius=1., colors=(1., 1., 1., 1.)):
- self.__bounds = None
- super(Spheres, self).__init__('points', None,
- position=positions,
- radius=radius,
- color=colors)
-
- def renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- ctx.viewport.light.setupProgram(ctx, prog)
-
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- prog.setUniformMatrix('projMat', ctx.projection.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- gl.glUniform2f(prog.uniforms['screenSize'], *ctx.viewport.size)
-
- self._draw(prog)
-
- def _bounds(self, dataBounds=False):
- if self.__bounds is None:
- self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
- # Support vertex with to 2 to 4 coordinates
- positions = self._attributes['position']
- radius = self._attributes['radius']
- self.__bounds[0, :positions.shape[1]] = \
- (positions - radius).min(axis=0)[:3]
- self.__bounds[1, :positions.shape[1]] = \
- (positions + radius).max(axis=0)[:3]
- return self.__bounds.copy()
-
-
-# Meshes ######################################################################
-
-class Mesh3D(Geometry):
- """A conventional 3D mesh"""
-
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 normal;
- attribute vec4 color;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- //uniform mat3 matrixInvTranspose;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- //vNormal = matrixInvTranspose * normalize(normal);
- vPosition = position;
- vNormal = normal;
- vColor = color;
- gl_Position = matrix * vec4(position, 1.0);
- }
- """,
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec4 vColor;
-
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self,
- positions,
- colors,
- normals=None,
- mode='triangles',
- indices=None,
- copy=True):
- assert mode in self._TRIANGLE_MODES
- super(Mesh3D, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors,
- copy=copy)
-
- self._culling = None
-
- @property
- def culling(self):
- """Face culling (str)
-
- One of 'back', 'front' or None.
- """
- return self._culling
-
- @culling.setter
- def culling(self, culling):
- assert culling in ('back', 'front', None)
- if culling != self._culling:
- self._culling = culling
- self.notify()
-
- def renderGL2(self, ctx):
- isnormals = 'normal' in self._attributes
- if isnormals:
- fragLightFunction = ctx.viewport.light.fragmentDef
- else:
- fragLightFunction = ctx.viewport.light.fragmentShaderFunctionNoop
-
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=fragLightFunction,
- lightingCall=ctx.viewport.light.fragmentCall)
- prog = ctx.glCtx.prog(self._shaders[0], fragment)
- prog.use()
-
- if isnormals:
- ctx.viewport.light.setupProgram(ctx, prog)
-
- if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
- gl.glCullFace(cullFace)
- gl.glEnable(gl.GL_CULL_FACE)
-
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
-
- ctx.setupProgram(prog)
-
- self._draw(prog)
-
- if self.culling is not None:
- gl.glDisable(gl.GL_CULL_FACE)
-
-
-class ColormapMesh3D(Geometry):
- """A 3D mesh with color computed from a colormap"""
-
- _shaders = ("""
- attribute vec3 position;
- attribute vec3 normal;
- attribute float value;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- //uniform mat3 matrixInvTranspose;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying float vValue;
-
- void main(void)
- {
- vCameraPosition = transformMat * vec4(position, 1.0);
- //vNormal = matrixInvTranspose * normalize(normal);
- vPosition = position;
- vNormal = normal;
- vValue = value;
- gl_Position = matrix * vec4(position, 1.0);
- }
- """,
- string.Template("""
- uniform float alpha;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying float vValue;
-
- $colormapDecl
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- vec4 color = $colormapCall(vValue);
- gl_FragColor = $lightingCall(color, vPosition, vNormal);
- gl_FragColor.a *= alpha;
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- def __init__(self,
- position,
- value,
- colormap=None,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
- super(ColormapMesh3D, self).__init__(mode, indices,
- position=position,
- normal=normal,
- value=value,
- copy=copy)
-
- self._alpha = 1.0
- self._lineWidth = 1.0
- self._lineSmooth = True
- self._culling = None
- self._colormap = colormap or Colormap() # Default colormap
- self._colormap.addListener(self._cmapChanged)
-
- lineWidth = event.notifyProperty('_lineWidth', converter=float,
- doc="Width of the line in pixels.")
-
- lineSmooth = event.notifyProperty(
- '_lineSmooth',
- converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
-
- alpha = event.notifyProperty(
- '_alpha', converter=float,
- doc="Transparency of the mesh, float in [0, 1]")
-
- @property
- def culling(self):
- """Face culling (str)
-
- One of 'back', 'front' or None.
- """
- return self._culling
-
- @culling.setter
- def culling(self, culling):
- assert culling in ('back', 'front', None)
- if culling != self._culling:
- self._culling = culling
- self.notify()
-
- @property
- def colormap(self):
- """The colormap used to render the image"""
- return self._colormap
-
- def _cmapChanged(self, source, *args, **kwargs):
- """Broadcast colormap changes"""
- self.notify(*args, **kwargs)
-
- def renderGL2(self, ctx):
- if 'normal' in self._attributes:
- self._renderGL2(ctx)
- else: # Disable lighting
- with self.viewport.light.turnOff():
- self._renderGL2(ctx)
-
- def _renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall,
- colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
- program = ctx.glCtx.prog(self._shaders[0], fragment)
- program.use()
-
- ctx.viewport.light.setupProgram(ctx, program)
- ctx.setupProgram(program)
- self.colormap.setupProgram(ctx, program)
-
- if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
- gl.glCullFace(cullFace)
- gl.glEnable(gl.GL_CULL_FACE)
-
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
-
- if self.drawMode in self._LINE_MODES:
- gl.glLineWidth(self.lineWidth)
- with gl.enabled(gl.GL_LINE_SMOOTH, self.lineSmooth):
- self._draw(program)
- else:
- self._draw(program)
-
- if self.culling is not None:
- gl.glDisable(gl.GL_CULL_FACE)
-
-
-# ImageData ##################################################################
-
-class _Image(Geometry):
- """Base class for ImageData and ImageRgba"""
-
- _shaders = ("""
- attribute vec2 position;
-
- uniform mat4 matrix;
- uniform mat4 transformMat;
- uniform vec2 dataScale;
-
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec3 vNormal;
- varying vec2 vTexCoords;
-
- void main(void)
- {
- vec4 positionVec4 = vec4(position, 0.0, 1.0);
- vCameraPosition = transformMat * positionVec4;
- vPosition = positionVec4.xyz;
- vTexCoords = dataScale * position;
- gl_Position = matrix * positionVec4;
- }
- """,
- string.Template("""
- varying vec4 vCameraPosition;
- varying vec3 vPosition;
- varying vec2 vTexCoords;
- uniform sampler2D data;
- uniform float alpha;
-
- $imageDecl
- $sceneDecl
- $lightingFunction
-
- void main(void)
- {
- $scenePreCall(vCameraPosition);
-
- vec4 color = imageColor(data, vTexCoords);
- color.a *= alpha;
- if (color.a == 0.) { /* Discard fully transparent pixels */
- discard;
- }
-
- vec3 normal = vec3(0.0, 0.0, 1.0);
- gl_FragColor = $lightingCall(color, vPosition, normal);
-
- $scenePostCall(vCameraPosition);
- }
- """))
-
- _UNIT_SQUARE = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32)
-
- def __init__(self, data, copy=True):
- super(_Image, self).__init__(mode='triangle_strip',
- position=self._UNIT_SQUARE)
-
- self._texture = None
- self._update_texture = True
- self._update_texture_filter = False
- self._data = None
- self.setData(data, copy)
- self._alpha = 1.
- self._interpolation = 'linear'
-
- self.isBackfaceVisible = True
-
- def setData(self, data, copy=True):
- assert isinstance(data, numpy.ndarray)
-
- if copy:
- data = numpy.array(data, copy=True)
-
- self._data = data
- self._update_texture = True
- # By updating the position rather than always using a unit square
- # we benefit from Geometry bounds handling
- self.setAttribute('position', self._UNIT_SQUARE * (self._data.shape[1], self._data.shape[0]))
- self.notify()
-
- def getData(self, copy=True):
- return numpy.array(self._data, copy=copy)
-
- @property
- def interpolation(self):
- """The texture interpolation mode: 'linear' or 'nearest'"""
- return self._interpolation
-
- @interpolation.setter
- def interpolation(self, interpolation):
- assert interpolation in ('linear', 'nearest')
- self._interpolation = interpolation
- self._update_texture_filter = True
- self.notify()
-
- @property
- def alpha(self):
- """Transparency of the image, float in [0, 1]"""
- return self._alpha
-
- @alpha.setter
- def alpha(self, alpha):
- self._alpha = float(alpha)
- self.notify()
-
- def _textureFormat(self):
- """Implement this method to provide texture internal format and format
-
- :return: 2-tuple of gl flags (internalFormat, format)
- """
- raise NotImplementedError(
- "This method must be implemented in a subclass")
-
- def prepareGL2(self, ctx):
- if self._texture is None or self._update_texture:
- if self._texture is not None:
- self._texture.discard()
-
- if self.interpolation == 'nearest':
- filter_ = gl.GL_NEAREST
- else:
- filter_ = gl.GL_LINEAR
- self._update_texture = False
- self._update_texture_filter = False
- if self._data.size == 0:
- self._texture = None
- else:
- internalFormat, format_ = self._textureFormat()
- self._texture = _glutils.Texture(
- internalFormat,
- self._data,
- format_,
- minFilter=filter_,
- magFilter=filter_,
- wrap=gl.GL_CLAMP_TO_EDGE)
-
- if self._update_texture_filter and self._texture is not None:
- self._update_texture_filter = False
- if self.interpolation == 'nearest':
- filter_ = gl.GL_NEAREST
- else:
- filter_ = gl.GL_LINEAR
- self._texture.minFilter = filter_
- self._texture.magFilter = filter_
-
- super(_Image, self).prepareGL2(ctx)
-
- def renderGL2(self, ctx):
- if self._texture is None:
- return # Nothing to render
-
- with self.viewport.light.turnOff():
- self._renderGL2(ctx)
-
- def _renderGL2PreDrawHook(self, ctx, program):
- """Override in subclass to run code before calling gl draw"""
- pass
-
- def _shaderImageColorDecl(self):
- """Returns fragment shader imageColor function declaration"""
- raise NotImplementedError(
- "This method must be implemented in a subclass")
-
- def _renderGL2(self, ctx):
- fragment = self._shaders[1].substitute(
- sceneDecl=ctx.fragDecl,
- scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost,
- lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall,
- imageDecl=self._shaderImageColorDecl()
- )
- program = ctx.glCtx.prog(self._shaders[0], fragment)
- program.use()
-
- ctx.viewport.light.setupProgram(ctx, program)
-
- if not self.isBackfaceVisible:
- gl.glCullFace(gl.GL_BACK)
- gl.glEnable(gl.GL_CULL_FACE)
-
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
-
- shape = self._data.shape
- gl.glUniform2f(program.uniforms['dataScale'], 1./shape[1], 1./shape[0])
-
- gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
-
- ctx.setupProgram(program)
-
- self._texture.bind()
-
- self._renderGL2PreDrawHook(ctx, program)
-
- self._draw(program)
-
- if not self.isBackfaceVisible:
- gl.glDisable(gl.GL_CULL_FACE)
-
-
-class ImageData(_Image):
- """Display a 2x2 data array with a texture."""
-
- _imageDecl = string.Template("""
- $colormapDecl
-
- vec4 imageColor(sampler2D data, vec2 texCoords) {
- float value = texture2D(data, texCoords).r;
- vec4 color = $colormapCall(value);
- return color;
- }
- """)
-
- def __init__(self, data, copy=True, colormap=None):
- super(ImageData, self).__init__(data, copy=copy)
-
- self._colormap = colormap or Colormap() # Default colormap
- self._colormap.addListener(self._cmapChanged)
-
- def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C', dtype=numpy.float32)
- # TODO support (u)int8|16
- assert data.ndim == 2
-
- super(ImageData, self).setData(data, copy=False)
-
- @property
- def colormap(self):
- """The colormap used to render the image"""
- return self._colormap
-
- def _cmapChanged(self, source, *args, **kwargs):
- """Broadcast colormap changes"""
- self.notify(*args, **kwargs)
-
- def _textureFormat(self):
- return gl.GL_R32F, gl.GL_RED
-
- def _renderGL2PreDrawHook(self, ctx, program):
- self.colormap.setupProgram(ctx, program)
-
- def _shaderImageColorDecl(self):
- return self._imageDecl.substitute(
- colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
-
-
-# ImageRgba ##################################################################
-
-class ImageRgba(_Image):
- """Display a 2x2 RGBA image with a texture.
-
- Supports images of float in [0, 1] and uint8.
- """
-
- _imageDecl = """
- vec4 imageColor(sampler2D data, vec2 texCoords) {
- vec4 color = texture2D(data, texCoords);
- return color;
- }
- """
-
- def __init__(self, data, copy=True):
- super(ImageRgba, self).__init__(data, copy=copy)
-
- def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C')
- assert data.ndim == 3
- assert data.shape[2] in (3, 4)
- if data.dtype.kind == 'f':
- if data.dtype != numpy.dtype(numpy.float32):
- _logger.warning("Converting image data to float32")
- data = numpy.array(data, dtype=numpy.float32, copy=False)
- else:
- assert data.dtype == numpy.dtype(numpy.uint8)
-
- super(ImageRgba, self).setData(data, copy=False)
-
- def _textureFormat(self):
- format_ = gl.GL_RGBA if self._data.shape[2] == 4 else gl.GL_RGB
- return format_, format_
-
- def _shaderImageColorDecl(self):
- return self._imageDecl
-
-
-# Group ######################################################################
-
-# TODO lighting, clipping as groups?
-# group composition?
-
-class GroupDepthOffset(core.Group):
- """A group using 2-pass rendering and glDepthRange to avoid Z-fighting"""
-
- def __init__(self, children=(), epsilon=None):
- super(GroupDepthOffset, self).__init__(children)
- self._epsilon = epsilon
- self.isDepthRangeOn = True
-
- def prepareGL2(self, ctx):
- if self._epsilon is None:
- depthbits = gl.glGetInteger(gl.GL_DEPTH_BITS)
- self._epsilon = 1. / (1 << (depthbits - 1))
-
- def renderGL2(self, ctx):
- if self.isDepthRangeOn:
- self._renderGL2WithDepthRange(ctx)
- else:
- super(GroupDepthOffset, self).renderGL2(ctx)
-
- def _renderGL2WithDepthRange(self, ctx):
- # gl.glDepthFunc(gl.GL_LESS)
- with gl.enabled(gl.GL_CULL_FACE):
- gl.glCullFace(gl.GL_BACK)
- for child in self.children:
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
- gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
-
- child.render(ctx)
-
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
- gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
-
- child.render(ctx)
-
- gl.glCullFace(gl.GL_FRONT)
- for child in reversed(self.children):
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
- gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
-
- child.render(ctx)
-
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
- gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
-
- child.render(ctx)
-
- gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(0., 1.)
- # gl.glDepthFunc(gl.GL_LEQUAL)
- # TODO use epsilon for all rendering?
- # TODO issue with picking in depth buffer!
-
-
-class GroupNoDepth(core.Group):
- """A group rendering its children without writing to the depth buffer
-
- :param bool mask: True (default) to disable writing in the depth buffer
- :param bool notest: True (default) to disable depth test
- """
-
- def __init__(self, children=(), mask=True, notest=True):
- super(GroupNoDepth, self).__init__(children)
- self._mask = bool(mask)
- self._notest = bool(notest)
-
- def renderGL2(self, ctx):
- if self._mask:
- gl.glDepthMask(gl.GL_FALSE)
-
- with gl.disabled(gl.GL_DEPTH_TEST, disable=self._notest):
- super(GroupNoDepth, self).renderGL2(ctx)
-
- if self._mask:
- gl.glDepthMask(gl.GL_TRUE)
-
-
-class GroupBBox(core.PrivateGroup):
- """A group displaying a bounding box around the children."""
-
- def __init__(self, children=(), color=(1., 1., 1., 1.)):
- super(GroupBBox, self).__init__()
- self._group = core.Group(children)
-
- self._boxTransforms = transform.TransformList((transform.Translate(),))
-
- # Using 1 of 3 primitives to render axes and/or bounding box
- # To avoid z-fighting between axes and bounding box
- self._boxWithAxes = BoxWithAxes(color)
- self._boxWithAxes.smooth = False
- self._boxWithAxes.transforms = self._boxTransforms
-
- self._box = Box(stroke=color, fill=(1., 1., 1., 0.))
- self._box.strokeSmooth = False
- self._box.transforms = self._boxTransforms
- self._box.visible = False
-
- self._axes = Axes()
- self._axes.smooth = False
- self._axes.transforms = self._boxTransforms
- self._axes.visible = False
-
- self.strokeWidth = 2.
-
- self._children = [self._boxWithAxes, self._box, self._axes, self._group]
-
- def _updateBoxAndAxes(self):
- """Update bbox and axes position and size according to children."""
- bounds = self._group.bounds(dataBounds=True)
- if bounds is not None:
- origin = bounds[0]
- size = bounds[1] - bounds[0]
- else:
- origin, size = (0., 0., 0.), (1., 1., 1.)
-
- self._boxTransforms[0].translation = origin
-
- self._boxWithAxes.size = size
- self._box.size = size
- self._axes.size = size
-
- def _bounds(self, dataBounds=False):
- self._updateBoxAndAxes()
- return super(GroupBBox, self)._bounds(dataBounds)
-
- def prepareGL2(self, ctx):
- self._updateBoxAndAxes()
- super(GroupBBox, self).prepareGL2(ctx)
-
- # Give access to _group children
-
- @property
- def children(self):
- return self._group.children
-
- @children.setter
- def children(self, iterable):
- self._group.children = iterable
-
- # Give access to box color and stroke width
-
- @property
- def color(self):
- """The RGBA color to use for the box: 4 float in [0, 1]"""
- return self._box.strokeColor
-
- @color.setter
- def color(self, color):
- self._box.strokeColor = color
- self._boxWithAxes.color = color
-
- @property
- def strokeWidth(self):
- """The width of the stroke lines in pixels (float)"""
- return self._box.strokeWidth
-
- @strokeWidth.setter
- def strokeWidth(self, width):
- width = float(width)
- self._box.strokeWidth = width
- self._boxWithAxes.width = width
- self._axes.width = width
-
- # Toggle axes visibility
-
- def _updateBoxAndAxesVisibility(self, axesVisible, boxVisible):
- """Update visible flags of box and axes primitives accordingly.
-
- :param bool axesVisible: True to display axes
- :param bool boxVisible: True to display bounding box
- """
- self._boxWithAxes.visible = boxVisible and axesVisible
- self._box.visible = boxVisible and not axesVisible
- self._axes.visible = not boxVisible and axesVisible
-
- @property
- def axesVisible(self):
- """Whether axes are displayed or not (bool)"""
- return self._boxWithAxes.visible or self._axes.visible
-
- @axesVisible.setter
- def axesVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=bool(visible),
- boxVisible=self.boxVisible)
-
- @property
- def boxVisible(self):
- """Whether bounding box is displayed or not (bool)"""
- return self._boxWithAxes.visible or self._box.visible
-
- @boxVisible.setter
- def boxVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=self.axesVisible,
- boxVisible=bool(visible))
-
-
-# Clipping Plane ##############################################################
-
-class ClipPlane(PlaneInGroup):
- """A clipping plane attached to a box"""
-
- def renderGL2(self, ctx):
- super(ClipPlane, self).renderGL2(ctx)
-
- if self.visible:
- # Set-up clipping plane for following brothers
-
- # No need of perspective divide, no projection
- point = ctx.objectToCamera.transformPoint(self.plane.point,
- perspectiveDivide=False)
- normal = ctx.objectToCamera.transformNormal(self.plane.normal)
- ctx.setClipPlane(point, normal)
-
- def postRender(self, ctx):
- if self.visible:
- # Disable clip planes
- ctx.setClipPlane()
diff --git a/silx/gui/plot3d/scene/test/__init__.py b/silx/gui/plot3d/scene/test/__init__.py
deleted file mode 100644
index fc4621e..0000000
--- a/silx/gui/plot3d/scene/test/__init__.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-import unittest
-
-from .test_transform import suite as test_transform_suite
-from .test_utils import suite as test_utils_suite
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(test_transform_suite())
- testsuite.addTest(test_utils_suite())
- return testsuite
diff --git a/silx/gui/plot3d/scene/test/test_transform.py b/silx/gui/plot3d/scene/test/test_transform.py
deleted file mode 100644
index 9ea0af1..0000000
--- a/silx/gui/plot3d/scene/test/test_transform.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "05/01/2017"
-
-
-import numpy
-import unittest
-
-from silx.gui.plot3d.scene import transform
-
-
-class TestTransformList(unittest.TestCase):
-
- def assertSameArrays(self, a, b):
- return self.assertTrue(numpy.allclose(a, b, atol=1e-06))
-
- def testTransformList(self):
- """Minimalistic test of TransformList"""
- transforms = transform.TransformList()
- refmatrix = numpy.identity(4, dtype=numpy.float32)
- self.assertSameArrays(refmatrix, transforms.matrix)
-
- # Append translate
- transforms.append(transform.Translate(1., 1., 1.))
- refmatrix = numpy.array(((1., 0., 0., 1.),
- (0., 1., 0., 1.),
- (0., 0., 1., 1.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
- self.assertSameArrays(refmatrix, transforms.matrix)
-
- # Extend scale
- transforms.extend([transform.Scale(0.1, 2., 1.)])
- refmatrix = numpy.dot(refmatrix,
- numpy.array(((0.1, 0., 0., 0.),
- (0., 2., 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)),
- dtype=numpy.float32))
- self.assertSameArrays(refmatrix, transforms.matrix)
-
- # Insert rotate
- transforms.insert(0, transform.Rotate(360.))
- self.assertSameArrays(refmatrix, transforms.matrix)
-
- # Update translate and check for listener called
- self._callCount = 0
-
- def listener(source):
- self._callCount += 1
- transforms.addListener(listener)
-
- transforms[1].tx += 1
- self.assertEqual(self._callCount, 1)
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestTransformList))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/scene/test/test_utils.py b/silx/gui/plot3d/scene/test/test_utils.py
deleted file mode 100644
index 4a2d515..0000000
--- a/silx/gui/plot3d/scene/test/test_utils.py
+++ /dev/null
@@ -1,275 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/01/2018"
-
-
-import unittest
-from silx.utils.testutils import ParametricTestCase
-
-import numpy
-
-from silx.gui.plot3d.scene import utils
-
-
-# angleBetweenVectors #########################################################
-
-class TestAngleBetweenVectors(ParametricTestCase):
-
- TESTS = { # name: (refvector, vectors, norm, refangles)
- 'single vector':
- ((1., 0., 0.), (1., 0., 0.), (0., 0., 1.), 0.),
- 'single vector, no norm':
- ((1., 0., 0.), (1., 0., 0.), None, 0.),
-
- 'with orthogonal norm':
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- (0., 0., 1.),
- (0., 90., 180., 270.)),
-
- 'with coplanar norm': # = similar to no norm
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- (1., 0., 0.),
- (0., 90., 180., 90.)),
-
- 'without norm':
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- None,
- (0., 90., 180., 90.)),
-
- 'not unit vectors':
- ((2., 2., 0.), ((1., 1., 0.), (1., -1., 0.)), None, (0., 90.)),
- }
-
- def testAngleBetweenVectorsFunction(self):
- for name, params in self.TESTS.items():
- refvector, vectors, norm, refangles = params
- with self.subTest(name):
- refangles = numpy.radians(refangles)
-
- refvector = numpy.array(refvector)
- vectors = numpy.array(vectors)
- if norm is not None:
- norm = numpy.array(norm)
-
- testangles = utils.angleBetweenVectors(
- refvector, vectors, norm)
-
- self.assertTrue(
- numpy.allclose(testangles, refangles, atol=1e-5))
-
-
-# Plane #######################################################################
-
-class AssertNotificationContext(object):
- """Context that checks if an event.Notifier is sending events."""
-
- def __init__(self, notifier, count=1):
- """Initializer.
-
- :param event.Notifier notifier: The notifier to test.
- :param int count: The expected number of calls.
- """
- self._notifier = notifier
- self._callCount = None
- self._count = count
-
- def __enter__(self):
- self._callCount = 0
- self._notifier.addListener(self._callback)
-
- def __exit__(self, exc_type, exc_value, traceback):
- # Do not return True so exceptions are propagated
- self._notifier.removeListener(self._callback)
- assert self._callCount == self._count
- self._callCount = None
-
- def _callback(self, *args, **kwargs):
- self._callCount += 1
-
-
-class TestPlaneParameters(ParametricTestCase):
- """Test Plane.parameters read/write and notifications."""
-
- PARAMETERS = {
- 'unit normal': (1., 0., 0., 1.),
- 'not unit normal': (1., 1., 0., 1.),
- 'd = 0': (1., 0., 0., 0.)
- }
-
- def testParameters(self):
- """Check parameters read/write and notification."""
- plane = utils.Plane()
-
- for name, parameters in self.PARAMETERS.items():
- with self.subTest(name, parameters=parameters):
- with AssertNotificationContext(plane):
- plane.parameters = parameters
-
- # Plane parameters are converted to have a unit normal
- normparams = parameters / numpy.linalg.norm(parameters[:3])
- self.assertTrue(numpy.allclose(plane.parameters, normparams))
-
- ZEROS_PARAMETERS = (
- (0., 0., 0., 0.),
- (0., 0., 0., 1.)
- )
-
- ZEROS = 0., 0., 0., 0.
-
- def testParametersNoPlane(self):
- """Test Plane.parameters with ||normal|| == 0 ."""
- plane = utils.Plane()
- plane.parameters = self.ZEROS
-
- for parameters in self.ZEROS_PARAMETERS:
- with self.subTest(parameters=parameters):
- with AssertNotificationContext(plane, count=0):
- plane.parameters = parameters
- self.assertTrue(
- numpy.allclose(plane.parameters, self.ZEROS, 0., 0.))
-
-
-# unindexArrays ###############################################################
-
-class TestUnindexArrays(ParametricTestCase):
- """Test unindexArrays function."""
-
- def testBasicModes(self):
- """Test for modes: points, lines and triangles"""
- indices = numpy.array((1, 2, 0))
- arrays = (numpy.array((0., 1., 2.)),
- numpy.array(((0, 0), (1, 1), (2, 2))))
- refresults = (numpy.array((1., 2., 0.)),
- numpy.array(((1, 1), (2, 2), (0, 0))))
-
- for mode in ('points', 'lines', 'triangles'):
- with self.subTest(mode=mode):
- testresults = utils.unindexArrays(mode, indices, *arrays)
- for ref, test in zip(refresults, testresults):
- self.assertTrue(numpy.equal(ref, test).all())
-
- def testPackedLines(self):
- """Test for modes: line_strip, loop"""
- indices = numpy.array((1, 2, 0))
- arrays = (numpy.array((0., 1., 2.)),
- numpy.array(((0, 0), (1, 1), (2, 2))))
- results = {
- 'line_strip': (
- numpy.array((1., 2., 2., 0.)),
- numpy.array(((1, 1), (2, 2), (2, 2), (0, 0)))),
- 'loop': (
- numpy.array((1., 2., 2., 0., 0., 1.)),
- numpy.array(((1, 1), (2, 2), (2, 2), (0, 0), (0, 0), (1, 1)))),
- }
-
- for mode, refresults in results.items():
- with self.subTest(mode=mode):
- testresults = utils.unindexArrays(mode, indices, *arrays)
- for ref, test in zip(refresults, testresults):
- self.assertTrue(numpy.equal(ref, test).all())
-
- def testPackedTriangles(self):
- """Test for modes: triangle_strip, fan"""
- indices = numpy.array((1, 2, 0, 3))
- arrays = (numpy.array((0., 1., 2., 3.)),
- numpy.array(((0, 0), (1, 1), (2, 2), (3, 3))))
- results = {
- 'triangle_strip': (
- numpy.array((1., 2., 0., 2., 0., 3.)),
- numpy.array(((1, 1), (2, 2), (0, 0), (2, 2), (0, 0), (3, 3)))),
- 'fan': (
- numpy.array((1., 2., 0., 1., 0., 3.)),
- numpy.array(((1, 1), (2, 2), (0, 0), (1, 1), (0, 0), (3, 3)))),
- }
-
- for mode, refresults in results.items():
- with self.subTest(mode=mode):
- testresults = utils.unindexArrays(mode, indices, *arrays)
- for ref, test in zip(refresults, testresults):
- self.assertTrue(numpy.equal(ref, test).all())
-
- def testBadIndices(self):
- """Test with negative indices and indices higher than array length"""
- arrays = numpy.array((0, 1)), numpy.array((0, 1, 2))
-
- # negative indices
- with self.assertRaises(AssertionError):
- utils.unindexArrays('points', (-1, 0), *arrays)
-
- # Too high indices
- with self.assertRaises(AssertionError):
- utils.unindexArrays('points', (0, 10), *arrays)
-
-
-# triangleNormals #############################################################
-
-class TestTriangleNormals(ParametricTestCase):
- """Test triangleNormals function."""
-
- def test(self):
- """Test for modes: points, lines and triangles"""
- positions = numpy.array(
- ((0., 0., 0.), (1., 0., 0.), (0., 1., 0.), # normal = Z
- (1., 1., 1.), (1., 2., 3.), (4., 5., 6.), # Random triangle
- # Degenerated triangles:
- (0., 0., 0.), (1., 0., 0.), (2., 0., 0.), # Colinear points
- (1., 1., 1.), (1., 1., 1.), (1., 1., 1.), # All same point
- ),
- dtype='float32')
-
- normals = numpy.array(
- ((0., 0., 1.),
- (-0.40824829, 0.81649658, -0.40824829),
- (0., 0., 0.),
- (0., 0., 0.)),
- dtype='float32')
-
- testnormals = utils.trianglesNormal(positions)
- self.assertTrue(numpy.allclose(testnormals, normals))
-
-
-# suite #######################################################################
-
-def suite():
- testsuite = unittest.TestSuite()
- for test in (TestAngleBetweenVectors,
- TestPlaneParameters,
- TestUnindexArrays,
- TestTriangleNormals):
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(test))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/scene/text.py b/silx/gui/plot3d/scene/text.py
deleted file mode 100644
index bacc2e6..0000000
--- a/silx/gui/plot3d/scene/text.py
+++ /dev/null
@@ -1,535 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 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.
-#
-# ###########################################################################*/
-"""Primitive displaying a text field in the scene."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import logging
-import numpy
-
-from silx.gui.colors import rgba
-
-from ... import _glutils
-from ..._glutils import gl
-
-from ..._glutils import font as _font
-from ...plot._utils import ticklayout
-
-from . import event, primitives, core, transform
-
-
-_logger = logging.getLogger(__name__)
-
-
-class Font(event.Notifier):
- """Description of a font.
-
- :param str name: Family of the font
- :param int size: Size of the font in points
- :param int weight: Font weight
- :param bool italic: True for italic font, False (default) otherwise
- """
-
- def __init__(self, name=None, size=-1, weight=-1, italic=False):
- self._name = name if name is not None else _font.getDefaultFontFamily()
- self._size = size
- self._weight = weight
- self._italic = italic
- super(Font, self).__init__()
-
- name = event.notifyProperty(
- '_name',
- doc="""Name of the font (str)""",
- converter=str)
-
- size = event.notifyProperty(
- '_size',
- doc="""Font size in points (int)""",
- converter=int)
-
- weight = event.notifyProperty(
- '_weight',
- doc="""Font size in points (int)""",
- converter=int)
-
- italic = event.notifyProperty(
- '_italic',
- doc="""True for italic (bool)""",
- converter=bool)
-
-
-class Text2D(primitives.Geometry):
- """Text field as a 2D texture displayed with bill-boarding
-
- :param str text: Text to display
- :param Font font: The font to use
- """
-
- # Text anchor values
- CENTER = 'center'
-
- LEFT = 'left'
- RIGHT = 'right'
-
- TOP = 'top'
- BASELINE = 'baseline'
- BOTTOM = 'bottom'
-
- _ALIGN = LEFT, CENTER, RIGHT
- _VALIGN = TOP, BASELINE, CENTER, BOTTOM
-
- _rasterTextCache = {}
- """Internal cache storing already rasterized text"""
- # TODO limit cache size and discard least recent used
-
- def __init__(self, text='', font=None):
- self._dirtyTexture = True
- self._dirtyAlign = True
- self._baselineOffset = 0
- self._text = text
- self._font = font if font is not None else Font()
- self._foreground = 1., 1., 1., 1.
- self._background = 0., 0., 0., 0.
- self._overlay = False
- self._align = 'left'
- self._valign = 'baseline'
- self._devicePixelRatio = 1.0 # Store it to check for changes
-
- self._texture = None
- self._textureDirty = True
-
- super(Text2D, self).__init__(
- 'triangle_strip',
- copy=False,
- # Keep an array for position as it is bound to attr 0 and MUST
- # be active and an array at least on Mac OS X
- position=numpy.zeros((4, 3), dtype=numpy.float32),
- vertexID=numpy.arange(4., dtype=numpy.float32).reshape(4, 1),
- offsetInViewportCoords=(0., 0.))
-
- @property
- def text(self):
- """Text displayed by this primitive (str)"""
- return self._text
-
- @text.setter
- def text(self, text):
- text = str(text)
- if text != self._text:
- self._dirtyTexture = True
- self._text = text
- self.notify()
-
- @property
- def font(self):
- """Font to use to raster text (Font)"""
- return self._font
-
- @font.setter
- def font(self, font):
- self._font.removeListener(self._fontChanged)
- self._font = font
- self._font.addListener(self._fontChanged)
- self._fontChanged(self) # Which calls notify and primitive as dirty
-
- def _fontChanged(self, source):
- """Listen for font change"""
- self._dirtyTexture = True
- self.notify()
-
- foreground = event.notifyProperty(
- '_foreground', doc="""RGBA color of the text: 4 float in [0, 1]""",
- converter=rgba)
-
- background = event.notifyProperty(
- '_background',
- doc="RGBA background color of the text field: 4 float in [0, 1]",
- converter=rgba)
-
- overlay = event.notifyProperty(
- '_overlay',
- doc="True to always display text on top of the scene (default: False)",
- converter=bool)
-
- def _setAlign(self, align):
- assert align in self._ALIGN
- self._align = align
- self._dirtyAlign = True
- self.notify()
-
- align = property(
- lambda self: self._align,
- _setAlign,
- doc="""Horizontal anchor position of the text field (str).
-
- Either 'left' (default), 'center' or 'right'.""")
-
- def _setVAlign(self, valign):
- assert valign in self._VALIGN
- self._valign = valign
- self._dirtyAlign = True
- self.notify()
-
- valign = property(
- lambda self: self._valign,
- _setVAlign,
- doc="""Vertical anchor position of the text field (str).
-
- Either 'top', 'baseline' (default), 'center' or 'bottom'""")
-
- def _raster(self, devicePixelRatio):
- """Raster current primitive to a bitmap
-
- :param float devicePixelRatio:
- The ratio between device and device-independent pixels
- :return: Corresponding image in grayscale and baseline offset from top
- :rtype: (HxW numpy.ndarray of uint8, int)
- """
- params = (self.text,
- self.font.name,
- self.font.size,
- self.font.weight,
- self.font.italic,
- devicePixelRatio)
-
- if params not in self._rasterTextCache: # Add to cache
- self._rasterTextCache[params] = _font.rasterText(*params)
-
- array, offset = self._rasterTextCache[params]
- return array.copy(), offset
-
- def _bounds(self, dataBounds=False):
- return None
-
- def prepareGL2(self, context):
- # Check if devicePixelRatio has changed since last rendering
- devicePixelRatio = context.glCtx.devicePixelRatio
- if self._devicePixelRatio != devicePixelRatio:
- self._devicePixelRatio = devicePixelRatio
- self._dirtyTexture = True
-
- if self._dirtyTexture:
- self._dirtyTexture = False
-
- if self._texture is not None:
- self._texture.discard()
- self._texture = None
- self._baselineOffset = 0
-
- if self.text:
- image, self._baselineOffset = self._raster(
- self._devicePixelRatio)
- self._texture = _glutils.Texture(
- gl.GL_R8, image, gl.GL_RED,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
- self._texture.prepare()
- self._dirtyAlign = True # To force update of offset
-
- if self._dirtyAlign:
- self._dirtyAlign = False
-
- if self._texture is not None:
- height, width = self._texture.shape
-
- if self._align == 'left':
- ox = 0.
- elif self._align == 'center':
- ox = - width // 2
- elif self._align == 'right':
- ox = - width
- else:
- _logger.error("Unsupported align: %s", self._align)
- ox = 0.
-
- if self._valign == 'top':
- oy = 0.
- elif self._valign == 'baseline':
- oy = self._baselineOffset
- elif self._valign == 'center':
- oy = height // 2
- elif self._valign == 'bottom':
- oy = height
- else:
- _logger.error("Unsupported valign: %s", self._valign)
- oy = 0.
-
- offsets = (ox, oy) + numpy.array(
- ((0., 0.), (width, 0.), (0., -height), (width, -height)),
- dtype=numpy.float32)
- self.setAttribute('offsetInViewportCoords', offsets)
-
- super(Text2D, self).prepareGL2(context)
-
- def renderGL2(self, context):
- if not self.text:
- return # Nothing to render
-
- program = context.glCtx.prog(*self._shaders)
- program.use()
-
- program.setUniformMatrix('matrix', context.objectToNDC.matrix)
- gl.glUniform2f(
- program.uniforms['viewportSize'], *context.viewport.size)
- gl.glUniform4f(program.uniforms['foreground'], *self.foreground)
- gl.glUniform4f(program.uniforms['background'], *self.background)
- gl.glUniform1i(program.uniforms['texture'], self._texture.texUnit)
- gl.glUniform1i(program.uniforms['isOverlay'],
- 1 if self._overlay else 0)
-
- self._texture.bind()
-
- if not self._overlay or not gl.glGetBoolean(gl.GL_DEPTH_TEST):
- self._draw(program)
- else: # overlay and depth test currently enabled
- gl.glDisable(gl.GL_DEPTH_TEST)
- self._draw(program)
- gl.glEnable(gl.GL_DEPTH_TEST)
-
- # TODO texture atlas + viewportSize as attribute to chain text rendering
-
- _shaders = (
- """
- attribute vec3 position;
- attribute vec2 offsetInViewportCoords; /* Offset in pixels (y upward) */
- attribute float vertexID; /* Index of rectangle corner */
-
- uniform mat4 matrix;
- uniform vec2 viewportSize; /* Width, height of the viewport */
- uniform int isOverlay;
-
- varying vec2 texCoords;
-
- void main(void)
- {
- vec4 clipPos = matrix * vec4(position, 1.0);
- vec4 ndcPos = clipPos / clipPos.w; /* Perspective divide */
-
- /* Align ndcPos with pixels in viewport-like coords (origin useless) */
- vec2 viewportPos = floor((ndcPos.xy + vec2(1.0, 1.0)) * 0.5 * viewportSize);
-
- /* Apply offset in viewport coords */
- viewportPos += offsetInViewportCoords;
-
- /* Convert back to NDC */
- vec2 pointPos = 2.0 * viewportPos / viewportSize - vec2(1.0, 1.0);
- float z = (isOverlay != 0) ? -1.0 : ndcPos.z;
- gl_Position = vec4(pointPos, z, 1.0);
-
- /* Index : texCoords:
- * 0: (0., 0.)
- * 1: (1., 0.)
- * 2: (0., 1.)
- * 3: (1., 1.)
- */
- texCoords = vec2(vertexID == 0.0 || vertexID == 2.0 ? 0.0 : 1.0,
- vertexID < 1.5 ? 0.0 : 1.0);
- }
- """, # noqa
-
- """
- varying vec2 texCoords;
-
- uniform vec4 foreground;
- uniform vec4 background;
- uniform sampler2D texture;
-
- void main(void)
- {
- float value = texture2D(texture, texCoords).r;
-
- if (background.a != 0.0) {
- gl_FragColor = mix(background, foreground, value);
- } else {
- gl_FragColor = foreground;
- gl_FragColor.a *= value;
- if (gl_FragColor.a <= 0.01) {
- discard;
- }
- }
- }
- """)
-
-
-class LabelledAxes(primitives.GroupBBox):
- """A group displaying a bounding box with axes labels around its children.
- """
-
- def __init__(self):
- super(LabelledAxes, self).__init__()
- self._ticksForBounds = None
-
- self._font = Font()
-
- # TODO offset labels from anchor in pixels
-
- self._xlabel = Text2D(font=self._font)
- self._xlabel.align = 'center'
- self._xlabel.transforms = [self._boxTransforms,
- transform.Translate(tx=0.5)]
- self._children.append(self._xlabel)
-
- self._ylabel = Text2D(font=self._font)
- self._ylabel.align = 'center'
- self._ylabel.transforms = [self._boxTransforms,
- transform.Translate(ty=0.5)]
- self._children.append(self._ylabel)
-
- self._zlabel = Text2D(font=self._font)
- self._zlabel.align = 'center'
- self._zlabel.transforms = [self._boxTransforms,
- transform.Translate(tz=0.5)]
- self._children.append(self._zlabel)
-
- self._tickLines = primitives.Lines( # Init tick lines with dummy pos
- positions=((0., 0., 0.), (0., 0., 0.)),
- mode='lines')
- self._tickLines.visible = False
- self._children.append(self._tickLines)
-
- self._tickLabels = core.Group()
- self._children.append(self._tickLabels)
-
- @property
- def font(self):
- """Font of axes text labels (Font)"""
- return self._font
-
- @font.setter
- def font(self, font):
- self._font = font
- self._xlabel.font = font
- self._ylabel.font = font
- self._zlabel.font = font
- for label in self._tickLabels.children:
- label.font = font
-
- @property
- def xlabel(self):
- """Text label of the X axis (str)"""
- return self._xlabel.text
-
- @xlabel.setter
- def xlabel(self, text):
- self._xlabel.text = text
-
- @property
- def ylabel(self):
- """Text label of the Y axis (str)"""
- return self._ylabel.text
-
- @ylabel.setter
- def ylabel(self, text):
- self._ylabel.text = text
-
- @property
- def zlabel(self):
- """Text label of the Z axis (str)"""
- return self._zlabel.text
-
- @zlabel.setter
- def zlabel(self, text):
- self._zlabel.text = text
-
- def _updateTicks(self):
- """Check if ticks need update and update them if needed."""
- bounds = self._group.bounds(transformed=False, dataBounds=True)
- if bounds is None: # No content
- if self._ticksForBounds is not None:
- self._ticksForBounds = None
- self._tickLines.visible = False
- self._tickLabels.children = [] # Reset previous labels
-
- elif (self._ticksForBounds is None or
- not numpy.all(numpy.equal(bounds, self._ticksForBounds))):
- self._ticksForBounds = bounds
-
- # Update ticks
- # TODO make ticks having a constant length on the screen
- ticklength = numpy.abs(bounds[1] - bounds[0]) / 20.
-
- xticks, xlabels = ticklayout.ticks(*bounds[:, 0])
- yticks, ylabels = ticklayout.ticks(*bounds[:, 1])
- zticks, zlabels = ticklayout.ticks(*bounds[:, 2])
-
- # Update tick lines
- coords = numpy.empty(
- ((len(xticks) + len(yticks) + len(zticks)), 4, 3),
- dtype=numpy.float32)
- coords[:, :, :] = bounds[0, :] # account for offset from origin
-
- xcoords = coords[:len(xticks)]
- xcoords[:, :, 0] = numpy.asarray(xticks)[:, numpy.newaxis]
- xcoords[:, 1, 1] += ticklength[1] # X ticks on XY plane
- xcoords[:, 3, 2] += ticklength[2] # X ticks on XZ plane
-
- ycoords = coords[len(xticks):len(xticks) + len(yticks)]
- ycoords[:, :, 1] = numpy.asarray(yticks)[:, numpy.newaxis]
- ycoords[:, 1, 0] += ticklength[0] # Y ticks on XY plane
- ycoords[:, 3, 2] += ticklength[2] # Y ticks on YZ plane
-
- zcoords = coords[len(xticks) + len(yticks):]
- zcoords[:, :, 2] = numpy.asarray(zticks)[:, numpy.newaxis]
- zcoords[:, 1, 0] += ticklength[0] # Z ticks on XZ plane
- zcoords[:, 3, 1] += ticklength[1] # Z ticks on YZ plane
-
- self._tickLines.setAttribute('position', coords.reshape(-1, 3))
- self._tickLines.visible = True
-
- # Update labels
- offsets = bounds[0] - ticklength
- labels = []
- for tick, label in zip(xticks, xlabels):
- text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=tick, ty=offsets[1], tz=offsets[2])]
- labels.append(text)
-
- for tick, label in zip(yticks, ylabels):
- text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=offsets[0], ty=tick, tz=offsets[2])]
- labels.append(text)
-
- for tick, label in zip(zticks, zlabels):
- text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=offsets[0], ty=offsets[1], tz=tick)]
- labels.append(text)
-
- self._tickLabels.children = labels # Reset previous labels
-
- def prepareGL2(self, context):
- self._updateTicks()
- super(LabelledAxes, self).prepareGL2(context)
diff --git a/silx/gui/plot3d/scene/transform.py b/silx/gui/plot3d/scene/transform.py
deleted file mode 100644
index 43b739b..0000000
--- a/silx/gui/plot3d/scene/transform.py
+++ /dev/null
@@ -1,1027 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2020 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.
-#
-# ###########################################################################*/
-"""This module provides 4x4 matrix operation and classes to handle them."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-import itertools
-import numpy
-
-from . import event
-
-
-# Functions ###################################################################
-
-# Projections
-
-def mat4LookAtDir(position, direction, up):
- """Creates matrix to look in direction from position.
-
- :param position: Array-like 3 coordinates of the point of view position.
- :param direction: Array-like 3 coordinates of the sight direction vector.
- :param up: Array-like 3 coordinates of the upward direction
- in the image plane.
- :returns: Corresponding matrix.
- :rtype: numpy.ndarray of shape (4, 4)
- """
- assert len(position) == 3
- assert len(direction) == 3
- assert len(up) == 3
-
- direction = numpy.array(direction, copy=True, dtype=numpy.float32)
- dirnorm = numpy.linalg.norm(direction)
- assert dirnorm != 0.
- direction /= dirnorm
-
- side = numpy.cross(direction,
- numpy.array(up, copy=False, dtype=numpy.float32))
- sidenorm = numpy.linalg.norm(side)
- assert sidenorm != 0.
- up = numpy.cross(side / sidenorm, direction)
- upnorm = numpy.linalg.norm(up)
- assert upnorm != 0.
- up /= upnorm
-
- matrix = numpy.identity(4, dtype=numpy.float32)
- matrix[0, :3] = side
- matrix[1, :3] = up
- matrix[2, :3] = -direction
- return numpy.dot(matrix,
- mat4Translate(-position[0], -position[1], -position[2]))
-
-
-def mat4LookAt(position, center, up):
- """Creates matrix to look at center from position.
-
- See gluLookAt.
-
- :param position: Array-like 3 coordinates of the point of view position.
- :param center: Array-like 3 coordinates of the center of the scene.
- :param up: Array-like 3 coordinates of the upward direction
- in the image plane.
- :returns: Corresponding matrix.
- :rtype: numpy.ndarray of shape (4, 4)
- """
- position = numpy.array(position, copy=False, dtype=numpy.float32)
- center = numpy.array(center, copy=False, dtype=numpy.float32)
- direction = center - position
- return mat4LookAtDir(position, direction, up)
-
-
-def mat4Frustum(left, right, bottom, top, near, far):
- """Creates a frustum projection matrix.
-
- See glFrustum.
- """
- return numpy.array((
- (2.*near / (right-left), 0., (right+left) / (right-left), 0.),
- (0., 2.*near / (top-bottom), (top+bottom) / (top-bottom), 0.),
- (0., 0., -(far+near) / (far-near), -2.*far*near / (far-near)),
- (0., 0., -1., 0.)), dtype=numpy.float32)
-
-
-def mat4Perspective(fovy, width, height, near, far):
- """Creates a perspective projection matrix.
-
- Similar to gluPerspective.
-
- :param float fovy: Field of view angle in degrees in the y direction.
- :param float width: Width of the viewport.
- :param float height: Height of the viewport.
- :param float near: Distance to the near plane (strictly positive).
- :param float far: Distance to the far plane (strictly positive).
- :return: Corresponding matrix.
- :rtype: numpy.ndarray of shape (4, 4)
- """
- assert fovy != 0
- assert height != 0
- assert width != 0
- assert near > 0.
- assert far > near
- aspectratio = width / height
- f = 1. / numpy.tan(numpy.radians(fovy) / 2.)
- return numpy.array((
- (f / aspectratio, 0., 0., 0.),
- (0., f, 0., 0.),
- (0., 0., (far + near) / (near - far), 2. * far * near / (near - far)),
- (0., 0., -1., 0.)), dtype=numpy.float32)
-
-
-def mat4Orthographic(left, right, bottom, top, near, far):
- """Creates an orthographic (i.e., parallel) projection matrix.
-
- See glOrtho.
- """
- return numpy.array((
- (2. / (right - left), 0., 0., - (right + left) / (right - left)),
- (0., 2. / (top - bottom), 0., - (top + bottom) / (top - bottom)),
- (0., 0., -2. / (far - near), - (far + near) / (far - near)),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-# Affine
-
-def mat4Translate(tx, ty, tz):
- """4x4 translation matrix."""
- return numpy.array((
- (1., 0., 0., tx),
- (0., 1., 0., ty),
- (0., 0., 1., tz),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4Scale(sx, sy, sz):
- """4x4 scale matrix."""
- return numpy.array((
- (sx, 0., 0., 0.),
- (0., sy, 0., 0.),
- (0., 0., sz, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4RotateFromAngleAxis(angle, x=0., y=0., z=1.):
- """4x4 rotation matrix from angle and axis.
-
- :param float angle: The rotation angle in radians.
- :param float x: The rotation vector x coordinate.
- :param float y: The rotation vector y coordinate.
- :param float z: The rotation vector z coordinate.
- """
- ca = numpy.cos(angle)
- sa = numpy.sin(angle)
- return numpy.array((
- ((1.-ca) * x*x + ca, (1.-ca) * x*y - sa*z, (1.-ca) * x*z + sa*y, 0.),
- ((1.-ca) * x*y + sa*z, (1.-ca) * y*y + ca, (1.-ca) * y*z - sa*x, 0.),
- ((1.-ca) * x*z - sa*y, (1.-ca) * y*z + sa*x, (1.-ca) * z*z + ca, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4RotateFromQuaternion(quaternion):
- """4x4 rotation matrix from quaternion.
-
- :param quaternion: Array-like unit quaternion stored as (x, y, z, w)
- """
- quaternion = numpy.array(quaternion, copy=True)
- quaternion /= numpy.linalg.norm(quaternion)
-
- qx, qy, qz, qw = quaternion
- return numpy.array((
- (1. - 2.*(qy**2 + qz**2), 2.*(qx*qy - qw*qz), 2.*(qx*qz + qw*qy), 0.),
- (2.*(qx*qy + qw*qz), 1. - 2.*(qx**2 + qz**2), 2.*(qy*qz - qw*qx), 0.),
- (2.*(qx*qz - qw*qy), 2.*(qy*qz + qw*qx), 1. - 2.*(qx**2 + qy**2), 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4Shear(axis, sx=0., sy=0., sz=0.):
- """4x4 shear matrix: Skew two axes relative to a third fixed one.
-
- shearFactor = tan(shearAngle)
-
- :param str axis: The axis to keep constant and shear against.
- In 'x', 'y', 'z'.
- :param float sx: The shear factor for the X axis relative to axis.
- :param float sy: The shear factor for the Y axis relative to axis.
- :param float sz: The shear factor for the Z axis relative to axis.
- """
- assert axis in ('x', 'y', 'z')
-
- matrix = numpy.identity(4, dtype=numpy.float32)
-
- # Make the shear column
- index = 'xyz'.find(axis)
- shearcolumn = numpy.array((sx, sy, sz, 0.), dtype=numpy.float32)
- shearcolumn[index] = 1.
- matrix[:, index] = shearcolumn
- return matrix
-
-
-# Transforms ##################################################################
-
-class Transform(event.Notifier):
-
- def __init__(self, static=False):
- """Base class for (row-major) 4x4 matrix transforms.
-
- :param bool static: False (default) to reset cache when changed,
- True for static matrices.
- """
- super(Transform, self).__init__()
- self._matrix = None
- self._inverse = None
- if not static:
- self.addListener(self._changed) # Listening self for changes
-
- def __repr__(self):
- return '%s(%s)' % (self.__class__.__init__,
- repr(self.getMatrix(copy=False)))
-
- def inverse(self):
- """Return the Transform of the inverse.
-
- The returned Transform is static, it is not updated when this
- Transform is modified.
-
- :return: A Transform which is the inverse of this Transform.
- """
- return Inverse(self)
-
- # Matrix
-
- def _makeMatrix(self):
- """Override to build matrix"""
- return numpy.identity(4, dtype=numpy.float32)
-
- def _makeInverse(self):
- """Override to build inverse matrix."""
- return numpy.linalg.inv(self.getMatrix(copy=False))
-
- def getMatrix(self, copy=True):
- """The 4x4 matrix of this transform.
-
- :param bool copy: True (the default) to get a copy of the matrix,
- False to get the internal matrix, do not modify!
- :return: 4x4 matrix of this transform.
- """
- if self._matrix is None:
- self._matrix = self._makeMatrix()
- if copy:
- return self._matrix.copy()
- else:
- return self._matrix
-
- matrix = property(getMatrix, doc="The 4x4 matrix of this transform.")
-
- def getInverseMatrix(self, copy=False):
- """The 4x4 matrix of the inverse of this transform.
-
- :param bool copy: True (the default) to get a copy of the matrix,
- False to get the internal matrix, do not modify!
- :return: 4x4 matrix of the inverse of this transform.
- """
- if self._inverse is None:
- self._inverse = self._makeInverse()
- if copy:
- return self._inverse.copy()
- else:
- return self._inverse
-
- inverseMatrix = property(
- getInverseMatrix,
- doc="The 4x4 matrix of the inverse of this transform.")
-
- # Listener
-
- def _changed(self, source):
- """Default self listener reseting matrix cache."""
- self._matrix = None
- self._inverse = None
-
- # Multiplication with vectors
-
- def transformPoints(self, points, direct=True, perspectiveDivide=False):
- """Apply the transform to an array of points.
-
- :param points: 2D array of N vectors of 3 or 4 coordinates
- :param bool direct: Whether to apply the direct (True, the default)
- or inverse (False) transform.
- :param bool perspectiveDivide: Whether to apply the perspective divide
- (True) or not (False, the default).
- :return: The transformed points.
- :rtype: numpy.ndarray of same shape as points.
- """
- if direct:
- matrix = self.getMatrix(copy=False)
- else:
- matrix = self.getInverseMatrix(copy=False)
-
- points = numpy.array(points, copy=False)
- assert points.ndim == 2
-
- points = numpy.transpose(points)
-
- dimension = points.shape[0]
- assert dimension in (3, 4)
-
- if dimension == 3: # Add 4th coordinate
- points = numpy.append(
- points,
- numpy.ones((1, points.shape[1]), dtype=points.dtype),
- axis=0)
-
- result = numpy.transpose(numpy.dot(matrix, points))
-
- if perspectiveDivide:
- mask = result[:, 3] != 0.
- result[mask] /= result[mask, 3][:, numpy.newaxis]
-
- return result[:, :3] if dimension == 3 else result
-
- @staticmethod
- def _prepareVector(vector, w):
- """Add 4th coordinate (w) to vector if missing."""
- assert len(vector) in (3, 4)
- vector = numpy.array(vector, copy=False, dtype=numpy.float32)
- if len(vector) == 3:
- vector = numpy.append(vector, w)
- return vector
-
- def transformPoint(self, point, direct=True, perspectiveDivide=False):
- """Apply the transform to a point.
-
- :param point: Array-like vector of 3 or 4 coordinates.
- :param bool direct: Whether to apply the direct (True, the default)
- or inverse (False) transform.
- :param bool perspectiveDivide: Whether to apply the perspective divide
- (True) or not (False, the default).
- :return: The transformed point.
- :rtype: numpy.ndarray of same length as point.
- """
- if direct:
- matrix = self.getMatrix(copy=False)
- else:
- matrix = self.getInverseMatrix(copy=False)
- result = numpy.dot(matrix, self._prepareVector(point, 1.))
-
- if perspectiveDivide and result[3] != 0.:
- result /= result[3]
-
- if len(point) == 3:
- return result[:3]
- else:
- return result
-
- def transformDir(self, direction, direct=True):
- """Apply the transform to a direction.
-
- :param direction: Array-like vector of 3 coordinates.
- :param bool direct: Whether to apply the direct (True, the default)
- or inverse (False) transform.
- :return: The transformed direction.
- :rtype: numpy.ndarray of length 3.
- """
- if direct:
- matrix = self.getMatrix(copy=False)
- else:
- matrix = self.getInverseMatrix(copy=False)
- return numpy.dot(matrix[:3, :3], direction[:3])
-
- def transformNormal(self, normal, direct=True):
- """Apply the transform to a normal: R = (M-1)t * V.
-
- :param normal: Array-like vector of 3 coordinates.
- :param bool direct: Whether to apply the direct (True, the default)
- or inverse (False) transform.
- :return: The transformed normal.
- :rtype: numpy.ndarray of length 3.
- """
- if direct:
- matrix = self.getInverseMatrix(copy=False).T
- else:
- matrix = self.getMatrix(copy=False).T
- return numpy.dot(matrix[:3, :3], normal[:3])
-
- _CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
- dtype=numpy.float32)
- """Unit cube corners used by :meth:`transformBounds`"""
-
- def transformBounds(self, bounds, direct=True):
- """Apply the transform to an axes-aligned rectangular box.
-
- :param bounds: Min and max coords of the box for each axes.
- :type bounds: 2x3 numpy.ndarray
- :param bool direct: Whether to apply the direct (True, the default)
- or inverse (False) transform.
- :return: Axes-aligned rectangular box including the transformed box.
- :rtype: 2x3 numpy.ndarray of float32
- """
- corners = numpy.ones((8, 4), dtype=numpy.float32)
- corners[:, :3] = bounds[0] + \
- self._CUBE_CORNERS * (bounds[1] - bounds[0])
-
- if direct:
- matrix = self.getMatrix(copy=False)
- else:
- matrix = self.getInverseMatrix(copy=False)
-
- # Transform corners
- cornerstransposed = numpy.dot(matrix, corners.T)
- cornerstransposed = cornerstransposed / cornerstransposed[3]
-
- # Get min/max for each axis
- transformedbounds = numpy.empty((2, 3), dtype=numpy.float32)
- transformedbounds[0] = cornerstransposed.T[:, :3].min(axis=0)
- transformedbounds[1] = cornerstransposed.T[:, :3].max(axis=0)
-
- return transformedbounds
-
-
-class Inverse(Transform):
- """Transform which is the inverse of another one.
-
- Static: It never gets updated.
- """
-
- def __init__(self, transform):
- """Initializer.
-
- :param Transform transform: The transform to invert.
- """
-
- super(Inverse, self).__init__(static=True)
- self._matrix = transform.getInverseMatrix(copy=True)
- self._inverse = transform.getMatrix(copy=True)
-
-
-class TransformList(Transform, event.HookList):
- """List of transforms."""
-
- def __init__(self, iterable=()):
- Transform.__init__(self)
- event.HookList.__init__(self, iterable)
-
- def _listWillChangeHook(self, methodName, *args, **kwargs):
- for item in self:
- item.removeListener(self._transformChanged)
-
- def _listWasChangedHook(self, methodName, *args, **kwargs):
- for item in self:
- item.addListener(self._transformChanged)
- self.notify()
-
- def _transformChanged(self, source):
- """Listen to transform changes of the list and its items."""
- if source is not self: # Avoid infinite recursion
- self.notify()
-
- def _makeMatrix(self):
- matrix = numpy.identity(4, dtype=numpy.float32)
- for transform in self:
- matrix = numpy.dot(matrix, transform.getMatrix(copy=False))
- return matrix
-
-
-class StaticTransformList(Transform):
- """Transform that is a snapshot of a list of Transforms
-
- It does not keep reference to the list of Transforms.
-
- :param iterable: Iterable of Transform used for initialization
- """
-
- def __init__(self, iterable=()):
- super(StaticTransformList, self).__init__(static=True)
- matrix = numpy.identity(4, dtype=numpy.float32)
- for transform in iterable:
- matrix = numpy.dot(matrix, transform.getMatrix(copy=False))
- self._matrix = matrix # Init matrix once
-
-
-# Affine ######################################################################
-
-class Matrix(Transform):
-
- def __init__(self, matrix=None):
- """4x4 Matrix.
-
- :param matrix: 4x4 array-like matrix or None for identity matrix.
- """
- super(Matrix, self).__init__(static=True)
- self.setMatrix(matrix)
-
- def setMatrix(self, matrix=None):
- """Update the 4x4 Matrix.
-
- :param matrix: 4x4 array-like matrix or None for identity matrix.
- """
- if matrix is None:
- self._matrix = numpy.identity(4, dtype=numpy.float32)
- else:
- matrix = numpy.array(matrix, copy=True, dtype=numpy.float32)
- assert matrix.shape == (4, 4)
- self._matrix = matrix
- # Reset cached inverse as Transform is declared static
- self._inverse = None
- self.notify()
-
- # Redefined here to add a setter
- matrix = property(Transform.getMatrix, setMatrix,
- doc="The 4x4 matrix of this transform.")
-
-
-class Translate(Transform):
- """4x4 translation matrix."""
-
- def __init__(self, tx=0., ty=0., tz=0.):
- super(Translate, self).__init__()
- self._tx, self._ty, self._tz = 0., 0., 0.
- self.setTranslate(tx, ty, tz)
-
- def _makeMatrix(self):
- return mat4Translate(self.tx, self.ty, self.tz)
-
- def _makeInverse(self):
- return mat4Translate(-self.tx, -self.ty, -self.tz)
-
- @property
- def tx(self):
- return self._tx
-
- @tx.setter
- def tx(self, tx):
- self.setTranslate(tx=tx)
-
- @property
- def ty(self):
- return self._ty
-
- @ty.setter
- def ty(self, ty):
- self.setTranslate(ty=ty)
-
- @property
- def tz(self):
- return self._tz
-
- @tz.setter
- def tz(self, tz):
- self.setTranslate(tz=tz)
-
- @property
- def translation(self):
- return numpy.array((self.tx, self.ty, self.tz), dtype=numpy.float32)
-
- @translation.setter
- def translation(self, translations):
- tx, ty, tz = translations
- self.setTranslate(tx, ty, tz)
-
- def setTranslate(self, tx=None, ty=None, tz=None):
- if tx is not None:
- self._tx = tx
- if ty is not None:
- self._ty = ty
- if tz is not None:
- self._tz = tz
- self.notify()
-
-
-class Scale(Transform):
- """4x4 scale matrix."""
-
- def __init__(self, sx=1., sy=1., sz=1.):
- super(Scale, self).__init__()
- self._sx, self._sy, self._sz = 0., 0., 0.
- self.setScale(sx, sy, sz)
-
- def _makeMatrix(self):
- return mat4Scale(self.sx, self.sy, self.sz)
-
- def _makeInverse(self):
- return mat4Scale(1. / self.sx, 1. / self.sy, 1. / self.sz)
-
- @property
- def sx(self):
- return self._sx
-
- @sx.setter
- def sx(self, sx):
- self.setScale(sx=sx)
-
- @property
- def sy(self):
- return self._sy
-
- @sy.setter
- def sy(self, sy):
- self.setScale(sy=sy)
-
- @property
- def sz(self):
- return self._sz
-
- @sz.setter
- def sz(self, sz):
- self.setScale(sz=sz)
-
- @property
- def scale(self):
- return numpy.array((self._sx, self._sy, self._sz), dtype=numpy.float32)
-
- @scale.setter
- def scale(self, scales):
- sx, sy, sz = scales
- self.setScale(sx, sy, sz)
-
- def setScale(self, sx=None, sy=None, sz=None):
- if sx is not None:
- assert sx != 0.
- self._sx = sx
- if sy is not None:
- assert sy != 0.
- self._sy = sy
- if sz is not None:
- assert sz != 0.
- self._sz = sz
- self.notify()
-
-
-class Rotate(Transform):
-
- def __init__(self, angle=0., ax=0., ay=0., az=1.):
- """4x4 rotation matrix.
-
- :param float angle: The rotation angle in degrees.
- :param float ax: The x coordinate of the rotation axis.
- :param float ay: The y coordinate of the rotation axis.
- :param float az: The z coordinate of the rotation axis.
- """
- super(Rotate, self).__init__()
- self._angle = 0.
- self._axis = None
- self.setAngleAxis(angle, (ax, ay, az))
-
- @property
- def angle(self):
- """The rotation angle in degrees."""
- return self._angle
-
- @angle.setter
- def angle(self, angle):
- self.setAngleAxis(angle=angle)
-
- @property
- def axis(self):
- """The normalized rotation axis as a numpy.ndarray."""
- return self._axis.copy()
-
- @axis.setter
- def axis(self, axis):
- self.setAngleAxis(axis=axis)
-
- def setAngleAxis(self, angle=None, axis=None):
- """Update the angle and/or axis of the rotation.
-
- :param float angle: The rotation angle in degrees.
- :param axis: Array-like axis vector (3 coordinates).
- """
- if angle is not None:
- self._angle = angle
- if axis is not None:
- assert len(axis) == 3
- axis = numpy.array(axis, copy=True, dtype=numpy.float32)
- assert axis.size == 3
- norm = numpy.linalg.norm(axis)
- if norm == 0.: # No axis, set rotation angle to 0.
- self._angle = 0.
- self._axis = numpy.array((0., 0., 1.), dtype=numpy.float32)
- else:
- self._axis = axis / norm
-
- if angle is not None or axis is not None:
- self.notify()
-
- @property
- def quaternion(self):
- """Rotation unit quaternion as (x, y, z, w).
-
- Where: ||(x, y, z)|| = sin(angle/2), w = cos(angle/2).
- """
- if numpy.linalg.norm(self._axis) == 0.:
- return numpy.array((0., 0., 0., 1.), dtype=numpy.float32)
-
- else:
- quaternion = numpy.empty((4,), dtype=numpy.float32)
- halfangle = 0.5 * numpy.radians(self.angle)
- quaternion[0:3] = numpy.sin(halfangle) * self._axis
- quaternion[3] = numpy.cos(halfangle)
- return quaternion
-
- @quaternion.setter
- def quaternion(self, quaternion):
- assert len(quaternion) == 4
-
- # Normalize quaternion
- quaternion = numpy.array(quaternion, copy=True)
- quaternion /= numpy.linalg.norm(quaternion)
-
- # Get angle
- sinhalfangle = numpy.linalg.norm(quaternion[0:3])
- coshalfangle = quaternion[3]
- angle = 2. * numpy.arctan2(sinhalfangle, coshalfangle)
-
- # Axis will be normalized in setAngleAxis
- self.setAngleAxis(numpy.degrees(angle), quaternion[0:3])
-
- def _makeMatrix(self):
- angle = numpy.radians(self.angle, dtype=numpy.float32)
- return mat4RotateFromAngleAxis(angle, *self.axis)
-
- def _makeInverse(self):
- return numpy.array(self.getMatrix(copy=False).transpose(),
- copy=True, order='C',
- dtype=numpy.float32)
-
-
-class Shear(Transform):
-
- def __init__(self, axis, sx=0., sy=0., sz=0.):
- """4x4 shear/skew matrix of 2 axes relative to the third one.
-
- :param str axis: The axis to keep fixed, in 'x', 'y', 'z'
- :param float sx: The shear factor for the x axis.
- :param float sy: The shear factor for the y axis.
- :param float sz: The shear factor for the z axis.
- """
- assert axis in ('x', 'y', 'z')
- super(Shear, self).__init__()
- self._axis = axis
- self._factors = sx, sy, sz
-
- @property
- def axis(self):
- """The axis against which other axes are skewed."""
- return self._axis
-
- @property
- def factors(self):
- """The shear factors: shearFactor = tan(shearAngle)"""
- return self._factors
-
- def _makeMatrix(self):
- return mat4Shear(self.axis, *self.factors)
-
- def _makeInverse(self):
- sx, sy, sz = self.factors
- return mat4Shear(self.axis, -sx, -sy, -sz)
-
-
-# Projection ##################################################################
-
-class _Projection(Transform):
- """Base class for projection matrix.
-
- Handles near and far clipping plane values.
- Subclasses must implement :meth:`_makeMatrix`.
-
- :param float near: Distance to the near plane.
- :param float far: Distance to the far plane.
- :param bool checkDepthExtent: Toggle checks near > 0 and far > near.
- :param size:
- Viewport's size used to compute the aspect ratio (width, height).
- :type size: 2-tuple of float
- """
-
- def __init__(self, near, far, checkDepthExtent=False, size=(1., 1.)):
- super(_Projection, self).__init__()
- self._checkDepthExtent = checkDepthExtent
- self._depthExtent = 1, 10
- self.setDepthExtent(near, far) # set _depthExtent
- self._size = 1., 1.
- self.size = size # set _size
-
- def setDepthExtent(self, near=None, far=None):
- """Set the extent of the visible area along the viewing direction.
-
- :param float near: The near clipping plane Z coord.
- :param float far: The far clipping plane Z coord.
- """
- near = float(near) if near is not None else self._depthExtent[0]
- far = float(far) if far is not None else self._depthExtent[1]
-
- if self._checkDepthExtent:
- assert near > 0.
- assert far > near
-
- self._depthExtent = near, far
- self.notify()
-
- @property
- def near(self):
- """Distance to the near plane."""
- return self._depthExtent[0]
-
- @near.setter
- def near(self, near):
- if near != self.near:
- self.setDepthExtent(near=near)
-
- @property
- def far(self):
- """Distance to the far plane."""
- return self._depthExtent[1]
-
- @far.setter
- def far(self, far):
- if far != self.far:
- self.setDepthExtent(far=far)
-
- @property
- def size(self):
- """Viewport size as a 2-tuple of float (width, height)."""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 2
- self._size = tuple(size)
- self.notify()
-
-
-class Orthographic(_Projection):
- """Orthographic (i.e., parallel) projection which can keep aspect ratio.
-
- Clipping planes are adjusted to match the aspect ratio of
- the :attr:`size` attribute if :attr:`keepaspect` is True.
-
- In this case, the left, right, bottom and top parameters defines the area
- which must always remain visible.
- Effective clipping planes are adjusted to keep the aspect ratio.
-
- :param float left: Coord of the left clipping plane.
- :param float right: Coord of the right clipping plane.
- :param float bottom: Coord of the bottom clipping plane.
- :param float top: Coord of the top clipping plane.
- :param float near: Distance to the near plane.
- :param float far: Distance to the far plane.
- :param size:
- Viewport's size used to compute the aspect ratio (width, height).
- :type size: 2-tuple of float
- :param bool keepaspect:
- True (default) to keep aspect ratio, False otherwise.
- """
-
- def __init__(self, left=0., right=1., bottom=1., top=0., near=-1., far=1.,
- size=(1., 1.), keepaspect=True):
- self._left, self._right = left, right
- self._bottom, self._top = bottom, top
- self._keepaspect = bool(keepaspect)
- super(Orthographic, self).__init__(near, far, checkDepthExtent=False,
- size=size)
- # _update called when setting size
-
- def _makeMatrix(self):
- return mat4Orthographic(
- self.left, self.right, self.bottom, self.top, self.near, self.far)
-
- def _update(self, left, right, bottom, top):
- if self.keepaspect:
- width, height = self.size
- aspect = width / height
-
- orthoaspect = abs(left - right) / abs(bottom - top)
-
- if orthoaspect >= aspect: # Keep width, enlarge height
- newheight = \
- numpy.sign(top - bottom) * abs(left - right) / aspect
- bottom = 0.5 * (bottom + top) - 0.5 * newheight
- top = bottom + newheight
-
- else: # Keep height, enlarge width
- newwidth = \
- numpy.sign(right - left) * abs(bottom - top) * aspect
- left = 0.5 * (left + right) - 0.5 * newwidth
- right = left + newwidth
-
- # Store values
- self._left, self._right = left, right
- self._bottom, self._top = bottom, top
-
- def setClipping(self, left=None, right=None, bottom=None, top=None):
- """Set the clipping planes of the projection.
-
- Parameters are adjusted to keep aspect ratio.
- If a clipping plane coord is not provided, it uses its current value
-
- :param float left: Coord of the left clipping plane.
- :param float right: Coord of the right clipping plane.
- :param float bottom: Coord of the bottom clipping plane.
- :param float top: Coord of the top clipping plane.
- """
- left = float(left) if left is not None else self.left
- right = float(right) if right is not None else self.right
- bottom = float(bottom) if bottom is not None else self.bottom
- top = float(top) if top is not None else self.top
-
- self._update(left, right, bottom, top)
- self.notify()
-
- left = property(lambda self: self._left,
- doc="Coord of the left clipping plane.")
-
- right = property(lambda self: self._right,
- doc="Coord of the right clipping plane.")
-
- bottom = property(lambda self: self._bottom,
- doc="Coord of the bottom clipping plane.")
-
- top = property(lambda self: self._top,
- doc="Coord of the top clipping plane.")
-
- @property
- def size(self):
- """Viewport size as a 2-tuple of float (width, height)"""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 2
- size = float(size[0]), float(size[1])
- if size != self._size:
- self._size = size
- self._update(self.left, self.right, self.bottom, self.top)
- self.notify()
-
- @property
- def keepaspect(self):
- """True to keep aspect ratio, False otherwise."""
- return self._keepaspect
-
- @keepaspect.setter
- def keepaspect(self, aspect):
- aspect = bool(aspect)
- if aspect != self._keepaspect:
- self._keepaspect = aspect
- self._update(self.left, self.right, self.bottom, self.top)
- self.notify()
-
-
-class Ortho2DWidget(_Projection):
- """Orthographic projection with pixel as unit.
-
- Provides same coordinates as widgets:
- origin: top left, X axis goes left, Y axis goes down.
-
- :param float near: Z coordinate of the near clipping plane.
- :param float far: Z coordinante of the far clipping plane.
- :param size:
- Viewport's size used to compute the aspect ratio (width, height).
- :type size: 2-tuple of float
- """
-
- def __init__(self, near=-1., far=1., size=(1., 1.)):
-
- super(Ortho2DWidget, self).__init__(near, far, size)
-
- def _makeMatrix(self):
- width, height = self.size
- return mat4Orthographic(0., width, height, 0., self.near, self.far)
-
-
-class Perspective(_Projection):
- """Perspective projection matrix defined by FOV and aspect ratio.
-
- :param float fovy: Vertical field-of-view in degrees.
- :param float near: The near clipping plane Z coord (stricly positive).
- :param float far: The far clipping plane Z coord (> near).
- :param size:
- Viewport's size used to compute the aspect ratio (width, height).
- :type size: 2-tuple of float
- """
-
- def __init__(self, fovy=90., near=0.1, far=1., size=(1., 1.)):
-
- super(Perspective, self).__init__(near, far, checkDepthExtent=True)
- self._fovy = 90.
- self.fovy = fovy # Set _fovy
- self.size = size # Set _ size
-
- def _makeMatrix(self):
- width, height = self.size
- return mat4Perspective(self.fovy, width, height, self.near, self.far)
-
- @property
- def fovy(self):
- """Vertical field-of-view in degrees."""
- return self._fovy
-
- @fovy.setter
- def fovy(self, fovy):
- self._fovy = float(fovy)
- self.notify()
diff --git a/silx/gui/plot3d/scene/utils.py b/silx/gui/plot3d/scene/utils.py
deleted file mode 100644
index c6cd129..0000000
--- a/silx/gui/plot3d/scene/utils.py
+++ /dev/null
@@ -1,662 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2020 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.
-#
-# ###########################################################################*/
-"""
-This module provides functions to generate indices, to check intersection
-and to handle planes.
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-import logging
-import numpy
-
-from . import event
-
-
-_logger = logging.getLogger(__name__)
-
-
-# numpy #######################################################################
-
-def _uniqueAlongLastAxis(a):
- """Numpy unique on the last axis of a 2D array
-
- Implemented here as not in numpy as of writing.
-
- See adding axis parameter to numpy.unique:
- https://github.com/numpy/numpy/pull/3584/files#r6225452
-
- :param array_like a: Input array.
- :return: Unique elements along the last axis.
- :rtype: numpy.ndarray
- """
- assert len(a.shape) == 2
-
- # Construct a type over last array dimension to run unique on a 1D array
- if a.dtype.char in numpy.typecodes['AllInteger']:
- # Bit-wise comparison of the 2 indices of a line at once
- # Expect a C contiguous array of shape N, 2
- uniquedt = numpy.dtype((numpy.void, a.itemsize * a.shape[-1]))
- elif a.dtype.char in numpy.typecodes['Float']:
- uniquedt = [('f{i}'.format(i=i), a.dtype) for i in range(a.shape[-1])]
- else:
- raise TypeError("Unsupported type {dtype}".format(dtype=a.dtype))
-
- uniquearray = numpy.unique(numpy.ascontiguousarray(a).view(uniquedt))
- return uniquearray.view(a.dtype).reshape((-1, a.shape[-1]))
-
-
-# conversions #################################################################
-
-def triangleToLineIndices(triangleIndices, unicity=False):
- """Generates lines indices from triangle indices.
-
- This is generating lines indices for the edges of the triangles.
-
- :param triangleIndices: The indices to draw a set of vertices as triangles.
- :type triangleIndices: numpy.ndarray
- :param bool unicity: If True remove duplicated lines,
- else (the default) returns all lines.
- :return: The indices to draw the edges of the triangles as lines.
- :rtype: 1D numpy.ndarray of uint16 or uint32.
- """
- # Makes sure indices ar packed by triangle
- triangleIndices = triangleIndices.reshape(-1, 3)
-
- # Pack line indices by triangle and by edge
- lineindices = numpy.empty((len(triangleIndices), 3, 2),
- dtype=triangleIndices.dtype)
- lineindices[:, 0] = triangleIndices[:, :2] # edge = t0, t1
- lineindices[:, 1] = triangleIndices[:, 1:] # edge =t1, t2
- lineindices[:, 2] = triangleIndices[:, ::2] # edge = t0, t2
-
- if unicity:
- lineindices = _uniqueAlongLastAxis(lineindices.reshape(-1, 2))
-
- # Make sure it is 1D
- lineindices.shape = -1
-
- return lineindices
-
-
-def verticesNormalsToLines(vertices, normals, scale=1.):
- """Return vertices of lines representing normals at given positions.
-
- :param vertices: Positions of the points.
- :type vertices: numpy.ndarray with shape: (nbPoints, 3)
- :param normals: Corresponding normals at the points.
- :type normals: numpy.ndarray with shape: (nbPoints, 3)
- :param float scale: The scale factor to apply to normals.
- :returns: Array of vertices to draw corresponding lines.
- :rtype: numpy.ndarray with shape: (nbPoints * 2, 3)
- """
- linevertices = numpy.empty((len(vertices) * 2, 3), dtype=vertices.dtype)
- linevertices[0::2] = vertices
- linevertices[1::2] = vertices + scale * normals
- return linevertices
-
-
-def unindexArrays(mode, indices, *arrays):
- """Convert indexed GL primitives to unindexed ones.
-
- Given indices in arrays and the OpenGL primitive they represent,
- return the unindexed equivalent.
-
- :param str mode:
- Kind of primitive represented by indices.
- In: points, lines, line_strip, loop, triangles, triangle_strip, fan.
- :param indices: Indices in other arrays
- :type indices: numpy.ndarray of dimension 1.
- :param arrays: Remaining arguments are arrays to convert
- :return: Converted arrays
- :rtype: tuple of numpy.ndarray
- """
- indices = numpy.array(indices, copy=False)
-
- assert mode in ('points',
- 'lines', 'line_strip', 'loop',
- 'triangles', 'triangle_strip', 'fan')
-
- if mode in ('lines', 'line_strip', 'loop'):
- assert len(indices) >= 2
- elif mode in ('triangles', 'triangle_strip', 'fan'):
- assert len(indices) >= 3
-
- assert indices.min() >= 0
- max_index = indices.max()
- for data in arrays:
- assert len(data) >= max_index
-
- if mode == 'line_strip':
- unpacked = numpy.empty((2 * (len(indices) - 1),), dtype=indices.dtype)
- unpacked[0::2] = indices[:-1]
- unpacked[1::2] = indices[1:]
- indices = unpacked
-
- elif mode == 'loop':
- unpacked = numpy.empty((2 * len(indices),), dtype=indices.dtype)
- unpacked[0::2] = indices
- unpacked[1:-1:2] = indices[1:]
- unpacked[-1] = indices[0]
- indices = unpacked
-
- elif mode == 'triangle_strip':
- unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
- unpacked[0::3] = indices[:-2]
- unpacked[1::3] = indices[1:-1]
- unpacked[2::3] = indices[2:]
- indices = unpacked
-
- elif mode == 'fan':
- unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
- unpacked[0::3] = indices[0]
- unpacked[1::3] = indices[1:-1]
- unpacked[2::3] = indices[2:]
- indices = unpacked
-
- return tuple(numpy.ascontiguousarray(data[indices]) for data in arrays)
-
-
-def triangleStripToTriangles(strip):
- """Convert a triangle strip to a set of triangles.
-
- The order of the corners is inverted for odd triangles.
-
- :param numpy.ndarray strip:
- Array of triangle corners of shape (N, 3).
- N must be at least 3.
- :return: Equivalent triangles corner as an array of shape (N, 3, 3)
- :rtype: numpy.ndarray
- """
- strip = numpy.array(strip).reshape(-1, 3)
- assert len(strip) >= 3
-
- triangles = numpy.empty((len(strip) - 2, 3, 3), dtype=strip.dtype)
- triangles[0::2, 0] = strip[0:-2:2]
- triangles[0::2, 1] = strip[1:-1:2]
- triangles[0::2, 2] = strip[2::2]
-
- triangles[1::2, 0] = strip[3::2]
- triangles[1::2, 1] = strip[2:-1:2]
- triangles[1::2, 2] = strip[1:-2:2]
-
- return triangles
-
-
-def trianglesNormal(positions):
- """Return normal for each triangle.
-
- :param positions: Serie of triangle's corners
- :type positions: numpy.ndarray of shape (NbTriangles*3, 3)
- :return: Normals corresponding to each position.
- :rtype: numpy.ndarray of shape (NbTriangles, 3)
- """
- assert positions.ndim == 2
- assert positions.shape[1] == 3
-
- positions = numpy.array(positions, copy=False).reshape(-1, 3, 3)
-
- normals = numpy.cross(positions[:, 1] - positions[:, 0],
- positions[:, 2] - positions[:, 0])
-
- # Normalize normals
- norms = numpy.linalg.norm(normals, axis=1)
- norms[norms == 0] = 1
-
- return normals / norms.reshape(-1, 1)
-
-
-# grid ########################################################################
-
-def gridVertices(dim0Array, dim1Array, dtype):
- """Generate an array of 2D positions from 2 arrays of 1D coordinates.
-
- :param dim0Array: 1D array-like of coordinates along the first dimension.
- :param dim1Array: 1D array-like of coordinates along the second dimension.
- :param numpy.dtype dtype: Data type of the output array.
- :return: Array of grid coordinates.
- :rtype: numpy.ndarray with shape: (len(dim0Array), len(dim1Array), 2)
- """
- grid = numpy.empty((len(dim0Array), len(dim1Array), 2), dtype=dtype)
- grid.T[0, :, :] = dim0Array
- grid.T[1, :, :] = numpy.array(dim1Array, copy=False)[:, None]
- return grid
-
-
-def triangleStripGridIndices(dim0, dim1):
- """Generate indices to draw a grid of vertices as a triangle strip.
-
- Vertices are expected to be stored as row-major (i.e., C contiguous).
-
- :param int dim0: The number of rows of vertices.
- :param int dim1: The number of columns of vertices.
- :return: The vertex indices
- :rtype: 1D numpy.ndarray of uint32
- """
- assert dim0 >= 2
- assert dim1 >= 2
-
- # Filling a row of squares +
- # an index before and one after for degenerated triangles
- indices = numpy.empty((dim0 - 1, 2 * (dim1 + 1)), dtype=numpy.uint32)
-
- # Init indices with minimum indices for each row of squares
- indices[:] = (dim1 * numpy.arange(dim0 - 1, dtype=numpy.uint32))[:, None]
-
- # Update indices with offset per row of squares
- offset = numpy.arange(dim1, dtype=numpy.uint32)
- indices[:, 1:-1:2] += offset
- offset += dim1
- indices[:, 2::2] += offset
- indices[:, -1] += offset[-1]
-
- # Remove extra indices for degenerated triangles before returning
- return indices.ravel()[1:-1]
-
- # Alternative:
- # indices = numpy.zeros(2 * dim1 * (dim0 - 1) + 2 * (dim0 - 2),
- # dtype=numpy.uint32)
- #
- # offset = numpy.arange(dim1, dtype=numpy.uint32)
- # for d0Index in range(dim0 - 1):
- # start = 2 * d0Index * (dim1 + 1)
- # end = start + 2 * dim1
- # if d0Index != 0:
- # indices[start - 2] = offset[-1]
- # indices[start - 1] = offset[0]
- # indices[start:end:2] = offset
- # offset += dim1
- # indices[start + 1:end:2] = offset
- # return indices
-
-
-def linesGridIndices(dim0, dim1):
- """Generate indices to draw a grid of vertices as lines.
-
- Vertices are expected to be stored as row-major (i.e., C contiguous).
-
- :param int dim0: The number of rows of vertices.
- :param int dim1: The number of columns of vertices.
- :return: The vertex indices.
- :rtype: 1D numpy.ndarray of uint32
- """
- # Horizontal and vertical lines
- nbsegmentalongdim1 = 2 * (dim1 - 1)
- nbsegmentalongdim0 = 2 * (dim0 - 1)
-
- indices = numpy.empty(nbsegmentalongdim1 * dim0 +
- nbsegmentalongdim0 * dim1,
- dtype=numpy.uint32)
-
- # Line indices over dim0
- onedim1line = (numpy.arange(nbsegmentalongdim1,
- dtype=numpy.uint32) + 1) // 2
- indices[:dim0 * nbsegmentalongdim1] = \
- (dim1 * numpy.arange(dim0, dtype=numpy.uint32)[:, None] +
- onedim1line[None, :]).ravel()
-
- # Line indices over dim1
- onedim0line = (numpy.arange(nbsegmentalongdim0,
- dtype=numpy.uint32) + 1) // 2
- indices[dim0 * nbsegmentalongdim1:] = \
- (numpy.arange(dim1, dtype=numpy.uint32)[:, None] +
- dim1 * onedim0line[None, :]).ravel()
-
- return indices
-
-
-# intersection ################################################################
-
-def angleBetweenVectors(refVector, vectors, norm=None):
- """Return the angle between 2 vectors.
-
- :param refVector: Coordinates of the reference vector.
- :type refVector: numpy.ndarray of shape: (NCoords,)
- :param vectors: Coordinates of the vector(s) to get angle from reference.
- :type vectors: numpy.ndarray of shape: (NCoords,) or (NbVector, NCoords)
- :param norm: A direction vector giving an orientation to the angles
- or None.
- :returns: The angles in radians in [0, pi] if norm is None
- else in [0, 2pi].
- :rtype: float or numpy.ndarray of shape (NbVectors,)
- """
- singlevector = len(vectors.shape) == 1
- if singlevector: # Make it a 2D array for the computation
- vectors = vectors.reshape(1, -1)
-
- assert len(refVector.shape) == 1
- assert len(vectors.shape) == 2
- assert len(refVector) == vectors.shape[1]
-
- # Normalize vectors
- refVector /= numpy.linalg.norm(refVector)
- vectors = numpy.array([v / numpy.linalg.norm(v) for v in vectors])
-
- dots = numpy.sum(refVector * vectors, axis=-1)
- angles = numpy.arccos(numpy.clip(dots, -1., 1.))
- if norm is not None:
- signs = numpy.sum(norm * numpy.cross(refVector, vectors), axis=-1) < 0.
- angles[signs] = numpy.pi * 2. - angles[signs]
-
- return angles[0] if singlevector else angles
-
-
-def segmentPlaneIntersect(s0, s1, planeNorm, planePt):
- """Compute the intersection of a segment with a plane.
-
- :param s0: First end of the segment
- :type s0: 1D numpy.ndarray-like of length 3
- :param s1: Second end of the segment
- :type s1: 1D numpy.ndarray-like of length 3
- :param planeNorm: Normal vector of the plane.
- :type planeNorm: numpy.ndarray of shape: (3,)
- :param planePt: A point of the plane.
- :type planePt: numpy.ndarray of shape: (3,)
- :return: The intersection points. The number of points goes
- from 0 (no intersection) to 2 (segment in the plane)
- :rtype: list of numpy.ndarray
- """
- s0, s1 = numpy.asarray(s0), numpy.asarray(s1)
-
- segdir = s1 - s0
- dotnormseg = numpy.dot(planeNorm, segdir)
- if dotnormseg == 0:
- # line and plane are parallels
- if numpy.dot(planeNorm, planePt - s0) == 0: # segment is in plane
- return [s0, s1]
- else: # No intersection
- return []
-
- alpha = - numpy.dot(planeNorm, s0 - planePt) / dotnormseg
- if 0. <= alpha <= 1.: # Intersection with segment
- return [s0 + alpha * segdir]
- else: # intersection outside segment
- return []
-
-
-def boxPlaneIntersect(boxVertices, boxLineIndices, planeNorm, planePt):
- """Return intersection points between a box and a plane.
-
- :param boxVertices: Position of the corners of the box.
- :type boxVertices: numpy.ndarray with shape: (8, 3)
- :param boxLineIndices: Indices of the box edges.
- :type boxLineIndices: numpy.ndarray-like with shape: (12, 2)
- :param planeNorm: Normal vector of the plane.
- :type planeNorm: numpy.ndarray of shape: (3,)
- :param planePt: A point of the plane.
- :type planePt: numpy.ndarray of shape: (3,)
- :return: The found intersection points
- :rtype: numpy.ndarray with 2 dimensions
- """
- segments = numpy.take(boxVertices, boxLineIndices, axis=0)
-
- points = set() # Gather unique intersection points
- for seg in segments:
- for point in segmentPlaneIntersect(seg[0], seg[1], planeNorm, planePt):
- points.add(tuple(point))
- points = numpy.array(list(points))
-
- if len(points) <= 2:
- return numpy.array(())
- elif len(points) == 3:
- return points
- else: # len(points) > 3
- # Order point to have a polyline lying on the unit cube's faces
- vectors = points - numpy.mean(points, axis=0)
- angles = angleBetweenVectors(vectors[0], vectors, planeNorm)
- points = numpy.take(points, numpy.argsort(angles), axis=0)
- return points
-
-
-def clipSegmentToBounds(segment, bounds):
- """Clip segment to volume aligned with axes.
-
- :param numpy.ndarray segment: (p0, p1)
- :param numpy.ndarray bounds: (lower corner, upper corner)
- :return: Either clipped (p0, p1) or None if outside volume
- :rtype: Union[None,List[numpy.ndarray]]
- """
- segment = numpy.array(segment, copy=False)
- bounds = numpy.array(bounds, copy=False)
-
- p0, p1 = segment
- # Get intersection points of ray with volume boundary planes
- # Line equation: P = offset * delta + p0
- delta = p1 - p0
- deltaNotZero = numpy.array(delta, copy=True)
- deltaNotZero[deltaNotZero == 0] = numpy.nan # Invalidated to avoid division by zero
- offsets = ((bounds - p0) / deltaNotZero).reshape(-1)
- points = offsets.reshape(-1, 1) * delta + p0
-
- # Avoid precision errors by using bounds value
- points.shape = 2, 3, 3 # Reshape 1 point per bound value
- for dim in range(3):
- points[:, dim, dim] = bounds[:, dim]
- points.shape = -1, 3 # Set back to 2D array
-
- # Find intersection points that are included in the volume
- mask = numpy.logical_and(numpy.all(bounds[0] <= points, axis=1),
- numpy.all(points <= bounds[1], axis=1))
- intersections = numpy.unique(offsets[mask])
- if len(intersections) != 2:
- return None
-
- intersections.sort()
- # Do p1 first as p0 is need to compute it
- if intersections[1] < 1: # clip p1
- segment[1] = intersections[1] * delta + p0
- if intersections[0] > 0: # clip p0
- segment[0] = intersections[0] * delta + p0
- return segment
-
-
-def segmentVolumeIntersect(segment, nbins):
- """Get bin indices intersecting with segment
-
- It should work with N dimensions.
- Coordinate convention (z, y, x) or (x, y, z) should not matter
- as long as segment and nbins are consistent.
-
- :param numpy.ndarray segment:
- Segment end points as a 2xN array of coordinates
- :param numpy.ndarray nbins:
- Shape of the volume with same coordinates order as segment
- :return: List of bins indices as a 2D array or None if no bins
- :rtype: Union[None,numpy.ndarray]
- """
- segment = numpy.asarray(segment)
- nbins = numpy.asarray(nbins)
-
- assert segment.ndim == 2
- assert segment.shape[0] == 2
- assert nbins.ndim == 1
- assert segment.shape[1] == nbins.size
-
- dim = len(nbins)
-
- bounds = numpy.array((numpy.zeros_like(nbins), nbins))
- segment = clipSegmentToBounds(segment, bounds)
- if segment is None:
- return None # Segment outside volume
- p0, p1 = segment
-
- # Get intersections
-
- # Get coordinates of bin edges crossing the segment
- clipped = numpy.ceil(numpy.clip(segment, 0, nbins))
- start = numpy.min(clipped, axis=0)
- stop = numpy.max(clipped, axis=0) # stop is NOT included
- edgesByDim = [numpy.arange(start[i], stop[i]) for i in range(dim)]
-
- # Line equation: P = t * delta + p0
- delta = p1 - p0
-
- # Get bin edge/line intersections as sorted points along the line
- # Get corresponding line parameters
- t = []
- if numpy.all(0 <= p0) and numpy.all(p0 <= nbins):
- t.append([0.]) # p0 within volume, add it
- t += [(edgesByDim[i] - p0[i]) / delta[i] for i in range(dim) if delta[i] != 0]
- if numpy.all(0 <= p1) and numpy.all(p1 <= nbins):
- t.append([1.]) # p1 within volume, add it
- t = numpy.concatenate(t)
- t.sort(kind='mergesort')
-
- # Remove duplicates
- unique = numpy.ones((len(t),), dtype=bool)
- numpy.not_equal(t[1:], t[:-1], out=unique[1:])
- t = t[unique]
-
- if len(t) < 2:
- return None # Not enough intersection points
-
- # bin edges/line intersection points
- points = t.reshape(-1, 1) * delta + p0
- centers = (points[:-1] + points[1:]) / 2.
- bins = numpy.floor(centers).astype(numpy.int64)
- return bins
-
-
-# Plane #######################################################################
-
-class Plane(event.Notifier):
- """Object handling a plane and notifying plane changes.
-
- :param point: A point on the plane.
- :type point: 3-tuple of float.
- :param normal: Normal of the plane.
- :type normal: 3-tuple of float.
- """
-
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
- super(Plane, self).__init__()
-
- assert len(point) == 3
- self._point = numpy.array(point, copy=True, dtype=numpy.float32)
- assert len(normal) == 3
- self._normal = numpy.array(normal, copy=True, dtype=numpy.float32)
- self.notify()
-
- def setPlane(self, point=None, normal=None):
- """Set plane point and normal and notify.
-
- :param point: A point on the plane.
- :type point: 3-tuple of float or None.
- :param normal: Normal of the plane.
- :type normal: 3-tuple of float or None.
- """
- planechanged = False
-
- if point is not None:
- assert len(point) == 3
- point = numpy.array(point, copy=True, dtype=numpy.float32)
- if not numpy.all(numpy.equal(self._point, point)):
- self._point = point
- planechanged = True
-
- if normal is not None:
- assert len(normal) == 3
- normal = numpy.array(normal, copy=True, dtype=numpy.float32)
-
- norm = numpy.linalg.norm(normal)
- if norm != 0.:
- normal /= norm
-
- if not numpy.all(numpy.equal(self._normal, normal)):
- self._normal = normal
- planechanged = True
-
- if planechanged:
- _logger.debug('Plane updated:\n\tpoint: %s\n\tnormal: %s',
- str(self._point), str(self._normal))
- self.notify()
-
- @property
- def point(self):
- """A point on the plane."""
- return self._point.copy()
-
- @point.setter
- def point(self, point):
- self.setPlane(point=point)
-
- @property
- def normal(self):
- """The (normalized) normal of the plane."""
- return self._normal.copy()
-
- @normal.setter
- def normal(self, normal):
- self.setPlane(normal=normal)
-
- @property
- def parameters(self):
- """Plane equation parameters: a*x + b*y + c*z + d = 0."""
- return numpy.append(self._normal,
- - numpy.dot(self._point, self._normal))
-
- @parameters.setter
- def parameters(self, parameters):
- assert len(parameters) == 4
- parameters = numpy.array(parameters, dtype=numpy.float32)
-
- # Normalize normal
- norm = numpy.linalg.norm(parameters[:3])
- if norm != 0:
- parameters /= norm
-
- normal = parameters[:3]
- point = - parameters[3] * normal
- self.setPlane(point, normal)
-
- @property
- def isPlane(self):
- """True if a plane is defined (i.e., ||normal|| != 0)."""
- return numpy.any(self.normal != 0.)
-
- def move(self, step):
- """Move the plane of step along the normal."""
- self.point += step * self.normal
-
- def segmentIntersection(self, s0, s1):
- """Compute the plane intersection with segment [s0, s1].
-
- :param s0: First end of the segment
- :type s0: 1D numpy.ndarray-like of length 3
- :param s1: Second end of the segment
- :type s1: 1D numpy.ndarray-like of length 3
- :return: The intersection points. The number of points goes
- from 0 (no intersection) to 2 (segment in the plane)
- :rtype: list of 1D numpy.ndarray
- """
- if not self.isPlane:
- return []
- else:
- return segmentPlaneIntersect(s0, s1, self.normal, self.point)
diff --git a/silx/gui/plot3d/scene/viewport.py b/silx/gui/plot3d/scene/viewport.py
deleted file mode 100644
index 6de640e..0000000
--- a/silx/gui/plot3d/scene/viewport.py
+++ /dev/null
@@ -1,603 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides a class to control a viewport on the rendering window.
-
-The :class:`Viewport` describes a Viewport rendering a scene.
-The attribute :attr:`scene` is the root group of the scene tree.
-:class:`RenderContext` handles the current state during rendering.
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import string
-import numpy
-
-from silx.gui.colors import rgba
-
-from ..._glutils import gl
-
-from . import camera
-from . import event
-from . import transform
-from .function import DirectionalLight, ClippingPlane, Fog
-
-
-class RenderContext(object):
- """Handle a current rendering context.
-
- An instance of this class is passed to rendering method through
- the scene during render.
-
- User should NEVER use an instance of this class beyond the method
- it is passed to as an argument (i.e., do not keep a reference to it).
-
- :param Viewport viewport: The viewport doing the rendering.
- :param Context glContext: The operating system OpenGL context in use.
- """
-
- _FRAGMENT_SHADER_SRC = string.Template("""
- void scene_post(vec4 cameraPosition) {
- gl_FragColor = $fogCall(gl_FragColor, cameraPosition);
- }
- """)
-
- def __init__(self, viewport, glContext):
- self._viewport = viewport
- self._glContext = glContext
- self._transformStack = [viewport.camera.extrinsic]
- self._clipPlane = ClippingPlane(normal=(0., 0., 0.))
-
- # cache
- self.__cache = {}
-
- def cache(self, key, factory, *args, **kwargs):
- """Lazy-loading cache to store values in the context for rendering
-
- :param key: The key to retrieve
- :param factory: A callback taking args and kwargs as arguments
- and returning the value to store.
- :return: The stored or newly allocated value
- """
- if key not in self.__cache:
- self.__cache[key] = factory(*args, **kwargs)
- return self.__cache[key]
-
- @property
- def viewport(self):
- """Viewport doing the current rendering"""
- return self._viewport
-
- @property
- def glCtx(self):
- """The OpenGL context in use"""
- return self._glContext
-
- @property
- def objectToCamera(self):
- """The current transform from object to camera coords.
-
- Do not modify.
- """
- return self._transformStack[-1]
-
- @property
- def projection(self):
- """Projection transform.
-
- Do not modify.
- """
- return self.viewport.camera.intrinsic
-
- @property
- def objectToNDC(self):
- """The transform from object to NDC (this includes projection).
-
- Do not modify.
- """
- return transform.StaticTransformList(
- (self.projection, self.objectToCamera))
-
- def pushTransform(self, transform_, multiply=True):
- """Push a :class:`Transform` on the transform stack.
-
- :param Transform transform_: The transform to add to the stack.
- :param bool multiply:
- True (the default) to multiply with the top of the stack,
- False to push the transform as is without multiplication.
- """
- if multiply:
- assert len(self._transformStack) >= 1
- transform_ = transform.StaticTransformList(
- (self._transformStack[-1], transform_))
-
- self._transformStack.append(transform_)
-
- def popTransform(self):
- """Pop the transform on top of the stack.
-
- :return: The Transform that is popped from the stack.
- """
- assert len(self._transformStack) > 1
- return self._transformStack.pop()
-
- @property
- def clipper(self):
- """The current clipping plane (ClippingPlane)"""
- return self._clipPlane
-
- def setClipPlane(self, point=(0., 0., 0.), normal=(0., 0., 0.)):
- """Set the clipping plane to use
-
- For now only handles a single clipping plane.
-
- :param point: A point of the plane
- :type point: 3-tuple of float
- :param normal: Normal vector of the plane or (0, 0, 0) for no clipping
- :type normal: 3-tuple of float
- """
- self._clipPlane = ClippingPlane(point, normal)
-
- def setupProgram(self, program):
- """Sets-up uniforms of a program using the context shader functions.
-
- :param GLProgram program: The program to set-up.
- It MUST be in use and using the context function.
- """
- self.clipper.setupProgram(self, program)
- self.viewport.fog.setupProgram(self, program)
-
- @property
- def fragDecl(self):
- """Fragment shader declaration for scene shader functions"""
- return '\n'.join((
- self.clipper.fragDecl,
- self.viewport.fog.fragDecl,
- self._FRAGMENT_SHADER_SRC.substitute(
- fogCall=self.viewport.fog.fragCall)))
-
- @property
- def fragCallPre(self):
- """Fragment shader call for scene shader functions (to do first)
-
- It takes the camera position (vec4) as argument.
- """
- return self.clipper.fragCall
-
- @property
- def fragCallPost(self):
- """Fragment shader call for scene shader functions (to do last)
-
- It takes the camera position (vec4) as argument.
- """
- return "scene_post"
-
-
-class Viewport(event.Notifier):
- """Rendering a single scene through a camera in part of a framebuffer.
-
- :param int framebuffer: The framebuffer ID this viewport is rendering into
- """
-
- def __init__(self, framebuffer=0):
- from . import Group # Here to avoid cyclic import
- super(Viewport, self).__init__()
- self._dirty = True
- self._origin = 0, 0
- self._size = 1, 1
- self._framebuffer = int(framebuffer)
- self.scene = Group() # The stuff to render, add overlaid scenes?
- self.scene._setParent(self)
- self.scene.addListener(self._changed)
- self._background = 0., 0., 0., 1.
- self._camera = camera.Camera(fovy=30., near=1., far=100.,
- position=(0., 0., 12.))
- self._camera.addListener(self._changed)
- self._transforms = transform.TransformList([self._camera])
-
- self._light = DirectionalLight(direction=(0., 0., -1.),
- ambient=(0.3, 0.3, 0.3),
- diffuse=(0.7, 0.7, 0.7))
- self._light.addListener(self._changed)
- self._fog = Fog()
- self._fog.isOn = False
- self._fog.addListener(self._changed)
-
- @property
- def transforms(self):
- """Proxy of camera transforms.
-
- Do not modify the list.
- """
- return self._transforms
-
- def _changed(self, *args, **kwargs):
- """Callback handling scene updates"""
- self._dirty = True
- self.notify()
-
- @property
- def dirty(self):
- """True if scene is dirty and needs redisplay."""
- return self._dirty
-
- def resetDirty(self):
- """Mark the scene as not being dirty.
-
- To call after rendering.
- """
- self._dirty = False
-
- @property
- def background(self):
- """Viewport's background color (4-tuple of float in [0, 1] or None)
-
- The background color is used to clear to viewport.
- If None, the viewport is not cleared
- """
- return self._background
-
- @background.setter
- def background(self, color):
- if color is not None:
- color = rgba(color)
- if self._background != color:
- self._background = color
- self._changed()
-
- @property
- def camera(self):
- """The camera used to render the scene."""
- return self._camera
-
- @property
- def light(self):
- """The light used to render the scene."""
- return self._light
-
- @property
- def fog(self):
- """The fog function used to render the scene"""
- return self._fog
-
- @property
- def origin(self):
- """Origin (ox, oy) of the viewport in pixels"""
- return self._origin
-
- @origin.setter
- def origin(self, origin):
- ox, oy = origin
- origin = int(ox), int(oy)
- if origin != self._origin:
- self._origin = origin
- self._changed()
-
- @property
- def size(self):
- """Size (width, height) of the viewport in pixels"""
- return self._size
-
- @size.setter
- def size(self, size):
- w, h = size
- size = int(w), int(h)
- if size != self._size:
- self._size = size
-
- self.camera.intrinsic.size = size
- self._changed()
-
- @property
- def shape(self):
- """Shape (height, width) of the viewport in pixels.
-
- This is a convenient wrapper to the inverse of size.
- """
- return self._size[1], self._size[0]
-
- @shape.setter
- def shape(self, shape):
- self.size = shape[1], shape[0]
-
- @property
- def framebuffer(self):
- """The framebuffer ID this viewport is rendering into (int)"""
- return self._framebuffer
-
- @framebuffer.setter
- def framebuffer(self, framebuffer):
- self._framebuffer = int(framebuffer)
-
- def render(self, glContext):
- """Perform the rendering of the viewport
-
- :param Context glContext: The context used for rendering"""
- # Get a chance to run deferred delete
- glContext.cleanGLGarbage()
-
- # OpenGL set-up: really need to be done once
- ox, oy = self.origin
- w, h = self.size
- gl.glViewport(ox, oy, w, h)
-
- gl.glEnable(gl.GL_SCISSOR_TEST)
- gl.glScissor(ox, oy, w, h)
-
- gl.glEnable(gl.GL_BLEND)
- gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
-
- gl.glEnable(gl.GL_DEPTH_TEST)
- gl.glDepthFunc(gl.GL_LEQUAL)
- gl.glDepthRange(0., 1.)
-
- # gl.glEnable(gl.GL_POLYGON_OFFSET_FILL)
- # gl.glPolygonOffset(1., 1.)
-
- gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
- gl.glEnable(gl.GL_LINE_SMOOTH)
-
- if self.background is None:
- gl.glClear(gl.GL_STENCIL_BUFFER_BIT |
- gl.GL_DEPTH_BUFFER_BIT)
- else:
- gl.glClearColor(*self.background)
-
- # Prepare OpenGL
- gl.glClear(gl.GL_COLOR_BUFFER_BIT |
- gl.GL_STENCIL_BUFFER_BIT |
- gl.GL_DEPTH_BUFFER_BIT)
-
- ctx = RenderContext(self, glContext)
- self.scene.render(ctx)
- self.scene.postRender(ctx)
-
- def adjustCameraDepthExtent(self):
- """Update camera depth extent to fit the scene bounds.
-
- Only near and far planes are updated.
- The scene might still not be fully visible
- (e.g., if spanning behind the viewpoint with perspective projection).
- """
- bounds = self.scene.bounds(transformed=True)
- if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
- bounds = self.camera.extrinsic.transformBounds(bounds)
-
- if isinstance(self.camera.intrinsic, transform.Perspective):
- # This needs to be reworked
- zbounds = - bounds[:, 2]
- zextent = max(numpy.fabs(zbounds[0] - zbounds[1]), 0.0001)
- near = max(zextent / 1000., 0.95 * zbounds[1])
- far = max(near + 0.1, 1.05 * zbounds[0])
-
- self.camera.intrinsic.setDepthExtent(near, far)
- elif isinstance(self.camera.intrinsic, transform.Orthographic):
- # Makes sure z bounds are included
- border = max(abs(bounds[:, 2]))
- self.camera.intrinsic.setDepthExtent(-border, border)
- else:
- raise RuntimeError('Unsupported camera', self.camera.intrinsic)
-
- def resetCamera(self):
- """Change camera to have the whole scene in the viewing frustum.
-
- It updates the camera position and depth extent.
- Camera sight direction and up are not affected.
- """
- bounds = self.scene.bounds(transformed=True)
- if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
- self.camera.resetCamera(bounds)
-
- def orbitCamera(self, direction, angle=1.):
- """Rotate the camera around center of the scene.
-
- :param str direction: Direction of movement relative to image plane.
- In: 'up', 'down', 'left', 'right'.
- :param float angle: he angle in degrees of the rotation.
- """
- bounds = self.scene.bounds(transformed=True)
- if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
- center = 0.5 * (bounds[0] + bounds[1])
- self.camera.orbit(direction, center, angle)
-
- def moveCamera(self, direction, step=0.1):
- """Move the camera relative to the image plane.
-
- :param str direction: Direction relative to image plane.
- One of: 'up', 'down', 'left', 'right',
- 'forward', 'backward'.
- :param float step: The ratio of data to step for each pan.
- """
- bounds = self.scene.bounds(transformed=True)
- if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
- bounds = self.camera.extrinsic.transformBounds(bounds)
- center = 0.5 * (bounds[0] + bounds[1])
- ndcCenter = self.camera.intrinsic.transformPoint(
- center, perspectiveDivide=True)
-
- step *= 2. # NDC has size 2
-
- if direction == 'up':
- ndcCenter[1] -= step
- elif direction == 'down':
- ndcCenter[1] += step
-
- elif direction == 'right':
- ndcCenter[0] -= step
- elif direction == 'left':
- ndcCenter[0] += step
-
- elif direction == 'forward':
- ndcCenter[2] += step
- elif direction == 'backward':
- ndcCenter[2] -= step
-
- else:
- raise ValueError('Unsupported direction: %s' % direction)
-
- newCenter = self.camera.intrinsic.transformPoint(
- ndcCenter, direct=False, perspectiveDivide=True)
-
- self.camera.move(direction, numpy.linalg.norm(newCenter - center))
-
- def windowToNdc(self, winX, winY, checkInside=True):
- """Convert position from window to normalized device coordinates.
-
- If window coordinates are int, they are moved half a pixel
- to be positioned at the center of pixel.
-
- :param winX: X window coord, origin left.
- :param winY: Y window coord, origin top.
- :param bool checkInside: If True, returns None if position is
- outside viewport.
- :return: (x, y) Normalize device coordinates in [-1, 1] or None.
- Origin center, x to the right, y goes upward.
- """
- ox, oy = self._origin
- width, height = self.size
-
- # If int, move it to the center of pixel
- if isinstance(winX, int):
- winX += 0.5
- if isinstance(winY, int):
- winY += 0.5
-
- x, y = winX - ox, winY - oy
-
- if checkInside and (x < 0. or x > width or y < 0. or y > height):
- return None # Out of viewport
-
- ndcx = 2. * x / float(width) - 1.
- ndcy = 1. - 2. * y / float(height)
- return ndcx, ndcy
-
- def ndcToWindow(self, ndcX, ndcY, checkInside=True):
- """Convert position from normalized device coordinates (NDC) to window.
-
- :param float ndcX: X NDC coord.
- :param float ndcY: Y NDC coord.
- :param bool checkInside: If True, returns None if position is
- outside viewport.
- :return: (x, y) window coordinates or None.
- Origin top-left, x to the right, y goes downward.
- """
- if (checkInside and
- (ndcX < -1. or ndcX > 1. or ndcY < -1. or ndcY > 1.)):
- return None # Outside viewport
-
- ox, oy = self._origin
- width, height = self.size
-
- winx = ox + width * 0.5 * (ndcX + 1.)
- winy = oy + height * 0.5 * (1. - ndcY)
- return winx, winy
-
- def _pickNdcZGL(self, x, y, offset=0):
- """Retrieve depth from depth buffer and return corresponding NDC Z.
-
- :param int x: In pixels in window coordinates, origin left.
- :param int y: In pixels in window coordinates, origin top.
- :param int offset: Number of pixels to look at around the given pixel
-
- :return: Normalize device Z coordinate of depth in [-1, 1]
- or None if outside viewport.
- :rtype: float or None
- """
- ox, oy = self._origin
- width, height = self.size
-
- x = int(x)
- y = height - int(y) # Invert y coord
-
- if x < ox or x > ox + width or y < oy or y > oy + height:
- # Outside viewport
- return None
-
- # Get depth from depth buffer in [0., 1.]
- # Bind used framebuffer to get depth
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebuffer)
-
- if offset == 0: # Fast path
- # glReadPixels is not GL|ES friendly
- depth = gl.glReadPixels(
- x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)[0]
- else:
- offset = abs(int(offset))
- size = 2*offset + 1
- depthPatch = gl.glReadPixels(
- x - offset, y - offset,
- size, size,
- gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)
- depthPatch = depthPatch.ravel() # Work in 1D
-
- # TODO cache sortedIndices to avoid computing it each time
- # Compute distance of each pixels to the center of the patch
- offsetToCenter = numpy.arange(- offset, offset + 1, dtype=numpy.float32) ** 2
- sqDistToCenter = numpy.add.outer(offsetToCenter, offsetToCenter)
-
- # Use distance to center to sort values from the patch
- sortedIndices = numpy.argsort(sqDistToCenter.ravel())
- sortedValues = depthPatch[sortedIndices]
-
- # Take first depth that is not 1 in the sorted values
- hits = sortedValues[sortedValues != 1.]
- depth = 1. if len(hits) == 0 else hits[0]
-
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
-
- # Z in NDC in [-1., 1.]
- return float(depth) * 2. - 1.
-
- def _getXZYGL(self, x, y):
- ndc = self.windowToNdc(x, y)
- if ndc is None:
- return None # Outside viewport
- ndcz = self._pickNdcZGL(x, y)
- ndcpos = numpy.array((ndc[0], ndc[1], ndcz, 1.), dtype=numpy.float32)
-
- camerapos = self.camera.intrinsic.transformPoint(
- ndcpos, direct=False, perspectiveDivide=True)
-
- scenepos = self.camera.extrinsic.transformPoint(camerapos,
- direct=False)
- return scenepos[:3]
-
- def pick(self, x, y):
- pass
- # ndcX, ndcY = self.windowToNdc(x, y)
- # ndcNearPt = ndcX, ndcY, -1.
- # ndcFarPT = ndcX, ndcY, 1.
diff --git a/silx/gui/plot3d/scene/window.py b/silx/gui/plot3d/scene/window.py
deleted file mode 100644
index baa76a2..0000000
--- a/silx/gui/plot3d/scene/window.py
+++ /dev/null
@@ -1,430 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-"""This module provides a class for Viewports rendering on the screen.
-
-The :class:`Window` renders a list of Viewports in the current framebuffer.
-The rendering can be performed in an off-screen framebuffer that is only
-updated when the scene has changed and not each time Qt is requiring a repaint.
-
-The :class:`Context` and :class:`ContextGL2` represent the operating system
-OpenGL context and handle OpenGL resources.
-"""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "10/01/2017"
-
-
-import weakref
-import numpy
-
-from ..._glutils import gl
-from ... import _glutils
-
-from . import event
-
-
-class Context(object):
- """Correspond to an operating system OpenGL context.
-
- User should NEVER use an instance of this class beyond the method
- it is passed to as an argument (i.e., do not keep a reference to it).
-
- :param glContextHandle: System specific OpenGL context handle.
- """
-
- def __init__(self, glContextHandle):
- self._context = glContextHandle
- self._isCurrent = False
- self._devicePixelRatio = 1.0
-
- @property
- def isCurrent(self):
- """Whether this OpenGL context is the current one or not."""
- return self._isCurrent
-
- def setCurrent(self, isCurrent=True):
- """Set the state of the OpenGL context to reflect OpenGL state.
-
- This should not be called from the scene graph, only in the
- wrapper that handle the OpenGL context to reflect its state.
-
- :param bool isCurrent: The state of the system OpenGL context.
- """
- self._isCurrent = bool(isCurrent)
-
- @property
- def devicePixelRatio(self):
- """Ratio between device and device independent pixels (float)
-
- This is useful for font rendering.
- """
- return self._devicePixelRatio
-
- @devicePixelRatio.setter
- def devicePixelRatio(self, ratio):
- assert ratio > 0
- self._devicePixelRatio = float(ratio)
-
- def __enter__(self):
- self.setCurrent(True)
- return self
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.setCurrent(False)
-
- @property
- def glContext(self):
- """The handle to the OpenGL context provided by the system."""
- return self._context
-
- def cleanGLGarbage(self):
- """This is releasing OpenGL resource that are no longer used."""
- pass
-
-
-class ContextGL2(Context):
- """Handle a system GL2 context.
-
- User should NEVER use an instance of this class beyond the method
- it is passed to as an argument (i.e., do not keep a reference to it).
-
- :param glContextHandle: System specific OpenGL context handle.
- """
- def __init__(self, glContextHandle):
- super(ContextGL2, self).__init__(glContextHandle)
-
- self._programs = {} # GL programs already compiled
- self._vbos = {} # GL Vbos already set
- self._vboGarbage = [] # Vbos waiting to be discarded
-
- # programs
-
- def prog(self, vertexShaderSrc, fragmentShaderSrc, attrib0='position'):
- """Cache program within context.
-
- WARNING: No clean-up.
-
- :param str vertexShaderSrc: Vertex shader source code
- :param str fragmentShaderSrc: Fragment shader source code
- :param str attrib0:
- Attribute's name to bind to position 0 (default: 'position').
- On some platform, this attribute MUST be active and with an
- array attached to it in order for the rendering to occur....
- """
- assert self.isCurrent
- key = vertexShaderSrc, fragmentShaderSrc, attrib0
- program = self._programs.get(key, None)
- if program is None:
- program = _glutils.Program(
- vertexShaderSrc, fragmentShaderSrc, attrib0=attrib0)
- self._programs[key] = program
- return program
-
- # VBOs
-
- def makeVbo(self, data=None, sizeInBytes=None,
- usage=None, target=None):
- """Create a VBO in this context with the data.
-
- Current limitations:
-
- - One array per VBO
- - Do not support sharing VertexBuffer across VboAttrib
-
- Automatically discards the VBO when the returned
- :class:`VertexBuffer` istance is deleted.
-
- :param numpy.ndarray data: 2D array of data to store in VBO or None.
- :param int sizeInBytes: Size of the VBO or None.
- It should be <= data.nbytes if both are given.
- :param usage: OpenGL usage define in VertexBuffer._USAGES.
- :param target: OpenGL target in VertexBuffer._TARGETS.
- :return: The VertexBuffer created in this context.
- """
- assert self.isCurrent
- vbo = _glutils.VertexBuffer(data, sizeInBytes, usage, target)
- vboref = weakref.ref(vbo, self._deadVbo)
- # weakref is hashable as far as target is
- self._vbos[vboref] = vbo.name
- return vbo
-
- def makeVboAttrib(self, data, usage=None, target=None):
- """Create a VBO from data and returns the associated VBOAttrib.
-
- Automatically discards the VBO when the returned
- :class:`VBOAttrib` istance is deleted.
-
- :param numpy.ndarray data: 2D array of data to store in VBO or None.
- :param usage: OpenGL usage define in VertexBuffer._USAGES.
- :param target: OpenGL target in VertexBuffer._TARGETS.
- :returns: A VBOAttrib instance created in this context.
- """
- assert self.isCurrent
- vbo = self.makeVbo(data, usage=usage, target=target)
-
- assert len(data.shape) <= 2
- dimension = 1 if len(data.shape) == 1 else data.shape[1]
-
- return _glutils.VertexBufferAttrib(
- vbo,
- type_=_glutils.numpyToGLType(data.dtype),
- size=data.shape[0],
- dimension=dimension,
- offset=0,
- stride=0)
-
- def _deadVbo(self, vboRef):
- """Callback handling dead VBOAttribs."""
- vboid = self._vbos.pop(vboRef)
- if self.isCurrent:
- # Direct delete if context is active
- gl.glDeleteBuffers(vboid)
- else:
- # Deferred VBO delete if context is not active
- self._vboGarbage.append(vboid)
-
- def cleanGLGarbage(self):
- """Delete OpenGL resources that are pending for destruction.
-
- This requires the associated OpenGL context to be active.
- This is meant to be called before rendering.
- """
- assert self.isCurrent
- if self._vboGarbage:
- vboids = self._vboGarbage
- gl.glDeleteBuffers(vboids)
- self._vboGarbage = []
-
-
-class Window(event.Notifier):
- """OpenGL Framebuffer where to render viewports
-
- :param str mode: Rendering mode to use:
-
- - 'direct' to render everything for each render call
- - 'framebuffer' to cache viewport rendering in a texture and
- update the texture only when needed.
- """
-
- _position = numpy.array(((-1., -1., 0., 0.),
- (1., -1., 1., 0.),
- (-1., 1., 0., 1.),
- (1., 1., 1., 1.)),
- dtype=numpy.float32)
-
- _shaders = ("""
- attribute vec4 position;
- varying vec2 textureCoord;
-
- void main(void) {
- gl_Position = vec4(position.x, position.y, 0., 1.);
- textureCoord = position.zw;
- }
- """,
- """
- uniform sampler2D texture;
- varying vec2 textureCoord;
-
- void main(void) {
- gl_FragColor = texture2D(texture, textureCoord);
- gl_FragColor.a = 1.0;
- }
- """)
-
- def __init__(self, mode='framebuffer'):
- super(Window, self).__init__()
- self._dirty = True
- self._size = 0, 0
- self._contexts = {} # To map system GL context id to Context objects
- self._viewports = event.NotifierList()
- self._viewports.addListener(self._updated)
- self._framebufferid = 0
- self._framebuffers = {} # Cache of framebuffers
-
- assert mode in ('direct', 'framebuffer')
- self._isframebuffer = mode == 'framebuffer'
-
- @property
- def dirty(self):
- """True if this object or any attached viewports is dirty."""
- for viewport in self._viewports:
- if viewport.dirty:
- return True
- return self._dirty
-
- @property
- def size(self):
- """Size (width, height) of the window in pixels"""
- return self._size
-
- @size.setter
- def size(self, size):
- w, h = size
- size = int(w), int(h)
- if size != self._size:
- self._size = size
- self._dirty = True
- self.notify()
-
- @property
- def shape(self):
- """Shape (height, width) of the window in pixels.
-
- This is a convenient wrapper to the reverse of size.
- """
- return self._size[1], self._size[0]
-
- @shape.setter
- def shape(self, shape):
- self.size = shape[1], shape[0]
-
- @property
- def viewports(self):
- """List of viewports to render in the corresponding framebuffer"""
- return self._viewports
-
- @viewports.setter
- def viewports(self, iterable):
- self._viewports.removeListener(self._updated)
- self._viewports = event.NotifierList(iterable)
- self._viewports.addListener(self._updated)
- self._updated(self)
-
- def _updated(self, source, *args, **kwargs):
- self._dirty = True
- self.notify(*args, **kwargs)
-
- framebufferid = property(lambda self: self._framebufferid,
- doc="Framebuffer ID used to perform rendering")
-
- def grab(self, glcontext):
- """Returns the raster of the scene as an RGB numpy array
-
- :returns: OpenGL scene RGB bitmap
- as an array of dimension (height, width, 3)
- :rtype: numpy.ndarray of uint8
- """
- height, width = self.shape
- image = numpy.empty((height, width, 3), dtype=numpy.uint8)
-
- previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebufferid)
- gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
- gl.glReadPixels(
- 0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, image)
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)
-
- # glReadPixels gives bottom to top,
- # while images are stored as top to bottom
- image = numpy.flipud(image)
-
- return numpy.array(image, copy=False, order='C')
-
- def render(self, glcontext, devicePixelRatio):
- """Perform the rendering of attached viewports
-
- :param glcontext: System identifier of the OpenGL context
- :param float devicePixelRatio:
- Ratio between device and device-independent pixels
- """
- if glcontext not in self._contexts:
- self._contexts[glcontext] = ContextGL2(glcontext) # New context
-
- with self._contexts[glcontext] as context:
- context.devicePixelRatio = devicePixelRatio
- if self._isframebuffer:
- self._renderWithOffscreenFramebuffer(context)
- else:
- self._renderDirect(context)
-
- self._dirty = False
-
- def _renderDirect(self, context):
- """Perform the direct rendering of attached viewports
-
- :param Context context: Object wrapping OpenGL context
- """
- for viewport in self._viewports:
- viewport.framebuffer = self.framebufferid
- viewport.render(context)
- viewport.resetDirty()
-
- def _renderWithOffscreenFramebuffer(self, context):
- """Renders viewports in a texture and render this texture on screen.
-
- The texture is updated only if viewport or size has changed.
-
- :param ContextGL2 context: Object wrappign OpenGL context
- """
- if self.dirty or context not in self._framebuffers:
- # Need to redraw framebuffer content
-
- if (context not in self._framebuffers or
- self._framebuffers[context].shape != self.shape):
- # Need to rebuild framebuffer
-
- if context in self._framebuffers:
- self._framebuffers[context].discard()
-
- fbo = _glutils.FramebufferTexture(gl.GL_RGBA,
- shape=self.shape,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
- self._framebuffers[context] = fbo
- self._framebufferid = fbo.name
-
- # Render in framebuffer
- with self._framebuffers[context]:
- self._renderDirect(context)
-
- # Render framebuffer texture to screen
- fbo = self._framebuffers[context]
- height, width = fbo.shape
-
- program = context.prog(*self._shaders)
- program.use()
-
- gl.glViewport(0, 0, width, height)
- gl.glDisable(gl.GL_BLEND)
- gl.glDisable(gl.GL_DEPTH_TEST)
- gl.glDisable(gl.GL_SCISSOR_TEST)
- # gl.glScissor(0, 0, width, height)
- gl.glClearColor(0., 0., 0., 0.)
- gl.glClear(gl.GL_COLOR_BUFFER_BIT)
- gl.glUniform1i(program.uniforms['texture'], fbo.texture.texUnit)
- gl.glEnableVertexAttribArray(program.attributes['position'])
- gl.glVertexAttribPointer(program.attributes['position'],
- 4,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._position)
- fbo.texture.bind()
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._position))
- gl.glBindTexture(gl.GL_TEXTURE_2D, 0)
diff --git a/silx/gui/plot3d/setup.py b/silx/gui/plot3d/setup.py
deleted file mode 100644
index 59c0230..0000000
--- a/silx/gui/plot3d/setup.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('plot3d', parent_package, top_path)
- config.add_subpackage('_model')
- config.add_subpackage('actions')
- config.add_subpackage('items')
- config.add_subpackage('scene')
- config.add_subpackage('scene.test')
- config.add_subpackage('tools')
- config.add_subpackage('tools.test')
- config.add_subpackage('test')
- config.add_subpackage('utils')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
diff --git a/silx/gui/plot3d/test/__init__.py b/silx/gui/plot3d/test/__init__.py
deleted file mode 100644
index 77172d1..0000000
--- a/silx/gui/plot3d/test/__init__.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# 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."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "09/11/2017"
-
-
-import logging
-import unittest
-from silx.test.utils import test_options
-
-
-_logger = logging.getLogger(__name__)
-
-
-def suite():
- testsuite = unittest.TestSuite()
-
- if not test_options.WITH_GL_TEST:
- # Explicitly disabled tests
- msg = "silx.gui.plot3d tests disabled: %s" % test_options.WITH_GL_TEST_REASON
- _logger.warning(msg)
-
- class SkipPlot3DTest(unittest.TestCase):
- def runTest(self):
- self.skipTest(test_options.WITH_GL_TEST_REASON)
-
- testsuite.addTest(SkipPlot3DTest())
- return testsuite
-
- # Import here to avoid loading modules if tests are disabled
-
- from ..scene.test import suite as sceneTestSuite
- from ..tools.test import suite as toolsTestSuite
- from .testGL import suite as testGLSuite
- from .testScalarFieldView import suite as testScalarFieldViewSuite
- from .testSceneWidget import suite as testSceneWidgetSuite
- from .testSceneWidgetPicking import suite as testSceneWidgetPickingSuite
- from .testSceneWindow import suite as testSceneWindowSuite
- from .testStatsWidget import suite as testStatsWidgetSuite
-
- testsuite = unittest.TestSuite()
- testsuite.addTest(testGLSuite())
- testsuite.addTest(sceneTestSuite())
- testsuite.addTest(testScalarFieldViewSuite())
- testsuite.addTest(testSceneWidgetSuite())
- testsuite.addTest(testSceneWidgetPickingSuite())
- testsuite.addTest(testSceneWindowSuite())
- testsuite.addTest(toolsTestSuite())
- testsuite.addTest(testStatsWidgetSuite())
- return testsuite
diff --git a/silx/gui/plot3d/test/testGL.py b/silx/gui/plot3d/test/testGL.py
deleted file mode 100644
index ae167ab..0000000
--- a/silx/gui/plot3d/test/testGL.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# 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()
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestOpenGL))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/test/testScalarFieldView.py b/silx/gui/plot3d/test/testScalarFieldView.py
deleted file mode 100644
index d9c743b..0000000
--- a/silx/gui/plot3d/test/testScalarFieldView.py
+++ /dev/null
@@ -1,139 +0,0 @@
-# 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)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestScalarFieldView))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/test/testSceneWidget.py b/silx/gui/plot3d/test/testSceneWidget.py
deleted file mode 100644
index 13ddd37..0000000
--- a/silx/gui/plot3d/test/testSceneWidget.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# 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()
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestSceneWidget))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/test/testSceneWidgetPicking.py b/silx/gui/plot3d/test/testSceneWidgetPicking.py
deleted file mode 100644
index aea30f6..0000000
--- a/silx/gui/plot3d/test/testSceneWidgetPicking.py
+++ /dev/null
@@ -1,326 +0,0 @@
-# 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)
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestSceneWidgetPicking))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/test/testSceneWindow.py b/silx/gui/plot3d/test/testSceneWindow.py
deleted file mode 100644
index 8cf6b81..0000000
--- a/silx/gui/plot3d/test/testSceneWindow.py
+++ /dev/null
@@ -1,245 +0,0 @@
-# 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)
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestSceneWindow))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/test/testStatsWidget.py b/silx/gui/plot3d/test/testStatsWidget.py
deleted file mode 100644
index bcab1a4..0000000
--- a/silx/gui/plot3d/test/testStatsWidget.py
+++ /dev/null
@@ -1,216 +0,0 @@
-# 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)))
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestSceneWidget))
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestScalarFieldView))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/tools/GroupPropertiesWidget.py b/silx/gui/plot3d/tools/GroupPropertiesWidget.py
deleted file mode 100644
index ec995a3..0000000
--- a/silx/gui/plot3d/tools/GroupPropertiesWidget.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 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.
-#
-# ###########################################################################*/
-""":class:`GroupPropertiesWidget` allows to reset properties in a GroupItem."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-from ....gui import qt
-from ....gui.colors import Colormap
-from ....gui.dialog.ColormapDialog import ColormapDialog
-
-from ..items import SymbolMixIn, ColormapMixIn
-
-
-class GroupPropertiesWidget(qt.QWidget):
- """Set properties of all items in a :class:`GroupItem`
-
- :param QWidget parent:
- """
-
- MAX_MARKER_SIZE = 20
- """Maximum value for marker size"""
-
- MAX_LINE_WIDTH = 10
- """Maximum value for line width"""
-
- def __init__(self, parent=None):
- super(GroupPropertiesWidget, self).__init__(parent)
- self._group = None
- self.setEnabled(False)
-
- # Set widgets
- layout = qt.QFormLayout(self)
- self.setLayout(layout)
-
- # Colormap
- colormapButton = qt.QPushButton('Set...')
- colormapButton.setToolTip("Set colormap for all items")
- colormapButton.clicked.connect(self._colormapButtonClicked)
- layout.addRow('Colormap', colormapButton)
-
- self._markerComboBox = qt.QComboBox(self)
- self._markerComboBox.addItems(SymbolMixIn.getSupportedSymbolNames())
-
- # Marker
- markerButton = qt.QPushButton('Set')
- markerButton.setToolTip("Set marker for all items")
- markerButton.clicked.connect(self._markerButtonClicked)
-
- markerLayout = qt.QHBoxLayout()
- markerLayout.setContentsMargins(0, 0, 0, 0)
- markerLayout.addWidget(self._markerComboBox, 1)
- markerLayout.addWidget(markerButton, 0)
-
- layout.addRow('Marker', markerLayout)
-
- # Marker size
- self._markerSizeSlider = qt.QSlider()
- self._markerSizeSlider.setOrientation(qt.Qt.Horizontal)
- self._markerSizeSlider.setSingleStep(1)
- self._markerSizeSlider.setRange(1, self.MAX_MARKER_SIZE)
- self._markerSizeSlider.setValue(1)
-
- markerSizeButton = qt.QPushButton('Set')
- markerSizeButton.setToolTip("Set marker size for all items")
- markerSizeButton.clicked.connect(self._markerSizeButtonClicked)
-
- markerSizeLayout = qt.QHBoxLayout()
- markerSizeLayout.setContentsMargins(0, 0, 0, 0)
- markerSizeLayout.addWidget(qt.QLabel('1'))
- markerSizeLayout.addWidget(self._markerSizeSlider, 1)
- markerSizeLayout.addWidget(qt.QLabel(str(self.MAX_MARKER_SIZE)))
- markerSizeLayout.addWidget(markerSizeButton, 0)
-
- layout.addRow('Marker Size', markerSizeLayout)
-
- # Line width
- self._lineWidthSlider = qt.QSlider()
- self._lineWidthSlider.setOrientation(qt.Qt.Horizontal)
- self._lineWidthSlider.setSingleStep(1)
- self._lineWidthSlider.setRange(1, self.MAX_LINE_WIDTH)
- self._lineWidthSlider.setValue(1)
-
- lineWidthButton = qt.QPushButton('Set')
- lineWidthButton.setToolTip("Set line width for all items")
- lineWidthButton.clicked.connect(self._lineWidthButtonClicked)
-
- lineWidthLayout = qt.QHBoxLayout()
- lineWidthLayout.setContentsMargins(0, 0, 0, 0)
- lineWidthLayout.addWidget(qt.QLabel('1'))
- lineWidthLayout.addWidget(self._lineWidthSlider, 1)
- lineWidthLayout.addWidget(qt.QLabel(str(self.MAX_LINE_WIDTH)))
- lineWidthLayout.addWidget(lineWidthButton, 0)
-
- layout.addRow('Line Width', lineWidthLayout)
-
- self._colormapDialog = None # To store dialog
- self._colormap = Colormap()
-
- def getGroup(self):
- """Returns the :class:`GroupItem` this widget is attached to.
-
- :rtype: Union[GroupItem, None]
- """
- return self._group
-
- def setGroup(self, group):
- """Set the :class:`GroupItem` this widget is attached to.
-
- :param GroupItem group: GroupItem to control (or None)
- """
- self._group = group
- if group is not None:
- self.setEnabled(True)
-
- def _colormapButtonClicked(self, checked=False):
- """Handle colormap button clicked"""
- group = self.getGroup()
- if group is None:
- return
-
- if self._colormapDialog is None:
- self._colormapDialog = ColormapDialog(self)
- self._colormapDialog.setColormap(self._colormap)
-
- previousColormap = self._colormapDialog.getColormap()
- if self._colormapDialog.exec_():
- colormap = self._colormapDialog.getColormap()
-
- for item in group.visit():
- if isinstance(item, ColormapMixIn):
- itemCmap = item.getColormap()
- cmapName = colormap.getName()
- if cmapName is not None:
- itemCmap.setName(colormap.getName())
- else:
- itemCmap.setColormapLUT(colormap.getColormapLUT())
- itemCmap.setNormalization(colormap.getNormalization())
- itemCmap.setGammaNormalizationParameter(
- colormap.getGammaNormalizationParameter())
- itemCmap.setVRange(colormap.getVMin(), colormap.getVMax())
- else:
- # Reset colormap
- self._colormapDialog.setColormap(previousColormap)
-
- def _markerButtonClicked(self, checked=False):
- """Handle marker set button clicked"""
- group = self.getGroup()
- if group is None:
- return
-
- marker = self._markerComboBox.currentText()
- for item in group.visit():
- if isinstance(item, SymbolMixIn):
- item.setSymbol(marker)
-
- def _markerSizeButtonClicked(self, checked=False):
- """Handle marker size set button clicked"""
- group = self.getGroup()
- if group is None:
- return
-
- markerSize = self._markerSizeSlider.value()
- for item in group.visit():
- if isinstance(item, SymbolMixIn):
- item.setSymbolSize(markerSize)
-
- def _lineWidthButtonClicked(self, checked=False):
- """Handle line width set button clicked"""
- group = self.getGroup()
- if group is None:
- return
-
- lineWidth = self._lineWidthSlider.value()
- for item in group.visit():
- if hasattr(item, 'setLineWidth'):
- item.setLineWidth(lineWidth)
diff --git a/silx/gui/plot3d/tools/PositionInfoWidget.py b/silx/gui/plot3d/tools/PositionInfoWidget.py
deleted file mode 100644
index 78f2959..0000000
--- a/silx/gui/plot3d/tools/PositionInfoWidget.py
+++ /dev/null
@@ -1,219 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-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.
-#
-# ###########################################################################*/
-"""This module provides a widget that displays data values of a SceneWidget.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/10/2018"
-
-
-import logging
-import weakref
-
-from ... import qt
-from .. import actions
-from .. import items
-from ..items import volume
-from ..SceneWidget import SceneWidget
-
-
-_logger = logging.getLogger(__name__)
-
-
-class PositionInfoWidget(qt.QWidget):
- """Widget displaying information about picked position
-
- :param QWidget parent: See :class:`QWidget`
- """
-
- def __init__(self, parent=None):
- super(PositionInfoWidget, self).__init__(parent)
- self._sceneWidgetRef = None
-
- self.setToolTip("Double-click on a data point to show its value")
- layout = qt.QBoxLayout(qt.QBoxLayout.LeftToRight, self)
-
- self._xLabel = self._addInfoField('X')
- self._yLabel = self._addInfoField('Y')
- self._zLabel = self._addInfoField('Z')
- self._dataLabel = self._addInfoField('Data')
- self._itemLabel = self._addInfoField('Item')
-
- layout.addStretch(1)
-
- self._action = actions.mode.PickingModeAction(parent=self)
- self._action.setText('Selection')
- self._action.setToolTip(
- 'Toggle selection information update with left button click')
- self._action.sigSceneClicked.connect(self.pick)
- self._action.changed.connect(self.__actionChanged)
- self._action.setChecked(False) # Disabled by default
- self.__actionChanged() # Sync action/widget
-
- def __actionChanged(self):
- """Handle toggle action change signal"""
- if self.toggleAction().isChecked() != self.isEnabled():
- self.setEnabled(self.toggleAction().isChecked())
-
- def toggleAction(self):
- """The action to toggle the picking mode.
-
- :rtype: QAction
- """
- return self._action
-
- def _addInfoField(self, label):
- """Add a description: info widget to this widget
-
- :param str label: Description label
- :return: The QLabel used to display the info
- :rtype: QLabel
- """
- subLayout = qt.QHBoxLayout()
- subLayout.setContentsMargins(0, 0, 0, 0)
-
- subLayout.addWidget(qt.QLabel(label + ':'))
-
- widget = qt.QLabel('-')
- widget.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignVCenter)
- widget.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
- widget.setMinimumWidth(widget.fontMetrics().width('#######'))
- subLayout.addWidget(widget)
-
- subLayout.addStretch(1)
-
- layout = self.layout()
- layout.addLayout(subLayout)
- return widget
-
- def getSceneWidget(self):
- """Returns the associated :class:`SceneWidget` or None.
-
- :rtype: Union[None,~silx.gui.plot3d.SceneWidget.SceneWidget]
- """
- if self._sceneWidgetRef is None:
- return None
- else:
- return self._sceneWidgetRef()
-
- def setSceneWidget(self, widget):
- """Set the associated :class:`SceneWidget`
-
- :param ~silx.gui.plot3d.SceneWidget.SceneWidget widget:
- 3D scene for which to display information
- """
- if widget is not None and not isinstance(widget, SceneWidget):
- raise ValueError("widget must be a SceneWidget or None")
-
- self._sceneWidgetRef = None if widget is None else weakref.ref(widget)
-
- self.toggleAction().setPlot3DWidget(widget)
-
- def clear(self):
- """Clean-up displayed values"""
- for widget in (self._xLabel, self._yLabel, self._zLabel,
- self._dataLabel, self._itemLabel):
- widget.setText('-')
-
- _SUPPORTED_ITEMS = (items.Scatter3D,
- items.Scatter2D,
- items.ImageData,
- items.ImageRgba,
- items.HeightMapData,
- items.HeightMapRGBA,
- items.Mesh,
- items.Box,
- items.Cylinder,
- items.Hexagon,
- volume.CutPlane,
- volume.Isosurface)
- """Type of items that are picked"""
-
- def _isSupportedItem(self, item):
- """Returns True if item is of supported type
-
- :param Item3D item: The Item3D to check
- :rtype: bool
- """
- return isinstance(item, self._SUPPORTED_ITEMS)
-
- def pick(self, x, y):
- """Pick items in the associated SceneWidget and display result
-
- Only the closest point is displayed.
-
- :param int x: X coordinate in pixel in the SceneWidget
- :param int y: Y coordinate in pixel in the SceneWidget
- """
- self.clear()
-
- sceneWidget = self.getSceneWidget()
- if sceneWidget is None: # No associated widget
- _logger.info('Picking without associated SceneWidget')
- return
-
- # Find closest (and latest in the tree) supported item
- closestNdcZ = float('inf')
- picking = None
- for result in sceneWidget.pickItems(x, y,
- condition=self._isSupportedItem):
- ndcZ = result.getPositions('ndc', copy=False)[0, 2]
- if ndcZ <= closestNdcZ:
- closestNdcZ = ndcZ
- picking = result
-
- if picking is None:
- return # No picked item
-
- item = picking.getItem()
- self._itemLabel.setText(item.getLabel())
- positions = picking.getPositions('scene', copy=False)
- x, y, z = positions[0]
- self._xLabel.setText("%g" % x)
- self._yLabel.setText("%g" % y)
- self._zLabel.setText("%g" % z)
-
- data = picking.getData(copy=False)
- if data is not None:
- data = data[0]
- if hasattr(data, '__len__'):
- text = ' '.join(["%.3g"] * len(data)) % tuple(data)
- else:
- text = "%g" % data
- self._dataLabel.setText(text)
-
- def updateInfo(self):
- """Update information according to cursor position"""
- widget = self.getSceneWidget()
- if widget is None:
- _logger.info('Update without associated SceneWidget')
- self.clear()
- return
-
- position = widget.mapFromGlobal(qt.QCursor.pos())
- self.pick(position.x(), position.y())
diff --git a/silx/gui/plot3d/tools/ViewpointTools.py b/silx/gui/plot3d/tools/ViewpointTools.py
deleted file mode 100644
index 0607382..0000000
--- a/silx/gui/plot3d/tools/ViewpointTools.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-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.
-#
-# ###########################################################################*/
-"""This module provides a toolbar to control Plot3DWidget viewpoint."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/09/2017"
-
-
-import weakref
-
-from silx.gui import qt
-from silx.gui.icons import getQIcon
-from .. import actions
-
-
-class ViewpointToolButton(qt.QToolButton):
- """A toolbutton with a drop-down list of ways to reset the viewpoint.
-
- :param parent: See :class:`QToolButton`
- """
-
- def __init__(self, parent=None):
- super(ViewpointToolButton, self).__init__(parent)
-
- self._plot3DRef = None
-
- menu = qt.QMenu(self)
- menu.addAction(actions.viewpoint.FrontViewpointAction(parent=self))
- menu.addAction(actions.viewpoint.BackViewpointAction(parent=self))
- menu.addAction(actions.viewpoint.TopViewpointAction(parent=self))
- menu.addAction(actions.viewpoint.BottomViewpointAction(parent=self))
- menu.addAction(actions.viewpoint.RightViewpointAction(parent=self))
- menu.addAction(actions.viewpoint.LeftViewpointAction(parent=self))
- menu.addAction(actions.viewpoint.SideViewpointAction(parent=self))
-
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
- self.setIcon(getQIcon('cube'))
- self.setToolTip('Reset the viewpoint to a defined position')
-
- def setPlot3DWidget(self, widget):
- """Set the Plot3DWidget this toolbar is associated with
-
- :param ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget widget:
- The widget to control
- """
- self._plot3DRef = None if widget is None else weakref.ref(widget)
-
- for action in self.menu().actions():
- action.setPlot3DWidget(widget)
-
- def getPlot3DWidget(self):
- """Return the Plot3DWidget associated to this toolbar.
-
- If no widget is associated, it returns None.
-
- :rtype: ~silx.gui.plot3d.Plot3DWidget.Plot3DWidget or None
- """
- return None if self._plot3DRef is None else self._plot3DRef()
diff --git a/silx/gui/plot3d/tools/__init__.py b/silx/gui/plot3d/tools/__init__.py
deleted file mode 100644
index c8b8d21..0000000
--- a/silx/gui/plot3d/tools/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides tool widgets that can be attached to a plot3DWidget."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/09/2017"
-
-from .toolbars import InteractiveModeToolBar # noqa
-from .toolbars import OutputToolBar # noqa
-from .toolbars import ViewpointToolBar # noqa
-from .ViewpointTools import ViewpointToolButton # noqa
diff --git a/silx/gui/plot3d/tools/test/__init__.py b/silx/gui/plot3d/tools/test/__init__.py
deleted file mode 100644
index 2dbc0ab..0000000
--- a/silx/gui/plot3d/tools/test/__init__.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 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.
-#
-# ###########################################################################*/
-"""plot3d tools test suite."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/10/2018"
-
-
-import unittest
-from .testPositionInfoWidget import suite as testPositionInfoWidgetSuite
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(testPositionInfoWidgetSuite())
- return testsuite
diff --git a/silx/gui/plot3d/tools/test/testPositionInfoWidget.py b/silx/gui/plot3d/tools/test/testPositionInfoWidget.py
deleted file mode 100644
index 4520a2a..0000000
--- a/silx/gui/plot3d/tools/test/testPositionInfoWidget.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 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 PositionInfoWidget"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/10/2018"
-
-
-import unittest
-
-import numpy
-
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui import qt
-
-from silx.gui.plot3d.SceneWidget import SceneWidget
-from silx.gui.plot3d.tools.PositionInfoWidget import PositionInfoWidget
-
-
-class TestPositionInfoWidget(TestCaseQt):
- """Tests PositionInfoWidget"""
-
- def setUp(self):
- super(TestPositionInfoWidget, self).setUp()
- self.sceneWidget = SceneWidget()
- self.sceneWidget.resize(300, 300)
- self.sceneWidget.show()
-
- self.positionInfoWidget = PositionInfoWidget()
- self.positionInfoWidget.setSceneWidget(self.sceneWidget)
- self.positionInfoWidget.show()
- self.qWaitForWindowExposed(self.positionInfoWidget)
-
- # self.qWaitForWindowExposed(self.widget)
-
- def tearDown(self):
- self.qapp.processEvents()
-
- self.sceneWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.sceneWidget.close()
- del self.sceneWidget
-
- self.positionInfoWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.positionInfoWidget.close()
- del self.positionInfoWidget
- super(TestPositionInfoWidget, self).tearDown()
-
- def test(self):
- """Test PositionInfoWidget"""
- self.assertIs(self.positionInfoWidget.getSceneWidget(),
- self.sceneWidget)
-
- data = numpy.arange(100)
- self.sceneWidget.add2DScatter(x=data, y=data, value=data)
- self.sceneWidget.resetZoom('front')
-
- # Double click at the center
- self.mouseDClick(self.sceneWidget, button=qt.Qt.LeftButton)
-
- # Clear displayed value
- self.positionInfoWidget.clear()
-
- # Update info from API
- self.positionInfoWidget.pick(x=10, y=10)
-
- # Remove SceneWidget
- self.positionInfoWidget.setSceneWidget(None)
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestPositionInfoWidget))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot3d/tools/toolbars.py b/silx/gui/plot3d/tools/toolbars.py
deleted file mode 100644
index d4f32db..0000000
--- a/silx/gui/plot3d/tools/toolbars.py
+++ /dev/null
@@ -1,209 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""This module provides toolbars with tools for a Plot3DWidget.
-
-It provides the following toolbars:
-
-- :class:`InteractiveModeToolBar` with:
- - Set interactive mode to rotation
- - Set interactive mode to pan
-
-- :class:`OutputToolBar` with:
- - Copy
- - Save
- - Video
- - Print
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/09/2017"
-
-import logging
-import weakref
-
-from silx.gui import qt
-
-from .ViewpointTools import ViewpointToolButton
-from .. import actions
-
-_logger = logging.getLogger(__name__)
-
-
-class Plot3DWidgetToolBar(qt.QToolBar):
- """Base class for toolbar associated to a Plot3DWidget
-
- :param parent: See :class:`QWidget`
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, title=''):
- super(Plot3DWidgetToolBar, self).__init__(title, parent)
-
- self._plot3DRef = None
-
- def _plot3DWidgetChanged(self, widget):
- """Handle change of Plot3DWidget and sync actions
-
- :param Plot3DWidget widget:
- """
- for action in self.actions():
- if isinstance(action, actions.Plot3DAction):
- action.setPlot3DWidget(widget)
-
- def setPlot3DWidget(self, widget):
- """Set the Plot3DWidget this toolbar is associated with
-
- :param Plot3DWidget widget: The widget to control
- """
- self._plot3DRef = None if widget is None else weakref.ref(widget)
- self._plot3DWidgetChanged(widget)
-
- def getPlot3DWidget(self):
- """Return the Plot3DWidget associated to this toolbar.
-
- If no widget is associated, it returns None.
-
- :rtype: qt.QWidget
- """
- return None if self._plot3DRef is None else self._plot3DRef()
-
-
-class InteractiveModeToolBar(Plot3DWidgetToolBar):
- """Toolbar providing icons to change the interaction mode
-
- :param parent: See :class:`QWidget`
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, title='Plot3D Interaction'):
- super(InteractiveModeToolBar, self).__init__(parent, title)
-
- self._rotateAction = actions.mode.RotateArcballAction(parent=self)
- self.addAction(self._rotateAction)
-
- self._panAction = actions.mode.PanAction(parent=self)
- self.addAction(self._panAction)
-
- def getRotateAction(self):
- """Returns the QAction setting rotate interaction of the Plot3DWidget
-
- :rtype: qt.QAction
- """
- return self._rotateAction
-
- def getPanAction(self):
- """Returns the QAction setting pan interaction of the Plot3DWidget
-
- :rtype: qt.QAction
- """
- return self._panAction
-
-
-class OutputToolBar(Plot3DWidgetToolBar):
- """Toolbar providing icons to copy, save and print the OpenGL scene
-
- :param parent: See :class:`QWidget`
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, title='Plot3D Output'):
- super(OutputToolBar, self).__init__(parent, title)
-
- self._copyAction = actions.io.CopyAction(parent=self)
- self.addAction(self._copyAction)
-
- self._saveAction = actions.io.SaveAction(parent=self)
- self.addAction(self._saveAction)
-
- self._videoAction = actions.io.VideoAction(parent=self)
- self.addAction(self._videoAction)
-
- self._printAction = actions.io.PrintAction(parent=self)
- self.addAction(self._printAction)
-
- def getCopyAction(self):
- """Returns the QAction performing copy to clipboard of the Plot3DWidget
-
- :rtype: qt.QAction
- """
- return self._copyAction
-
- def getSaveAction(self):
- """Returns the QAction performing save to file of the Plot3DWidget
-
- :rtype: qt.QAction
- """
- return self._saveAction
-
- def getVideoRecordAction(self):
- """Returns the QAction performing record video of the Plot3DWidget
-
- :rtype: qt.QAction
- """
- return self._videoAction
-
- def getPrintAction(self):
- """Returns the QAction performing printing of the Plot3DWidget
-
- :rtype: qt.QAction
- """
- return self._printAction
-
-
-class ViewpointToolBar(Plot3DWidgetToolBar):
- """A toolbar providing icons to reset the viewpoint.
-
- :param parent: See :class:`QToolBar`
- :param str title: Title of the toolbar
- """
-
- def __init__(self, parent=None, title='Viewpoint control'):
- super(ViewpointToolBar, self).__init__(parent, title)
-
- self._viewpointToolButton = ViewpointToolButton(parent=self)
- self.addWidget(self._viewpointToolButton)
- self._rotateViewpointAction = actions.viewpoint.RotateViewpoint(parent=self)
- self.addAction(self._rotateViewpointAction)
-
- def _plot3DWidgetChanged(self, widget):
- self.getViewpointToolButton().setPlot3DWidget(widget)
- super(ViewpointToolBar, self)._plot3DWidgetChanged(widget)
-
- def getViewpointToolButton(self):
- """Returns the ViewpointToolButton to set viewpoint of the Plot3DWidget
-
- :rtype: ViewpointToolButton
- """
- return self._viewpointToolButton
-
- def getRotateViewpointAction(self):
- """Returns the QAction to start/stop rotation of the Plot3DWidget
-
- :rtype: qt.QAction
- """
- return self._rotateViewpointAction
diff --git a/silx/gui/plot3d/utils/__init__.py b/silx/gui/plot3d/utils/__init__.py
deleted file mode 100644
index 99d3e08..0000000
--- a/silx/gui/plot3d/utils/__init__.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "18/10/2016"
diff --git a/silx/gui/plot3d/utils/mng.py b/silx/gui/plot3d/utils/mng.py
deleted file mode 100644
index 8049a2f..0000000
--- a/silx/gui/plot3d/utils/mng.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 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.
-#
-# ###########################################################################*/
-"""This module provides basic writing Mulitple-image Network Graphics files.
-
-It only supports RGB888 images of the same shape stored as
-MNG-VLC (very low complexity) format.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/12/2016"
-
-
-import logging
-import struct
-import zlib
-
-import numpy
-
-_logger = logging.getLogger(__name__)
-
-
-def _png_chunk(name, data):
- """Return a PNG chunk
-
- :param str name: Chunk type
- :param byte data: Chunk payload
- """
- length = struct.pack('>I', len(data))
- name = [char.encode('ascii') for char in name]
- chunk = struct.pack('cccc', *name) + data
- crc = struct.pack('>I', zlib.crc32(chunk) & 0xffffffff)
- return length + chunk + crc
-
-
-def convert(images, nb_images=0, fps=25):
- """Convert RGB images to MNG-VLC format.
-
- See http://www.libpng.org/pub/mng/spec/
- See http://www.libpng.org/pub/png/book/
- See http://www.libpng.org/pub/png/spec/1.2/
-
- :param images: iterator of RGB888 images
- :type images: iterator of numpy.ndarray of dimension 3
- :param int nb_images: The number of images indicated in the MNG header
- :param int fps: The frame rate indicated in the MNG header
- :return: An iterator of MNG chunks as bytes
- """
- first_image = True
-
- for image in images:
- if first_image:
- first_image = False
-
- height, width = image.shape[:2]
-
- # MNG signature
- yield b'\x8aMNG\r\n\x1a\n'
-
- # MHDR chunk: File header
- yield _png_chunk('MHDR', struct.pack(
- ">IIIIIII",
- width,
- height,
- fps, # ticks
- nb_images + 1, # layer count
- nb_images, # frame count
- nb_images, # play time
- 1)) # profile: MNG-VLC no alpha: only least significant bit 1
-
- assert image.shape == (height, width, 3)
- assert image.dtype == numpy.dtype('uint8')
-
- # IHDR chunk: Image header
- depth = 8 # 8 bit per channel
- color_type = 2 # 'truecolor' = RGB
- interlace = 0 # No
- yield _png_chunk('IHDR', struct.pack(">IIBBBBB",
- width,
- height,
- depth,
- color_type,
- 0, 0, interlace))
-
- # Add filter 'None' before each scanline
- prepared_data = b'\x00' + b'\x00'.join(
- line.tobytes() for line in image) # TODO optimize that
- compressed_data = zlib.compress(prepared_data, 8)
-
- # IDAT chunk: Payload
- yield _png_chunk('IDAT', compressed_data)
-
- # IEND chunk: Image footer
- yield _png_chunk('IEND', b'')
-
- # MEND chunk: footer
- yield _png_chunk('MEND', b'')