summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/SceneWidget.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/SceneWidget.py')
-rw-r--r--silx/gui/plot3d/SceneWidget.py687
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')