summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/items/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/items/core.py')
-rw-r--r--silx/gui/plot3d/items/core.py201
1 files changed, 179 insertions, 22 deletions
diff --git a/silx/gui/plot3d/items/core.py b/silx/gui/plot3d/items/core.py
index e549e59..0aefced 100644
--- a/silx/gui/plot3d/items/core.py
+++ b/silx/gui/plot3d/items/core.py
@@ -41,6 +41,7 @@ from ... import qt
from ...plot.items import ItemChangedType
from .. import scene
from ..scene import axes, primitives, transform
+from ._pick import PickContext
@enum.unique
@@ -219,6 +220,53 @@ class Item3D(qt.QObject):
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.
@@ -256,12 +304,14 @@ class DataItem3D(Item3D):
self._rotationCenter = 0., 0., 0.
- self._getScenePrimitive().transforms = [
+ self.__transforms = transform.TransformList([
self._translate,
self._rotateForwardTranslation,
self._rotate,
self._rotateBackwardTranslation,
- self._transformObjectToRotate]
+ self._transformObjectToRotate])
+
+ self._getScenePrimitive().transforms = self.__transforms
def _updated(self, event=None):
"""Handle MixIn class updates.
@@ -274,6 +324,13 @@ class DataItem3D(Item3D):
# 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.
@@ -452,7 +509,92 @@ class DataItem3D(Item3D):
self._updated(Item3DChangedType.BOUNDING_BOX_VISIBLE)
-class _BaseGroupItem(DataItem3D):
+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)
@@ -474,9 +616,16 @@ class _BaseGroupItem(DataItem3D):
:param Union[GroupBBox, None] group:
The scene group to use for rendering
"""
- DataItem3D.__init__(self, parent=parent, group=group)
+ 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
@@ -493,11 +642,11 @@ class _BaseGroupItem(DataItem3D):
item.setParent(self)
if index is None:
- self._getScenePrimitive().children.append(
+ self._getGroupPrimitive().children.append(
item._getScenePrimitive())
self._items.append(item)
else:
- self._getScenePrimitive().children.insert(
+ self._getGroupPrimitive().children.insert(
index, item._getScenePrimitive())
self._items.insert(index, item)
self.sigItemAdded.emit(item)
@@ -518,7 +667,7 @@ class _BaseGroupItem(DataItem3D):
if item not in self.getItems():
raise ValueError("Item3D not in group: %s" % str(item))
- self._getScenePrimitive().children.remove(item._getScenePrimitive())
+ self._getGroupPrimitive().children.remove(item._getScenePrimitive())
self._items.remove(item)
item.setParent(None)
self.sigItemRemoved.emit(item)
@@ -528,21 +677,6 @@ class _BaseGroupItem(DataItem3D):
for item in self.getItems():
self.removeItem(item)
- 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
-
class GroupItem(_BaseGroupItem):
"""Group of items sharing a common transform."""
@@ -620,3 +754,26 @@ class GroupWithAxesItem(_BaseGroupItem):
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