Skip to main content
`Ctrl`+`K`

# Source code for silx.gui.plot3d.items._pick

    # /*##########################################################################
    #
    # Copyright (c) 2018-2020 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 classes supporting item picking.
    """

    __authors__ = ["T. Vincent"]
    __license__ = "MIT"
    __date__ = "24/09/2018"

    import logging
    import numpy

    from ...plot.items._pick import PickingResult as _PickingResult
    from ..scene import Viewport, Base
    from silx._utils import NP_OPTIONAL_COPY

    _logger = logging.getLogger(__name__)

    class PickContext(object):
        """Store information related to current picking

        :param int x: Widget coordinate
        :param int y: Widget coordinate
        :param ~silx.gui.plot3d.scene.Viewport viewport:
            Viewport where picking occurs
        :param Union[None,callable] condition:
            Test whether each item needs to be picked or not.
        """

        def __init__(self, x, y, viewport, condition):
            self._widgetPosition = x, y
            assert isinstance(viewport, Viewport)
            self._viewport = viewport
            self._ndcZRange = -1.0, 1.0
            self._enabled = True
            self._condition = condition

        def copy(self):
            """Returns a copy

            :rtype: PickContent
            """
            x, y = self.getWidgetPosition()
            context = PickContext(x, y, self.getViewport(), self._condition)
            context.setNDCZRange(*self._ndcZRange)
            context.setEnabled(self.isEnabled())
            return context

        def isItemPickable(self, item):
            """Check condition for the given item.

            :param Item3D item:
            :return: Whether to process the item (True) or to skip it (False)
            :rtype: bool
            """
            return self._condition is None or self._condition(item)

        def getViewport(self):
            """Returns viewport where picking occurs

            :rtype: ~silx.gui.plot3d.scene.Viewport
            """
            return self._viewport

        def getWidgetPosition(self):
            """Returns (x, y) position in pixel in the widget

            Origin is at the top-left corner of the widget,
            X from left to right, Y goes downward.

            :rtype: List[int]
            """
            return self._widgetPosition

        def setEnabled(self, enabled):
            """Set whether picking is enabled or not

            :param bool enabled: True to enable picking, False otherwise
            """
            self._enabled = bool(enabled)

        def isEnabled(self):
            """Returns True if picking is currently enabled, False otherwise.

            :rtype: bool
            """
            return self._enabled

        def setNDCZRange(self, near=-1.0, far=1.0):
            """Set near and far Z value in normalized device coordinates

            This allows to clip the ray to a subset of the NDC range

            :param float near: Near segment end point Z coordinate
            :param float far: Far segment end point Z coordinate
            """
            self._ndcZRange = near, far

        def getNDCPosition(self):
            """Return Normalized device coordinates of picked point.

            :return: (x, y) in NDC coordinates or None if outside viewport.
            :rtype: Union[None,List[float]]
            """
            if not self.isEnabled():
                return None

            # Convert x, y from window to NDC
            x, y = self.getWidgetPosition()
            return self.getViewport().windowToNdc(x, y, checkInside=True)

        def getPickingSegment(self, frame):
            """Returns picking segment in requested coordinate frame.

            :param Union[str,Base] frame:
                The frame in which to get the picking segment,
                either a keyword: 'ndc', 'camera', 'scene' or a scene
                :class:`~silx.gui.plot3d.scene.Base` object.
            :return: Near and far points of the segment as (x, y, z, w)
                or None if picked point is outside viewport
            :rtype: Union[None,numpy.ndarray]
            """
            assert frame in ("ndc", "camera", "scene") or isinstance(frame, Base)

            positionNdc = self.getNDCPosition()
            if positionNdc is None:
                return None

            near, far = self._ndcZRange
            rayNdc = numpy.array(
                (positionNdc + (near, 1.0), positionNdc + (far, 1.0)), dtype=numpy.float64
            )
            if frame == "ndc":
                return rayNdc

            viewport = self.getViewport()

            rayCamera = viewport.camera.intrinsic.transformPoints(
                rayNdc, direct=False, perspectiveDivide=True
            )
            if frame == "camera":
                return rayCamera

            rayScene = viewport.camera.extrinsic.transformPoints(rayCamera, direct=False)
            if frame == "scene":
                return rayScene

            # frame is a scene Base object
            rayObject = frame.objectToSceneTransform.transformPoints(rayScene, direct=False)
            return rayObject

    class PickingResult(_PickingResult):
        """Class to access picking information in a 3D scene."""

        def __init__(self, item, positions, indices=None, fetchdata=None):
            """Init

            :param ~silx.gui.plot3d.items.Item3D item: The picked item
            :param numpy.ndarray positions:
                Nx3 array-like of picked positions (x, y, z) in item coordinates.
            :param numpy.ndarray indices: Array-like of indices of picked data.
                Either 1D or 2D with dim0: data dimension and dim1: indices.
                No copy is made.
            :param callable fetchdata: Optional function with a bool copy argument
                to provide an alternative function to access item data.
                Default is to use `item.getData`.
            """
            super(PickingResult, self).__init__(item, indices)

            self._objectPositions = numpy.asarray(positions, dtype=numpy.float64)

            # Store matrices to generate positions on demand
            primitive = item._getScenePrimitive()
            self._objectToSceneTransform = primitive.objectToSceneTransform
            self._objectToNDCTransform = primitive.objectToNDCTransform
            self._scenePositions = None
            self._ndcPositions = None

            self._fetchdata = fetchdata

        def getData(self, copy=True):
            """Returns picked data values

            :param bool copy: True (default) to get a copy,
                False to return internal arrays
            :rtype: Union[None,numpy.ndarray]
            """

            indices = self.getIndices(copy=False)
            if indices is None or len(indices) == 0:
                return None

            item = self.getItem()
            if self._fetchdata is None:
                if hasattr(item, "getData"):
                    data = item.getData(copy=False)
                else:
                    return None
            else:
                data = self._fetchdata(copy=False)

            return numpy.array(data[indices], copy=copy or NP_OPTIONAL_COPY)

        def getPositions(self, frame="scene", copy=True):
            """Returns picking positions in item coordinates.

            :param str frame: The frame in which the positions are returned
                Either 'scene' for world space,
                'ndc' for normalized device coordinates or 'object' for item frame.
            :param bool copy: True (default) to get a copy,
                False to return internal arrays
            :return: Nx3 array of (x, y, z) coordinates
            :rtype: numpy.ndarray
            """
            if frame == "ndc":
                if self._ndcPositions is None:  # Lazy-loading
                    self._ndcPositions = self._objectToNDCTransform.transformPoints(
                        self._objectPositions, perspectiveDivide=True
                    )

                positions = self._ndcPositions

            elif frame == "scene":
                if self._scenePositions is None:  # Lazy-loading
                    self._scenePositions = self._objectToSceneTransform.transformPoints(
                        self._objectPositions
                    )

                positions = self._scenePositions

            elif frame == "object":
                positions = self._objectPositions

            else:
                raise ValueError("Unsupported frame argument: %s" % str(frame))

            return numpy.array(positions, copy=copy or NP_OPTIONAL_COPY)