diff options
Diffstat (limited to 'src/silx/gui/plot3d/items')
-rw-r--r-- | src/silx/gui/plot3d/items/__init__.py | 9 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/_pick.py | 50 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/clipplane.py | 28 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/core.py | 124 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/image.py | 92 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/mesh.py | 367 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/mixins.py | 80 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/scatter.py | 224 | ||||
-rw-r--r-- | src/silx/gui/plot3d/items/volume.py | 135 |
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 |