diff options
Diffstat (limited to 'silx/gui/plot3d/items')
-rw-r--r-- | silx/gui/plot3d/items/__init__.py | 43 | ||||
-rw-r--r-- | silx/gui/plot3d/items/_pick.py | 265 | ||||
-rw-r--r-- | silx/gui/plot3d/items/clipplane.py | 136 | ||||
-rw-r--r-- | silx/gui/plot3d/items/core.py | 779 | ||||
-rw-r--r-- | silx/gui/plot3d/items/image.py | 425 | ||||
-rw-r--r-- | silx/gui/plot3d/items/mesh.py | 792 | ||||
-rw-r--r-- | silx/gui/plot3d/items/mixins.py | 288 | ||||
-rw-r--r-- | silx/gui/plot3d/items/scatter.py | 617 | ||||
-rw-r--r-- | silx/gui/plot3d/items/volume.py | 886 |
9 files changed, 0 insertions, 4231 deletions
diff --git a/silx/gui/plot3d/items/__init__.py b/silx/gui/plot3d/items/__init__.py deleted file mode 100644 index e7c4af1..0000000 --- a/silx/gui/plot3d/items/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2021 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 package provides classes that describes :class:`.SceneWidget` content. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "15/11/2017" - - -from .core import DataItem3D, Item3D, GroupItem, GroupWithAxesItem # noqa -from .core import ItemChangedType, Item3DChangedType # noqa -from .mixins import (ColormapMixIn, ComplexMixIn, InterpolationMixIn, # noqa - PlaneMixIn, SymbolMixIn) # noqa -from .clipplane import ClipPlane # noqa -from .image import ImageData, ImageRgba, HeightMapData, HeightMapRGBA # noqa -from .mesh import Mesh, ColormapMesh, Box, Cylinder, Hexagon # noqa -from .scatter import Scatter2D, Scatter3D # noqa -from .volume import ComplexField3D, ScalarField3D # noqa diff --git a/silx/gui/plot3d/items/_pick.py b/silx/gui/plot3d/items/_pick.py deleted file mode 100644 index 0d6a495..0000000 --- a/silx/gui/plot3d/items/_pick.py +++ /dev/null @@ -1,265 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# 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. -""" - -from __future__ import absolute_import - -__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 - - -_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., 1. - 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., far=1.): - """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.), - positionNdc + (far, 1.)), - 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.array( - positions, copy=False, 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) - - 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) diff --git a/silx/gui/plot3d/items/clipplane.py b/silx/gui/plot3d/items/clipplane.py deleted file mode 100644 index 3e819d0..0000000 --- a/silx/gui/plot3d/items/clipplane.py +++ /dev/null @@ -1,136 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# 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 -# 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 scene clip plane class. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "15/11/2017" - - -import numpy - -from ..scene import primitives, utils - -from ._pick import PickingResult -from .core import Item3D -from .mixins import PlaneMixIn - - -class ClipPlane(Item3D, PlaneMixIn): - """Represents a clipping plane that clips following items within the group. - - For now only on clip plane is allowed at once in a scene. - """ - - def __init__(self, parent=None): - plane = primitives.ClipPlane() - Item3D.__init__(self, parent=parent, primitive=plane) - PlaneMixIn.__init__(self, plane=plane) - - def __pickPreProcessing(self, context): - """Common processing for :meth:`_pickPostProcess` and :meth:`_pickFull` - - :param PickContext context: Current picking context - :return None or (bounds, intersection points, rayObject) - """ - plane = self._getPlane() - planeParent = plane.parent - if planeParent is None: - return None - - rayObject = context.getPickingSegment(frame=plane) - if rayObject is None: - return None - - bounds = planeParent.bounds(dataBounds=True) - rayClip = utils.clipSegmentToBounds(rayObject[:, :3], bounds) - if rayClip is None: - return None # Ray is outside parent's bounding box - - points = utils.segmentPlaneIntersect( - rayObject[0, :3], - rayObject[1, :3], - planeNorm=self.getNormal(), - planePt=self.getPoint()) - - # A single intersection inside bounding box - picked = (len(points) == 1 and - numpy.all(bounds[0] <= points[0]) and - numpy.all(points[0] <= bounds[1])) - - return picked, points, rayObject - - def _pick(self, context): - # Perform picking before modifying context - result = super(ClipPlane, self)._pick(context) - - # Modify context if needed - if self.isVisible() and context.isEnabled(): - info = self.__pickPreProcessing(context) - if info is not None: - picked, points, rayObject = info - plane = self._getPlane() - - if picked: # A single intersection inside bounding box - # Clip NDC z range for following brother items - ndcIntersect = plane.objectToNDCTransform.transformPoint( - points[0], perspectiveDivide=True) - ndcNormal = plane.objectToNDCTransform.transformNormal( - self.getNormal()) - if ndcNormal[2] < 0: - context.setNDCZRange(-1., ndcIntersect[2]) - else: - context.setNDCZRange(ndcIntersect[2], 1.) - - else: - # TODO check this might not be correct - rayObject[:, 3] = 1. # Make sure 4h coordinate is one - if numpy.sum(rayObject[0] * self.getParameters()) < 0.: - # Disable picking for remaining brothers - context.setEnabled(False) - - return result - - def _pickFastCheck(self, context): - return True - - 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] - """ - info = self.__pickPreProcessing(context) - if info is not None: - picked, points, _ = info - - if picked: - return PickingResult(self, positions=[points[0]]) - - return None diff --git a/silx/gui/plot3d/items/core.py b/silx/gui/plot3d/items/core.py deleted file mode 100644 index ab2ceb6..0000000 --- a/silx/gui/plot3d/items/core.py +++ /dev/null @@ -1,779 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-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 the base class for items of the :class:`.SceneWidget`. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "15/11/2017" - -from collections import defaultdict -import enum - -import numpy -import six - -from ... import qt -from ...plot.items import ItemChangedType -from .. import scene -from ..scene import axes, primitives, transform -from ._pick import PickContext - - -@enum.unique -class Item3DChangedType(enum.Enum): - """Type of modification provided by :attr:`Item3D.sigItemChanged` signal.""" - - INTERPOLATION = 'interpolationChanged' - """Item3D image interpolation changed flag.""" - - TRANSFORM = 'transformChanged' - """Item3D transform changed flag.""" - - HEIGHT_MAP = 'heightMapChanged' - """Item3D height map changed flag.""" - - ISO_LEVEL = 'isoLevelChanged' - """Isosurface level changed flag.""" - - LABEL = 'labelChanged' - """Item's label changed flag.""" - - BOUNDING_BOX_VISIBLE = 'boundingBoxVisibleChanged' - """Item's bounding box visibility changed""" - - ROOT_ITEM = 'rootItemChanged' - """Item's root changed flag.""" - - -class Item3D(qt.QObject): - """Base class representing an item in the scene. - - :param parent: The View widget this item belongs to. - :param primitive: An optional primitive to use as scene primitive - """ - - _LABEL_INDICES = defaultdict(int) - """Store per class label indices""" - - sigItemChanged = qt.Signal(object) - """Signal emitted when an item's property has changed. - - It provides a flag describing which property of the item has changed. - See :class:`ItemChangedType` and :class:`Item3DChangedType` - for flags description. - """ - - def __init__(self, parent, primitive=None): - qt.QObject.__init__(self, parent) - - if primitive is None: - primitive = scene.Group() - - self._primitive = primitive - - self.__syncForegroundColor() - - labelIndex = self._LABEL_INDICES[self.__class__] - self._label = six.text_type(self.__class__.__name__) - if labelIndex != 0: - self._label += u' %d' % labelIndex - self._LABEL_INDICES[self.__class__] += 1 - - if isinstance(parent, Item3D): - parent.sigItemChanged.connect(self.__parentItemChanged) - - def setParent(self, parent): - """Override set parent to handle root item change""" - previousParent = self.parent() - if isinstance(previousParent, Item3D): - previousParent.sigItemChanged.disconnect(self.__parentItemChanged) - - super(Item3D, self).setParent(parent) - - if isinstance(parent, Item3D): - parent.sigItemChanged.connect(self.__parentItemChanged) - - self._updated(Item3DChangedType.ROOT_ITEM) - - def __parentItemChanged(self, event): - """Handle updates of the parent if it is an Item3D - - :param Item3DChangedType event: - """ - if event == Item3DChangedType.ROOT_ITEM: - self._updated(Item3DChangedType.ROOT_ITEM) - - def root(self): - """Returns the root of the scene this item belongs to. - - The root is the up-most Item3D in the scene tree hierarchy. - - :rtype: Union[Item3D, None] - """ - root = None - ancestor = self.parent() - while isinstance(ancestor, Item3D): - root = ancestor - ancestor = ancestor.parent() - - return root - - def _getScenePrimitive(self): - """Return the group containing the item rendering""" - return self._primitive - - def _updated(self, event=None): - """Handle MixIn class updates. - - :param event: The event to send to :attr:`sigItemChanged` signal. - """ - if event == Item3DChangedType.ROOT_ITEM: - self.__syncForegroundColor() - - if event is not None: - self.sigItemChanged.emit(event) - - # Label - - def getLabel(self): - """Returns the label associated to this item. - - :rtype: str - """ - return self._label - - def setLabel(self, label): - """Set the label associated to this item. - - :param str label: - """ - label = six.text_type(label) - if label != self._label: - self._label = label - self._updated(Item3DChangedType.LABEL) - - # Visibility - - def isVisible(self): - """Returns True if item is visible, else False - - :rtype: bool - """ - return self._getScenePrimitive().visible - - def setVisible(self, visible=True): - """Set the visibility of the item in the scene. - - :param bool visible: True (default) to show the item, False to hide - """ - visible = bool(visible) - primitive = self._getScenePrimitive() - if visible != primitive.visible: - primitive.visible = visible - self._updated(ItemChangedType.VISIBLE) - - # Foreground color - - def _setForegroundColor(self, color): - """Set the foreground color of the item. - - The default implementation does nothing, override it in subclass. - - :param color: RGBA color - :type color: tuple of 4 float in [0., 1.] - """ - if hasattr(super(Item3D, self), '_setForegroundColor'): - super(Item3D, self)._setForegroundColor(color) - - def __syncForegroundColor(self): - """Retrieve foreground color from parent and update this item""" - # Look-up for SceneWidget to get its foreground color - root = self.root() - if root is not None: - widget = root.parent() - if isinstance(widget, qt.QWidget): - 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. - - :param parent: The View widget this item belongs to. - :param Union[GroupBBox, None] group: - The scene group to use for rendering - """ - - def __init__(self, parent, group=None): - if group is None: - group = primitives.GroupBBox() - - # Set-up bounding box - group.boxVisible = False - group.axesVisible = False - else: - assert isinstance(group, primitives.GroupBBox) - - Item3D.__init__(self, parent=parent, primitive=group) - - # Transformations - self._translate = transform.Translate() - self._rotateForwardTranslation = transform.Translate() - self._rotate = transform.Rotate() - self._rotateBackwardTranslation = transform.Translate() - self._translateFromRotationCenter = transform.Translate() - self._matrix = transform.Matrix() - self._scale = transform.Scale() - # Group transforms to do to data before rotation - # This is useful to handle rotation center relative to bbox - self._transformObjectToRotate = transform.TransformList( - [self._matrix, self._scale]) - self._transformObjectToRotate.addListener(self._updateRotationCenter) - - self._rotationCenter = 0., 0., 0. - - self.__transforms = transform.TransformList([ - self._translate, - self._rotateForwardTranslation, - self._rotate, - self._rotateBackwardTranslation, - self._transformObjectToRotate]) - - self._getScenePrimitive().transforms = self.__transforms - - def _updated(self, event=None): - """Handle MixIn class updates. - - :param event: The event to send to :attr:`sigItemChanged` signal. - """ - if event == ItemChangedType.DATA: - self._updateRotationCenter() - super(DataItem3D, self)._updated(event) - - # 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. - - :param float sx: Scale factor along the X axis - :param float sy: Scale factor along the Y axis - :param float sz: Scale factor along the Z axis - """ - scale = numpy.array((sx, sy, sz), dtype=numpy.float32) - if not numpy.all(numpy.equal(scale, self.getScale())): - self._scale.scale = scale - self._updated(Item3DChangedType.TRANSFORM) - - def getScale(self): - """Returns the scales provided by :meth:`setScale`. - - :rtype: numpy.ndarray - """ - return self._scale.scale - - def setTranslation(self, x=0., y=0., z=0.): - """Set the translation of the origin of the item in the scene. - - :param float x: Offset of the data origin on the X axis - :param float y: Offset of the data origin on the Y axis - :param float z: Offset of the data origin on the Z axis - """ - translation = numpy.array((x, y, z), dtype=numpy.float32) - if not numpy.all(numpy.equal(translation, self.getTranslation())): - self._translate.translation = translation - self._updated(Item3DChangedType.TRANSFORM) - - def getTranslation(self): - """Returns the offset set by :meth:`setTranslation`. - - :rtype: numpy.ndarray - """ - return self._translate.translation - - _ROTATION_CENTER_TAGS = 'lower', 'center', 'upper' - - def _updateRotationCenter(self, *args, **kwargs): - """Update rotation center relative to bounding box""" - center = [] - for index, position in enumerate(self.getRotationCenter()): - # Patch position relative to bounding box - if position in self._ROTATION_CENTER_TAGS: - bounds = self._getScenePrimitive().bounds( - transformed=False, dataBounds=True) - bounds = self._transformObjectToRotate.transformBounds(bounds) - - if bounds is None: - position = 0. - elif position == 'lower': - position = bounds[0, index] - elif position == 'center': - position = 0.5 * (bounds[0, index] + bounds[1, index]) - elif position == 'upper': - position = bounds[1, index] - - center.append(position) - - if not numpy.all(numpy.equal( - center, self._rotateForwardTranslation.translation)): - self._rotateForwardTranslation.translation = center - self._rotateBackwardTranslation.translation = \ - - self._rotateForwardTranslation.translation - self._updated(Item3DChangedType.TRANSFORM) - - def setRotationCenter(self, x=0., y=0., z=0.): - """Set the center of rotation of the item. - - Position of the rotation center is either a float - for an absolute position or one of the following - string to define a position relative to the item's bounding box: - 'lower', 'center', 'upper' - - :param x: rotation center position on the X axis - :rtype: float or str - :param y: rotation center position on the Y axis - :rtype: float or str - :param z: rotation center position on the Z axis - :rtype: float or str - """ - center = [] - for position in (x, y, z): - if isinstance(position, six.string_types): - assert position in self._ROTATION_CENTER_TAGS - else: - position = float(position) - center.append(position) - center = tuple(center) - - if center != self._rotationCenter: - self._rotationCenter = center - self._updateRotationCenter() - - def getRotationCenter(self): - """Returns the rotation center set by :meth:`setRotationCenter`. - - :rtype: 3-tuple of float or str - """ - return self._rotationCenter - - def setRotation(self, angle=0., axis=(0., 0., 1.)): - """Set the rotation of the item in the scene - - :param float angle: The rotation angle in degrees. - :param axis: The (x, y, z) coordinates of the rotation axis. - """ - axis = numpy.array(axis, dtype=numpy.float32) - assert axis.ndim == 1 - assert axis.size == 3 - if (self._rotate.angle != angle or - not numpy.all(numpy.equal(axis, self._rotate.axis))): - self._rotate.setAngleAxis(angle, axis) - self._updated(Item3DChangedType.TRANSFORM) - - def getRotation(self): - """Returns the rotation set by :meth:`setRotation`. - - :return: (angle, axis) - :rtype: 2-tuple (float, numpy.ndarray) - """ - return self._rotate.angle, self._rotate.axis - - def setMatrix(self, matrix=None): - """Set the transform matrix - - :param numpy.ndarray matrix: 3x3 transform matrix - """ - matrix4x4 = numpy.identity(4, dtype=numpy.float32) - - if matrix is not None: - matrix = numpy.array(matrix, dtype=numpy.float32) - assert matrix.shape == (3, 3) - matrix4x4[:3, :3] = matrix - - if not numpy.all(numpy.equal(matrix4x4, self._matrix.getMatrix())): - self._matrix.setMatrix(matrix4x4) - self._updated(Item3DChangedType.TRANSFORM) - - def getMatrix(self): - """Returns the matrix set by :meth:`setMatrix` - - :return: 3x3 matrix - :rtype: numpy.ndarray""" - return self._matrix.getMatrix(copy=True)[:3, :3] - - # Bounding box - - def _setForegroundColor(self, color): - """Set the color of the bounding box - - :param color: RGBA color as 4 floats in [0, 1] - """ - self._getScenePrimitive().color = color - super(DataItem3D, self)._setForegroundColor(color) - - def isBoundingBoxVisible(self): - """Returns item's bounding box visibility. - - :rtype: bool - """ - return self._getScenePrimitive().boxVisible - - def setBoundingBoxVisible(self, visible): - """Set item's bounding box visibility. - - :param bool visible: - True to show the bounding box, False (default) to hide it - """ - visible = bool(visible) - primitive = self._getScenePrimitive() - if visible != primitive.boxVisible: - primitive.boxVisible = visible - self._updated(Item3DChangedType.BOUNDING_BOX_VISIBLE) - - -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) - """Signal emitted when a new item is added to the group. - - The newly added item is provided by this signal - """ - - sigItemRemoved = qt.Signal(object) - """Signal emitted when an item is removed from the group. - - The removed item is provided by this signal. - """ - - 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 - """ - 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 - - :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 group. - """ - assert isinstance(item, Item3D) - assert item.parent() in (None, self) - - if item in self.getItems(): - raise ValueError("Item3D already in group: %s" % item) - - item.setParent(self) - if index is None: - self._getGroupPrimitive().children.append( - item._getScenePrimitive()) - self._items.append(item) - else: - self._getGroupPrimitive().children.insert( - index, item._getScenePrimitive()) - self._items.insert(index, item) - self.sigItemAdded.emit(item) - - def getItems(self): - """Returns the list of items currently present in the group. - - :rtype: tuple - """ - return tuple(self._items) - - def removeItem(self, item): - """Remove an item from the scene. - - :param Item3D item: The item to remove from the scene - :raises ValueError: If the item does not belong to the group - """ - if item not in self.getItems(): - raise ValueError("Item3D not in group: %s" % str(item)) - - self._getGroupPrimitive().children.remove(item._getScenePrimitive()) - self._items.remove(item) - item.setParent(None) - self.sigItemRemoved.emit(item) - - def clearItems(self): - """Remove all item from the group.""" - for item in self.getItems(): - self.removeItem(item) - - -class GroupItem(_BaseGroupItem): - """Group of items sharing a common transform.""" - - def __init__(self, parent=None): - super(GroupItem, self).__init__(parent=parent) - - -class GroupWithAxesItem(_BaseGroupItem): - """ - Group of items sharing a common transform surrounded with labelled axes. - """ - - def __init__(self, parent=None): - """Class representing a group of items in the scene with labelled axes. - - :param parent: The View widget this item belongs to. - """ - super(GroupWithAxesItem, self).__init__(parent=parent, - group=axes.LabelledAxes()) - - # Axes labels - - def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None): - """Set the text labels of the axes. - - :param str xlabel: Label of the X axis, None to leave unchanged. - :param str ylabel: Label of the Y axis, None to leave unchanged. - :param str zlabel: Label of the Z axis, None to leave unchanged. - """ - labelledAxes = self._getScenePrimitive() - if xlabel is not None: - labelledAxes.xlabel = xlabel - - if ylabel is not None: - labelledAxes.ylabel = ylabel - - if zlabel is not None: - labelledAxes.zlabel = zlabel - - class _Labels(tuple): - """Return type of :meth:`getAxesLabels`""" - - def getXLabel(self): - """Label of the X axis (str)""" - return self[0] - - def getYLabel(self): - """Label of the Y axis (str)""" - return self[1] - - def getZLabel(self): - """Label of the Z axis (str)""" - return self[2] - - def getAxesLabels(self): - """Returns the text labels of the axes - - >>> group = GroupWithAxesItem() - >>> group.setAxesLabels(xlabel='X') - - You can get the labels either as a 3-tuple: - - >>> xlabel, ylabel, zlabel = group.getAxesLabels() - - Or as an object with methods getXLabel, getYLabel and getZLabel: - - >>> labels = group.getAxesLabels() - >>> labels.getXLabel() - ... 'X' - - :return: object describing the labels - """ - labelledAxes = self._getScenePrimitive() - 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 diff --git a/silx/gui/plot3d/items/image.py b/silx/gui/plot3d/items/image.py deleted file mode 100644 index 4e2b396..0000000 --- a/silx/gui/plot3d/items/image.py +++ /dev/null @@ -1,425 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2021 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 2D data and RGB(A) image item class. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "15/11/2017" - -import numpy - -from ..scene import primitives, utils -from .core import DataItem3D, ItemChangedType -from .mixins import ColormapMixIn, InterpolationMixIn -from ._pick import PickingResult - - -class _Image(DataItem3D, InterpolationMixIn): - """Base class for images - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - DataItem3D.__init__(self, parent=parent) - InterpolationMixIn.__init__(self) - - def _setPrimitive(self, primitive): - InterpolationMixIn._setPrimitive(self, primitive) - - def getData(self, copy=True): - raise NotImplementedError() - - 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] - """ - rayObject = context.getPickingSegment(frame=self._getScenePrimitive()) - if rayObject is None: - return None - - points = utils.segmentPlaneIntersect( - rayObject[0, :3], - rayObject[1, :3], - planeNorm=numpy.array((0., 0., 1.), dtype=numpy.float64), - planePt=numpy.array((0., 0., 0.), dtype=numpy.float64)) - - if len(points) == 1: # Single intersection - if points[0][0] < 0. or points[0][1] < 0.: - return None # Outside image - row, column = int(points[0][1]), int(points[0][0]) - data = self.getData(copy=False) - height, width = data.shape[:2] - if row < height and column < width: - return PickingResult( - self, - positions=[(points[0][0], points[0][1], 0.)], - indices=([row], [column])) - else: - return None # Outside image - else: # Either no intersection or segment and image are coplanar - return None - - -class ImageData(_Image, ColormapMixIn): - """Description of a 2D image data. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - _Image.__init__(self, parent=parent) - ColormapMixIn.__init__(self) - - self._data = numpy.zeros((0, 0), dtype=numpy.float32) - - self._image = primitives.ImageData(self._data) - self._getScenePrimitive().children.append(self._image) - - # Connect scene primitive to mix-in class - ColormapMixIn._setSceneColormap(self, self._image.colormap) - _Image._setPrimitive(self, self._image) - - def setData(self, data, copy=True): - """Set the image data to display. - - The data will be casted to float32. - - :param numpy.ndarray data: The image data - :param bool copy: True (default) to copy the data, - False to use as is (do not modify!). - """ - self._image.setData(data, copy=copy) - self._setColormappedData(self.getData(copy=False), copy=False) - self._updated(ItemChangedType.DATA) - - def getData(self, copy=True): - """Get the image data. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :rtype: numpy.ndarray - :return: The image data - """ - return self._image.getData(copy=copy) - - -class ImageRgba(_Image, InterpolationMixIn): - """Description of a 2D data RGB(A) image. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - _Image.__init__(self, parent=parent) - InterpolationMixIn.__init__(self) - - self._data = numpy.zeros((0, 0, 3), dtype=numpy.float32) - - self._image = primitives.ImageRgba(self._data) - self._getScenePrimitive().children.append(self._image) - - # Connect scene primitive to mix-in class - _Image._setPrimitive(self, self._image) - - def setData(self, data, copy=True): - """Set the RGB(A) image data to display. - - Supported array format: float32 in [0, 1], uint8. - - :param numpy.ndarray data: - The RGBA image data as an array of shape (H, W, Channels) - :param bool copy: True (default) to copy the data, - False to use as is (do not modify!). - """ - self._image.setData(data, copy=copy) - self._updated(ItemChangedType.DATA) - - def getData(self, copy=True): - """Get the image data. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :rtype: numpy.ndarray - :return: The image data - """ - return self._image.getData(copy=copy) - - -class _HeightMap(DataItem3D): - """Base class for 2D data array displayed as a height field. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - DataItem3D.__init__(self, parent=parent) - self.__data = numpy.zeros((0, 0), dtype=numpy.float32) - - 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 - - # TODO no colormapped or color data - # Project data to NDC - heightData = self.getData(copy=False) - if heightData.size == 0: - return # Nothing displayed - - height, width = heightData.shape - z = numpy.ravel(heightData) - y, x = numpy.mgrid[0:height, 0:width] - dataPoints = numpy.transpose((numpy.ravel(x), - numpy.ravel(y), - z, - numpy.ones_like(z))) - - primitive = self._getScenePrimitive() - - 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 += 1. # symbol size - 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: - # Convert indices from 1D to 2D - return PickingResult(self, - positions=dataPoints[picked, :3], - indices=(picked // width, picked % width), - fetchdata=self.getData) - else: - return None - - def setData(self, data, copy: bool=True): - """Set the height field data. - - :param data: - :param copy: True (default) to copy the data, - False to use as is (do not modify!). - """ - data = numpy.array(data, copy=copy) - assert data.ndim == 2 - - self.__data = data - self._updated(ItemChangedType.DATA) - - def getData(self, copy: bool=True) -> numpy.ndarray: - """Get the height field 2D data. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - """ - return numpy.array(self.__data, copy=copy) - - -class HeightMapData(_HeightMap, ColormapMixIn): - """Description of a 2D height field associated to a colormapped dataset. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - _HeightMap.__init__(self, parent=parent) - ColormapMixIn.__init__(self) - - self.__data = numpy.zeros((0, 0), dtype=numpy.float32) - - def _updated(self, event=None): - if event == ItemChangedType.DATA: - self.__updateScene() - super()._updated(event=event) - - def __updateScene(self): - """Update display primitive to use""" - self._getScenePrimitive().children = [] # Remove previous primitives - ColormapMixIn._setSceneColormap(self, None) - - if not self.isVisible(): - return # Update when visible - - data = self.getColormappedData(copy=False) - heightData = self.getData(copy=False) - - if data.size == 0 or heightData.size == 0: - return # Nothing to display - - # Display as a set of points - height, width = heightData.shape - # Generates coordinates - y, x = numpy.mgrid[0:height, 0:width] - - if data.shape != heightData.shape: # data and height size miss-match - # Colormapped data is interpolated (nearest-neighbour) to match the height field - data = data[numpy.floor(y * data.shape[0] / height).astype(numpy.int), - numpy.floor(x * data.shape[1] / height).astype(numpy.int)] - - x = numpy.ravel(x) - y = numpy.ravel(y) - - primitive = primitives.Points( - x=x, - y=y, - z=numpy.ravel(heightData), - value=numpy.ravel(data), - size=1) - primitive.marker = 's' - ColormapMixIn._setSceneColormap(self, primitive.colormap) - self._getScenePrimitive().children = [primitive] - - def setColormappedData(self, data, copy: bool=True): - """Set the 2D data used to compute colors. - - :param data: 2D array of data - :param copy: True (default) to copy the data, - False to use as is (do not modify!). - """ - data = numpy.array(data, copy=copy) - assert data.ndim == 2 - - self.__data = data - self._updated(ItemChangedType.DATA) - - def getColormappedData(self, copy: bool=True) -> numpy.ndarray: - """Returns the 2D data used to compute colors. - - :param copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - """ - return numpy.array(self.__data, copy=copy) - - -class HeightMapRGBA(_HeightMap): - """Description of a 2D height field associated to a RGB(A) image. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - _HeightMap.__init__(self, parent=parent) - - self.__rgba = numpy.zeros((0, 0, 3), dtype=numpy.float32) - - def _updated(self, event=None): - if event == ItemChangedType.DATA: - self.__updateScene() - super()._updated(event=event) - - def __updateScene(self): - """Update display primitive to use""" - self._getScenePrimitive().children = [] # Remove previous primitives - - if not self.isVisible(): - return # Update when visible - - rgba = self.getColorData(copy=False) - heightData = self.getData(copy=False) - if rgba.size == 0 or heightData.size == 0: - return # Nothing to display - - # Display as a set of points - height, width = heightData.shape - # Generates coordinates - y, x = numpy.mgrid[0:height, 0:width] - - if rgba.shape[:2] != heightData.shape: # image and height size miss-match - # RGBA data is interpolated (nearest-neighbour) to match the height field - rgba = rgba[numpy.floor(y * rgba.shape[0] / height).astype(numpy.int), - numpy.floor(x * rgba.shape[1] / height).astype(numpy.int)] - - x = numpy.ravel(x) - y = numpy.ravel(y) - - primitive = primitives.ColorPoints( - x=x, - y=y, - z=numpy.ravel(heightData), - color=rgba.reshape(-1, rgba.shape[-1]), - size=1) - primitive.marker = 's' - self._getScenePrimitive().children = [primitive] - - def setColorData(self, data, copy: bool=True): - """Set the RGB(A) image to use. - - Supported array format: float32 in [0, 1], uint8. - - :param data: - The RGBA image data as an array of shape (H, W, Channels) - :param copy: True (default) to copy the data, - False to use as is (do not modify!). - """ - data = numpy.array(data, copy=copy) - assert data.ndim == 3 - assert data.shape[-1] in (3, 4) - # TODO check type - - self.__rgba = data - self._updated(ItemChangedType.DATA) - - def getColorData(self, copy: bool=True) -> numpy.ndarray: - """Get the RGB(A) image data. - - :param copy: True (default) to get a copy, - False to get internal representation (do not modify!). - """ - return numpy.array(self.__rgba, copy=copy) diff --git a/silx/gui/plot3d/items/mesh.py b/silx/gui/plot3d/items/mesh.py deleted file mode 100644 index 4e19939..0000000 --- a/silx/gui/plot3d/items/mesh.py +++ /dev/null @@ -1,792 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-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 regular mesh item class. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "17/07/2018" - - -import logging -import numpy - -from ... import _glutils as glu -from ..scene import primitives, utils, function -from ..scene.transform import Rotate -from .core import DataItem3D, ItemChangedType -from .mixins import ColormapMixIn -from ._pick import PickingResult - - -_logger = logging.getLogger(__name__) - - -class _MeshBase(DataItem3D): - """Base class for :class:`Mesh' and :class:`ColormapMesh`. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - DataItem3D.__init__(self, parent=parent) - self._mesh = None - - def _setMesh(self, mesh): - """Set mesh primitive - - :param Union[None,Geometry] mesh: The scene primitive - """ - self._getScenePrimitive().children = [] # Remove any previous mesh - - self._mesh = mesh - if self._mesh is not None: - self._getScenePrimitive().children.append(self._mesh) - - self._updated(ItemChangedType.DATA) - - def _getMesh(self): - """Returns the underlying Mesh scene primitive""" - return self._mesh - - def getPositionData(self, copy=True): - """Get the mesh vertex positions. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: The (x, y, z) positions as a (N, 3) array - :rtype: numpy.ndarray - """ - if self._getMesh() is None: - return numpy.empty((0, 3), dtype=numpy.float32) - else: - return self._getMesh().getAttribute('position', copy=copy) - - def getNormalData(self, copy=True): - """Get the mesh vertex normals. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: The normals as a (N, 3) array, a single normal or None - :rtype: Union[numpy.ndarray,None] - """ - if self._getMesh() is None: - return None - else: - return self._getMesh().getAttribute('normal', copy=copy) - - def getIndices(self, copy=True): - """Get the vertex indices. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: The vertex indices as an array or None. - :rtype: Union[numpy.ndarray,None] - """ - if self._getMesh() is None: - return None - else: - return self._getMesh().getIndices(copy=copy) - - def getDrawMode(self): - """Get mesh rendering mode. - - :return: The drawing mode of this primitive - :rtype: str - """ - return self._getMesh().drawMode - - 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] - """ - rayObject = context.getPickingSegment(frame=self._getScenePrimitive()) - if rayObject is None: # No picking outside viewport - return None - rayObject = rayObject[:, :3] - - positions = self.getPositionData(copy=False) - if positions.size == 0: - return None - - mode = self.getDrawMode() - - vertexIndices = self.getIndices(copy=False) - if vertexIndices is not None: # Expand indices - positions = utils.unindexArrays(mode, vertexIndices, positions)[0] - triangles = positions.reshape(-1, 3, 3) - else: - if mode == 'triangles': - triangles = positions.reshape(-1, 3, 3) - - elif mode == 'triangle_strip': - # Expand strip - triangles = numpy.empty((len(positions) - 2, 3, 3), - dtype=positions.dtype) - triangles[:, 0] = positions[:-2] - triangles[:, 1] = positions[1:-1] - triangles[:, 2] = positions[2:] - - elif mode == 'fan': - # Expand fan - triangles = numpy.empty((len(positions) - 2, 3, 3), - dtype=positions.dtype) - triangles[:, 0] = positions[0] - triangles[:, 1] = positions[1:-1] - triangles[:, 2] = positions[2:] - - else: - _logger.warning("Unsupported draw mode: %s" % mode) - return None - - trianglesIndices, t, barycentric = glu.segmentTrianglesIntersection( - rayObject, triangles) - - if len(trianglesIndices) == 0: - return None - - points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0] - - # Get vertex index from triangle index and closest point in triangle - closest = numpy.argmax(barycentric, axis=1) - - if mode == 'triangles': - indices = trianglesIndices * 3 + closest - - elif mode == 'triangle_strip': - indices = trianglesIndices + closest - - elif mode == 'fan': - indices = trianglesIndices + closest # For corners 1 and 2 - indices[closest == 0] = 0 # For first corner (common) - - if vertexIndices is not None: - # Convert from indices in expanded triangles to input vertices - indices = vertexIndices[indices] - - return PickingResult(self, - positions=points, - indices=indices, - fetchdata=self.getPositionData) - - -class Mesh(_MeshBase): - """Description of mesh. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - _MeshBase.__init__(self, parent=parent) - - def setData(self, - position, - color, - normal=None, - mode='triangles', - indices=None, - copy=True): - """Set mesh geometry data. - - Supported drawing modes are: 'triangles', 'triangle_strip', 'fan' - - :param numpy.ndarray position: - Position (x, y, z) of each vertex as a (N, 3) array - :param numpy.ndarray color: Colors for each point or a single color - :param Union[numpy.ndarray,None] normal: Normals for each point or None (default) - :param str mode: The drawing mode. - :param Union[List[int],None] indices: - Array of vertex indices or None to use arrays directly. - :param bool copy: True (default) to copy the data, - False to use as is (do not modify!). - """ - assert mode in ('triangles', 'triangle_strip', 'fan') - if position is None or len(position) == 0: - mesh = None - else: - mesh = primitives.Mesh3D( - position, color, normal, mode=mode, indices=indices, copy=copy) - self._setMesh(mesh) - - def getData(self, copy=True): - """Get the mesh geometry. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: The positions, colors, normals and mode - :rtype: tuple of numpy.ndarray - """ - return (self.getPositionData(copy=copy), - self.getColorData(copy=copy), - self.getNormalData(copy=copy), - self.getDrawMode()) - - def getColorData(self, copy=True): - """Get the mesh vertex colors. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: The RGBA colors as a (N, 4) array or a single color - :rtype: numpy.ndarray - """ - if self._getMesh() is None: - return numpy.empty((0, 4), dtype=numpy.float32) - else: - return self._getMesh().getAttribute('color', copy=copy) - - -class ColormapMesh(_MeshBase, ColormapMixIn): - """Description of mesh which color is defined by scalar and a colormap. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - _MeshBase.__init__(self, parent=parent) - ColormapMixIn.__init__(self, function.Colormap()) - - def setData(self, - position, - value, - normal=None, - mode='triangles', - indices=None, - copy=True): - """Set mesh geometry data. - - Supported drawing modes are: 'triangles', 'triangle_strip', 'fan' - - :param numpy.ndarray position: - Position (x, y, z) of each vertex as a (N, 3) array - :param numpy.ndarray value: Data value for each vertex. - :param Union[numpy.ndarray,None] normal: Normals for each point or None (default) - :param str mode: The drawing mode. - :param Union[List[int],None] indices: - Array of vertex indices or None to use arrays directly. - :param bool copy: True (default) to copy the data, - False to use as is (do not modify!). - """ - assert mode in ('triangles', 'triangle_strip', 'fan') - if position is None or len(position) == 0: - mesh = None - else: - mesh = primitives.ColormapMesh3D( - position=position, - value=numpy.array(value, copy=False).reshape(-1, 1), # Make it a 2D array - colormap=self._getSceneColormap(), - normal=normal, - mode=mode, - indices=indices, - copy=copy) - self._setMesh(mesh) - - self._setColormappedData(self.getValueData(copy=False), copy=False) - - def getData(self, copy=True): - """Get the mesh geometry. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: The positions, values, normals and mode - :rtype: tuple of numpy.ndarray - """ - return (self.getPositionData(copy=copy), - self.getValueData(copy=copy), - self.getNormalData(copy=copy), - self.getDrawMode()) - - def getValueData(self, copy=True): - """Get the mesh vertex values. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: Array of data values - :rtype: numpy.ndarray - """ - if self._getMesh() is None: - return numpy.empty((0,), dtype=numpy.float32) - else: - return self._getMesh().getAttribute('value', copy=copy) - - -class _CylindricalVolume(DataItem3D): - """Class that represents a volume with a rotational symmetry along z - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - DataItem3D.__init__(self, parent=parent) - self._mesh = None - self._nbFaces = 0 - - def getPosition(self, copy=True): - """Get primitive positions. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: Position of the primitives as a (N, 3) array. - :rtype: numpy.ndarray - """ - raise NotImplementedError("Must be implemented in subclass") - - def _setData(self, position, radius, height, angles, color, flatFaces, - rotation): - """Set volume geometry data. - - :param numpy.ndarray position: - Center position (x, y, z) of each volume as (N, 3) array. - :param float radius: External radius ot the volume. - :param float height: Height of the volume(s). - :param numpy.ndarray angles: Angles of the edges. - :param numpy.array color: RGB color of the volume(s). - :param bool flatFaces: - If the volume as flat faces or not. Used for normals calculation. - """ - - self._getScenePrimitive().children = [] # Remove any previous mesh - - if position is None or len(position) == 0: - self._mesh = None - self._nbFaces = 0 - else: - self._nbFaces = len(angles) - 1 - - volume = numpy.empty(shape=(len(angles) - 1, 12, 3), - dtype=numpy.float32) - normal = numpy.empty(shape=(len(angles) - 1, 12, 3), - dtype=numpy.float32) - - for i in range(0, len(angles) - 1): - # c6 - # /\ - # / \ - # / \ - # c4|------|c5 - # | \ | - # | \ | - # | \ | - # | \ | - # c2|------|c3 - # \ / - # \ / - # \/ - # c1 - c1 = numpy.array([0, 0, -height/2]) - c1 = rotation.transformPoint(c1) - c2 = numpy.array([radius * numpy.cos(angles[i]), - radius * numpy.sin(angles[i]), - -height/2]) - c2 = rotation.transformPoint(c2) - c3 = numpy.array([radius * numpy.cos(angles[i+1]), - radius * numpy.sin(angles[i+1]), - -height/2]) - c3 = rotation.transformPoint(c3) - c4 = numpy.array([radius * numpy.cos(angles[i]), - radius * numpy.sin(angles[i]), - height/2]) - c4 = rotation.transformPoint(c4) - c5 = numpy.array([radius * numpy.cos(angles[i+1]), - radius * numpy.sin(angles[i+1]), - height/2]) - c5 = rotation.transformPoint(c5) - c6 = numpy.array([0, 0, height/2]) - c6 = rotation.transformPoint(c6) - - volume[i] = numpy.array([c1, c3, c2, - c2, c3, c4, - c3, c5, c4, - c4, c5, c6]) - if flatFaces: - normal[i] = numpy.array([numpy.cross(c3-c1, c2-c1), # c1 - numpy.cross(c2-c3, c1-c3), # c3 - numpy.cross(c1-c2, c3-c2), # c2 - numpy.cross(c3-c2, c4-c2), # c2 - numpy.cross(c4-c3, c2-c3), # c3 - numpy.cross(c2-c4, c3-c4), # c4 - numpy.cross(c5-c3, c4-c3), # c3 - numpy.cross(c4-c5, c3-c5), # c5 - numpy.cross(c3-c4, c5-c4), # c4 - numpy.cross(c5-c4, c6-c4), # c4 - numpy.cross(c6-c5, c5-c5), # c5 - numpy.cross(c4-c6, c5-c6)]) # c6 - else: - normal[i] = numpy.array([numpy.cross(c3-c1, c2-c1), - numpy.cross(c2-c3, c1-c3), - numpy.cross(c1-c2, c3-c2), - c2-c1, c3-c1, c4-c6, # c2 c2 c4 - c3-c1, c5-c6, c4-c6, # c3 c5 c4 - numpy.cross(c5-c4, c6-c4), - numpy.cross(c6-c5, c5-c5), - numpy.cross(c4-c6, c5-c6)]) - - # Multiplication according to the number of positions - vertices = numpy.tile(volume.reshape(-1, 3), (len(position), 1))\ - .reshape((-1, 3)) - normals = numpy.tile(normal.reshape(-1, 3), (len(position), 1))\ - .reshape((-1, 3)) - - # Translations - numpy.add(vertices, numpy.tile(position, (1, (len(angles)-1) * 12)) - .reshape((-1, 3)), out=vertices) - - # Colors - if numpy.ndim(color) == 2: - color = numpy.tile(color, (1, 12 * (len(angles) - 1)))\ - .reshape(-1, 3) - - self._mesh = primitives.Mesh3D( - vertices, color, normals, mode='triangles', copy=False) - self._getScenePrimitive().children.append(self._mesh) - - self._updated(ItemChangedType.DATA) - - 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] - """ - if self._mesh is None or self._nbFaces == 0: - return None - - rayObject = context.getPickingSegment(frame=self._getScenePrimitive()) - if rayObject is None: # No picking outside viewport - return None - rayObject = rayObject[:, :3] - - positions = self._mesh.getAttribute('position', copy=False) - triangles = positions.reshape(-1, 3, 3) # 'triangle' draw mode - - trianglesIndices, t = glu.segmentTrianglesIntersection( - rayObject, triangles)[:2] - - if len(trianglesIndices) == 0: - return None - - # Get object index from triangle index - indices = trianglesIndices // (4 * self._nbFaces) - - # Select closest intersection point for each primitive - indices, firstIndices = numpy.unique(indices, return_index=True) - t = t[firstIndices] - - # Resort along t as result of numpy.unique is not sorted by t - sortedIndices = numpy.argsort(t) - t = t[sortedIndices] - indices = indices[sortedIndices] - - points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0] - - return PickingResult(self, - positions=points, - indices=indices, - fetchdata=self.getPosition) - - -class Box(_CylindricalVolume): - """Description of a box. - - Can be used to draw one box or many similar boxes. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - super(Box, self).__init__(parent) - self.position = None - self.size = None - self.color = None - self.rotation = None - self.setData() - - def setData(self, size=(1, 1, 1), color=(1, 1, 1), - position=(0, 0, 0), rotation=(0, (0, 0, 0))): - """ - Set Box geometry data. - - :param numpy.array size: Size (dx, dy, dz) of the box(es). - :param numpy.array color: RGB color of the box(es). - :param numpy.ndarray position: - Center position (x, y, z) of each box as a (N, 3) array. - :param tuple(float, array) rotation: - Angle (in degrees) and axis of rotation. - If (0, (0, 0, 0)) (default), the hexagonal faces are on - xy plane and a side face is aligned with x axis. - """ - self.position = numpy.atleast_2d(numpy.array(position, copy=True)) - self.size = numpy.array(size, copy=True) - self.color = numpy.array(color, copy=True) - self.rotation = Rotate(rotation[0], - rotation[1][0], rotation[1][1], rotation[1][2]) - - assert (numpy.ndim(self.color) == 1 or - len(self.color) == len(self.position)) - - diagonal = numpy.sqrt(self.size[0]**2 + self.size[1]**2) - alpha = 2 * numpy.arcsin(self.size[1] / diagonal) - beta = 2 * numpy.arcsin(self.size[0] / diagonal) - angles = numpy.array([0, - alpha, - alpha + beta, - alpha + beta + alpha, - 2 * numpy.pi]) - numpy.subtract(angles, 0.5 * alpha, out=angles) - self._setData(self.position, - numpy.sqrt(self.size[0]**2 + self.size[1]**2)/2, - self.size[2], - angles, - self.color, - True, - self.rotation) - - def getPosition(self, copy=True): - """Get box(es) position(s). - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: Position of the box(es) as a (N, 3) array. - :rtype: numpy.ndarray - """ - return numpy.array(self.position, copy=copy) - - def getSize(self): - """Get box(es) size. - - :return: Size (dx, dy, dz) of the box(es). - :rtype: numpy.ndarray - """ - return numpy.array(self.size, copy=True) - - def getColor(self, copy=True): - """Get box(es) color. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: RGB color of the box(es). - :rtype: numpy.ndarray - """ - return numpy.array(self.color, copy=copy) - - -class Cylinder(_CylindricalVolume): - """Description of a cylinder. - - Can be used to draw one cylinder or many similar cylinders. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - super(Cylinder, self).__init__(parent) - self.position = None - self.radius = None - self.height = None - self.color = None - self.nbFaces = 0 - self.rotation = None - self.setData() - - def setData(self, radius=1, height=1, color=(1, 1, 1), nbFaces=20, - position=(0, 0, 0), rotation=(0, (0, 0, 0))): - """ - Set the cylinder geometry data - - :param float radius: Radius of the cylinder(s). - :param float height: Height of the cylinder(s). - :param numpy.array color: RGB color of the cylinder(s). - :param int nbFaces: - Number of faces for cylinder approximation (default 20). - :param numpy.ndarray position: - Center position (x, y, z) of each cylinder as a (N, 3) array. - :param tuple(float, array) rotation: - Angle (in degrees) and axis of rotation. - If (0, (0, 0, 0)) (default), the hexagonal faces are on - xy plane and a side face is aligned with x axis. - """ - self.position = numpy.atleast_2d(numpy.array(position, copy=True)) - self.radius = float(radius) - self.height = float(height) - self.color = numpy.array(color, copy=True) - self.nbFaces = int(nbFaces) - self.rotation = Rotate(rotation[0], - rotation[1][0], rotation[1][1], rotation[1][2]) - - assert (numpy.ndim(self.color) == 1 or - len(self.color) == len(self.position)) - - angles = numpy.linspace(0, 2*numpy.pi, self.nbFaces + 1) - self._setData(self.position, - self.radius, - self.height, - angles, - self.color, - False, - self.rotation) - - def getPosition(self, copy=True): - """Get cylinder(s) position(s). - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: Position(s) of the cylinder(s) as a (N, 3) array. - :rtype: numpy.ndarray - """ - return numpy.array(self.position, copy=copy) - - def getRadius(self): - """Get cylinder(s) radius. - - :return: Radius of the cylinder(s). - :rtype: float - """ - return self.radius - - def getHeight(self): - """Get cylinder(s) height. - - :return: Height of the cylinder(s). - :rtype: float - """ - return self.height - - def getColor(self, copy=True): - """Get cylinder(s) color. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: RGB color of the cylinder(s). - :rtype: numpy.ndarray - """ - return numpy.array(self.color, copy=copy) - - -class Hexagon(_CylindricalVolume): - """Description of a uniform hexagonal prism. - - Can be used to draw one hexagonal prim or many similar hexagonal - prisms. - - :param parent: The View widget this item belongs to. - """ - - def __init__(self, parent=None): - super(Hexagon, self).__init__(parent) - self.position = None - self.radius = 0 - self.height = 0 - self.color = None - self.rotation = None - self.setData() - - def setData(self, radius=1, height=1, color=(1, 1, 1), - position=(0, 0, 0), rotation=(0, (0, 0, 0))): - """ - Set the uniform hexagonal prism geometry data - - :param float radius: External radius of the hexagonal prism - :param float height: Height of the hexagonal prism - :param numpy.array color: RGB color of the prism(s) - :param numpy.ndarray position: - Center position (x, y, z) of each prism as a (N, 3) array - :param tuple(float, array) rotation: - Angle (in degrees) and axis of rotation. - If (0, (0, 0, 0)) (default), the hexagonal faces are on - xy plane and a side face is aligned with x axis. - """ - self.position = numpy.atleast_2d(numpy.array(position, copy=True)) - self.radius = float(radius) - self.height = float(height) - self.color = numpy.array(color, copy=True) - self.rotation = Rotate(rotation[0], rotation[1][0], rotation[1][1], - rotation[1][2]) - - assert (numpy.ndim(self.color) == 1 or - len(self.color) == len(self.position)) - - angles = numpy.linspace(0, 2*numpy.pi, 7) - self._setData(self.position, - self.radius, - self.height, - angles, - self.color, - True, - self.rotation) - - def getPosition(self, copy=True): - """Get hexagonal prim(s) position(s). - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: Position(s) of hexagonal prism(s) as a (N, 3) array. - :rtype: numpy.ndarray - """ - return numpy.array(self.position, copy=copy) - - def getRadius(self): - """Get hexagonal prism(s) radius. - - :return: Radius of hexagon(s). - :rtype: float - """ - return self.radius - - def getHeight(self): - """Get hexagonal prism(s) height. - - :return: Height of hexagonal prism(s). - :rtype: float - """ - return self.height - - def getColor(self, copy=True): - """Get hexagonal prism(s) color. - - :param bool copy: - True (default) to get a copy, - False to get internal representation (do not modify!). - :return: RGB color of the hexagonal prism(s). - :rtype: numpy.ndarray - """ - return numpy.array(self.color, copy=copy) diff --git a/silx/gui/plot3d/items/mixins.py b/silx/gui/plot3d/items/mixins.py deleted file mode 100644 index f512365..0000000 --- a/silx/gui/plot3d/items/mixins.py +++ /dev/null @@ -1,288 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-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 mix-in classes for :class:`Item3D`. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "24/04/2018" - - -import collections -import numpy - -from silx.math.combo import min_max - -from ...plot.items.core import ItemMixInBase -from ...plot.items.core import ColormapMixIn as _ColormapMixIn -from ...plot.items.core import SymbolMixIn as _SymbolMixIn -from ...plot.items.core import ComplexMixIn as _ComplexMixIn -from ...colors import rgba - -from ..scene import primitives -from .core import Item3DChangedType, ItemChangedType - - -class InterpolationMixIn(ItemMixInBase): - """Mix-in class for image interpolation mode - - :param str mode: 'linear' (default) or 'nearest' - :param primitive: - scene object for which to sync interpolation mode. - This object MUST have an interpolation property that is updated. - """ - - NEAREST_INTERPOLATION = 'nearest' - """Nearest interpolation mode (see :meth:`setInterpolation`)""" - - LINEAR_INTERPOLATION = 'linear' - """Linear interpolation mode (see :meth:`setInterpolation`)""" - - INTERPOLATION_MODES = NEAREST_INTERPOLATION, LINEAR_INTERPOLATION - """Supported interpolation modes for :meth:`setInterpolation`""" - - def __init__(self, mode=NEAREST_INTERPOLATION, primitive=None): - self.__primitive = primitive - self._syncPrimitiveInterpolation() - - self.__interpolationMode = None - self.setInterpolation(mode) - - def _setPrimitive(self, primitive): - - """Set the scene object for which to sync interpolation""" - self.__primitive = primitive - self._syncPrimitiveInterpolation() - - def _syncPrimitiveInterpolation(self): - """Synchronize scene object's interpolation""" - if self.__primitive is not None: - self.__primitive.interpolation = self.getInterpolation() - - def setInterpolation(self, mode): - """Set image interpolation mode - - :param str mode: 'nearest' or 'linear' - """ - mode = str(mode) - assert mode in self.INTERPOLATION_MODES - if mode != self.__interpolationMode: - self.__interpolationMode = mode - self._syncPrimitiveInterpolation() - self._updated(Item3DChangedType.INTERPOLATION) - - def getInterpolation(self): - """Returns the interpolation mode set by :meth:`setInterpolation` - - :rtype: str - """ - return self.__interpolationMode - - -class ColormapMixIn(_ColormapMixIn): - """Mix-in class for Item3D object with a colormap - - :param sceneColormap: - The plot3d scene colormap to sync with Colormap object. - """ - - def __init__(self, sceneColormap=None): - super(ColormapMixIn, self).__init__() - - self.__sceneColormap = sceneColormap - self._syncSceneColormap() - - def _colormapChanged(self): - """Handle colormap updates""" - self._syncSceneColormap() - super(ColormapMixIn, self)._colormapChanged() - - def _setSceneColormap(self, sceneColormap): - """Set the scene colormap to sync with Colormap object. - - :param sceneColormap: - The plot3d scene colormap to sync with Colormap object. - """ - self.__sceneColormap = sceneColormap - self._syncSceneColormap() - - def _getSceneColormap(self): - """Returns scene colormap that is sync""" - return self.__sceneColormap - - def _syncSceneColormap(self): - """Synchronizes scene's colormap with Colormap object""" - if self.__sceneColormap is not None: - colormap = self.getColormap() - - self.__sceneColormap.colormap = colormap.getNColors() - self.__sceneColormap.norm = colormap.getNormalization() - self.__sceneColormap.gamma = colormap.getGammaNormalizationParameter() - self.__sceneColormap.range_ = colormap.getColormapRange(self) - self.__sceneColormap.nancolor = rgba(colormap.getNaNColor()) - - -class ComplexMixIn(_ComplexMixIn): - __doc__ = _ComplexMixIn.__doc__ # Reuse docstring - - _SUPPORTED_COMPLEX_MODES = ( - _ComplexMixIn.ComplexMode.REAL, - _ComplexMixIn.ComplexMode.IMAGINARY, - _ComplexMixIn.ComplexMode.ABSOLUTE, - _ComplexMixIn.ComplexMode.PHASE, - _ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE) - """Overrides supported ComplexMode""" - - -class SymbolMixIn(_SymbolMixIn): - """Mix-in class for symbol and symbolSize properties for Item3D""" - - _SUPPORTED_SYMBOLS = collections.OrderedDict(( - ('o', 'Circle'), - ('d', 'Diamond'), - ('s', 'Square'), - ('+', 'Plus'), - ('x', 'Cross'), - ('*', 'Star'), - ('|', 'Vertical Line'), - ('_', 'Horizontal Line'), - ('.', 'Point'), - (',', 'Pixel'))) - - def _getSceneSymbol(self): - """Returns a symbol name and size suitable for scene primitives. - - :return: (symbol, size) - """ - symbol = self.getSymbol() - size = self.getSymbolSize() - if symbol == ',': # pixel - return 's', 1. - elif symbol == '.': # point - # Size as in plot OpenGL backend, mimic matplotlib - return 'o', numpy.ceil(0.5 * size) + 1. - else: - return symbol, size - - -class PlaneMixIn(ItemMixInBase): - """Mix-in class for plane items (based on PlaneInGroup primitive)""" - - def __init__(self, plane): - assert isinstance(plane, primitives.PlaneInGroup) - self.__plane = plane - self.__plane.alpha = 1. - self.__plane.addListener(self._planeChanged) - self.__plane.plane.addListener(self._planePositionChanged) - - def _getPlane(self): - """Returns plane primitive - - :rtype: primitives.PlaneInGroup - """ - return self.__plane - - def _planeChanged(self, source, *args, **kwargs): - """Handle events from the plane primitive""" - # Sync visibility - if source.visible != self.isVisible(): - self.setVisible(source.visible) - - def _planePositionChanged(self, source, *args, **kwargs): - """Handle update of cut plane position and normal""" - if self.__plane.visible: # TODO send even if hidden? or send also when showing if moved while hidden - self._updated(ItemChangedType.POSITION) - - # Plane position - - def moveToCenter(self): - """Move cut plane to center of data set""" - self.__plane.moveToCenter() - - def isValid(self): - """Returns whether the cut plane is defined or not (bool)""" - return self.__plane.isValid - - def getNormal(self): - """Returns the normal of the plane (as a unit vector) - - :return: Normal (nx, ny, nz), vector is 0 if no plane is defined - :rtype: numpy.ndarray - """ - return self.__plane.plane.normal - - def setNormal(self, normal): - """Set the normal of the plane - - :param normal: 3-tuple of float: nx, ny, nz - """ - self.__plane.plane.normal = normal - - def getPoint(self): - """Returns a point on the plane - - :return: (x, y, z) - :rtype: numpy.ndarray - """ - return self.__plane.plane.point - - def setPoint(self, point): - """Set a point contained in the plane. - - Warning: The plane might not intersect the bounding box of the data. - - :param point: (x, y, z) position - :type point: 3-tuple of float - """ - self.__plane.plane.point = point # TODO rework according to PR #1303 - - def getParameters(self): - """Returns the plane equation parameters: a*x + b*y + c*z + d = 0 - - :return: Plane equation parameters: (a, b, c, d) - :rtype: numpy.ndarray - """ - return self.__plane.plane.parameters - - def setParameters(self, parameters): - """Set the plane equation parameters: a*x + b*y + c*z + d = 0 - - Warning: The plane might not intersect the bounding box of the data. - The given parameters will be normalized. - - :param parameters: (a, b, c, d) equation parameters - """ - self.__plane.plane.parameters = parameters - - # Border stroke - - def _setForegroundColor(self, color): - """Set the color of the plane border. - - :param color: RGBA color as 4 floats in [0, 1] - """ - self.__plane.color = rgba(color) - if hasattr(super(PlaneMixIn, self), '_setForegroundColor'): - super(PlaneMixIn, self)._setForegroundColor(color) diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py deleted file mode 100644 index 24abaa5..0000000 --- a/silx/gui/plot3d/items/scatter.py +++ /dev/null @@ -1,617 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-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 2D and 3D scatter data item class. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "15/11/2017" - -try: - from collections import abc -except ImportError: # Python2 support - import collections as abc -import logging -import numpy - -from ....utils.deprecation import deprecated -from ... import _glutils as glu -from ...plot._utils.delaunay import delaunay -from ..scene import function, primitives, utils - -from ...plot.items import ScatterVisualizationMixIn -from .core import DataItem3D, Item3DChangedType, ItemChangedType -from .mixins import ColormapMixIn, SymbolMixIn -from ._pick import PickingResult - - -_logger = logging.getLogger(__name__) - - -class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn): - """Description of a 3D scatter plot. - - :param parent: The View widget this item belongs to. - """ - - # TODO supports different size for each point - - def __init__(self, parent=None): - DataItem3D.__init__(self, parent=parent) - ColormapMixIn.__init__(self) - SymbolMixIn.__init__(self) - - noData = numpy.zeros((0, 1), dtype=numpy.float32) - symbol, size = self._getSceneSymbol() - self._scatter = primitives.Points( - x=noData, y=noData, z=noData, value=noData, size=size) - self._scatter.marker = symbol - self._getScenePrimitive().children.append(self._scatter) - - # Connect scene primitive to mix-in class - ColormapMixIn._setSceneColormap(self, self._scatter.colormap) - - def _updated(self, event=None): - """Handle mix-in class updates""" - if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE): - symbol, size = self._getSceneSymbol() - self._scatter.marker = symbol - self._scatter.setAttribute('size', size, copy=True) - - super(Scatter3D, self)._updated(event) - - def setData(self, x, y, z, value, copy=True): - """Set the data of the scatter plot - - :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!) - """ - self._scatter.setAttribute('x', x, copy=copy) - self._scatter.setAttribute('y', y, copy=copy) - self._scatter.setAttribute('z', z, copy=copy) - self._scatter.setAttribute('value', value, copy=copy) - - self._setColormappedData(self.getValueData(copy=False), copy=False) - self._updated(ItemChangedType.DATA) - - def getData(self, copy=True): - """Returns data as provided to :meth:`setData`. - - :param bool copy: True to get a copy, - False to return internal data (do not modify!) - :return: (x, y, z, value) - """ - return (self.getXData(copy), - self.getYData(copy), - self.getZData(copy), - self.getValueData(copy)) - - def getXData(self, copy=True): - """Returns X data coordinates. - - :param bool copy: True to get a copy, - False to return internal array (do not modify!) - :return: X coordinates - :rtype: numpy.ndarray - """ - return self._scatter.getAttribute('x', copy=copy).reshape(-1) - - def getYData(self, copy=True): - """Returns Y data coordinates. - - :param bool copy: True to get a copy, - False to return internal array (do not modify!) - :return: Y coordinates - :rtype: numpy.ndarray - """ - return self._scatter.getAttribute('y', copy=copy).reshape(-1) - - def getZData(self, copy=True): - """Returns Z data coordinates. - - :param bool copy: True to get a copy, - False to return internal array (do not modify!) - :return: Z coordinates - :rtype: numpy.ndarray - """ - return self._scatter.getAttribute('z', copy=copy).reshape(-1) - - def getValueData(self, copy=True): - """Returns data values. - - :param bool copy: True to get a copy, - False to return internal array (do not modify!) - :return: data values - :rtype: numpy.ndarray - """ - return self._scatter.getAttribute('value', copy=copy).reshape(-1) - - @deprecated(reason="Consistency with PlotWidget items", - replacement="getValueData", since_version="0.10.0") - def getValues(self, copy=True): - return self.getValueData(copy) - - 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.getValueData) - else: - return None - - -class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn, - ScatterVisualizationMixIn): - """2D scatter data with settable visualization mode. - - :param parent: The View widget this item belongs to. - """ - - _VISUALIZATION_PROPERTIES = { - ScatterVisualizationMixIn.Visualization.POINTS: - ('symbol', 'symbolSize'), - ScatterVisualizationMixIn.Visualization.LINES: - ('lineWidth',), - ScatterVisualizationMixIn.Visualization.SOLID: (), - } - """Dict {visualization mode: property names used in this mode}""" - - _SUPPORTED_SCATTER_VISUALIZATION = tuple(_VISUALIZATION_PROPERTIES.keys()) - """Overrides supported Visualizations""" - - def __init__(self, parent=None): - DataItem3D.__init__(self, parent=parent) - ColormapMixIn.__init__(self) - SymbolMixIn.__init__(self) - ScatterVisualizationMixIn.__init__(self) - - self._heightMap = False - self._lineWidth = 1. - - self._x = numpy.zeros((0,), dtype=numpy.float32) - self._y = numpy.zeros((0,), dtype=numpy.float32) - self._value = numpy.zeros((0,), dtype=numpy.float32) - - self._cachedLinesIndices = None - self._cachedTrianglesIndices = None - - # Connect scene primitive to mix-in class - ColormapMixIn._setSceneColormap(self, function.Colormap()) - - def _updated(self, event=None): - """Handle mix-in class updates""" - if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE): - symbol, size = self._getSceneSymbol() - for child in self._getScenePrimitive().children: - if isinstance(child, primitives.Points): - child.marker = symbol - child.setAttribute('size', size, copy=True) - - elif event is ItemChangedType.VISIBLE: - # TODO smart update?, need dirty flags - self._updateScene() - - elif event is ItemChangedType.VISUALIZATION_MODE: - self._updateScene() - - super(Scatter2D, self)._updated(event) - - def isPropertyEnabled(self, name, visualization=None): - """Returns true if the property is used with visualization mode. - - :param str name: The name of the property to check, in: - 'lineWidth', 'symbol', 'symbolSize' - :param str visualization: - The visualization mode for which to get the info. - By default, it is the current visualization mode. - :return: - """ - assert name in ('lineWidth', 'symbol', 'symbolSize') - if visualization is None: - visualization = self.getVisualization() - assert visualization in self.supportedVisualizations() - return name in self._VISUALIZATION_PROPERTIES[visualization] - - def setHeightMap(self, heightMap): - """Set whether to display the data has a height map or not. - - When displayed as a height map, the data values are used as - z coordinates. - - :param bool heightMap: - True to display a height map, - False to display as 2D data with z=0 - """ - heightMap = bool(heightMap) - if heightMap != self.isHeightMap(): - self._heightMap = heightMap - self._updateScene() - self._updated(Item3DChangedType.HEIGHT_MAP) - - def isHeightMap(self): - """Returns True if data is displayed as a height map. - - :rtype: bool - """ - return self._heightMap - - def getLineWidth(self): - """Return the curve line width in pixels (float)""" - return self._lineWidth - - def setLineWidth(self, width): - """Set the width in pixel of the curve line - - See :meth:`getLineWidth`. - - :param float width: Width in pixels - """ - width = float(width) - assert width >= 1. - if width != self._lineWidth: - self._lineWidth = width - for child in self._getScenePrimitive().children: - if hasattr(child, 'lineWidth'): - child.lineWidth = width - self._updated(ItemChangedType.LINE_WIDTH) - - def setData(self, x, y, value, copy=True): - """Set the data represented by this item. - - 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 make a copy of the data, - False to avoid copy if possible (do not modify the arrays). - """ - x = numpy.array( - x, copy=copy, dtype=numpy.float32, order='C').reshape(-1) - y = numpy.array( - y, copy=copy, dtype=numpy.float32, order='C').reshape(-1) - assert len(x) == len(y) - - if isinstance(value, abc.Iterable): - value = numpy.array( - value, copy=copy, dtype=numpy.float32, order='C').reshape(-1) - assert len(value) == len(x) - else: # Single scalar - value = numpy.array((float(value),), dtype=numpy.float32) - - self._x = x - self._y = y - self._value = value - - # Reset cache - self._cachedLinesIndices = None - self._cachedTrianglesIndices = None - - self._setColormappedData(self.getValueData(copy=False), copy=False) - - self._updateScene() - - self._updated(ItemChangedType.DATA) - - def getData(self, copy=True): - """Returns data as provided to :meth:`setData`. - - :param bool copy: True to get a copy, - False to return internal data (do not modify!) - :return: (x, y, value) - """ - return (self.getXData(copy=copy), - self.getYData(copy=copy), - self.getValueData(copy=copy)) - - def getXData(self, copy=True): - """Returns X data coordinates. - - :param bool copy: True to get a copy, - False to return internal array (do not modify!) - :return: X coordinates - :rtype: numpy.ndarray - """ - return numpy.array(self._x, copy=copy) - - def getYData(self, copy=True): - """Returns Y data coordinates. - - :param bool copy: True to get a copy, - False to return internal array (do not modify!) - :return: Y coordinates - :rtype: numpy.ndarray - """ - return numpy.array(self._y, copy=copy) - - def getValueData(self, copy=True): - """Returns data values. - - :param bool copy: True to get a copy, - False to return internal array (do not modify!) - :return: data values - :rtype: numpy.ndarray - """ - return numpy.array(self._value, copy=copy) - - @deprecated(reason="Consistency with PlotWidget items", - replacement="getValueData", since_version="0.10.0") - def getValues(self, copy=True): - return self.getValueData(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.getValueData) - 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 = glu.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.getValueData) - - 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.getValueData(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 is self.Visualization.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 is self.Visualization.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 - - if not self.isVisible(): - return # Update when visible - - x, y, value = self.getData(copy=False) - if len(x) == 0: - return # Nothing to display - - mode = self.getVisualization() - heightMap = self.isHeightMap() - - if mode is self.Visualization.POINTS: - z = value if heightMap else 0. - symbol, size = self._getSceneSymbol() - primitive = primitives.Points( - x=x, y=y, z=z, value=value, - size=size, - colormap=self._getSceneColormap()) - primitive.marker = symbol - - else: - # TODO run delaunay in a thread - # Compute lines/triangles indices if not cached - if self._cachedTrianglesIndices is None: - triangulation = delaunay(x, y) - if triangulation is None: - return None - self._cachedTrianglesIndices = numpy.ravel( - triangulation.simplices.astype(numpy.uint32)) - - if (mode is self.Visualization.LINES and - self._cachedLinesIndices is None): - # Compute line indices - self._cachedLinesIndices = utils.triangleToLineIndices( - self._cachedTrianglesIndices, unicity=True) - - if mode is self.Visualization.LINES: - indices = self._cachedLinesIndices - renderMode = 'lines' - else: - indices = self._cachedTrianglesIndices - renderMode = 'triangles' - - # TODO supports x, y instead of copy - if heightMap: - if len(value) == 1: - value = numpy.ones_like(x) * value - coordinates = numpy.array((x, y, value), dtype=numpy.float32).T - else: - coordinates = numpy.array((x, y), dtype=numpy.float32).T - - # TODO option to enable/disable light, cache normals - # TODO smooth surface - if mode is self.Visualization.SOLID: - if heightMap: - coordinates = coordinates[indices] - if len(value) > 1: - value = value[indices] - triangleNormals = utils.trianglesNormal(coordinates) - normal = numpy.empty((len(triangleNormals) * 3, 3), - dtype=numpy.float32) - normal[0::3, :] = triangleNormals - normal[1::3, :] = triangleNormals - normal[2::3, :] = triangleNormals - indices = None - else: - normal = (0., 0., 1.) - else: - normal = None - - primitive = primitives.ColormapMesh3D( - coordinates, - value.reshape(-1, 1), # Makes it a 2D array - normal=normal, - colormap=self._getSceneColormap(), - indices=indices, - mode=renderMode) - primitive.lineWidth = self.getLineWidth() - primitive.lineSmooth = False - - self._getScenePrimitive().children = [primitive] diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py deleted file mode 100644 index f80fea2..0000000 --- a/silx/gui/plot3d/items/volume.py +++ /dev/null @@ -1,886 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-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 3D array item class and its sub-items. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "24/04/2018" - -import logging -import time -import numpy - -from silx.math.combo import min_max -from silx.math.marchingcubes import MarchingCubes -from silx.math.interpolate import interp3d - -from ....utils.proxy import docstring -from ... import _glutils as glu -from ... import qt -from ...colors import rgba - -from ..scene import cutplane, function, primitives, transform, utils - -from .core import BaseNodeItem, Item3D, ItemChangedType, Item3DChangedType -from .mixins import ColormapMixIn, ComplexMixIn, InterpolationMixIn, PlaneMixIn -from ._pick import PickingResult - - -_logger = logging.getLogger(__name__) - - -class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn): - """Class representing a cutting plane in a :class:`ScalarField3D` item. - - :param parent: 3D Data set in which the cut plane is applied. - """ - - def __init__(self, parent): - plane = cutplane.CutPlane(normal=(0, 1, 0)) - - Item3D.__init__(self, parent=None) - ColormapMixIn.__init__(self) - InterpolationMixIn.__init__(self) - PlaneMixIn.__init__(self, plane=plane) - - self._dataRange = None - self._data = None - - self._getScenePrimitive().children = [plane] - - # Connect scene primitive to mix-in class - ColormapMixIn._setSceneColormap(self, plane.colormap) - InterpolationMixIn._setPrimitive(self, plane) - - self.setParent(parent) - - def _updateData(self, data, range_): - """Update used dataset - - No copy is made. - - :param Union[numpy.ndarray[float],None] data: The dataset - :param Union[List[float],None] range_: - (min, min positive, max) values - """ - self._data = None if data is None else numpy.array(data, copy=False) - self._getPlane().setData(self._data, copy=False) - - # Store data range info as 3-tuple of values - self._dataRange = range_ - if range_ is None: - range_ = None, None, None - self._setColormappedData(self._data, copy=False, - min_=range_[0], - minPositive=range_[1], - max_=range_[2]) - - self._updated(ItemChangedType.DATA) - - def _syncDataWithParent(self): - """Synchronize this instance data with that of its parent""" - parent = self.parent() - if parent is None: - data, range_ = None, None - else: - data = parent.getData(copy=False) - range_ = parent.getDataRange() - self._updateData(data, range_) - - def _parentChanged(self, event): - """Handle data change in the parent this plane belongs to""" - if event == ItemChangedType.DATA: - self._syncDataWithParent() - - def setParent(self, parent): - oldParent = self.parent() - if isinstance(oldParent, Item3D): - oldParent.sigItemChanged.disconnect(self._parentChanged) - - super(CutPlane, self).setParent(parent) - - if isinstance(parent, Item3D): - parent.sigItemChanged.connect(self._parentChanged) - - self._syncDataWithParent() - - # Colormap - - def getDisplayValuesBelowMin(self): - """Return whether values <= colormap min are displayed or not. - - :rtype: bool - """ - return self._getPlane().colormap.displayValuesBelowMin - - def setDisplayValuesBelowMin(self, display): - """Set whether to display values <= colormap min. - - :param bool display: True to show values below min, - False to discard them - """ - display = bool(display) - if display != self.getDisplayValuesBelowMin(): - self._getPlane().colormap.displayValuesBelowMin = display - self._updated(ItemChangedType.ALPHA) - - def getDataRange(self): - """Return the range of the data as a 3-tuple of values. - - positive min is NaN if no data is positive. - - :return: (min, positive min, max) or None. - :rtype: Union[List[float],None] - """ - return None if self._dataRange is None else tuple(self._dataRange) - - def getData(self, copy=True): - """Return 3D dataset. - - :param bool copy: - True (default) to get a copy, - False to get the internal data (DO NOT modify!) - :return: The data set (or None if not set) - """ - if self._data is None: - return None - else: - return numpy.array(self._data, copy=copy) - - 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] - """ - rayObject = context.getPickingSegment(frame=self._getScenePrimitive()) - if rayObject is None: - return None - - points = utils.segmentPlaneIntersect( - rayObject[0, :3], - rayObject[1, :3], - planeNorm=self.getNormal(), - planePt=self.getPoint()) - - if len(points) == 1: # Single intersection - if numpy.any(points[0] < 0.): - return None # Outside volume - z, y, x = int(points[0][2]), int(points[0][1]), int(points[0][0]) - - data = self.getData(copy=False) - if data is None: - return None # No dataset - - depth, height, width = data.shape - if z < depth and y < height and x < width: - return PickingResult(self, - positions=[points[0]], - indices=([z], [y], [x])) - else: - return None # Outside image - else: # Either no intersection or segment and image are coplanar - return None - - -class Isosurface(Item3D): - """Class representing an iso-surface in a :class:`ScalarField3D` item. - - :param parent: The DataItem3D this iso-surface belongs to - """ - - def __init__(self, parent): - Item3D.__init__(self, parent=None) - self._data = None - self._level = float('nan') - self._autoLevelFunction = None - self._color = rgba('#FFD700FF') - self.setParent(parent) - - def _syncDataWithParent(self): - """Synchronize this instance data with that of its parent""" - parent = self.parent() - if parent is None: - self._data = None - else: - self._data = parent.getData(copy=False) - self._updateScenePrimitive() - - def _parentChanged(self, event): - """Handle data change in the parent this isosurface belongs to""" - if event == ItemChangedType.DATA: - self._syncDataWithParent() - - def setParent(self, parent): - oldParent = self.parent() - if isinstance(oldParent, Item3D): - oldParent.sigItemChanged.disconnect(self._parentChanged) - - super(Isosurface, self).setParent(parent) - - if isinstance(parent, Item3D): - parent.sigItemChanged.connect(self._parentChanged) - - self._syncDataWithParent() - - def getData(self, copy=True): - """Return 3D dataset. - - :param bool copy: - True (default) to get a copy, - False to get the internal data (DO NOT modify!) - :return: The data set (or None if not set) - """ - if self._data is None: - return None - else: - return numpy.array(self._data, copy=copy) - - def getLevel(self): - """Return the level of this iso-surface (float)""" - return self._level - - def setLevel(self, level): - """Set the value at which to build the iso-surface. - - Setting this value reset auto-level function - - :param float level: The value at which to build the iso-surface - """ - self._autoLevelFunction = None - level = float(level) - if level != self._level: - self._level = level - self._updateScenePrimitive() - self._updated(Item3DChangedType.ISO_LEVEL) - - def isAutoLevel(self): - """True if iso-level is rebuild for each data set.""" - return self.getAutoLevelFunction() is not None - - def getAutoLevelFunction(self): - """Return the function computing the iso-level (callable or None)""" - return self._autoLevelFunction - - def setAutoLevelFunction(self, autoLevel): - """Set the function used to compute the iso-level. - - WARNING: The function might get called in a thread. - - :param callable autoLevel: - A function taking a 3D numpy.ndarray of float32 and returning - a float used as iso-level. - Example: numpy.mean(data) + numpy.std(data) - """ - assert callable(autoLevel) - self._autoLevelFunction = autoLevel - self._updateScenePrimitive() - - def getColor(self): - """Return the color of this iso-surface (QColor)""" - return qt.QColor.fromRgbF(*self._color) - - def _updateColor(self, color): - """Handle update of color - - :param List[float] color: RGBA channels in [0, 1] - """ - primitive = self._getScenePrimitive() - if len(primitive.children) != 0: - primitive.children[0].setAttribute('color', color) - - def setColor(self, color): - """Set the color of the iso-surface - - :param color: RGBA color of the isosurface - :type color: QColor, str or array-like of 4 float in [0., 1.] - """ - color = rgba(color) - if color != self._color: - self._color = color - self._updateColor(self._color) - self._updated(ItemChangedType.COLOR) - - def _computeIsosurface(self): - """Compute isosurface for current state. - - :return: (vertices, normals, indices) arrays - :rtype: List[Union[None,numpy.ndarray]] - """ - data = self.getData(copy=False) - - if data is None: - if self.isAutoLevel(): - self._level = float('nan') - - else: - if self.isAutoLevel(): - st = time.time() - try: - level = float(self.getAutoLevelFunction()(data)) - - except Exception: - module_ = self.getAutoLevelFunction().__module__ - name = self.getAutoLevelFunction().__name__ - _logger.error( - "Error while executing iso level function %s.%s", - module_, - name, - exc_info=True) - level = float('nan') - - else: - _logger.info( - 'Computed iso-level in %f s.', time.time() - st) - - if level != self._level: - self._level = level - self._updated(Item3DChangedType.ISO_LEVEL) - - if numpy.isfinite(self._level): - st = time.time() - vertices, normals, indices = MarchingCubes( - data, - isolevel=self._level) - _logger.info('Computed iso-surface in %f s.', time.time() - st) - - if len(vertices) != 0: - return vertices, normals, indices - - return None, None, None - - def _updateScenePrimitive(self): - """Update underlying mesh""" - self._getScenePrimitive().children = [] - - vertices, normals, indices = self._computeIsosurface() - if vertices is not None: - mesh = primitives.Mesh3D(vertices, - colors=self._color, - normals=normals, - mode='triangles', - indices=indices, - copy=False) - self._getScenePrimitive().children = [mesh] - - 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] - """ - rayObject = context.getPickingSegment(frame=self._getScenePrimitive()) - if rayObject is None: - return None - rayObject = rayObject[:, :3] - - data = self.getData(copy=False) - bins = utils.segmentVolumeIntersect( - rayObject, numpy.array(data.shape) - 1) - if bins is None: - return None - - # gather bin data - offsets = [(i, j, k) for i in (0, 1) for j in (0, 1) for k in (0, 1)] - indices = bins[:, numpy.newaxis, :] + offsets - binsData = data[indices[:, :, 0], indices[:, :, 1], indices[:, :, 2]] - # binsData.shape = nbins, 8 - # TODO up-to this point everything can be done once for all isosurfaces - - # check bin candidates - level = self.getLevel() - mask = numpy.logical_and(numpy.nanmin(binsData, axis=1) <= level, - level <= numpy.nanmax(binsData, axis=1)) - bins = bins[mask] - binsData = binsData[mask] - - if len(bins) == 0: - return None # No bin candidate - - # do picking on candidates - intersections = [] - depths = [] - for currentBin, data in zip(bins, binsData): - mc = MarchingCubes(data.reshape(2, 2, 2), isolevel=level) - points = mc.get_vertices() + currentBin - triangles = points[mc.get_indices()] - t = glu.segmentTrianglesIntersection(rayObject, triangles)[1] - t = numpy.unique(t) # Duplicates happen on triangle edges - if len(t) != 0: - # Compute intersection points and get closest data point - points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0] - # Get closest data points by rounding to int - intersections.extend(points) - depths.extend(t) - - if len(intersections) == 0: - return None # No intersected triangles - - intersections = numpy.array(intersections)[numpy.argsort(depths)] - indices = numpy.transpose(numpy.round(intersections).astype(numpy.int64)) - return PickingResult(self, positions=intersections, indices=indices) - - -class ScalarField3D(BaseNodeItem): - """3D scalar field on a regular grid. - - :param parent: The View widget this item belongs to. - """ - - _CutPlane = CutPlane - """CutPlane class associated to this class""" - - _Isosurface = Isosurface - """Isosurface classe associated to this class""" - - def __init__(self, parent=None): - BaseNodeItem.__init__(self, parent=parent) - - # Gives this item the shape of the data, no matter - # of the isosurface/cut plane size - self._boundedGroup = primitives.BoundedGroup() - - # Store iso-surfaces - self._isosurfaces = [] - - self._data = None - self._dataRange = None - - self._cutPlane = self._CutPlane(parent=self) - self._cutPlane.setVisible(False) - - self._isogroup = primitives.GroupDepthOffset() - self._isogroup.transforms = [ - # Convert from z, y, x from marching cubes to x, y, z - transform.Matrix(( - (0., 0., 1., 0.), - (0., 1., 0., 0.), - (1., 0., 0., 0.), - (0., 0., 0., 1.))), - # Offset to match cutting plane coords - transform.Translate(0.5, 0.5, 0.5) - ] - - self._getScenePrimitive().children = [ - self._boundedGroup, - self._cutPlane._getScenePrimitive(), - self._isogroup] - - @staticmethod - def _computeRangeFromData(data): - """Compute range info (min, min positive, max) from data - - :param Union[numpy.ndarray,None] data: - :return: Union[List[float],None] - """ - if data is None: - return None - - dataRange = min_max(data, min_positive=True, finite=True) - if dataRange.minimum is None: # Only non-finite data - return None - - if dataRange is not None: - min_positive = dataRange.min_positive - if min_positive is None: - min_positive = float('nan') - return dataRange.minimum, min_positive, dataRange.maximum - - def setData(self, data, copy=True): - """Set the 3D scalar data represented by this item. - - Dataset order is zyx (i.e., first dimension is z). - - :param data: 3D array - :type data: 3D numpy.ndarray of float32 with shape at least (2, 2, 2) - :param bool copy: - True (default) to make a copy, - False to avoid copy (DO NOT MODIFY data afterwards) - """ - if data is None: - self._data = None - self._boundedGroup.shape = None - - else: - data = numpy.array(data, copy=copy, dtype=numpy.float32, order='C') - assert data.ndim == 3 - assert min(data.shape) >= 2 - - self._data = data - self._boundedGroup.shape = self._data.shape - - self._dataRange = self._computeRangeFromData(self._data) - self._updated(ItemChangedType.DATA) - - def getData(self, copy=True): - """Return 3D dataset. - - :param bool copy: - True (default) to get a copy, - False to get the internal data (DO NOT modify!) - :return: The data set (or None if not set) - """ - if self._data is None: - return None - else: - return numpy.array(self._data, copy=copy) - - def getDataRange(self): - """Return the range of the data as a 3-tuple of values. - - positive min is NaN if no data is positive. - - :return: (min, positive min, max) or None. - """ - return self._dataRange - - # Cut Plane - - def getCutPlanes(self): - """Return an iterable of all :class:`CutPlane` of this item. - - This includes hidden cut planes. - - For now, there is always one cut plane. - """ - return (self._cutPlane,) - - # Handle iso-surfaces - - # TODO rename to sigItemAdded|Removed? - sigIsosurfaceAdded = qt.Signal(object) - """Signal emitted when a new iso-surface is added to the view. - - The newly added iso-surface is provided by this signal - """ - - sigIsosurfaceRemoved = qt.Signal(object) - """Signal emitted when an iso-surface is removed from the view - - The removed iso-surface is provided by this signal. - """ - - def addIsosurface(self, level, color): - """Add an isosurface to this item. - - :param level: - The value at which to build the iso-surface or a callable - (e.g., a function) taking a 3D numpy.ndarray as input and - returning a float. - Example: numpy.mean(data) + numpy.std(data) - :type level: float or callable - :param color: RGBA color of the isosurface - :type color: str or array-like of 4 float in [0., 1.] - :return: isosurface object - :rtype: ~silx.gui.plot3d.items.volume.Isosurface - """ - isosurface = self._Isosurface(parent=self) - isosurface.setColor(color) - if callable(level): - isosurface.setAutoLevelFunction(level) - else: - isosurface.setLevel(level) - isosurface.sigItemChanged.connect(self._isosurfaceItemChanged) - - self._isosurfaces.append(isosurface) - - self._updateIsosurfaces() - - self.sigIsosurfaceAdded.emit(isosurface) - return isosurface - - def getIsosurfaces(self): - """Return an iterable of all :class:`.Isosurface` instance of this item""" - return tuple(self._isosurfaces) - - def removeIsosurface(self, isosurface): - """Remove an iso-surface from this item. - - :param ~silx.gui.plot3d.Plot3DWidget.Isosurface isosurface: - The isosurface object to remove - """ - if isosurface not in self.getIsosurfaces(): - _logger.warning( - "Try to remove isosurface that is not in the list: %s", - str(isosurface)) - else: - isosurface.sigItemChanged.disconnect(self._isosurfaceItemChanged) - self._isosurfaces.remove(isosurface) - self._updateIsosurfaces() - self.sigIsosurfaceRemoved.emit(isosurface) - - def clearIsosurfaces(self): - """Remove all :class:`.Isosurface` instances from this item.""" - for isosurface in self.getIsosurfaces(): - self.removeIsosurface(isosurface) - - def _isosurfaceItemChanged(self, event): - """Handle update of isosurfaces upon level changed""" - if event == Item3DChangedType.ISO_LEVEL: - self._updateIsosurfaces() - - def _updateIsosurfaces(self): - """Handle updates of iso-surfaces level and add/remove""" - # Sorting using minus, this supposes data 'object' to be max values - sortedIso = sorted(self.getIsosurfaces(), - key=lambda isosurface: - isosurface.getLevel()) - self._isogroup.children = [iso._getScenePrimitive() for iso in sortedIso] - - # BaseNodeItem - - def getItems(self): - """Returns the list of items currently present in this item. - - :rtype: tuple - """ - return self.getCutPlanes() + self.getIsosurfaces() - - -################## -# ComplexField3D # -################## - -class ComplexCutPlane(CutPlane, ComplexMixIn): - """Class representing a cutting plane in a :class:`ComplexField3D` item. - - :param parent: 3D Data set in which the cut plane is applied. - """ - - def __init__(self, parent): - ComplexMixIn.__init__(self) - CutPlane.__init__(self, parent=parent) - - def _syncDataWithParent(self): - """Synchronize this instance data with that of its parent""" - parent = self.parent() - if parent is None: - data, range_ = None, None - else: - mode = self.getComplexMode() - data = parent.getData(mode=mode, copy=False) - range_ = parent.getDataRange(mode=mode) - self._updateData(data, range_) - - def _updated(self, event=None): - """Handle update of the cut plane (and take care of mode change - - :param Union[None,ItemChangedType] event: The kind of update - """ - if event == ItemChangedType.COMPLEX_MODE: - self._syncDataWithParent() - super(ComplexCutPlane, self)._updated(event) - - -class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn): - """Class representing an iso-surface in a :class:`ComplexField3D` item. - - :param parent: The DataItem3D this iso-surface belongs to - """ - - _SUPPORTED_COMPLEX_MODES = \ - (ComplexMixIn.ComplexMode.NONE,) + ComplexMixIn._SUPPORTED_COMPLEX_MODES - """Overrides supported ComplexMode""" - - def __init__(self, parent): - ComplexMixIn.__init__(self) - ColormapMixIn.__init__(self, function.Colormap()) - Isosurface.__init__(self, parent=parent) - self.setComplexMode(self.ComplexMode.NONE) - - def _updateColor(self, color): - """Handle update of color - - :param List[float] color: RGBA channels in [0, 1] - """ - primitive = self._getScenePrimitive() - if (len(primitive.children) != 0 and - isinstance(primitive.children[0], primitives.ColormapMesh3D)): - primitive.children[0].alpha = self._color[3] - else: - super(ComplexIsosurface, self)._updateColor(color) - - def _syncDataWithParent(self): - """Synchronize this instance data with that of its parent""" - parent = self.parent() - if parent is None: - self._data = None - else: - self._data = parent.getData( - mode=parent.getComplexMode(), copy=False) - - if parent is None or self.getComplexMode() == self.ComplexMode.NONE: - self._setColormappedData(None, copy=False) - else: - self._setColormappedData( - parent.getData(mode=self.getComplexMode(), copy=False), - copy=False) - - self._updateScenePrimitive() - - def _parentChanged(self, event): - """Handle data change in the parent this isosurface belongs to""" - if event == ItemChangedType.COMPLEX_MODE: - self._syncDataWithParent() - super(ComplexIsosurface, self)._parentChanged(event) - - def _updated(self, event=None): - """Handle update of the isosurface (and take care of mode change) - - :param ItemChangedType event: The kind of update - """ - if event == ItemChangedType.COMPLEX_MODE: - self._syncDataWithParent() - - elif event in (ItemChangedType.COLORMAP, - Item3DChangedType.INTERPOLATION): - self._updateScenePrimitive() - super(ComplexIsosurface, self)._updated(event) - - def _updateScenePrimitive(self): - """Update underlying mesh""" - if self.getComplexMode() == self.ComplexMode.NONE: - super(ComplexIsosurface, self)._updateScenePrimitive() - - else: # Specific display for colormapped isosurface - self._getScenePrimitive().children = [] - - values = self.getColormappedData(copy=False) - if values is not None: - vertices, normals, indices = self._computeIsosurface() - if vertices is not None: - values = interp3d(values, vertices, method='linear_omp') - # TODO reuse isosurface when only color changes... - - mesh = primitives.ColormapMesh3D( - vertices, - value=values.reshape(-1, 1), - colormap=self._getSceneColormap(), - normal=normals, - mode='triangles', - indices=indices, - copy=False) - mesh.alpha = self._color[3] - self._getScenePrimitive().children = [mesh] - - -class ComplexField3D(ScalarField3D, ComplexMixIn): - """3D complex field on a regular grid. - - :param parent: The View widget this item belongs to. - """ - - _CutPlane = ComplexCutPlane - _Isosurface = ComplexIsosurface - - def __init__(self, parent=None): - self._dataRangeCache = None - - ComplexMixIn.__init__(self) - ScalarField3D.__init__(self, parent=parent) - - @docstring(ComplexMixIn) - def setComplexMode(self, mode): - mode = ComplexMixIn.ComplexMode.from_value(mode) - if mode != self.getComplexMode(): - self.clearIsosurfaces() # Reset isosurfaces - ComplexMixIn.setComplexMode(self, mode) - - def setData(self, data, copy=True): - """Set the 3D complex data represented by this item. - - Dataset order is zyx (i.e., first dimension is z). - - :param data: 3D array - :type data: 3D numpy.ndarray of float32 with shape at least (2, 2, 2) - :param bool copy: - True (default) to make a copy, - False to avoid copy (DO NOT MODIFY data afterwards) - """ - if data is None: - self._data = None - self._dataRangeCache = None - self._boundedGroup.shape = None - - else: - data = numpy.array(data, copy=copy, dtype=numpy.complex64, order='C') - assert data.ndim == 3 - assert min(data.shape) >= 2 - - self._data = data - self._dataRangeCache = {} - self._boundedGroup.shape = self._data.shape - - self._updated(ItemChangedType.DATA) - - def getData(self, copy=True, mode=None): - """Return 3D dataset. - - This method does not cache data converted to a specific mode, - it computes it for each request. - - :param bool copy: - True (default) to get a copy, - False to get the internal data (DO NOT modify!) - :param Union[None,Mode] mode: - The kind of data to retrieve. - If None (the default), it returns the complex data, - else it computes the requested scalar data. - :return: The data set (or None if not set) - :rtype: Union[numpy.ndarray,None] - """ - if mode is None: - return super(ComplexField3D, self).getData(copy=copy) - else: - return self._convertComplexData(self._data, mode) - - def getDataRange(self, mode=None): - """Return the range of the requested data as a 3-tuple of values. - - Positive min is NaN if no data is positive. - - :param Union[None,Mode] mode: - The kind of data for which to get the range information. - If None (the default), it returns the data range for the current mode, - else it returns the data range for the requested mode. - :return: (min, positive min, max) or None. - :rtype: Union[None,List[float]] - """ - if self._dataRangeCache is None: - return None - - if mode is None: - mode = self.getComplexMode() - - if mode not in self._dataRangeCache: - # Compute it and store it in cache - data = self.getData(copy=False, mode=mode) - self._dataRangeCache[mode] = self._computeRangeFromData(data) - - return self._dataRangeCache[mode] |