summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/items/scatter.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/items/scatter.py')
-rw-r--r--silx/gui/plot3d/items/scatter.py182
1 files changed, 177 insertions, 5 deletions
diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py
index 5eea455..a13c3db 100644
--- a/silx/gui/plot3d/items/scatter.py
+++ b/silx/gui/plot3d/items/scatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# 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
@@ -40,6 +40,7 @@ from ..scene import function, primitives, utils
from .core import DataItem3D, Item3DChangedType, ItemChangedType
from .mixins import ColormapMixIn, SymbolMixIn
+from ._pick import PickingResult
_logger = logging.getLevelName(__name__)
@@ -116,7 +117,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: X coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('x', copy=copy)
+ return self._scatter.getAttribute('x', copy=copy).reshape(-1)
def getYData(self, copy=True):
"""Returns Y data coordinates.
@@ -126,7 +127,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Y coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('y', copy=copy)
+ return self._scatter.getAttribute('y', copy=copy).reshape(-1)
def getZData(self, copy=True):
"""Returns Z data coordinates.
@@ -136,7 +137,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Z coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('z', copy=copy)
+ return self._scatter.getAttribute('z', copy=copy).reshape(-1)
def getValues(self, copy=True):
"""Returns data values.
@@ -146,7 +147,64 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: data values
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('value', copy=copy)
+ return self._scatter.getAttribute('value', copy=copy).reshape(-1)
+
+ 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.getValues)
+ else:
+ return None
class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
@@ -373,6 +431,120 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
"""
return numpy.array(self._value, copy=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.getValues)
+ 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 = utils.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.getValues)
+
+ 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.getValues(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 == '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 == '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