diff options
Diffstat (limited to 'silx/gui/plot3d/ScalarFieldView.py')
-rw-r--r-- | silx/gui/plot3d/ScalarFieldView.py | 430 |
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()) |