diff options
Diffstat (limited to 'silx/gui/plot3d/items/scatter.py')
-rw-r--r-- | silx/gui/plot3d/items/scatter.py | 182 |
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 |