diff options
Diffstat (limited to 'silx/gui/plot3d/SceneWidget.py')
-rw-r--r-- | silx/gui/plot3d/SceneWidget.py | 687 |
1 files changed, 0 insertions, 687 deletions
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') |