# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # ###########################################################################*/ """ This module provides base classes to implement models for 3D scene content """ from __future__ import absolute_import, division __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" import functools import logging import weakref import numpy from silx.third_party import six from ...utils.image import convertArrayToQImage from ...colors import preferredColormaps from ... import qt, icons from .. import items from ..items.volume import Isosurface, CutPlane from .core import AngleDegreeRow, BaseRow, ColorProxyRow, ProxyRow, StaticRow _logger = logging.getLogger(__name__) class _DirectionalLightProxy(qt.QObject): """Proxy to handle directional light with angles rather than vector. """ sigAzimuthAngleChanged = qt.Signal() """Signal sent when the azimuth angle has changed.""" sigAltitudeAngleChanged = qt.Signal() """Signal sent when altitude angle has changed.""" def __init__(self, light): super(_DirectionalLightProxy, self).__init__() self._light = light light.addListener(self._directionUpdated) self._azimuth = 0. self._altitude = 0. def getAzimuthAngle(self): """Returns the signed angle in the horizontal plane. Unit: degrees. The 0 angle corresponds to the axis perpendicular to the screen. :rtype: float """ return self._azimuth def getAltitudeAngle(self): """Returns the signed vertical angle from the horizontal plane. Unit: degrees. Range: [-90, +90] :rtype: float """ return self._altitude def setAzimuthAngle(self, angle): """Set the horizontal angle. :param float angle: Angle from -z axis in zx plane in degrees. """ if angle != self._azimuth: self._azimuth = angle self._updateLight() self.sigAzimuthAngleChanged.emit() def setAltitudeAngle(self, angle): """Set the horizontal angle. :param float angle: Angle from -z axis in zy plane in degrees. """ if angle != self._altitude: self._altitude = angle self._updateLight() self.sigAltitudeAngleChanged.emit() def _directionUpdated(self, *args, **kwargs): """Handle light direction update in the scene""" # Invert direction to manipulate the 'source' pointing to # the center of the viewport x, y, z = - self._light.direction # Horizontal plane is plane xz azimuth = numpy.degrees(numpy.arctan2(x, z)) altitude = numpy.degrees(numpy.pi/2. - numpy.arccos(y)) if (abs(azimuth - self.getAzimuthAngle()) > 0.01 and abs(abs(altitude) - 90.) >= 0.001): # Do not update when at zenith self.setAzimuthAngle(azimuth) if abs(altitude - self.getAltitudeAngle()) > 0.01: self.setAltitudeAngle(altitude) def _updateLight(self): """Update light direction in the scene""" azimuth = numpy.radians(self._azimuth) delta = numpy.pi/2. - numpy.radians(self._altitude) z = - numpy.sin(delta) * numpy.cos(azimuth) x = - numpy.sin(delta) * numpy.sin(azimuth) y = - numpy.cos(delta) self._light.direction = x, y, z class Settings(StaticRow): """Subtree for :class:`SceneWidget` style parameters. :param SceneWidget sceneWidget: The widget to control """ def __init__(self, sceneWidget): background = ColorProxyRow( name='Background', fget=sceneWidget.getBackgroundColor, fset=sceneWidget.setBackgroundColor, notify=sceneWidget.sigStyleChanged) foreground = ColorProxyRow( name='Foreground', fget=sceneWidget.getForegroundColor, fset=sceneWidget.setForegroundColor, notify=sceneWidget.sigStyleChanged) text = ColorProxyRow( name='Text', fget=sceneWidget.getTextColor, fset=sceneWidget.setTextColor, notify=sceneWidget.sigStyleChanged) highlight = ColorProxyRow( name='Highlight', fget=sceneWidget.getHighlightColor, fset=sceneWidget.setHighlightColor, notify=sceneWidget.sigStyleChanged) axesIndicator = ProxyRow( name='Axes Indicator', fget=sceneWidget.isOrientationIndicatorVisible, fset=sceneWidget.setOrientationIndicatorVisible, notify=sceneWidget.sigStyleChanged) # Light direction self._lightProxy = _DirectionalLightProxy(sceneWidget.viewport.light) azimuthNode = ProxyRow( name='Azimuth', fget=self._lightProxy.getAzimuthAngle, fset=self._lightProxy.setAzimuthAngle, notify=self._lightProxy.sigAzimuthAngleChanged, editorHint=(-90, 90)) altitudeNode = ProxyRow( name='Altitude', fget=self._lightProxy.getAltitudeAngle, fset=self._lightProxy.setAltitudeAngle, notify=self._lightProxy.sigAltitudeAngleChanged, editorHint=(-90, 90)) lightDirection = StaticRow(('Light Direction', None), children=(azimuthNode, altitudeNode)) # Settings row children = (background, foreground, text, highlight, axesIndicator, lightDirection) super(Settings, self).__init__(('Settings', None), children=children) class Item3DRow(StaticRow): """Represents an :class:`Item3D` with checkable visibility :param Item3D item: The scene item to represent. :param str name: The optional name of the item """ def __init__(self, item, name=None): if name is None: name = item.getLabel() super(Item3DRow, self).__init__((name, None)) self.setFlags( self.flags(0) | qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsSelectable, 0) self.setFlags(self.flags(1) | qt.Qt.ItemIsSelectable, 1) self._item = weakref.ref(item) item.sigItemChanged.connect(self._itemChanged) def _itemChanged(self, event): """Handle visibility change""" if event == items.ItemChangedType.VISIBLE: model = self.model() if model is not None: index = self.index(column=1) model.dataChanged.emit(index, index) def item(self): """Returns the :class:`Item3D` item or None""" return self._item() def data(self, column, role): if column == 0 and role == qt.Qt.CheckStateRole: item = self.item() if item is not None and item.isVisible(): return qt.Qt.Checked else: return qt.Qt.Unchecked elif column == 0 and role == qt.Qt.DecorationRole: return icons.getQIcon('item-3dim') else: return super(Item3DRow, self).data(column, role) def setData(self, column, value, role): if column == 0 and role == qt.Qt.CheckStateRole: item = self.item() if item is not None: item.setVisible(value == qt.Qt.Checked) return True else: return False return super(Item3DRow, self).setData(column, value, role) class DataItem3DBoundingBoxRow(ProxyRow): """Represents :class:`DataItem3D` bounding box visibility :param DataItem3D item: The item for which to display/control bounding box """ def __init__(self, item): super(DataItem3DBoundingBoxRow, self).__init__( name='Bounding box', fget=item.isBoundingBoxVisible, fset=item.setBoundingBoxVisible, notify=item.sigItemChanged) class MatrixProxyRow(ProxyRow): """Proxy for a row of a DataItem3D 3x3 matrix transform :param DataItem3D item: :param int index: Matrix row index """ def __init__(self, item, index): self._item = weakref.ref(item) self._index = index super(MatrixProxyRow, self).__init__( name='', fget=self._getMatrixRow, fset=self._setMatrixRow, notify=item.sigItemChanged) def _getMatrixRow(self): """Returns the matrix row. :rtype: QVector3D """ item = self._item() if item is not None: matrix = item.getMatrix() return qt.QVector3D(*matrix[self._index, :]) else: return None def _setMatrixRow(self, row): """Set the row of the matrix :param QVector3D row: Row values to set """ item = self._item() if item is not None: matrix = item.getMatrix() matrix[self._index, :] = row.x(), row.y(), row.z() item.setMatrix(matrix) def data(self, column, role): data = super(MatrixProxyRow, self).data(column, role) if column == 1 and role == qt.Qt.DisplayRole: # Convert QVector3D to text data = "%g; %g; %g" % (data.x(), data.y(), data.z()) return data class DataItem3DTransformRow(StaticRow): """Represents :class:`DataItem3D` transform parameters :param DataItem3D item: The item for which to display/control transform """ _ROTATION_CENTER_OPTIONS = 'Origin', 'Lower', 'Center', 'Upper' def __init__(self, item): super(DataItem3DTransformRow, self).__init__(('Transform', None)) self._item = weakref.ref(item) translation = ProxyRow(name='Translation', fget=item.getTranslation, fset=self._setTranslation, notify=item.sigItemChanged, toModelData=lambda data: qt.QVector3D(*data)) self.addRow(translation) # Here to keep a reference self._xSetCenter = functools.partial(self._setCenter, index=0) self._ySetCenter = functools.partial(self._setCenter, index=1) self._zSetCenter = functools.partial(self._setCenter, index=2) rotateCenter = StaticRow( ('Center', None), children=( ProxyRow(name='X axis', fget=item.getRotationCenter, fset=self._xSetCenter, notify=item.sigItemChanged, toModelData=functools.partial( self._centerToModelData, index=0), editorHint=self._ROTATION_CENTER_OPTIONS), ProxyRow(name='Y axis', fget=item.getRotationCenter, fset=self._ySetCenter, notify=item.sigItemChanged, toModelData=functools.partial( self._centerToModelData, index=1), editorHint=self._ROTATION_CENTER_OPTIONS), ProxyRow(name='Z axis', fget=item.getRotationCenter, fset=self._zSetCenter, notify=item.sigItemChanged, toModelData=functools.partial( self._centerToModelData, index=2), editorHint=self._ROTATION_CENTER_OPTIONS), )) rotate = StaticRow( ('Rotation', None), children=( AngleDegreeRow(name='Angle', fget=item.getRotation, fset=self._setAngle, notify=item.sigItemChanged, toModelData=lambda data: data[0]), ProxyRow(name='Axis', fget=item.getRotation, fset=self._setAxis, notify=item.sigItemChanged, toModelData=lambda data: qt.QVector3D(*data[1])), rotateCenter )) self.addRow(rotate) scale = ProxyRow(name='Scale', fget=item.getScale, fset=self._setScale, notify=item.sigItemChanged, toModelData=lambda data: qt.QVector3D(*data)) self.addRow(scale) matrix = StaticRow( ('Matrix', None), children=(MatrixProxyRow(item, 0), MatrixProxyRow(item, 1), MatrixProxyRow(item, 2))) self.addRow(matrix) def item(self): """Returns the :class:`Item3D` item or None""" return self._item() @staticmethod def _centerToModelData(center, index): """Convert rotation center information from scene to model. :param center: The center info from the scene :param int index: dimension to convert """ value = center[index] if isinstance(value, six.string_types): return value.title() elif value == 0.: return 'Origin' else: return six.text_type(value) def _setCenter(self, value, index): """Set one dimension of the rotation center. :param value: Value received through the model. :param int index: dimension to set """ item = self.item() if item is not None: if value == 'Origin': value = 0. elif value not in self._ROTATION_CENTER_OPTIONS: value = float(value) else: value = value.lower() center = list(item.getRotationCenter()) center[index] = value item.setRotationCenter(*center) def _setAngle(self, angle): """Set rotation angle. :param float angle: """ item = self.item() if item is not None: _, axis = item.getRotation() item.setRotation(angle, axis) def _setAxis(self, axis): """Set rotation axis. :param QVector3D axis: """ item = self.item() if item is not None: angle, _ = item.getRotation() item.setRotation(angle, (axis.x(), axis.y(), axis.z())) def _setTranslation(self, translation): """Set translation transform. :param QVector3D translation: """ item = self.item() if item is not None: item.setTranslation(translation.x(), translation.y(), translation.z()) def _setScale(self, scale): """Set scale transform. :param QVector3D scale: """ item = self.item() if item is not None: sx, sy, sz = scale.x(), scale.y(), scale.z() if sx == 0. or sy == 0. or sz == 0.: _logger.warning('Cannot set scale to 0: ignored') else: item.setScale(scale.x(), scale.y(), scale.z()) class GroupItemRow(Item3DRow): """Represents a :class:`GroupItem` with transforms and children :param GroupItem item: The scene group to represent. :param str name: The optional name of the group """ _CHILDREN_ROW_OFFSET = 2 """Number of rows for group parameters. Children are added after""" def __init__(self, item, name=None): super(GroupItemRow, self).__init__(item, name) self.addRow(DataItem3DBoundingBoxRow(item)) self.addRow(DataItem3DTransformRow(item)) item.sigItemAdded.connect(self._itemAdded) item.sigItemRemoved.connect(self._itemRemoved) for child in item.getItems(): self.addRow(nodeFromItem(child)) def _itemAdded(self, item): """Handle item addition to the group and add it to the model. :param Item3D item: added item """ group = self.item() if group is None: return row = group.getItems().index(item) self.addRow(nodeFromItem(item), row + self._CHILDREN_ROW_OFFSET) def _itemRemoved(self, item): """Handle item removal from the group and remove it from the model. :param Item3D item: removed item """ group = self.item() if group is None: return # Find item for row in self.children(): if isinstance(row, Item3DRow) and row.item() is item: self.removeRow(row) break # Got it else: raise RuntimeError("Model does not correspond to scene content") class InterpolationRow(ProxyRow): """Represents :class:`InterpolationMixIn` property. :param Item3D item: Scene item with interpolation property """ def __init__(self, item): modes = [mode.title() for mode in item.INTERPOLATION_MODES] super(InterpolationRow, self).__init__( name='Interpolation', fget=item.getInterpolation, fset=item.setInterpolation, notify=item.sigItemChanged, toModelData=lambda mode: mode.title(), fromModelData=lambda mode: mode.lower(), editorHint=modes) class _ColormapBaseProxyRow(ProxyRow): """Base class for colormap model row This class handle synchronization and signals from the item and the colormap """ _sigColormapChanged = qt.Signal() """Signal used internally to notify colormap (or data) update""" def __init__(self, item, *args, **kwargs): self._dataRange = None self._item = weakref.ref(item) self._colormap = item.getColormap() ProxyRow.__init__(self, *args, **kwargs) self._colormap.sigChanged.connect(self._colormapChanged) item.sigItemChanged.connect(self._itemChanged) self._sigColormapChanged.connect(self._modelUpdated) def item(self): """Returns the :class:`ColormapMixIn` item or None""" return self._item() def _getColormapRange(self): """Returns the range of the colormap for the current data. :return: Colormap range (min, max) """ if self._dataRange is None: item = self.item() if item is not None and self._colormap is not None: if hasattr(item, 'getDataRange'): data = item.getDataRange() else: data = item.getData(copy=False) self._dataRange = self._colormap.getColormapRange(data) else: # Fallback self._dataRange = 1, 100 return self._dataRange def _modelUpdated(self, *args, **kwargs): """Emit dataChanged in the model""" topLeft = self.index(column=0) bottomRight = self.index(column=1) model = self.model() if model is not None: model.dataChanged.emit(topLeft, bottomRight) def _colormapChanged(self): self._sigColormapChanged.emit() def _itemChanged(self, event): """Handle change of colormap or data in the item. :param ItemChangedType event: """ if event == items.ItemChangedType.COLORMAP: self._sigColormapChanged.emit() if self._colormap is not None: self._colormap.sigChanged.disconnect(self._colormapChanged) item = self.item() if item is not None: self._colormap = item.getColormap() self._colormap.sigChanged.connect(self._colormapChanged) else: self._colormap = None elif event == items.ItemChangedType.DATA: self._dataRange = None self._sigColormapChanged.emit() class _ColormapBoundRow(_ColormapBaseProxyRow): """ProxyRow for colormap min or max :param ColormapMixIn item: The item to handle :param str name: Name of the raw :param int index: 0 for Min and 1 of Max """ def __init__(self, item, name, index): self._index = index _ColormapBaseProxyRow.__init__( self, item, name=name, fget=self._getBound, fset=self._setBound) self.setToolTip('Colormap %s bound:\n' 'Check to set bound manually, ' 'uncheck for autoscale' % name.lower()) def _getRawBound(self): """Proxy to get raw colormap bound :rtype: float or None """ if self._colormap is None: return None elif self._index == 0: return self._colormap.getVMin() else: # self._index == 1 return self._colormap.getVMax() def _getBound(self): """Proxy to get colormap effective bound value :rtype: float """ if self._colormap is not None: bound = self._getRawBound() if bound is None: bound = self._getColormapRange()[self._index] return bound else: return 1. # Fallback def _setBound(self, value): """Proxy to set colormap bound. :param float value: """ if self._colormap is not None: if self._index == 0: min_ = value max_ = self._colormap.getVMax() else: # self._index == 1 min_ = self._colormap.getVMin() max_ = value if max_ is not None and min_ is not None and min_ > max_: min_, max_ = max_, min_ self._colormap.setVRange(min_, max_) def flags(self, column): if column == 0: return qt.Qt.ItemIsEnabled | qt.Qt.ItemIsUserCheckable elif column == 1: if self._getRawBound() is not None: flags = qt.Qt.ItemIsEditable | qt.Qt.ItemIsEnabled else: flags = qt.Qt.NoItemFlags # Disabled if autoscale return flags else: # Never event return super(_ColormapBoundRow, self).flags(column) def data(self, column, role): if column == 0 and role == qt.Qt.CheckStateRole: if self._getRawBound() is None: return qt.Qt.Unchecked else: return qt.Qt.Checked else: return super(_ColormapBoundRow, self).data(column, role) def setData(self, column, value, role): if column == 0 and role == qt.Qt.CheckStateRole: if self._colormap is not None: bound = self._getBound() if value == qt.Qt.Checked else None self._setBound(bound) return True else: return False return super(_ColormapBoundRow, self).setData(column, value, role) class ColormapRow(_ColormapBaseProxyRow): """Represents :class:`ColormapMixIn` property. :param Item3D item: Scene item with colormap property """ def __init__(self, item): super(ColormapRow, self).__init__( item, name='Colormap', fget=self._get) self._colormapImage = None self._colormapsMapping = {} for cmap in preferredColormaps(): self._colormapsMapping[cmap.title()] = cmap self.addRow(ProxyRow( name='Name', fget=self._getName, fset=self._setName, notify=self._sigColormapChanged, editorHint=list(self._colormapsMapping.keys()))) norms = [norm.title() for norm in self._colormap.NORMALIZATIONS] self.addRow(ProxyRow( name='Normalization', fget=self._getNormalization, fset=self._setNormalization, notify=self._sigColormapChanged, editorHint=norms)) self.addRow(_ColormapBoundRow(item, name='Min.', index=0)) self.addRow(_ColormapBoundRow(item, name='Max.', index=1)) self._sigColormapChanged.connect(self._updateColormapImage) def _get(self): """Getter for ProxyRow subclass""" return None def _getName(self): """Proxy for :meth:`Colormap.getName`""" if self._colormap is not None and self._colormap.getName() is not None: return self._colormap.getName().title() else: return '' def _setName(self, name): """Proxy for :meth:`Colormap.setName`""" # Convert back from titled to name if possible if self._colormap is not None: name = self._colormapsMapping.get(name, name) self._colormap.setName(name) def _getNormalization(self): """Proxy for :meth:`Colormap.getNormalization`""" if self._colormap is not None: return self._colormap.getNormalization().title() else: return '' def _setNormalization(self, normalization): """Proxy for :meth:`Colormap.setNormalization`""" if self._colormap is not None: return self._colormap.setNormalization(normalization.lower()) def _updateColormapImage(self, *args, **kwargs): """Notify colormap update to update the image in the tree""" if self._colormapImage is not None: self._colormapImage = None model = self.model() if model is not None: index = self.index(column=1) model.dataChanged.emit(index, index) def data(self, column, role): if column == 1 and role == qt.Qt.DecorationRole: if self._colormapImage is None: image = numpy.zeros((16, 130, 3), dtype=numpy.uint8) image[1:-1, 1:-1] = self._colormap.getNColors(image.shape[1] - 2)[:, :3] self._colormapImage = convertArrayToQImage(image) return self._colormapImage return super(ColormapRow, self).data(column, role) class SymbolRow(ProxyRow): """Represents :class:`SymbolMixIn` symbol property. :param Item3D item: Scene item with symbol property """ def __init__(self, item): names = [item.getSymbolName(s) for s in item.getSupportedSymbols()] super(SymbolRow, self).__init__( name='Marker', fget=item.getSymbolName, fset=item.setSymbol, notify=item.sigItemChanged, editorHint=names) class SymbolSizeRow(ProxyRow): """Represents :class:`SymbolMixIn` symbol size property. :param Item3D item: Scene item with symbol size property """ def __init__(self, item): super(SymbolSizeRow, self).__init__( name='Marker size', fget=item.getSymbolSize, fset=item.setSymbolSize, notify=item.sigItemChanged, editorHint=(1, 20)) # TODO link with OpenGL max point size class PlaneRow(ProxyRow): """Represents :class:`PlaneMixIn` property. :param Item3D item: Scene item with plane equation property """ def __init__(self, item): super(PlaneRow, self).__init__( name='Equation', fget=item.getParameters, fset=item.setParameters, notify=item.sigItemChanged, toModelData=lambda data: qt.QVector4D(*data), fromModelData=lambda data: (data.x(), data.y(), data.z(), data.w())) self._item = weakref.ref(item) def data(self, column, role): if column == 1 and role == qt.Qt.DisplayRole: item = self._item() if item is not None: params = item.getParameters() return ('%gx %+gy %+gz %+g = 0' % (params[0], params[1], params[2], params[3])) return super(PlaneRow, self).data(column, role) class RemoveIsosurfaceRow(BaseRow): """Class for Isosurface Delete button :param Isosurface isosurface: The isosurface item to attach the button to. """ def __init__(self, isosurface): super(RemoveIsosurfaceRow, self).__init__() self._isosurface = weakref.ref(isosurface) def createEditor(self): """Specific editor factory provided to the model""" editor = qt.QWidget() layout = qt.QHBoxLayout(editor) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) removeBtn = qt.QToolButton() removeBtn.setText('Delete') removeBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly) layout.addWidget(removeBtn) removeBtn.clicked.connect(self._removeClicked) layout.addStretch(1) return editor def isosurface(self): """Returns the controlled isosurface :rtype: Isosurface """ return self._isosurface() def data(self, column, role): if column == 0 and role == qt.Qt.UserRole: # editor hint return self.createEditor return super(RemoveIsosurfaceRow, self).data(column, role) def flags(self, column): flags = super(RemoveIsosurfaceRow, self).flags(column) if column == 0: flags |= qt.Qt.ItemIsEditable return flags def _removeClicked(self): """Handle Delete button clicked""" isosurface = self.isosurface() if isosurface is not None: scalarField3D = isosurface.parent() if scalarField3D is not None: scalarField3D.removeIsosurface(isosurface) class IsosurfaceRow(Item3DRow): """Represents an :class:`Isosurface` item. :param Isosurface item: Isosurface item """ _LEVEL_SLIDER_RANGE = 0, 1000 """Range given as editor hint""" def __init__(self, item): super(IsosurfaceRow, self).__init__(item, name=item.getLevel()) self.setFlags(self.flags(1) | qt.Qt.ItemIsEditable, 1) item.sigItemChanged.connect(self._levelChanged) self.addRow(ProxyRow( name='Level', fget=self._getValueForLevelSlider, fset=self._setLevelFromSliderValue, notify=item.sigItemChanged, editorHint=self._LEVEL_SLIDER_RANGE)) self.addRow(ColorProxyRow( name='Color', fget=self._rgbColor, fset=self._setRgbColor, notify=item.sigItemChanged)) self.addRow(ProxyRow( name='Opacity', fget=self._opacity, fset=self._setOpacity, notify=item.sigItemChanged, editorHint=(0, 255))) self.addRow(RemoveIsosurfaceRow(item)) def _getValueForLevelSlider(self): """Convert iso level to slider value. :rtype: int """ item = self.item() if item is not None: scalarField3D = item.parent() if scalarField3D is not None: dataRange = scalarField3D.getDataRange() if dataRange is not None: dataMin, dataMax = dataRange[0], dataRange[-1] offset = (item.getLevel() - dataMin) / (dataMax - dataMin) sliderMin, sliderMax = self._LEVEL_SLIDER_RANGE value = sliderMin + (sliderMax - sliderMin) * offset return value return 0 def _setLevelFromSliderValue(self, value): """Convert slider value to isolevel. :param int value: """ item = self.item() if item is not None: scalarField3D = item.parent() if scalarField3D is not None: dataRange = scalarField3D.getDataRange() if dataRange is not None: sliderMin, sliderMax = self._LEVEL_SLIDER_RANGE offset = (value - sliderMin) / (sliderMax - sliderMin) dataMin, dataMax = dataRange[0], dataRange[-1] level = dataMin + (dataMax - dataMin) * offset item.setLevel(level) def _rgbColor(self): """Proxy to get the isosurface's RGB color without transparency :rtype: QColor """ item = self.item() if item is None: return None else: color = item.getColor() color.setAlpha(255) return color def _setRgbColor(self, color): """Proxy to set the isosurface's RGB color without transparency :param QColor color: """ item = self.item() if item is not None: color.setAlpha(item.getColor().alpha()) item.setColor(color) def _opacity(self): """Proxy to get the isosurface's transparency :rtype: int """ item = self.item() return 255 if item is None else item.getColor().alpha() def _setOpacity(self, opacity): """Proxy to set the isosurface's transparency. :param int opacity: """ item = self.item() if item is not None: color = item.getColor() color.setAlpha(opacity) item.setColor(color) def _levelChanged(self, event): """Handle isosurface level changed and notify model :param ItemChangedType event: """ if event == items.Item3DChangedType.ISO_LEVEL: model = self.model() if model is not None: index = self.index(column=1) model.dataChanged.emit(index, index) def data(self, column, role): if column == 0: # Show color as decoration, not text if role == qt.Qt.DisplayRole: return None elif role == qt.Qt.DecorationRole: return self._rgbColor() elif column == 1 and role in (qt.Qt.DisplayRole, qt.Qt.EditRole): item = self.item() return None if item is None else item.getLevel() return super(IsosurfaceRow, self).data(column, role) def setData(self, column, value, role): if column == 1 and role == qt.Qt.EditRole: item = self.item() if item is not None: item.setLevel(value) return True return super(IsosurfaceRow, self).setData(column, value, role) class AddIsosurfaceRow(BaseRow): """Class for Isosurface create button :param ScalarField3D scalarField3D: The ScalarField3D item to attach the button to. """ def __init__(self, scalarField3D): super(AddIsosurfaceRow, self).__init__() self._scalarField3D = weakref.ref(scalarField3D) def createEditor(self): """Specific editor factory provided to the model""" editor = qt.QWidget() layout = qt.QHBoxLayout(editor) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) addBtn = qt.QToolButton() addBtn.setText('+') addBtn.setToolButtonStyle(qt.Qt.ToolButtonTextOnly) layout.addWidget(addBtn) addBtn.clicked.connect(self._addClicked) layout.addStretch(1) return editor def scalarField3D(self): """Returns the controlled ScalarField3D :rtype: ScalarField3D """ return self._scalarField3D() def data(self, column, role): if column == 0 and role == qt.Qt.UserRole: # editor hint return self.createEditor return super(AddIsosurfaceRow, self).data(column, role) def flags(self, column): flags = super(AddIsosurfaceRow, self).flags(column) if column == 0: flags |= qt.Qt.ItemIsEditable return flags def _addClicked(self): """Handle Delete button clicked""" scalarField3D = self.scalarField3D() if scalarField3D is not None: dataRange = scalarField3D.getDataRange() if dataRange is None: dataRange = 0., 1. scalarField3D.addIsosurface( numpy.mean((dataRange[0], dataRange[-1])), '#0000FF') class ScalarField3DIsoSurfacesRow(StaticRow): """Represents :class:`ScalarFieldView`'s isosurfaces :param ScalarFieldView scalarField3D: ScalarFieldView to control """ def __init__(self, scalarField3D): super(ScalarField3DIsoSurfacesRow, self).__init__( ('Isosurfaces', None)) self._scalarField3D = weakref.ref(scalarField3D) scalarField3D.sigIsosurfaceAdded.connect(self._isosurfaceAdded) scalarField3D.sigIsosurfaceRemoved.connect(self._isosurfaceRemoved) for item in scalarField3D.getIsosurfaces(): self.addRow(nodeFromItem(item)) self.addRow(AddIsosurfaceRow(scalarField3D)) def scalarField3D(self): """Returns the controlled ScalarField3D :rtype: ScalarField3D """ return self._scalarField3D() def _isosurfaceAdded(self, item): """Handle isosurface addition :param Isosurface item: added isosurface """ scalarField3D = self.scalarField3D() if scalarField3D is None: return row = scalarField3D.getIsosurfaces().index(item) self.addRow(nodeFromItem(item), row) def _isosurfaceRemoved(self, item): """Handle isosurface removal :param Isosurface item: removed isosurface """ scalarField3D = self.scalarField3D() if scalarField3D is None: return # Find item for row in self.children(): if row.item() is item: self.removeRow(row) break # Got it else: raise RuntimeError("Model does not correspond to scene content") class Scatter2DPropertyMixInRow(object): """Mix-in class that enable/disable row according to Scatter2D mode. :param Scatter2D item: :param str propertyName: Name of the Scatter2D property of this row """ def __init__(self, item, propertyName): assert propertyName in ('lineWidth', 'symbol', 'symbolSize') self.__propertyName = propertyName self.__isEnabled = item.isPropertyEnabled(propertyName) self.__updateFlags() item.sigItemChanged.connect(self.__itemChanged) def data(self, column, role): if column == 1 and not self.__isEnabled: # Discard data and editorHint if disabled return None else: return super(Scatter2DPropertyMixInRow, self).data(column, role) def __updateFlags(self): """Update model flags""" if self.__isEnabled: self.setFlags(qt.Qt.ItemIsEnabled, 0) self.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsEditable, 1) else: self.setFlags(qt.Qt.NoItemFlags) def __itemChanged(self, event): """Set flags to enable/disable the row""" if event == items.ItemChangedType.VISUALIZATION_MODE: item = self.sender() if item is not None: # This occurs with PySide/python2.7 self.__isEnabled = item.isPropertyEnabled(self.__propertyName) self.__updateFlags() # Notify model model = self.model() if model is not None: begin = self.index(column=0) end = self.index(column=1) model.dataChanged.emit(begin, end) class Scatter2DSymbolRow(Scatter2DPropertyMixInRow, SymbolRow): """Specific class for Scatter2D symbol. It is enabled/disabled according to visualization mode. :param Scatter2D item: """ def __init__(self, item): SymbolRow.__init__(self, item) Scatter2DPropertyMixInRow.__init__(self, item, 'symbol') class Scatter2DSymbolSizeRow(Scatter2DPropertyMixInRow, SymbolSizeRow): """Specific class for Scatter2D symbol size. It is enabled/disabled according to visualization mode. :param Scatter2D item: """ def __init__(self, item): SymbolSizeRow.__init__(self, item) Scatter2DPropertyMixInRow.__init__(self, item, 'symbolSize') class Scatter2DLineWidth(Scatter2DPropertyMixInRow, ProxyRow): """Specific class for Scatter2D symbol size. It is enabled/disabled according to visualization mode. :param Scatter2D item: """ def __init__(self, item): # TODO link editorHint with OpenGL max line width ProxyRow.__init__(self, name='Line width', fget=item.getLineWidth, fset=item.setLineWidth, notify=item.sigItemChanged, editorHint=(1, 10)) Scatter2DPropertyMixInRow.__init__(self, item, 'lineWidth') def initScatter2DNode(node, item): """Specific node init for Scatter2D to set order of parameters :param Item3DRow node: The model node to setup :param Scatter2D item: The Scatter2D the node is representing """ node.addRow(ProxyRow( name='Mode', fget=item.getVisualization, fset=item.setVisualization, notify=item.sigItemChanged, editorHint=[m.title() for m in item.supportedVisualizations()], toModelData=lambda data: data.title(), fromModelData=lambda data: data.lower())) node.addRow(ProxyRow( name='Height map', fget=item.isHeightMap, fset=item.setHeightMap, notify=item.sigItemChanged)) node.addRow(ColormapRow(item)) node.addRow(Scatter2DSymbolRow(item)) node.addRow(Scatter2DSymbolSizeRow(item)) node.addRow(Scatter2DLineWidth(item)) def initScalarField3DNode(node, item): """Specific node init for ScalarField3D :param Item3DRow node: The model node to setup :param ScalarField3D item: The ScalarField3D the node is representing """ node.addRow(nodeFromItem(item.getCutPlanes()[0])) # Add cut plane node.addRow(ScalarField3DIsoSurfacesRow(item)) def initScalarField3DCutPlaneNode(node, item): """Specific node init for ScalarField3D CutPlane :param Item3DRow node: The model node to setup :param CutPlane item: The CutPlane the node is representing """ node.addRow(PlaneRow(item)) node.addRow(ColormapRow(item)) node.addRow(ProxyRow( name='Values<=Min', fget=item.getDisplayValuesBelowMin, fset=item.setDisplayValuesBelowMin, notify=item.sigItemChanged)) node.addRow(InterpolationRow(item)) NODE_SPECIFIC_INIT = [ # class, init(node, item) (items.Scatter2D, initScatter2DNode), (items.ScalarField3D, initScalarField3DNode), (CutPlane, initScalarField3DCutPlaneNode), ] """List of specific node init for different item class""" def nodeFromItem(item): """Create :class:`Item3DRow` subclass corresponding to item :param Item3D item: The item fow which to create the node :rtype: Item3DRow """ assert isinstance(item, items.Item3D) # Item with specific model row class if isinstance(item, (items.GroupItem, items.GroupWithAxesItem)): return GroupItemRow(item) elif isinstance(item, Isosurface): return IsosurfaceRow(item) # Create Item3DRow and populate it node = Item3DRow(item) if isinstance(item, items.DataItem3D): node.addRow(DataItem3DBoundingBoxRow(item)) node.addRow(DataItem3DTransformRow(item)) # Specific extra init for cls, specificInit in NODE_SPECIFIC_INIT: if isinstance(item, cls): specificInit(node, item) break else: # Generic case: handle mixins for cls in item.__class__.__mro__: if cls is items.ColormapMixIn: node.addRow(ColormapRow(item)) elif cls is items.InterpolationMixIn: node.addRow(InterpolationRow(item)) elif cls is items.SymbolMixIn: node.addRow(SymbolRow(item)) node.addRow(SymbolSizeRow(item)) elif cls is items.PlaneMixIn: node.addRow(PlaneRow(item)) return node