summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot3d/items
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot3d/items')
-rw-r--r--src/silx/gui/plot3d/items/__init__.py9
-rw-r--r--src/silx/gui/plot3d/items/_pick.py50
-rw-r--r--src/silx/gui/plot3d/items/clipplane.py28
-rw-r--r--src/silx/gui/plot3d/items/core.py124
-rw-r--r--src/silx/gui/plot3d/items/image.py92
-rw-r--r--src/silx/gui/plot3d/items/mesh.py367
-rw-r--r--src/silx/gui/plot3d/items/mixins.py80
-rw-r--r--src/silx/gui/plot3d/items/scatter.py224
-rw-r--r--src/silx/gui/plot3d/items/volume.py135
9 files changed, 607 insertions, 502 deletions
diff --git a/src/silx/gui/plot3d/items/__init__.py b/src/silx/gui/plot3d/items/__init__.py
index 3d22103..b091ffc 100644
--- a/src/silx/gui/plot3d/items/__init__.py
+++ b/src/silx/gui/plot3d/items/__init__.py
@@ -31,8 +31,13 @@ __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 .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
diff --git a/src/silx/gui/plot3d/items/_pick.py b/src/silx/gui/plot3d/items/_pick.py
index 49e1a5b..aad5daf 100644
--- a/src/silx/gui/plot3d/items/_pick.py
+++ b/src/silx/gui/plot3d/items/_pick.py
@@ -53,7 +53,7 @@ class PickContext(object):
self._widgetPosition = x, y
assert isinstance(viewport, Viewport)
self._viewport = viewport
- self._ndcZRange = -1., 1.
+ self._ndcZRange = -1.0, 1.0
self._enabled = True
self._condition = condition
@@ -108,7 +108,7 @@ class PickContext(object):
"""
return self._enabled
- def setNDCZRange(self, near=-1., far=1.):
+ def setNDCZRange(self, near=-1.0, far=1.0):
"""Set near and far Z value in normalized device coordinates
This allows to clip the ray to a subset of the NDC range
@@ -142,36 +142,33 @@ class PickContext(object):
or None if picked point is outside viewport
:rtype: Union[None,numpy.ndarray]
"""
- assert frame in ('ndc', 'camera', 'scene') or isinstance(frame, Base)
+ 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':
+ rayNdc = numpy.array(
+ (positionNdc + (near, 1.0), positionNdc + (far, 1.0)), dtype=numpy.float64
+ )
+ if frame == "ndc":
return rayNdc
viewport = self.getViewport()
rayCamera = viewport.camera.intrinsic.transformPoints(
- rayNdc,
- direct=False,
- perspectiveDivide=True)
- if frame == 'camera':
+ rayNdc, direct=False, perspectiveDivide=True
+ )
+ if frame == "camera":
return rayCamera
- rayScene = viewport.camera.extrinsic.transformPoints(
- rayCamera, direct=False)
- if frame == 'scene':
+ 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)
+ rayObject = frame.objectToSceneTransform.transformPoints(rayScene, direct=False)
return rayObject
@@ -193,8 +190,7 @@ class PickingResult(_PickingResult):
"""
super(PickingResult, self).__init__(item, indices)
- self._objectPositions = numpy.array(
- positions, copy=False, dtype=numpy.float64)
+ self._objectPositions = numpy.array(positions, copy=False, dtype=numpy.float64)
# Store matrices to generate positions on demand
primitive = item._getScenePrimitive()
@@ -219,7 +215,7 @@ class PickingResult(_PickingResult):
item = self.getItem()
if self._fetchdata is None:
- if hasattr(item, 'getData'):
+ if hasattr(item, "getData"):
data = item.getData(copy=False)
else:
return None
@@ -228,7 +224,7 @@ class PickingResult(_PickingResult):
return numpy.array(data[indices], copy=copy)
- def getPositions(self, frame='scene', copy=True):
+ def getPositions(self, frame="scene", copy=True):
"""Returns picking positions in item coordinates.
:param str frame: The frame in which the positions are returned
@@ -239,24 +235,26 @@ class PickingResult(_PickingResult):
:return: Nx3 array of (x, y, z) coordinates
:rtype: numpy.ndarray
"""
- if frame == 'ndc':
+ if frame == "ndc":
if self._ndcPositions is None: # Lazy-loading
self._ndcPositions = self._objectToNDCTransform.transformPoints(
- self._objectPositions, perspectiveDivide=True)
+ self._objectPositions, perspectiveDivide=True
+ )
positions = self._ndcPositions
- elif frame == 'scene':
+ elif frame == "scene":
if self._scenePositions is None: # Lazy-loading
self._scenePositions = self._objectToSceneTransform.transformPoints(
- self._objectPositions)
+ self._objectPositions
+ )
positions = self._scenePositions
- elif frame == 'object':
+ elif frame == "object":
positions = self._objectPositions
else:
- raise ValueError('Unsupported frame argument: %s' % str(frame))
+ raise ValueError("Unsupported frame argument: %s" % str(frame))
return numpy.array(positions, copy=copy)
diff --git a/src/silx/gui/plot3d/items/clipplane.py b/src/silx/gui/plot3d/items/clipplane.py
index 83a3c0e..283230b 100644
--- a/src/silx/gui/plot3d/items/clipplane.py
+++ b/src/silx/gui/plot3d/items/clipplane.py
@@ -47,7 +47,8 @@ class ClipPlane(Item3D, PlaneMixIn):
def __init__(self, parent=None):
plane = primitives.ClipPlane()
Item3D.__init__(self, parent=parent, primitive=plane)
- PlaneMixIn.__init__(self, plane=plane)
+ PlaneMixIn.__init__(self)
+ self._setPlane(plane)
def __pickPreProcessing(self, context):
"""Common processing for :meth:`_pickPostProcess` and :meth:`_pickFull`
@@ -73,12 +74,15 @@ class ClipPlane(Item3D, PlaneMixIn):
rayObject[0, :3],
rayObject[1, :3],
planeNorm=self.getNormal(),
- planePt=self.getPoint())
+ 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]))
+ picked = (
+ len(points) == 1
+ and numpy.all(bounds[0] <= points[0])
+ and numpy.all(points[0] <= bounds[1])
+ )
return picked, points, rayObject
@@ -96,18 +100,20 @@ class ClipPlane(Item3D, PlaneMixIn):
if picked: # A single intersection inside bounding box
# Clip NDC z range for following brother items
ndcIntersect = plane.objectToNDCTransform.transformPoint(
- points[0], perspectiveDivide=True)
+ points[0], perspectiveDivide=True
+ )
ndcNormal = plane.objectToNDCTransform.transformNormal(
- self.getNormal())
+ self.getNormal()
+ )
if ndcNormal[2] < 0:
- context.setNDCZRange(-1., ndcIntersect[2])
+ context.setNDCZRange(-1.0, ndcIntersect[2])
else:
- context.setNDCZRange(ndcIntersect[2], 1.)
+ context.setNDCZRange(ndcIntersect[2], 1.0)
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.:
+ rayObject[:, 3] = 1.0 # Make sure 4h coordinate is one
+ if numpy.sum(rayObject[0] * self.getParameters()) < 0.0:
# Disable picking for remaining brothers
context.setEnabled(False)
diff --git a/src/silx/gui/plot3d/items/core.py b/src/silx/gui/plot3d/items/core.py
index 5fbe62c..4caf41d 100644
--- a/src/silx/gui/plot3d/items/core.py
+++ b/src/silx/gui/plot3d/items/core.py
@@ -44,25 +44,25 @@ from ._pick import PickContext
class Item3DChangedType(enum.Enum):
"""Type of modification provided by :attr:`Item3D.sigItemChanged` signal."""
- INTERPOLATION = 'interpolationChanged'
+ INTERPOLATION = "interpolationChanged"
"""Item3D image interpolation changed flag."""
- TRANSFORM = 'transformChanged'
+ TRANSFORM = "transformChanged"
"""Item3D transform changed flag."""
- HEIGHT_MAP = 'heightMapChanged'
+ HEIGHT_MAP = "heightMapChanged"
"""Item3D height map changed flag."""
- ISO_LEVEL = 'isoLevelChanged'
+ ISO_LEVEL = "isoLevelChanged"
"""Isosurface level changed flag."""
- LABEL = 'labelChanged'
+ LABEL = "labelChanged"
"""Item's label changed flag."""
- BOUNDING_BOX_VISIBLE = 'boundingBoxVisibleChanged'
+ BOUNDING_BOX_VISIBLE = "boundingBoxVisibleChanged"
"""Item's bounding box visibility changed"""
- ROOT_ITEM = 'rootItemChanged'
+ ROOT_ITEM = "rootItemChanged"
"""Item's root changed flag."""
@@ -85,7 +85,9 @@ class Item3D(qt.QObject):
"""
def __init__(self, parent, primitive=None):
- qt.QObject.__init__(self, parent)
+ qt.QObject.__init__(self)
+ if parent is not None:
+ self.setParent(parent)
if primitive is None:
primitive = scene.Group()
@@ -97,12 +99,9 @@ class Item3D(qt.QObject):
labelIndex = self._LABEL_INDICES[self.__class__]
self._label = str(self.__class__.__name__)
if labelIndex != 0:
- self._label += u' %d' % labelIndex
+ self._label += " %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()
@@ -203,7 +202,7 @@ class Item3D(qt.QObject):
:param color: RGBA color
:type color: tuple of 4 float in [0., 1.]
"""
- if hasattr(super(Item3D, self), '_setForegroundColor'):
+ if hasattr(super(Item3D, self), "_setForegroundColor"):
super(Item3D, self)._setForegroundColor(color)
def __syncForegroundColor(self):
@@ -213,8 +212,7 @@ class Item3D(qt.QObject):
if root is not None:
widget = root.parent()
if isinstance(widget, qt.QWidget):
- self._setForegroundColor(
- widget.getForegroundColor().getRgbF())
+ self._setForegroundColor(widget.getForegroundColor().getRgbF())
# picking
@@ -225,10 +223,12 @@ class Item3D(qt.QObject):
: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)):
+ if (
+ self.isVisible()
+ and context.isEnabled()
+ and context.isItemPickable(self)
+ and self._pickFastCheck(context)
+ ):
return self._pickFull(context)
return None
@@ -251,8 +251,10 @@ class Item3D(qt.QObject):
bounds = primitive.objectToNDCTransform.transformBounds(bounds)
- return (bounds[0, 0] <= positionNdc[0] <= bounds[1, 0] and
- bounds[0, 1] <= positionNdc[1] <= bounds[1, 1])
+ 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.
@@ -295,17 +297,21 @@ class DataItem3D(Item3D):
# 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._matrix, self._scale]
+ )
self._transformObjectToRotate.addListener(self._updateRotationCenter)
- self._rotationCenter = 0., 0., 0.
+ self._rotationCenter = 0.0, 0.0, 0.0
- self.__transforms = transform.TransformList([
- self._translate,
- self._rotateForwardTranslation,
- self._rotate,
- self._rotateBackwardTranslation,
- self._transformObjectToRotate])
+ self.__transforms = transform.TransformList(
+ [
+ self._translate,
+ self._rotateForwardTranslation,
+ self._rotate,
+ self._rotateBackwardTranslation,
+ self._transformObjectToRotate,
+ ]
+ )
self._getScenePrimitive().transforms = self.__transforms
@@ -327,7 +333,7 @@ class DataItem3D(Item3D):
"""
return self.__transforms
- def setScale(self, sx=1., sy=1., sz=1.):
+ def setScale(self, sx=1.0, sy=1.0, sz=1.0):
"""Set the scale of the item in the scene.
:param float sx: Scale factor along the X axis
@@ -346,7 +352,7 @@ class DataItem3D(Item3D):
"""
return self._scale.scale
- def setTranslation(self, x=0., y=0., z=0.):
+ def setTranslation(self, x=0.0, y=0.0, z=0.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
@@ -365,7 +371,7 @@ class DataItem3D(Item3D):
"""
return self._translate.translation
- _ROTATION_CENTER_TAGS = 'lower', 'center', 'upper'
+ _ROTATION_CENTER_TAGS = "lower", "center", "upper"
def _updateRotationCenter(self, *args, **kwargs):
"""Update rotation center relative to bounding box"""
@@ -374,28 +380,31 @@ class DataItem3D(Item3D):
# Patch position relative to bounding box
if position in self._ROTATION_CENTER_TAGS:
bounds = self._getScenePrimitive().bounds(
- transformed=False, dataBounds=True)
+ transformed=False, dataBounds=True
+ )
bounds = self._transformObjectToRotate.transformBounds(bounds)
if bounds is None:
- position = 0.
- elif position == 'lower':
+ position = 0.0
+ elif position == "lower":
position = bounds[0, index]
- elif position == 'center':
+ elif position == "center":
position = 0.5 * (bounds[0, index] + bounds[1, index])
- elif position == 'upper':
+ elif position == "upper":
position = bounds[1, index]
center.append(position)
- if not numpy.all(numpy.equal(
- center, self._rotateForwardTranslation.translation)):
+ if not numpy.all(
+ numpy.equal(center, self._rotateForwardTranslation.translation)
+ ):
self._rotateForwardTranslation.translation = center
- self._rotateBackwardTranslation.translation = \
- - self._rotateForwardTranslation.translation
+ self._rotateBackwardTranslation.translation = (
+ -self._rotateForwardTranslation.translation
+ )
self._updated(Item3DChangedType.TRANSFORM)
- def setRotationCenter(self, x=0., y=0., z=0.):
+ def setRotationCenter(self, x=0.0, y=0.0, z=0.0):
"""Set the center of rotation of the item.
Position of the rotation center is either a float
@@ -430,7 +439,7 @@ class DataItem3D(Item3D):
"""
return self._rotationCenter
- def setRotation(self, angle=0., axis=(0., 0., 1.)):
+ def setRotation(self, angle=0.0, axis=(0.0, 0.0, 1.0)):
"""Set the rotation of the item in the scene
:param float angle: The rotation angle in degrees.
@@ -439,8 +448,9 @@ class DataItem3D(Item3D):
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))):
+ if self._rotate.angle != angle or not numpy.all(
+ numpy.equal(axis, self._rotate.axis)
+ ):
self._rotate.setAngleAxis(angle, axis)
self._updated(Item3DChangedType.TRANSFORM)
@@ -522,7 +532,7 @@ class BaseNodeItem(DataItem3D):
:rtype: tuple
"""
- raise NotImplementedError('getItems must be implemented in subclass')
+ raise NotImplementedError("getItems must be implemented in subclass")
def visit(self, included=True):
"""Generator visiting the group content.
@@ -535,7 +545,7 @@ class BaseNodeItem(DataItem3D):
yield self
for child in self.getItems():
yield child
- if hasattr(child, 'visit'):
+ if hasattr(child, "visit"):
for item in child.visit(included=False):
yield item
@@ -554,8 +564,7 @@ class BaseNodeItem(DataItem3D):
"""
viewport = self._getScenePrimitive().viewport
if viewport is None:
- raise RuntimeError(
- 'Cannot perform picking: Item not attached to a widget')
+ raise RuntimeError("Cannot perform picking: Item not attached to a widget")
context = PickContext(x, y, viewport, condition)
for result in self._pickItems(context):
@@ -638,12 +647,10 @@ class _BaseGroupItem(BaseNodeItem):
item.setParent(self)
if index is None:
- self._getGroupPrimitive().children.append(
- item._getScenePrimitive())
+ self._getGroupPrimitive().children.append(item._getScenePrimitive())
self._items.append(item)
else:
- self._getGroupPrimitive().children.insert(
- index, item._getScenePrimitive())
+ self._getGroupPrimitive().children.insert(index, item._getScenePrimitive())
self._items.insert(index, item)
self.sigItemAdded.emit(item)
@@ -691,8 +698,9 @@ class GroupWithAxesItem(_BaseGroupItem):
:param parent: The View widget this item belongs to.
"""
- super(GroupWithAxesItem, self).__init__(parent=parent,
- group=axes.LabelledAxes())
+ super(GroupWithAxesItem, self).__init__(
+ parent=parent, group=axes.LabelledAxes()
+ )
# Axes labels
@@ -747,9 +755,9 @@ class GroupWithAxesItem(_BaseGroupItem):
:return: object describing the labels
"""
labelledAxes = self._getScenePrimitive()
- return self._Labels((labelledAxes.xlabel,
- labelledAxes.ylabel,
- labelledAxes.zlabel))
+ return self._Labels(
+ (labelledAxes.xlabel, labelledAxes.ylabel, labelledAxes.zlabel)
+ )
class RootGroupWithAxesItem(GroupWithAxesItem):
diff --git a/src/silx/gui/plot3d/items/image.py b/src/silx/gui/plot3d/items/image.py
index 669e97d..d4d31c6 100644
--- a/src/silx/gui/plot3d/items/image.py
+++ b/src/silx/gui/plot3d/items/image.py
@@ -66,11 +66,12 @@ class _Image(DataItem3D, InterpolationMixIn):
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))
+ planeNorm=numpy.array((0.0, 0.0, 1.0), dtype=numpy.float64),
+ planePt=numpy.array((0.0, 0.0, 0.0), dtype=numpy.float64),
+ )
if len(points) == 1: # Single intersection
- if points[0][0] < 0. or points[0][1] < 0.:
+ if points[0][0] < 0.0 or points[0][1] < 0.0:
return None # Outside image
row, column = int(points[0][1]), int(points[0][0])
data = self.getData(copy=False)
@@ -78,8 +79,9 @@ class _Image(DataItem3D, InterpolationMixIn):
if row < height and column < width:
return PickingResult(
self,
- positions=[(points[0][0], points[0][1], 0.)],
- indices=([row], [column]))
+ positions=[(points[0][0], points[0][1], 0.0)],
+ indices=([row], [column]),
+ )
else:
return None # Outside image
else: # Either no intersection or segment and image are coplanar
@@ -183,7 +185,7 @@ class _HeightMap(DataItem3D):
DataItem3D.__init__(self, parent=parent)
self.__data = numpy.zeros((0, 0), dtype=numpy.float32)
- def _pickFull(self, context, threshold=0., sort='depth'):
+ def _pickFull(self, context, threshold=0.0, sort="depth"):
"""Perform picking in this item at given widget position.
:param PickContext context: Current picking context
@@ -197,9 +199,9 @@ class _HeightMap(DataItem3D):
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ rayNdc = context.getPickingSegment(frame="ndc")
if rayNdc is None: # No picking outside viewport
return None
@@ -212,40 +214,46 @@ class _HeightMap(DataItem3D):
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)))
+ dataPoints = numpy.transpose(
+ (numpy.ravel(x), numpy.ravel(y), z, numpy.ones_like(z))
+ )
primitive = self._getScenePrimitive()
pointsNdc = primitive.objectToNDCTransform.transformPoints(
- dataPoints, perspectiveDivide=True)
+ 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(
+ threshold += 1.0 # symbol size
+ thresholdNdc = 2.0 * 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]
+ numpy.logical_and(
+ rayNdc[0, 2] <= pointsNdc[:, 2], pointsNdc[:, 2] <= rayNdc[1, 2]
+ ),
+ )
+ )[0]
- if sort == 'depth':
+ 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)
+ 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):
+ def setData(self, data, copy: bool = True):
"""Set the height field data.
:param data:
@@ -258,7 +266,7 @@ class _HeightMap(DataItem3D):
self.__data = data
self._updated(ItemChangedType.DATA)
- def getData(self, copy: bool=True) -> numpy.ndarray:
+ def getData(self, copy: bool = True) -> numpy.ndarray:
"""Get the height field 2D data.
:param bool copy:
@@ -306,23 +314,22 @@ class HeightMapData(_HeightMap, ColormapMixIn):
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.int32),
- numpy.floor(x * data.shape[1] / height).astype(numpy.int32)]
+ data = data[
+ numpy.floor(y * data.shape[0] / height).astype(numpy.int32),
+ numpy.floor(x * data.shape[1] / height).astype(numpy.int32),
+ ]
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'
+ 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):
+ def setColormappedData(self, data, copy: bool = True):
"""Set the 2D data used to compute colors.
:param data: 2D array of data
@@ -335,7 +342,7 @@ class HeightMapData(_HeightMap, ColormapMixIn):
self.__data = data
self._updated(ItemChangedType.DATA)
- def getColormappedData(self, copy: bool=True) -> numpy.ndarray:
+ def getColormappedData(self, copy: bool = True) -> numpy.ndarray:
"""Returns the 2D data used to compute colors.
:param copy:
@@ -380,8 +387,10 @@ class HeightMapRGBA(_HeightMap):
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.int32),
- numpy.floor(x * rgba.shape[1] / height).astype(numpy.int32)]
+ rgba = rgba[
+ numpy.floor(y * rgba.shape[0] / height).astype(numpy.int32),
+ numpy.floor(x * rgba.shape[1] / height).astype(numpy.int32),
+ ]
x = numpy.ravel(x)
y = numpy.ravel(y)
@@ -391,11 +400,12 @@ class HeightMapRGBA(_HeightMap):
y=y,
z=numpy.ravel(heightData),
color=rgba.reshape(-1, rgba.shape[-1]),
- size=1)
- primitive.marker = 's'
+ size=1,
+ )
+ primitive.marker = "s"
self._getScenePrimitive().children = [primitive]
- def setColorData(self, data, copy: bool=True):
+ def setColorData(self, data, copy: bool = True):
"""Set the RGB(A) image to use.
Supported array format: float32 in [0, 1], uint8.
@@ -413,7 +423,7 @@ class HeightMapRGBA(_HeightMap):
self.__rgba = data
self._updated(ItemChangedType.DATA)
- def getColorData(self, copy: bool=True) -> numpy.ndarray:
+ def getColorData(self, copy: bool = True) -> numpy.ndarray:
"""Get the RGB(A) image data.
:param copy: True (default) to get a copy,
diff --git a/src/silx/gui/plot3d/items/mesh.py b/src/silx/gui/plot3d/items/mesh.py
index dc1df3e..89056c3 100644
--- a/src/silx/gui/plot3d/items/mesh.py
+++ b/src/silx/gui/plot3d/items/mesh.py
@@ -82,7 +82,7 @@ class _MeshBase(DataItem3D):
if self._getMesh() is None:
return numpy.empty((0, 3), dtype=numpy.float32)
else:
- return self._getMesh().getAttribute('position', copy=copy)
+ return self._getMesh().getAttribute("position", copy=copy)
def getNormalData(self, copy=True):
"""Get the mesh vertex normals.
@@ -96,7 +96,7 @@ class _MeshBase(DataItem3D):
if self._getMesh() is None:
return None
else:
- return self._getMesh().getAttribute('normal', copy=copy)
+ return self._getMesh().getAttribute("normal", copy=copy)
def getIndices(self, copy=True):
"""Get the vertex indices.
@@ -143,21 +143,23 @@ class _MeshBase(DataItem3D):
positions = utils.unindexArrays(mode, vertexIndices, positions)[0]
triangles = positions.reshape(-1, 3, 3)
else:
- if mode == 'triangles':
+ if mode == "triangles":
triangles = positions.reshape(-1, 3, 3)
- elif mode == 'triangle_strip':
+ elif mode == "triangle_strip":
# Expand strip
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
+ 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':
+ elif mode == "fan":
# Expand fan
- triangles = numpy.empty((len(positions) - 2, 3, 3),
- dtype=positions.dtype)
+ triangles = numpy.empty(
+ (len(positions) - 2, 3, 3), dtype=positions.dtype
+ )
triangles[:, 0] = positions[0]
triangles[:, 1] = positions[1:-1]
triangles[:, 2] = positions[2:]
@@ -167,7 +169,8 @@ class _MeshBase(DataItem3D):
return None
trianglesIndices, t, barycentric = glu.segmentTrianglesIntersection(
- rayObject, triangles)
+ rayObject, triangles
+ )
if len(trianglesIndices) == 0:
return None
@@ -177,13 +180,13 @@ class _MeshBase(DataItem3D):
# Get vertex index from triangle index and closest point in triangle
closest = numpy.argmax(barycentric, axis=1)
- if mode == 'triangles':
+ if mode == "triangles":
indices = trianglesIndices * 3 + closest
- elif mode == 'triangle_strip':
+ elif mode == "triangle_strip":
indices = trianglesIndices + closest
- elif mode == 'fan':
+ elif mode == "fan":
indices = trianglesIndices + closest # For corners 1 and 2
indices[closest == 0] = 0 # For first corner (common)
@@ -191,10 +194,9 @@ class _MeshBase(DataItem3D):
# Convert from indices in expanded triangles to input vertices
indices = vertexIndices[indices]
- return PickingResult(self,
- positions=points,
- indices=indices,
- fetchdata=self.getPositionData)
+ return PickingResult(
+ self, positions=points, indices=indices, fetchdata=self.getPositionData
+ )
class Mesh(_MeshBase):
@@ -206,13 +208,9 @@ class Mesh(_MeshBase):
def __init__(self, parent=None):
_MeshBase.__init__(self, parent=parent)
- def setData(self,
- position,
- color,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
+ 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'
@@ -227,12 +225,13 @@ class Mesh(_MeshBase):
:param bool copy: True (default) to copy the data,
False to use as is (do not modify!).
"""
- assert mode in ('triangles', 'triangle_strip', 'fan')
+ 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)
+ position, color, normal, mode=mode, indices=indices, copy=copy
+ )
self._setMesh(mesh)
def getData(self, copy=True):
@@ -244,10 +243,12 @@ class Mesh(_MeshBase):
: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())
+ 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.
@@ -261,7 +262,7 @@ class Mesh(_MeshBase):
if self._getMesh() is None:
return numpy.empty((0, 4), dtype=numpy.float32)
else:
- return self._getMesh().getAttribute('color', copy=copy)
+ return self._getMesh().getAttribute("color", copy=copy)
class ColormapMesh(_MeshBase, ColormapMixIn):
@@ -274,13 +275,9 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
_MeshBase.__init__(self, parent=parent)
ColormapMixIn.__init__(self, function.Colormap())
- def setData(self,
- position,
- value,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
+ 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'
@@ -295,18 +292,21 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
:param bool copy: True (default) to copy the data,
False to use as is (do not modify!).
"""
- assert mode in ('triangles', 'triangle_strip', 'fan')
+ 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
+ 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)
+ copy=copy,
+ )
self._setMesh(mesh)
self._setColormappedData(self.getValueData(copy=False), copy=False)
@@ -320,10 +320,12 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
: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())
+ 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.
@@ -337,7 +339,7 @@ class ColormapMesh(_MeshBase, ColormapMixIn):
if self._getMesh() is None:
return numpy.empty((0,), dtype=numpy.float32)
else:
- return self._getMesh().getAttribute('value', copy=copy)
+ return self._getMesh().getAttribute("value", copy=copy)
class _CylindricalVolume(DataItem3D):
@@ -362,8 +364,7 @@ class _CylindricalVolume(DataItem3D):
"""
raise NotImplementedError("Must be implemented in subclass")
- def _setData(self, position, radius, height, angles, color, flatFaces,
- rotation):
+ def _setData(self, position, radius, height, angles, color, flatFaces, rotation):
"""Set volume geometry data.
:param numpy.ndarray position:
@@ -384,10 +385,8 @@ class _CylindricalVolume(DataItem3D):
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)
+ 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
@@ -404,71 +403,103 @@ class _CylindricalVolume(DataItem3D):
# \ /
# \/
# c1
- c1 = numpy.array([0, 0, -height/2])
+ 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 = 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 = 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 = 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 = 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 = 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])
+ 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
+ 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)])
+ 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))
+ 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)
+ 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)
+ color = numpy.tile(color, (1, 12 * (len(angles) - 1))).reshape(-1, 3)
self._mesh = primitives.Mesh3D(
- vertices, color, normals, mode='triangles', copy=False)
+ vertices, color, normals, mode="triangles", copy=False
+ )
self._getScenePrimitive().children.append(self._mesh)
self._updated(ItemChangedType.DATA)
@@ -488,11 +519,10 @@ class _CylindricalVolume(DataItem3D):
return None
rayObject = rayObject[:, :3]
- positions = self._mesh.getAttribute('position', copy=False)
+ positions = self._mesh.getAttribute("position", copy=False)
triangles = positions.reshape(-1, 3, 3) # 'triangle' draw mode
- trianglesIndices, t = glu.segmentTrianglesIntersection(
- rayObject, triangles)[:2]
+ trianglesIndices, t = glu.segmentTrianglesIntersection(rayObject, triangles)[:2]
if len(trianglesIndices) == 0:
return None
@@ -511,10 +541,9 @@ class _CylindricalVolume(DataItem3D):
points = t.reshape(-1, 1) * (rayObject[1] - rayObject[0]) + rayObject[0]
- return PickingResult(self,
- positions=points,
- indices=indices,
- fetchdata=self.getPosition)
+ return PickingResult(
+ self, positions=points, indices=indices, fetchdata=self.getPosition
+ )
class Box(_CylindricalVolume):
@@ -533,8 +562,13 @@ class Box(_CylindricalVolume):
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))):
+ def setData(
+ self,
+ size=(1, 1, 1),
+ color=(1, 1, 1),
+ position=(0, 0, 0),
+ rotation=(0, (0, 0, 0)),
+ ):
"""
Set Box geometry data.
@@ -550,28 +584,28 @@ class Box(_CylindricalVolume):
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])
+ 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))
+ assert numpy.ndim(self.color) == 1 or len(self.color) == len(self.position)
- diagonal = numpy.sqrt(self.size[0]**2 + self.size[1]**2)
+ 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])
+ 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)
+ 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).
@@ -622,8 +656,15 @@ class Cylinder(_CylindricalVolume):
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))):
+ 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
@@ -644,20 +685,22 @@ class Cylinder(_CylindricalVolume):
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)
+ 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).
@@ -716,8 +759,14 @@ class Hexagon(_CylindricalVolume):
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))):
+ 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
@@ -735,20 +784,22 @@ class Hexagon(_CylindricalVolume):
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)
+ 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).
@@ -758,7 +809,7 @@ class Hexagon(_CylindricalVolume):
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):
diff --git a/src/silx/gui/plot3d/items/mixins.py b/src/silx/gui/plot3d/items/mixins.py
index 45b569d..c69c3ac 100644
--- a/src/silx/gui/plot3d/items/mixins.py
+++ b/src/silx/gui/plot3d/items/mixins.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -29,11 +29,8 @@ __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
@@ -53,24 +50,21 @@ class InterpolationMixIn(ItemMixInBase):
This object MUST have an interpolation property that is updated.
"""
- NEAREST_INTERPOLATION = 'nearest'
+ NEAREST_INTERPOLATION = "nearest"
"""Nearest interpolation mode (see :meth:`setInterpolation`)"""
- LINEAR_INTERPOLATION = 'linear'
+ 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
+ def __init__(self):
+ self.__primitive = None
+ self.__interpolationMode = self.NEAREST_INTERPOLATION
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()
@@ -151,24 +145,28 @@ class ComplexMixIn(_ComplexMixIn):
_ComplexMixIn.ComplexMode.IMAGINARY,
_ComplexMixIn.ComplexMode.ABSOLUTE,
_ComplexMixIn.ComplexMode.PHASE,
- _ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
+ _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')))
+ _SUPPORTED_SYMBOLS = dict(
+ (
+ ("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.
@@ -177,11 +175,11 @@ class SymbolMixIn(_SymbolMixIn):
"""
symbol = self.getSymbol()
size = self.getSymbolSize()
- if symbol == ',': # pixel
- return 's', 1.
- elif symbol == '.': # point
+ if symbol == ",": # pixel
+ return "s", 1.0
+ elif symbol == ".": # point
# Size as in plot OpenGL backend, mimic matplotlib
- return 'o', numpy.ceil(0.5 * size) + 1.
+ return "o", numpy.ceil(0.5 * size) + 1.0
else:
return symbol, size
@@ -189,18 +187,24 @@ class SymbolMixIn(_SymbolMixIn):
class PlaneMixIn(ItemMixInBase):
"""Mix-in class for plane items (based on PlaneInGroup primitive)"""
- def __init__(self, plane):
+ def __init__(self):
+ self.__plane = None
+ self._setPlane(primitives.PlaneInGroup())
+
+ def _setPlane(self, plane: primitives.PlaneInGroup):
+ """Set plane primitive"""
+ if self.__plane is not None:
+ self.__plane.removeListener(self._planeChanged)
+ self.__plane.plane.removeListener(self._planePositionChanged)
+
assert isinstance(plane, primitives.PlaneInGroup)
self.__plane = plane
- self.__plane.alpha = 1.
+ self.__plane.alpha = 1.0
self.__plane.addListener(self._planeChanged)
self.__plane.plane.addListener(self._planePositionChanged)
- def _getPlane(self):
- """Returns plane primitive
-
- :rtype: primitives.PlaneInGroup
- """
+ def _getPlane(self) -> primitives.PlaneInGroup:
+ """Returns plane primitive"""
return self.__plane
def _planeChanged(self, source, *args, **kwargs):
@@ -211,7 +215,9 @@ class PlaneMixIn(ItemMixInBase):
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
+ if (
+ self.__plane.visible
+ ): # TODO send even if hidden? or send also when showing if moved while hidden
self._updated(ItemChangedType.POSITION)
# Plane position
@@ -283,5 +289,5 @@ class PlaneMixIn(ItemMixInBase):
:param color: RGBA color as 4 floats in [0, 1]
"""
self.__plane.color = rgba(color)
- if hasattr(super(PlaneMixIn, self), '_setForegroundColor'):
+ if hasattr(super(PlaneMixIn, self), "_setForegroundColor"):
super(PlaneMixIn, self)._setForegroundColor(color)
diff --git a/src/silx/gui/plot3d/items/scatter.py b/src/silx/gui/plot3d/items/scatter.py
index c93db88..b8f2f39 100644
--- a/src/silx/gui/plot3d/items/scatter.py
+++ b/src/silx/gui/plot3d/items/scatter.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2023 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
@@ -28,16 +28,13 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import logging
+import sys
import numpy
+from matplotlib.tri import Triangulation
-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
@@ -65,7 +62,8 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
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)
+ x=noData, y=noData, z=noData, value=noData, size=size
+ )
self._scatter.marker = symbol
self._getScenePrimitive().children.append(self._scatter)
@@ -77,7 +75,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
if event in (ItemChangedType.SYMBOL, ItemChangedType.SYMBOL_SIZE):
symbol, size = self._getSceneSymbol()
self._scatter.marker = symbol
- self._scatter.setAttribute('size', size, copy=True)
+ self._scatter.setAttribute("size", size, copy=True)
super(Scatter3D, self)._updated(event)
@@ -92,10 +90,10 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
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._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)
@@ -107,10 +105,12 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
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))
+ return (
+ self.getXData(copy),
+ self.getYData(copy),
+ self.getZData(copy),
+ self.getValueData(copy),
+ )
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -120,7 +120,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: X coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('x', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("x", copy=copy).reshape(-1)
def getYData(self, copy=True):
"""Returns Y data coordinates.
@@ -130,7 +130,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Y coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('y', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("y", copy=copy).reshape(-1)
def getZData(self, copy=True):
"""Returns Z data coordinates.
@@ -140,7 +140,7 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Z coordinates
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('z', copy=copy).reshape(-1)
+ return self._scatter.getAttribute("z", copy=copy).reshape(-1)
def getValueData(self, copy=True):
"""Returns data values.
@@ -150,14 +150,9 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: data values
:rtype: numpy.ndarray
"""
- return self._scatter.getAttribute('value', copy=copy).reshape(-1)
+ 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'):
+ def _pickFull(self, context, threshold=0.0, sort="depth"):
"""Perform picking in this item at given widget position.
:param PickContext context: Current picking context
@@ -171,9 +166,9 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ rayNdc = context.getPickingSegment(frame="ndc")
if rayNdc is None: # No picking outside viewport
return None
@@ -184,49 +179,57 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
primitive = self._getScenePrimitive()
- dataPoints = numpy.transpose((xData,
- self.getYData(copy=False),
- self.getZData(copy=False),
- numpy.ones_like(xData)))
+ dataPoints = numpy.transpose(
+ (
+ xData,
+ self.getYData(copy=False),
+ self.getZData(copy=False),
+ numpy.ones_like(xData),
+ )
+ )
pointsNdc = primitive.objectToNDCTransform.transformPoints(
- dataPoints, perspectiveDivide=True)
+ 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(
+ thresholdNdc = 2.0 * 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]
+ numpy.logical_and(
+ rayNdc[0, 2] <= pointsNdc[:, 2], pointsNdc[:, 2] <= rayNdc[1, 2]
+ ),
+ )
+ )[0]
- if sort == 'depth':
+ 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)
+ return PickingResult(
+ self,
+ positions=dataPoints[picked, :3],
+ indices=picked,
+ fetchdata=self.getValueData,
+ )
else:
return None
-class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
- ScatterVisualizationMixIn):
+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.POINTS: ("symbol", "symbolSize"),
+ ScatterVisualizationMixIn.Visualization.LINES: ("lineWidth",),
ScatterVisualizationMixIn.Visualization.SOLID: (),
}
"""Dict {visualization mode: property names used in this mode}"""
@@ -241,7 +244,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
ScatterVisualizationMixIn.__init__(self)
self._heightMap = False
- self._lineWidth = 1.
+ self._lineWidth = 1.0
self._x = numpy.zeros((0,), dtype=numpy.float32)
self._y = numpy.zeros((0,), dtype=numpy.float32)
@@ -260,7 +263,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
for child in self._getScenePrimitive().children:
if isinstance(child, primitives.Points):
child.marker = symbol
- child.setAttribute('size', size, copy=True)
+ child.setAttribute("size", size, copy=True)
elif event is ItemChangedType.VISIBLE:
# TODO smart update?, need dirty flags
@@ -281,7 +284,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
By default, it is the current visualization mode.
:return:
"""
- assert name in ('lineWidth', 'symbol', 'symbolSize')
+ assert name in ("lineWidth", "symbol", "symbolSize")
if visualization is None:
visualization = self.getVisualization()
assert visualization in self.supportedVisualizations()
@@ -322,11 +325,11 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
:param float width: Width in pixels
"""
width = float(width)
- assert width >= 1.
+ assert width >= 1.0
if width != self._lineWidth:
self._lineWidth = width
for child in self._getScenePrimitive().children:
- if hasattr(child, 'lineWidth'):
+ if hasattr(child, "lineWidth"):
child.lineWidth = width
self._updated(ItemChangedType.LINE_WIDTH)
@@ -342,15 +345,14 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
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)
+ 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)
+ 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)
@@ -376,9 +378,11 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
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))
+ return (
+ self.getXData(copy=copy),
+ self.getYData(copy=copy),
+ self.getValueData(copy=copy),
+ )
def getXData(self, copy=True):
"""Returns X data coordinates.
@@ -410,12 +414,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
"""
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'):
+ def _pickPoints(self, context, points, threshold=1.0, sort="depth"):
"""Perform picking while in 'points' visualization mode
:param PickContext context: Current picking context
@@ -429,34 +428,41 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
:return: Object holding the results or None
:rtype: Union[None,PickingResult]
"""
- assert sort in ('index', 'depth')
+ assert sort in ("index", "depth")
- rayNdc = context.getPickingSegment(frame='ndc')
+ 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)
+ 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]
+ 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':
+ 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)
+ return PickingResult(
+ self,
+ positions=points[picked, :3],
+ indices=picked,
+ fetchdata=self.getValueData,
+ )
else:
return None
@@ -477,7 +483,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
trianglesIndices = self._cachedTrianglesIndices.reshape(-1, 3)
triangles = points[trianglesIndices, :3]
selectedIndices, t, barycentric = glu.segmentTrianglesIntersection(
- rayObject, triangles)
+ rayObject, triangles
+ )
closest = numpy.argmax(barycentric, axis=1)
indices = trianglesIndices.reshape(-1, 3)[selectedIndices, closest]
@@ -488,10 +495,9 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
# 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)
+ return PickingResult(
+ self, positions=positions, indices=indices, fetchdata=self.getValueData
+ )
def _pickFull(self, context):
"""Perform picking in this item at given widget position.
@@ -509,22 +515,20 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
else:
zData = numpy.zeros_like(xData)
- points = numpy.transpose((xData,
- self.getYData(copy=False),
- zData,
- numpy.ones_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))
+ return self._pickPoints(context, points, threshold=max(3.0, threshold))
elif mode is self.Visualization.LINES:
# Picking only at point
- return self._pickPoints(context, points, threshold=5.)
+ return self._pickPoints(context, points, threshold=5.0)
else: # mode == 'solid'
return self._pickSolid(context, points)
@@ -543,36 +547,38 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
heightMap = self.isHeightMap()
if mode is self.Visualization.POINTS:
- z = value if heightMap else 0.
+ z = value if heightMap else 0.0
symbol, size = self._getSceneSymbol()
primitive = primitives.Points(
- x=x, y=y, z=z, value=value,
- size=size,
- colormap=self._getSceneColormap())
+ 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:
+ try:
+ triangulation = Triangulation(x, y)
+ except (RuntimeError, ValueError):
+ _logger.debug("Delaunay tesselation failed: %s", sys.exc_info()[1])
return None
self._cachedTrianglesIndices = numpy.ravel(
- triangulation.simplices.astype(numpy.uint32))
+ triangulation.triangles.astype(numpy.uint32)
+ )
- if (mode is self.Visualization.LINES and
- self._cachedLinesIndices is None):
+ if mode is self.Visualization.LINES and self._cachedLinesIndices is None:
# Compute line indices
self._cachedLinesIndices = utils.triangleToLineIndices(
- self._cachedTrianglesIndices, unicity=True)
+ self._cachedTrianglesIndices, unicity=True
+ )
if mode is self.Visualization.LINES:
indices = self._cachedLinesIndices
- renderMode = 'lines'
+ renderMode = "lines"
else:
indices = self._cachedTrianglesIndices
- renderMode = 'triangles'
+ renderMode = "triangles"
# TODO supports x, y instead of copy
if heightMap:
@@ -590,14 +596,15 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
if len(value) > 1:
value = value[indices]
triangleNormals = utils.trianglesNormal(coordinates)
- normal = numpy.empty((len(triangleNormals) * 3, 3),
- dtype=numpy.float32)
+ 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.)
+ normal = (0.0, 0.0, 1.0)
else:
normal = None
@@ -607,7 +614,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
normal=normal,
colormap=self._getSceneColormap(),
indices=indices,
- mode=renderMode)
+ mode=renderMode,
+ )
primitive.lineWidth = self.getLineWidth()
primitive.lineSmooth = False
diff --git a/src/silx/gui/plot3d/items/volume.py b/src/silx/gui/plot3d/items/volume.py
index b3007fa..7696794 100644
--- a/src/silx/gui/plot3d/items/volume.py
+++ b/src/silx/gui/plot3d/items/volume.py
@@ -58,12 +58,13 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
"""
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)
+ PlaneMixIn.__init__(self)
+
+ plane = cutplane.CutPlane(normal=(0, 1, 0))
+ self._setPlane(plane)
self._dataRange = None
self._data = None
@@ -92,10 +93,13 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
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._setColormappedData(
+ self._data,
+ copy=False,
+ min_=range_[0],
+ minPositive=range_[1],
+ max_=range_[2],
+ )
self._updated(ItemChangedType.DATA)
@@ -184,10 +188,11 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
rayObject[0, :3],
rayObject[1, :3],
planeNorm=self.getNormal(),
- planePt=self.getPoint())
+ planePt=self.getPoint(),
+ )
if len(points) == 1: # Single intersection
- if numpy.any(points[0] < 0.):
+ if numpy.any(points[0] < 0.0):
return None # Outside volume
z, y, x = int(points[0][2]), int(points[0][1]), int(points[0][0])
@@ -197,9 +202,9 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
depth, height, width = data.shape
if z < depth and y < height and x < width:
- return PickingResult(self,
- positions=[points[0]],
- indices=([z], [y], [x]))
+ 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
@@ -215,9 +220,9 @@ class Isosurface(Item3D):
def __init__(self, parent):
Item3D.__init__(self, parent=None)
self._data = None
- self._level = float('nan')
+ self._level = float("nan")
self._autoLevelFunction = None
- self._color = rgba('#FFD700FF')
+ self._color = rgba("#FFD700FF")
self.setParent(parent)
def _syncDataWithParent(self):
@@ -310,7 +315,7 @@ class Isosurface(Item3D):
"""
primitive = self._getScenePrimitive()
if len(primitive.children) != 0:
- primitive.children[0].setAttribute('color', color)
+ primitive.children[0].setAttribute("color", color)
def setColor(self, color):
"""Set the color of the iso-surface
@@ -334,7 +339,7 @@ class Isosurface(Item3D):
if data is None:
if self.isAutoLevel():
- self._level = float('nan')
+ self._level = float("nan")
else:
if self.isAutoLevel():
@@ -349,12 +354,12 @@ class Isosurface(Item3D):
"Error while executing iso level function %s.%s",
module_,
name,
- exc_info=True)
- level = float('nan')
+ exc_info=True,
+ )
+ level = float("nan")
else:
- _logger.info(
- 'Computed iso-level in %f s.', time.time() - st)
+ _logger.info("Computed iso-level in %f s.", time.time() - st)
if level != self._level:
self._level = level
@@ -362,10 +367,8 @@ class Isosurface(Item3D):
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)
+ 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
@@ -378,12 +381,14 @@ class Isosurface(Item3D):
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)
+ mesh = primitives.Mesh3D(
+ vertices,
+ colors=self._color,
+ normals=normals,
+ mode="triangles",
+ indices=indices,
+ copy=False,
+ )
self._getScenePrimitive().children = [mesh]
def _pickFull(self, context):
@@ -399,8 +404,7 @@ class Isosurface(Item3D):
rayObject = rayObject[:, :3]
data = self.getData(copy=False)
- bins = utils.segmentVolumeIntersect(
- rayObject, numpy.array(data.shape) - 1)
+ bins = utils.segmentVolumeIntersect(rayObject, numpy.array(data.shape) - 1)
if bins is None:
return None
@@ -413,8 +417,10 @@ class Isosurface(Item3D):
# check bin candidates
level = self.getLevel()
- mask = numpy.logical_and(numpy.nanmin(binsData, axis=1) <= level,
- level <= numpy.nanmax(binsData, axis=1))
+ mask = numpy.logical_and(
+ numpy.nanmin(binsData, axis=1) <= level,
+ level <= numpy.nanmax(binsData, axis=1),
+ )
bins = bins[mask]
binsData = binsData[mask]
@@ -476,19 +482,23 @@ class ScalarField3D(BaseNodeItem):
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.))),
+ transform.Matrix(
+ (
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 1.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ )
+ ),
# Offset to match cutting plane coords
- transform.Translate(0.5, 0.5, 0.5)
+ transform.Translate(0.5, 0.5, 0.5),
]
self._getScenePrimitive().children = [
self._boundedGroup,
self._cutPlane._getScenePrimitive(),
- self._isogroup]
+ self._isogroup,
+ ]
@staticmethod
def _computeRangeFromData(data):
@@ -507,7 +517,7 @@ class ScalarField3D(BaseNodeItem):
if dataRange is not None:
min_positive = dataRange.min_positive
if min_positive is None:
- min_positive = float('nan')
+ min_positive = float("nan")
return dataRange.minimum, min_positive, dataRange.maximum
def setData(self, data, copy=True):
@@ -526,7 +536,7 @@ class ScalarField3D(BaseNodeItem):
self._boundedGroup.shape = None
else:
- data = numpy.array(data, copy=copy, dtype=numpy.float32, order='C')
+ data = numpy.array(data, copy=copy, dtype=numpy.float32, order="C")
assert data.ndim == 3
assert min(data.shape) >= 2
@@ -625,8 +635,8 @@ class ScalarField3D(BaseNodeItem):
"""
if isosurface not in self.getIsosurfaces():
_logger.warning(
- "Try to remove isosurface that is not in the list: %s",
- str(isosurface))
+ "Try to remove isosurface that is not in the list: %s", str(isosurface)
+ )
else:
isosurface.sigItemChanged.disconnect(self._isosurfaceItemChanged)
self._isosurfaces.remove(isosurface)
@@ -646,8 +656,9 @@ class ScalarField3D(BaseNodeItem):
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())
+ sortedIso = sorted(
+ self.getIsosurfaces(), key=lambda isosurface: -isosurface.getLevel()
+ )
self._isogroup.children = [iso._getScenePrimitive() for iso in sortedIso]
# BaseNodeItem
@@ -664,6 +675,7 @@ class ScalarField3D(BaseNodeItem):
# ComplexField3D #
##################
+
class ComplexCutPlane(CutPlane, ComplexMixIn):
"""Class representing a cutting plane in a :class:`ComplexField3D` item.
@@ -701,8 +713,9 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
:param parent: The DataItem3D this iso-surface belongs to
"""
- _SUPPORTED_COMPLEX_MODES = \
- (ComplexMixIn.ComplexMode.NONE,) + ComplexMixIn._SUPPORTED_COMPLEX_MODES
+ _SUPPORTED_COMPLEX_MODES = (
+ ComplexMixIn.ComplexMode.NONE,
+ ) + ComplexMixIn._SUPPORTED_COMPLEX_MODES
"""Overrides supported ComplexMode"""
def __init__(self, parent):
@@ -717,8 +730,9 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
:param List[float] color: RGBA channels in [0, 1]
"""
primitive = self._getScenePrimitive()
- if (len(primitive.children) != 0 and
- isinstance(primitive.children[0], primitives.ColormapMesh3D)):
+ 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)
@@ -729,15 +743,14 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
if parent is None:
self._data = None
else:
- self._data = parent.getData(
- mode=parent.getComplexMode(), copy=False)
+ 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)
+ parent.getData(mode=self.getComplexMode(), copy=False), copy=False
+ )
self._updateScenePrimitive()
@@ -755,8 +768,7 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
if event == ItemChangedType.COMPLEX_MODE:
self._syncDataWithParent()
- elif event in (ItemChangedType.COLORMAP,
- Item3DChangedType.INTERPOLATION):
+ elif event in (ItemChangedType.COLORMAP, Item3DChangedType.INTERPOLATION):
self._updateScenePrimitive()
super(ComplexIsosurface, self)._updated(event)
@@ -772,7 +784,7 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
if values is not None:
vertices, normals, indices = self._computeIsosurface()
if vertices is not None:
- values = interp3d(values, vertices, method='linear_omp')
+ values = interp3d(values, vertices, method="linear_omp")
# TODO reuse isosurface when only color changes...
mesh = primitives.ColormapMesh3D(
@@ -780,9 +792,10 @@ class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
value=values.reshape(-1, 1),
colormap=self._getSceneColormap(),
normal=normals,
- mode='triangles',
+ mode="triangles",
indices=indices,
- copy=False)
+ copy=False,
+ )
mesh.alpha = self._color[3]
self._getScenePrimitive().children = [mesh]
@@ -826,7 +839,7 @@ class ComplexField3D(ScalarField3D, ComplexMixIn):
self._boundedGroup.shape = None
else:
- data = numpy.array(data, copy=copy, dtype=numpy.complex64, order='C')
+ data = numpy.array(data, copy=copy, dtype=numpy.complex64, order="C")
assert data.ndim == 3
assert min(data.shape) >= 2