summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/ScalarFieldView.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/ScalarFieldView.py')
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py430
1 files changed, 293 insertions, 137 deletions
diff --git a/silx/gui/plot3d/ScalarFieldView.py b/silx/gui/plot3d/ScalarFieldView.py
index 6a4d9d4..a41999b 100644
--- a/silx/gui/plot3d/ScalarFieldView.py
+++ b/silx/gui/plot3d/ScalarFieldView.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -56,48 +56,6 @@ from .tools import InteractiveModeToolBar
_logger = logging.getLogger(__name__)
-class _BoundedGroup(scene.Group):
- """Group with data bounds"""
-
- _shape = None # To provide a default value without overriding __init__
-
- @property
- def shape(self):
- """Data shape (depth, height, width) of this group or None"""
- return self._shape
-
- @shape.setter
- def shape(self, shape):
- if shape is None:
- self._shape = None
- else:
- depth, height, width = shape
- self._shape = float(depth), float(height), float(width)
-
- @property
- def size(self):
- """Data size (width, height, depth) of this group or None"""
- shape = self.shape
- if shape is None:
- return None
- else:
- return shape[2], shape[1], shape[0]
-
- @size.setter
- def size(self, size):
- if size is None:
- self.shape = None
- else:
- self.shape = size[2], size[1], size[0]
-
- def _bounds(self, dataBounds=False):
- if dataBounds and self.size is not None:
- return numpy.array(((0., 0., 0.), self.size),
- dtype=numpy.float32)
- else:
- return super(_BoundedGroup, self)._bounds(dataBounds)
-
-
class Isosurface(qt.QObject):
"""Class representing an iso-surface
@@ -272,24 +230,26 @@ class SelectedRegion(object):
:param arrayRange: Range of the selection in the array
((zmin, zmax), (ymin, ymax), (xmin, xmax))
+ :param dataBBox: Bounding box of the selection in data coordinates
+ ((xmin, xmax), (ymin, ymax), (zmin, zmax))
:param translation: Offset from array to data coordinates (ox, oy, oz)
:param scale: Scale from array to data coordinates (sx, sy, sz)
"""
- def __init__(self, arrayRange,
+ def __init__(self, arrayRange, dataBBox,
translation=(0., 0., 0.),
scale=(1., 1., 1.)):
self._arrayRange = numpy.array(arrayRange, copy=True, dtype=numpy.int)
assert self._arrayRange.shape == (3, 2)
assert numpy.all(self._arrayRange[:, 1] >= self._arrayRange[:, 0])
+
+ self._dataRange = dataBBox
+
self._translation = numpy.array(translation, dtype=numpy.float32)
assert self._translation.shape == (3,)
self._scale = numpy.array(scale, dtype=numpy.float32)
assert self._scale.shape == (3,)
- self._dataRange = (self._translation.reshape(3, -1) +
- self._arrayRange[::-1] * self._scale.reshape(3, -1))
-
def getArrayRange(self):
"""Returns array ranges of the selection: 3x2 array of int
@@ -311,6 +271,10 @@ class SelectedRegion(object):
def getDataRange(self):
"""Range in the data coordinates of the selection: 3x2 array of float
+ When the transform matrix is not the identity matrix
+ (e.g., rotation, skew) the returned range is the one of the selected
+ region bounding box in data coordinates.
+
:return: A numpy array with ((xmin, xmax), (ymin, ymax), (zmin, zmax))
:rtype: numpy.ndarray
"""
@@ -336,7 +300,8 @@ class SelectedRegion(object):
class CutPlane(qt.QObject):
"""Class representing a cutting plane
- :param ScalarFieldView sfView: Widget in which the cut plane is applied.
+ :param ~silx.gui.plot3d.ScalarFieldView.ScalarFieldView sfView:
+ Widget in which the cut plane is applied.
"""
sigVisibilityChanged = qt.Signal(bool)
@@ -357,6 +322,12 @@ class CutPlane(qt.QObject):
This signal provides the new colormap.
"""
+ sigTransparencyChanged = qt.Signal()
+ """Signal emitted when the transparency of the plane has changed.
+
+ This signal is emitted when calling :meth:`setDisplayValuesBelowMin`.
+ """
+
sigInterpolationChanged = qt.Signal(str)
"""Signal emitted when the cut plane interpolation has changed
@@ -367,12 +338,22 @@ class CutPlane(qt.QObject):
super(CutPlane, self).__init__(parent=sfView)
self._dataRange = None
+ self._visible = False
+
+ self.__syncPlane = True
- self._plane = cutplane.CutPlane(normal=(0, 1, 0))
- self._plane.alpha = 1.
- self._plane.visible = self._visible = False
- self._plane.addListener(self._planeChanged)
- self._plane.plane.addListener(self._planePositionChanged)
+ # Plane stroke on the outer bounding box
+ self._planeStroke = primitives.PlaneInGroup(normal=(0, 1, 0))
+ self._planeStroke.visible = self._visible
+ self._planeStroke.addListener(self._planeChanged)
+ self._planeStroke.plane.addListener(self._planePositionChanged)
+
+ # Plane with texture on the data bounding box
+ self._dataPlane = cutplane.CutPlane(normal=(0, 1, 0))
+ self._dataPlane.strokeVisible = False
+ self._dataPlane.alpha = 1.
+ self._dataPlane.visible = self._visible
+ self._dataPlane.plane.addListener(self._planePositionChanged)
self._colormap = Colormap(
name='gray', normalization='linear', vmin=None, vmax=None)
@@ -380,14 +361,40 @@ class CutPlane(qt.QObject):
self._updateSceneColormap()
sfView.sigDataChanged.connect(self._sfViewDataChanged)
+ sfView.sigTransformChanged.connect(self._sfViewTransformChanged)
+
+ def _get3DPrimitives(self):
+ """Return the cut plane scene node."""
+ return self._planeStroke, self._dataPlane
+
+ def _keepPlaneInBBox(self):
+ """Makes sure the plane intersect its parent bounding box if any"""
+ bounds = self._planeStroke.parent.bounds(dataBounds=True)
+ if bounds is not None:
+ self._planeStroke.plane.point = numpy.clip(
+ self._planeStroke.plane.point,
+ a_min=bounds[0], a_max=bounds[1])
+
+ @staticmethod
+ def _syncPlanes(master, slave):
+ """Move slave PlaneInGroup so that it is coplanar with master.
+
+ :param PlaneInGroup master: Reference PlaneInGroup
+ :param PlaneInGroup slave: PlaneInGroup to align
+ """
+ masterToSlave = transform.StaticTransformList([
+ slave.objectToSceneTransform.inverse(),
+ master.objectToSceneTransform])
- def _get3DPrimitive(self):
- """Return the cut plane scene node"""
- return self._plane
+ point = masterToSlave.transformPoint(
+ master.plane.point)
+ normal = masterToSlave.transformNormal(
+ master.plane.normal)
+ slave.plane.setPlane(point, normal)
def _sfViewDataChanged(self):
"""Handle data change in the ScalarFieldView this plane belongs to"""
- self._plane.setData(self.sender().getData(), copy=False)
+ self._dataPlane.setData(self.sender().getData(), copy=False)
# Store data range info as 3-tuple of values
self._dataRange = self.sender().getDataRange()
@@ -398,6 +405,15 @@ class CutPlane(qt.QObject):
if self.getColormap().isAutoscale():
self._updateSceneColormap()
+ self._keepPlaneInBBox()
+
+ def _sfViewTransformChanged(self):
+ """Handle transform changed in the ScalarFieldView"""
+ self._keepPlaneInBBox()
+ self._syncPlanes(master=self._planeStroke,
+ slave=self._dataPlane)
+ self.sigPlaneChanged.emit()
+
def _planeChanged(self, source, *args, **kwargs):
"""Handle events from the plane primitive"""
# Using _visible for now, until scene as more info in events
@@ -407,68 +423,144 @@ class CutPlane(qt.QObject):
def _planePositionChanged(self, source, *args, **kwargs):
"""Handle update of cut plane position and normal"""
- if self._plane.visible:
- self.sigPlaneChanged.emit()
+ if self.__syncPlane:
+ self.__syncPlane = False
+ if source is self._planeStroke.plane:
+ self._syncPlanes(master=self._planeStroke,
+ slave=self._dataPlane)
+ elif source is self._dataPlane.plane:
+ self._syncPlanes(master=self._dataPlane,
+ slave=self._planeStroke)
+ else:
+ _logger.error('Received an unknown object %s',
+ str(source))
+
+ if self._planeStroke.visible or self._dataPlane.visible:
+ self.sigPlaneChanged.emit()
+
+ self.__syncPlane = True
# Plane position
def moveToCenter(self):
"""Move cut plane to center of data set"""
- self._plane.moveToCenter()
+ self._planeStroke.moveToCenter()
def isValid(self):
"""Returns whether the cut plane is defined or not (bool)"""
- return self._plane.isValid
+ return self._planeStroke.isValid
+
+ def _plane(self, coordinates='array'):
+ """Returns the scene plane to set.
+
+ :param str coordinates: The coordinate system to use:
+ Either 'scene' or 'array' (default)
+ :rtype: Plane
+ :raise ValueError: If coordinates is not correct
+ """
+ if coordinates == 'scene':
+ return self._planeStroke.plane
+ elif coordinates == 'array':
+ return self._dataPlane.plane
+ else:
+ raise ValueError(
+ 'Unsupported coordinates: %s' % str(coordinates))
- def getNormal(self):
+ def getNormal(self, coordinates='array'):
"""Returns the normal of the plane (as a unit vector)
+ :param str coordinates: The coordinate system to use:
+ Either 'scene' or 'array' (default)
:return: Normal (nx, ny, nz), vector is 0 if no plane is defined
:rtype: numpy.ndarray
+ :raise ValueError: If coordinates is not correct
"""
- return self._plane.plane.normal
+ return self._plane(coordinates).normal
- def setNormal(self, normal):
- """Set the normal of the plane
+ def setNormal(self, normal, coordinates='array'):
+ """Set the normal of the plane.
:param normal: 3-tuple of float: nx, ny, nz
+ :param str coordinates: The coordinate system to use:
+ Either 'scene' or 'array' (default)
+ :raise ValueError: If coordinates is not correct
"""
- self._plane.plane.normal = normal
+ self._plane(coordinates).normal = normal
- def getPoint(self):
- """Returns a point on the plane
+ def getPoint(self, coordinates='array'):
+ """Returns a point on the plane.
+ :param str coordinates: The coordinate system to use:
+ Either 'scene' or 'array' (default)
:return: (x, y, z)
:rtype: numpy.ndarray
+ :raise ValueError: If coordinates is not correct
+ """
+ return self._plane(coordinates).point
+
+ def setPoint(self, point, constraint=True, coordinates='array'):
+ """Set a point contained in the plane.
+
+ Warning: The plane might not intersect the bounding box of the data.
+
+ :param point: (x, y, z) position
+ :type point: 3-tuple of float
+ :param bool constraint:
+ True (default) to make sure the plane intersect data bounding box,
+ False to set the plane without any constraint.
+ :raise ValueError: If coordinates is not correc
"""
- return self._plane.plane.point
+ self._plane(coordinates).point = point
+ if constraint:
+ self._keepPlaneInBBox()
- def getParameters(self):
+ def getParameters(self, coordinates='array'):
"""Returns the plane equation parameters: a*x + b*y + c*z + d = 0
+ :param str coordinates: The coordinate system to use:
+ Either 'scene' or 'array' (default)
:return: Plane equation parameters: (a, b, c, d)
:rtype: numpy.ndarray
+ :raise ValueError: If coordinates is not correct
+ """
+ return self._plane(coordinates).parameters
+
+ def setParameters(self, parameters, constraint=True, coordinates='array'):
+ """Set the plane equation parameters: a*x + b*y + c*z + d = 0
+
+ Warning: The plane might not intersect the bounding box of the data.
+
+ :param parameters: (a, b, c, d) plane equation parameters.
+ :type parameters: 4-tuple of float
+ :param bool constraint:
+ True (default) to make sure the plane intersect data bounding box,
+ False to set the plane without any constraint.
+ :raise ValueError: If coordinates is not correc
"""
- return self._plane.plane.parameters
+ self._plane(coordinates).parameters = parameters
+ if constraint:
+ self._keepPlaneInBBox()
# Visibility
def isVisible(self):
"""Returns True if the plane is visible, False otherwise"""
- return self._plane.visible
+ return self._planeStroke.visible
def setVisible(self, visible):
"""Set the visibility of the plane
:param bool visible: True to make plane visible
"""
- self._plane.visible = visible
+ visible = bool(visible)
+ self._planeStroke.visible = visible
+ self._dataPlane.visible = visible
# Border stroke
def getStrokeColor(self):
"""Returns the color of the plane border (QColor)"""
- return qt.QColor.fromRgbF(*self._plane.color)
+ return qt.QColor.fromRgbF(*self._planeStroke.color)
def setStrokeColor(self, color):
"""Set the color of the plane border.
@@ -477,7 +569,9 @@ class CutPlane(qt.QObject):
:type color:
QColor, str or array-like of 3 or 4 float in [0., 1.] or uint8
"""
- self._plane.color = rgba(color)
+ color = rgba(color)
+ self._planeStroke.color = color
+ self._dataPlane.color = color
# Data
@@ -501,7 +595,7 @@ class CutPlane(qt.QObject):
:return: 'nearest' or 'linear'
:rtype: str
"""
- return self._plane.interpolation
+ return self._dataPlane.interpolation
def setInterpolation(self, interpolation):
"""Set the interpolation used to display to cut plane
@@ -511,7 +605,7 @@ class CutPlane(qt.QObject):
:param str interpolation: 'nearest' or 'linear'
"""
if interpolation != self.getInterpolation():
- self._plane.interpolation = interpolation
+ self._dataPlane.interpolation = interpolation
self.sigInterpolationChanged.emit(interpolation)
# Colormap
@@ -527,11 +621,29 @@ class CutPlane(qt.QObject):
# """
# self._plane.alpha = alpha
+ def getDisplayValuesBelowMin(self):
+ """Return whether values <= colormap min are displayed or not.
+
+ :rtype: bool
+ """
+ return self._dataPlane.colormap.displayValuesBelowMin
+
+ def setDisplayValuesBelowMin(self, display):
+ """Set whether to display values <= colormap min.
+
+ :param bool display: True to show values below min,
+ False to discard them
+ """
+ display = bool(display)
+ if display != self.getDisplayValuesBelowMin():
+ self._dataPlane.colormap.displayValuesBelowMin = display
+ self.sigTransparencyChanged.emit()
+
def getColormap(self):
"""Returns the colormap set by :meth:`setColormap`.
:return: The colormap
- :rtype: Colormap
+ :rtype: ~silx.gui.plot.Colormap.Colormap
"""
return self._colormap
@@ -548,7 +660,7 @@ class CutPlane(qt.QObject):
:param name: Name of the colormap in
'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
Or Colormap object.
- :type name: str or Colormap
+ :type name: str or ~silx.gui.plot.Colormap.Colormap
:param str norm: Colormap mapping: 'linear' or 'log'.
:param float vmin: The minimum value of the range or None for autoscale
:param float vmax: The maximum value of the range or None for autoscale
@@ -578,21 +690,14 @@ class CutPlane(qt.QObject):
:return: 2-tuple of float
"""
- return self._plane.colormap.range_
+ return self._dataPlane.colormap.range_
def _updateSceneColormap(self):
"""Synchronizes scene's colormap with Colormap object"""
colormap = self.getColormap()
- sceneCMap = self._plane.colormap
+ sceneCMap = self._dataPlane.colormap
- indices = numpy.linspace(0., 1., 256)
- colormapDisp = Colormap(name=colormap.getName(),
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None,
- colors=colormap.getColormapLUT())
- colors = colormapDisp.applyToData(indices)
- sceneCMap.colormap = colors
+ sceneCMap.colormap = colormap.getNColors()
sceneCMap.norm = colormap.getNormalization()
range_ = colormap.getColormapRange(data=self._dataRange)
@@ -614,14 +719,14 @@ class _CutPlaneImage(object):
def __init__(self, cutPlane):
# Init attributes with default values
self._isValid = False
- self._data = numpy.array([])
+ self._data = numpy.zeros((0, 0), dtype=numpy.float32)
+ self._index = 0
self._xLabel = ''
self._yLabel = ''
self._normalLabel = ''
- self._scale = 1., 1.
- self._translation = 0., 0.
- self._index = 0
- self._position = 0.
+ self._scale = float('nan'), float('nan')
+ self._translation = float('nan'), float('nan')
+ self._position = float('nan')
sfView = cutPlane.parent()
if not sfView or not cutPlane.isValid():
@@ -633,19 +738,30 @@ class _CutPlaneImage(object):
_logger.info("No data available")
return
- normal = cutPlane.getNormal()
- point = numpy.array(cutPlane.getPoint(), dtype=numpy.int)
+ normal = cutPlane.getNormal(coordinates='array')
+ point = cutPlane.getPoint(coordinates='array')
- if numpy.all(numpy.equal(normal, (1., 0., 0.))):
- index = max(0, min(point[0], data.shape[2] - 1))
+ if numpy.linalg.norm(numpy.cross(normal, (1., 0., 0.))) < 0.0017:
+ if not 0 <= point[0] <= data.shape[2]:
+ _logger.info("Plane outside dataset")
+ return
+ index = max(0, min(int(point[0]), data.shape[2] - 1))
slice_ = data[:, :, index]
xAxisIndex, yAxisIndex, normalAxisIndex = 1, 2, 0 # y, z, x
- elif numpy.all(numpy.equal(normal, (0., 1., 0.))):
- index = max(0, min(point[1], data.shape[1] - 1))
+
+ elif numpy.linalg.norm(numpy.cross(normal, (0., 1., 0.))) < 0.0017:
+ if not 0 <= point[1] <= data.shape[1]:
+ _logger.info("Plane outside dataset")
+ return
+ index = max(0, min(int(point[1]), data.shape[1] - 1))
slice_ = numpy.transpose(data[:, index, :])
xAxisIndex, yAxisIndex, normalAxisIndex = 2, 0, 1 # z, x, y
- elif numpy.all(numpy.equal(normal, (0., 0., 1.))):
- index = max(0, min(point[2], data.shape[0] - 1))
+
+ elif numpy.linalg.norm(numpy.cross(normal, (0., 0., 1.))) < 0.0017:
+ if not 0 <= point[2] <= data.shape[0]:
+ _logger.info("Plane outside dataset")
+ return
+ index = max(0, min(int(point[2]), data.shape[0] - 1))
slice_ = data[index, :, :]
xAxisIndex, yAxisIndex, normalAxisIndex = 0, 1, 2 # x, y, z
else:
@@ -657,21 +773,25 @@ class _CutPlaneImage(object):
self._isValid = True
self._data = numpy.array(slice_, copy=True)
+ self._index = index
- labels = sfView.getAxesLabels()
- scale = sfView.getScale()
- translation = sfView.getTranslation()
+ # Only store extra information when no transform matrix is set
+ # Otherwise this information can be meaningless
+ if numpy.all(numpy.equal(sfView.getTransformMatrix(),
+ numpy.identity(3, dtype=numpy.float32))):
+ labels = sfView.getAxesLabels()
+ self._xLabel = labels[xAxisIndex]
+ self._yLabel = labels[yAxisIndex]
+ self._normalLabel = labels[normalAxisIndex]
- self._xLabel = labels[xAxisIndex]
- self._yLabel = labels[yAxisIndex]
- self._normalLabel = labels[normalAxisIndex]
+ scale = sfView.getScale()
+ self._scale = scale[xAxisIndex], scale[yAxisIndex]
- self._scale = scale[xAxisIndex], scale[yAxisIndex]
- self._translation = translation[xAxisIndex], translation[yAxisIndex]
+ translation = sfView.getTranslation()
+ self._translation = translation[xAxisIndex], translation[yAxisIndex]
- self._index = index
- self._position = float(index * scale[normalAxisIndex] +
- translation[normalAxisIndex])
+ self._position = float(index * scale[normalAxisIndex] +
+ translation[normalAxisIndex])
def isValid(self):
"""Returns True if the cut plane image is defined (bool)"""
@@ -727,6 +847,13 @@ class ScalarFieldView(Plot3DWindow):
sigDataChanged = qt.Signal()
"""Signal emitted when the scalar data field has changed."""
+ sigTransformChanged = qt.Signal()
+ """Signal emitted when the transformation has changed.
+
+ It is emitted by :meth:`setTranslation`, :meth:`setTransformMatrix`,
+ :meth:`setScale`.
+ """
+
sigSelectedRegionChanged = qt.Signal(object)
"""Signal emitted when the selected region has changed.
@@ -745,6 +872,7 @@ class ScalarFieldView(Plot3DWindow):
# Transformations
self._dataScale = transform.Scale()
self._dataTranslate = transform.Translate()
+ self._dataTransform = transform.Matrix() # default to identity
self._foregroundColor = 1., 1., 1., 1.
self._highlightColor = 0.7, 0.7, 0., 1.
@@ -752,8 +880,13 @@ class ScalarFieldView(Plot3DWindow):
self._data = None
self._dataRange = None
- self._group = _BoundedGroup()
- self._group.transforms = [self._dataTranslate, self._dataScale]
+ self._group = primitives.BoundedGroup()
+ self._group.transforms = [
+ self._dataTranslate, self._dataTransform, self._dataScale]
+
+ self._bbox = axes.LabelledAxes()
+ self._bbox.children = [self._group]
+ self.getPlot3DWidget().viewport.scene.children.append(self._bbox)
self._selectionBox = primitives.Box()
self._selectionBox.strokeSmooth = False
@@ -766,7 +899,9 @@ class ScalarFieldView(Plot3DWindow):
self._cutPlane = CutPlane(sfView=self)
self._cutPlane.sigVisibilityChanged.connect(
self._planeVisibilityChanged)
- self._group.children.append(self._cutPlane._get3DPrimitive())
+ planeStroke, dataPlane = self._cutPlane._get3DPrimitives()
+ self._bbox.children.append(planeStroke)
+ self._group.children.append(dataPlane)
self._isogroup = primitives.GroupDepthOffset()
self._isogroup.transforms = [
@@ -781,10 +916,6 @@ class ScalarFieldView(Plot3DWindow):
]
self._group.children.append(self._isogroup)
- self._bbox = axes.LabelledAxes()
- self._bbox.children = [self._group]
- self.getPlot3DWidget().viewport.scene.children.append(self._bbox)
-
self._initPanPlaneAction()
self._updateColors()
@@ -932,9 +1063,10 @@ class ScalarFieldView(Plot3DWindow):
"""Creates and init the pan plane action"""
self._panPlaneAction = qt.QAction(self)
self._panPlaneAction.setIcon(icons.getQIcon('3d-plane-pan'))
- self._panPlaneAction.setText('plane')
+ self._panPlaneAction.setText('Pan plane')
self._panPlaneAction.setCheckable(True)
- self._panPlaneAction.setToolTip('pan the cutting plane')
+ self._panPlaneAction.setToolTip(
+ 'Pan the cutting plane. Press <b>Ctrl</b> to rotate the scene.')
self._panPlaneAction.setEnabled(False)
self._panPlaneAction.triggered[bool].connect(self._planeActionTriggered)
@@ -972,24 +1104,21 @@ class ScalarFieldView(Plot3DWindow):
sceneScale = self.getPlot3DWidget().viewport.scene.transforms[0]
if mode == 'plane':
- self.getPlot3DWidget().setInteractiveMode(None)
-
- self.getPlot3DWidget().eventHandler = \
- interaction.PanPlaneZoomOnWheelControl(
- self.getPlot3DWidget().viewport,
- self._cutPlane._get3DPrimitive(),
- mode='position',
- scaleTransform=sceneScale)
- else:
- self.getPlot3DWidget().setInteractiveMode(mode)
+ mode = interaction.PanPlaneZoomOnWheelControl(
+ self.getPlot3DWidget().viewport,
+ self._cutPlane._get3DPrimitives()[0],
+ mode='position',
+ orbitAroundCenter=False,
+ scaleTransform=sceneScale)
+
+ self.getPlot3DWidget().setInteractiveMode(mode)
self._updateColors()
def getInteractiveMode(self):
"""Returns the current interaction mode, see :meth:`setInteractiveMode`
"""
- if (isinstance(self.getPlot3DWidget().eventHandler,
- interaction.PanPlaneZoomOnWheelControl) or
- self.getPlot3DWidget().eventHandler is None):
+ if isinstance(self.getPlot3DWidget().eventHandler,
+ interaction.PanPlaneZoomOnWheelControl):
return 'plane'
else:
return self.getPlot3DWidget().getInteractiveMode()
@@ -1085,6 +1214,7 @@ class ScalarFieldView(Plot3DWindow):
scale = numpy.array((sx, sy, sz), dtype=numpy.float32)
if not numpy.all(numpy.equal(scale, self.getScale())):
self._dataScale.scale = scale
+ self.sigTransformChanged.emit()
self.centerScene() # Reset viewpoint
def getScale(self):
@@ -1102,6 +1232,7 @@ class ScalarFieldView(Plot3DWindow):
translation = numpy.array((x, y, z), dtype=numpy.float32)
if not numpy.all(numpy.equal(translation, self.getTranslation())):
self._dataTranslate.translation = translation
+ self.sigTransformChanged.emit()
self.centerScene() # Reset viewpoint
def getTranslation(self):
@@ -1109,6 +1240,28 @@ class ScalarFieldView(Plot3DWindow):
"""
return self._dataTranslate.translation
+ def setTransformMatrix(self, matrix3x3):
+ """Set the transform matrix applied to the data.
+
+ :param numpy.ndarray matrix: 3x3 transform matrix
+ """
+ matrix3x3 = numpy.array(matrix3x3, copy=True, dtype=numpy.float32)
+ if not numpy.all(numpy.equal(matrix3x3, self.getTransformMatrix())):
+ matrix = numpy.identity(4, dtype=numpy.float32)
+ matrix[:3, :3] = matrix3x3
+ self._dataTransform.setMatrix(matrix)
+ self.sigTransformChanged.emit()
+ self.centerScene() # Reset viewpoint
+
+ def getTransformMatrix(self):
+ """Returns the transform matrix applied to the data.
+
+ See :meth:`setTransformMatrix`.
+
+ :rtype: numpy.ndarray
+ """
+ return self._dataTransform.getMatrix()[:3, :3]
+
# Axes labels
def isBoundingBoxVisible(self):
@@ -1123,7 +1276,8 @@ class ScalarFieldView(Plot3DWindow):
:param bool visible: True to show axes, False to hide
"""
- self._bbox.boxVisible = bool(visible)
+ visible = bool(visible)
+ self._bbox.boxVisible = visible
def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None):
"""Set the text labels of the axes.
@@ -1297,7 +1451,9 @@ class ScalarFieldView(Plot3DWindow):
if self._selectedRange is None:
return None
else:
- return SelectedRegion(self._selectedRange,
+ dataBBox = self._group.transforms.transformBounds(
+ self._selectedRange[::-1].T).T
+ return SelectedRegion(self._selectedRange, dataBBox,
translation=self.getTranslation(),
scale=self.getScale())