summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/items
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/items')
-rw-r--r--silx/gui/plot3d/items/__init__.py43
-rw-r--r--silx/gui/plot3d/items/_pick.py265
-rw-r--r--silx/gui/plot3d/items/clipplane.py136
-rw-r--r--silx/gui/plot3d/items/core.py779
-rw-r--r--silx/gui/plot3d/items/image.py425
-rw-r--r--silx/gui/plot3d/items/mesh.py792
-rw-r--r--silx/gui/plot3d/items/mixins.py288
-rw-r--r--silx/gui/plot3d/items/scatter.py617
-rw-r--r--silx/gui/plot3d/items/volume.py886
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]