diff options
Diffstat (limited to 'silx/gui/plot3d/_model/items.py')
-rw-r--r-- | silx/gui/plot3d/_model/items.py | 1760 |
1 files changed, 0 insertions, 1760 deletions
diff --git a/silx/gui/plot3d/_model/items.py b/silx/gui/plot3d/_model/items.py deleted file mode 100644 index be51663..0000000 --- a/silx/gui/plot3d/_model/items.py +++ /dev/null @@ -1,1760 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2020 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" - - -from collections import OrderedDict -import functools -import logging -import weakref - -import numpy -import six - -from ...utils.image import convertArrayToQImage -from ...colors import preferredColormaps -from ... import qt, icons -from .. import items -from ..items.volume import Isosurface, CutPlane, ComplexIsosurface -from ..Plot3DWidget import Plot3DWidget - - -from .core import AngleDegreeRow, BaseRow, ColorProxyRow, ProxyRow, StaticRow - - -_logger = logging.getLogger(__name__) - - -class ItemProxyRow(ProxyRow): - """Provides a node to proxy a data accessible through functions. - - It listens on sigItemChanged to trigger the update. - - Warning: Only weak reference are kept on fget and fset. - - :param Item3D item: The item to - :param str name: The name of this node - :param callable fget: A callable returning the data - :param callable fset: - An optional callable setting the data with data as a single argument. - :param events: - An optional event kind or list of event kinds to react upon. - :param callable toModelData: - An optional callable to convert from fget - callable to data returned by the model. - :param callable fromModelData: - An optional callable converting data provided to the model to - data for fset. - :param editorHint: Data to provide as UserRole for editor selection/setup - """ - - def __init__(self, - item, - name='', - fget=None, - fset=None, - events=None, - toModelData=None, - fromModelData=None, - editorHint=None): - super(ItemProxyRow, self).__init__( - name=name, - fget=fget, - fset=fset, - notify=None, - toModelData=toModelData, - fromModelData=fromModelData, - editorHint=editorHint) - - if isinstance(events, (items.ItemChangedType, - items.Item3DChangedType)): - events = (events,) - self.__events = events - item.sigItemChanged.connect(self.__itemChanged) - - def __itemChanged(self, event): - """Handle item changed - - :param Union[ItemChangedType,Item3DChangedType] event: - """ - if self.__events is None or event in self.__events: - self._notified() - - -class ItemColorProxyRow(ColorProxyRow, ItemProxyRow): - """Combines :class:`ColorProxyRow` and :class:`ItemProxyRow`""" - - def __init__(self, *args, **kwargs): - ItemProxyRow.__init__(self, *args, **kwargs) - - -class ItemAngleDegreeRow(AngleDegreeRow, ItemProxyRow): - """Combines :class:`AngleDegreeRow` and :class:`ItemProxyRow`""" - - def __init__(self, *args, **kwargs): - ItemProxyRow.__init__(self, *args, **kwargs) - - -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: int - """ - return self._azimuth - - def getAltitudeAngle(self): - """Returns the signed vertical angle from the horizontal plane. - - Unit: degrees. - Range: [-90, +90] - - :rtype: int - """ - return self._altitude - - def setAzimuthAngle(self, angle): - """Set the horizontal angle. - - :param int angle: Angle from -z axis in zx plane in degrees. - """ - angle = int(round(angle)) - if angle != self._azimuth: - self._azimuth = angle - self._updateLight() - self.sigAzimuthAngleChanged.emit() - - def setAltitudeAngle(self, angle): - """Set the horizontal angle. - - :param int angle: Angle from -z axis in zy plane in degrees. - """ - angle = int(round(angle)) - 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 = int(round(numpy.degrees(numpy.arctan2(x, z)))) - altitude = int(round(numpy.degrees(numpy.pi/2. - numpy.arccos(y)))) - - if azimuth != self.getAzimuthAngle(): - self.setAzimuthAngle(azimuth) - - if altitude != self.getAltitudeAngle(): - 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) - if delta == 0.: # Avoids zenith position - delta = 0.0001 - 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)) - - # Fog - fog = ProxyRow( - name='Fog', - fget=sceneWidget.getFogMode, - fset=sceneWidget.setFogMode, - notify=sceneWidget.sigStyleChanged, - toModelData=lambda mode: mode is Plot3DWidget.FogMode.LINEAR, - fromModelData=lambda mode: Plot3DWidget.FogMode.LINEAR if mode else Plot3DWidget.FogMode.NONE) - - # Settings row - children = (background, foreground, text, highlight, - axesIndicator, lightDirection, fog) - super(Settings, self).__init__(('Settings', None), children=children) - - -class Item3DRow(BaseRow): - """Represents an :class:`Item3D` with checkable visibility - - :param Item3D item: The scene item to represent. - :param str name: The optional name of the item - """ - - _EVENTS = items.ItemChangedType.VISIBLE, items.Item3DChangedType.LABEL - """Events for which to update the first column in the tree""" - - def __init__(self, item, name=None): - self.__name = None if name is None else six.text_type(name) - super(Item3DRow, self).__init__() - - 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 model update upon change""" - if event in self._EVENTS: - model = self.model() - if model is not None: - index = self.index(column=0) - 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: - if 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 role == qt.Qt.DecorationRole: - return icons.getQIcon('item-3dim') - - elif role == qt.Qt.DisplayRole: - if self.__name is None: - item = self.item() - return '' if item is None else item.getLabel() - else: - return self.__name - - 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) - - def columnCount(self): - return 2 - - -class DataItem3DBoundingBoxRow(ItemProxyRow): - """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__( - item=item, - name='Bounding box', - fget=item.isBoundingBoxVisible, - fset=item.setBoundingBoxVisible, - events=items.Item3DChangedType.BOUNDING_BOX_VISIBLE) - - -class MatrixProxyRow(ItemProxyRow): - """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__( - item=item, - name='', - fget=self._getMatrixRow, - fset=self._setMatrixRow, - events=items.Item3DChangedType.TRANSFORM) - - 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 = ItemProxyRow( - item=item, - name='Translation', - fget=item.getTranslation, - fset=self._setTranslation, - events=items.Item3DChangedType.TRANSFORM, - 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=( - ItemProxyRow(item=item, - name='X axis', - fget=item.getRotationCenter, - fset=self._xSetCenter, - events=items.Item3DChangedType.TRANSFORM, - toModelData=functools.partial( - self._centerToModelData, index=0), - editorHint=self._ROTATION_CENTER_OPTIONS), - ItemProxyRow(item=item, - name='Y axis', - fget=item.getRotationCenter, - fset=self._ySetCenter, - events=items.Item3DChangedType.TRANSFORM, - toModelData=functools.partial( - self._centerToModelData, index=1), - editorHint=self._ROTATION_CENTER_OPTIONS), - ItemProxyRow(item=item, - name='Z axis', - fget=item.getRotationCenter, - fset=self._zSetCenter, - events=items.Item3DChangedType.TRANSFORM, - toModelData=functools.partial( - self._centerToModelData, index=2), - editorHint=self._ROTATION_CENTER_OPTIONS), - )) - - rotate = StaticRow( - ('Rotation', None), - children=( - ItemAngleDegreeRow( - item=item, - name='Angle', - fget=item.getRotation, - fset=self._setAngle, - events=items.Item3DChangedType.TRANSFORM, - toModelData=lambda data: data[0]), - ItemProxyRow( - item=item, - name='Axis', - fget=item.getRotation, - fset=self._setAxis, - events=items.Item3DChangedType.TRANSFORM, - toModelData=lambda data: qt.QVector3D(*data[1])), - rotateCenter - )) - self.addRow(rotate) - - scale = ItemProxyRow( - item=item, - name='Scale', - fget=item.getScale, - fset=self._setScale, - events=items.Item3DChangedType.TRANSFORM, - 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(ItemProxyRow): - """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__( - item=item, - name='Interpolation', - fget=item.getInterpolation, - fset=item.setInterpolation, - events=items.Item3DChangedType.INTERPOLATION, - 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._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) - """ - item = self.item() - if item is not None and self._colormap is not None: - return self._colormap.getColormapRange(item) - else: - return 1, 100 # Fallback - - 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._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 _ColormapGammaRow(_ColormapBaseProxyRow): - """ProxyRow for colormap gamma normalization parameter - - :param ColormapMixIn item: The item to handle - :param str name: Name of the raw - """ - - def __init__(self, item): - _ColormapBaseProxyRow.__init__( - self, - item, - name="Gamma", - fget=self._getGammaNormalizationParameter, - fset=self._setGammaNormalizationParameter) - - self.setToolTip('Colormap gamma correction parameter:\n' - 'Only meaningful for gamma normalization.') - - def _getGammaNormalizationParameter(self): - """Proxy for :meth:`Colormap.getGammaNormalizationParameter`""" - if self._colormap is not None: - return self._colormap.getGammaNormalizationParameter() - else: - return 0.0 - - def _setGammaNormalizationParameter(self, gamma): - """Proxy for :meth:`Colormap.setGammaNormalizationParameter`""" - if self._colormap is not None: - return self._colormap.setGammaNormalizationParameter(gamma) - - def _getNormalization(self): - """Proxy for :meth:`Colormap.getNormalization`""" - if self._colormap is not None: - return self._colormap.getNormalization() - else: - return '' - - def flags(self, column): - if column in (0, 1): - if self._getNormalization() == 'gamma': - flags = qt.Qt.ItemIsEditable | qt.Qt.ItemIsEnabled - else: - flags = qt.Qt.NoItemFlags # Disabled if not gamma correction - return flags - - else: # Never event - return super(_ColormapGammaRow, self).flags(column) - - -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(_ColormapGammaRow(item)) - - modes = [mode.title() for mode in self._colormap.AUTOSCALE_MODES] - self.addRow(ProxyRow( - name='Autoscale Mode', - fget=self._getAutoscaleMode, - fset=self._setAutoscaleMode, - notify=self._sigColormapChanged, - editorHint=modes)) - - self.addRow(_ColormapBoundRow(item, name='Min.', index=0)) - self.addRow(_ColormapBoundRow(item, name='Max.', index=1)) - - self._sigColormapChanged.connect(self._updateColormapImage) - - def getColormapImage(self): - """Returns image representing the colormap or None - - :rtype: Union[QImage,None] - """ - if self._colormapImage is None and self._colormap is not 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 - - 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 _getAutoscaleMode(self): - """Proxy for :meth:`Colormap.getAutoscaleMode`""" - if self._colormap is not None: - return self._colormap.getAutoscaleMode().title() - else: - return '' - - def _setAutoscaleMode(self, mode): - """Proxy for :meth:`Colormap.setAutoscaleMode`""" - if self._colormap is not None: - return self._colormap.setAutoscaleMode(mode.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: - return self.getColormapImage() - else: - return super(ColormapRow, self).data(column, role) - - -class SymbolRow(ItemProxyRow): - """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__( - item=item, - name='Marker', - fget=item.getSymbolName, - fset=item.setSymbol, - events=items.ItemChangedType.SYMBOL, - editorHint=names) - - -class SymbolSizeRow(ItemProxyRow): - """Represents :class:`SymbolMixIn` symbol size property. - - :param Item3D item: Scene item with symbol size property - """ - - def __init__(self, item): - super(SymbolSizeRow, self).__init__( - item=item, - name='Marker size', - fget=item.getSymbolSize, - fset=item.setSymbolSize, - events=items.ItemChangedType.SYMBOL_SIZE, - editorHint=(1, 20)) # TODO link with OpenGL max point size - - -class PlaneEquationRow(ItemProxyRow): - """Represents :class:`PlaneMixIn` as plane equation. - - :param Item3D item: Scene item with plane equation property - """ - - def __init__(self, item): - super(PlaneEquationRow, self).__init__( - item=item, - name='Equation', - fget=item.getParameters, - fset=item.setParameters, - events=items.ItemChangedType.POSITION, - 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(PlaneEquationRow, self).data(column, role) - - -class PlaneRow(ItemProxyRow): - """Represents :class:`PlaneMixIn` property. - - :param Item3D item: Scene item with plane equation property - """ - - _PLANES = OrderedDict((('Plane 0', (1., 0., 0.)), - ('Plane 1', (0., 1., 0.)), - ('Plane 2', (0., 0., 1.)), - ('-', None))) - """Mapping of plane names to normals""" - - _PLANE_ICONS = {'Plane 0': '3d-plane-normal-x', - 'Plane 1': '3d-plane-normal-y', - 'Plane 2': '3d-plane-normal-z', - '-': '3d-plane'} - """Mapping of plane names to normals""" - - def __init__(self, item): - super(PlaneRow, self).__init__( - item=item, - name='Plane', - fget=self.__getPlaneName, - fset=self.__setPlaneName, - events=items.ItemChangedType.POSITION, - editorHint=tuple(self._PLANES.keys())) - self._item = weakref.ref(item) - self._lastName = None - - self.addRow(PlaneEquationRow(item)) - - def _notified(self, *args, **kwargs): - """Handle notification of modification - - Here only send if plane name actually changed - """ - if self._lastName != self.__getPlaneName(): - super(PlaneRow, self)._notified() - - def __getPlaneName(self): - """Returns name of plane // to axes or '-' - - :rtype: str - """ - item = self._item() - planeNormal = item.getNormal() if item is not None else None - - for name, normal in self._PLANES.items(): - if numpy.array_equal(planeNormal, normal): - return name - return '-' - - def __setPlaneName(self, data): - """Set plane normal according to given plane name - - :param str data: Selected plane name - """ - item = self._item() - if item is not None: - for name, normal in self._PLANES.items(): - if data == name and normal is not None: - item.setNormal(normal) - - def data(self, column, role): - if column == 1 and role == qt.Qt.DecorationRole: - return icons.getQIcon(self._PLANE_ICONS[self.__getPlaneName()]) - data = super(PlaneRow, self).data(column, role) - if column == 1 and role == qt.Qt.DisplayRole: - self._lastName = data - return data - - -class ComplexModeRow(ItemProxyRow): - """Represents :class:`items.ComplexMixIn` symbol property. - - :param Item3D item: Scene item with symbol property - """ - - def __init__(self, item, name='Mode'): - names = [m.value.replace('_', ' ').title() - for m in item.supportedComplexModes()] - super(ComplexModeRow, self).__init__( - item=item, - name=name, - fget=item.getComplexMode, - fset=item.setComplexMode, - events=items.ItemChangedType.COMPLEX_MODE, - toModelData=lambda data: data.value.replace('_', ' ').title(), - fromModelData=lambda data: data.lower().replace(' ', '_'), - editorHint=names) - - -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: - volume = isosurface.parent() - if volume is not None: - volume.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""" - - _EVENTS = items.ItemChangedType.VISIBLE, items.ItemChangedType.COLOR - """Events for which to update the first column in the tree""" - - 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(ItemProxyRow( - item=item, - name='Level', - fget=self._getValueForLevelSlider, - fset=self._setLevelFromSliderValue, - events=items.Item3DChangedType.ISO_LEVEL, - editorHint=self._LEVEL_SLIDER_RANGE)) - - self.addRow(ItemColorProxyRow( - item=item, - name='Color', - fget=self._rgbColor, - fset=self._setRgbColor, - events=items.ItemChangedType.COLOR)) - - self.addRow(ItemProxyRow( - item=item, - name='Opacity', - fget=self._opacity, - fset=self._setOpacity, - events=items.ItemChangedType.COLOR, - 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: - volume = item.parent() - if volume is not None: - dataRange = volume.getDataRange() - if dataRange is not None: - dataMin, dataMax = dataRange[0], dataRange[-1] - if dataMax != dataMin: - offset = (item.getLevel() - dataMin) / (dataMax - dataMin) - else: - offset = 0. - - 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: - volume = item.parent() - if volume is not None: - dataRange = volume.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 ComplexIsosurfaceRow(IsosurfaceRow): - """Represents an :class:`ComplexIsosurface` item. - - :param ComplexIsosurface item: - """ - - _EVENTS = (items.ItemChangedType.VISIBLE, - items.ItemChangedType.COLOR, - items.ItemChangedType.COMPLEX_MODE) - """Events for which to update the first column in the tree""" - - def __init__(self, item): - super(ComplexIsosurfaceRow, self).__init__(item) - - self.addRow(ComplexModeRow(item, "Color Complex Mode"), index=1) - for row in self.children(): - if isinstance(row, ColorProxyRow): - self._colorRow = row - break - else: - raise RuntimeError("Cannot retrieve Color tree row") - self._colormapRow = ColormapRow(item) - - self.__updateRowsForItem(item) - item.sigItemChanged.connect(self.__itemChanged) - - def __itemChanged(self, event): - """Update enabled/disabled rows""" - if event == items.ItemChangedType.COMPLEX_MODE: - item = self.sender() - self.__updateRowsForItem(item) - - def __updateRowsForItem(self, item): - """Update rows for item - - :param item: - """ - if not isinstance(item, ComplexIsosurface): - return - - if item.getComplexMode() == items.ComplexMixIn.ComplexMode.NONE: - removed = self._colormapRow - added = self._colorRow - else: - removed = self._colorRow - added = self._colormapRow - - # Remove unwanted rows - if removed in self.children(): - self.removeRow(removed) - - # Add required rows - if added not in self.children(): - self.addRow(added, index=2) - - def data(self, column, role): - if column == 0 and role == qt.Qt.DecorationRole: - item = self.item() - if (item is not None and - item.getComplexMode() != items.ComplexMixIn.ComplexMode.NONE): - return self._colormapRow.getColormapImage() - - return super(ComplexIsosurfaceRow, self).data(column, role) - - -class AddIsosurfaceRow(BaseRow): - """Class for Isosurface create button - - :param Union[ScalarField3D,ComplexField3D] volume: - The volume item to attach the button to. - """ - - def __init__(self, volume): - super(AddIsosurfaceRow, self).__init__() - self._volume = weakref.ref(volume) - - 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 volume(self): - """Returns the controlled volume item - - :rtype: Union[ScalarField3D,ComplexField3D] - """ - return self._volume() - - 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""" - volume = self.volume() - if volume is not None: - dataRange = volume.getDataRange() - if dataRange is None: - dataRange = 0., 1. - - volume.addIsosurface( - numpy.mean((dataRange[0], dataRange[-1])), - '#0000FF') - - -class VolumeIsoSurfacesRow(StaticRow): - """Represents :class:`ScalarFieldView`'s isosurfaces - - :param Union[ScalarField3D,ComplexField3D] volume: - Volume item to control - """ - - def __init__(self, volume): - super(VolumeIsoSurfacesRow, self).__init__( - ('Isosurfaces', None)) - self._volume = weakref.ref(volume) - - volume.sigIsosurfaceAdded.connect(self._isosurfaceAdded) - volume.sigIsosurfaceRemoved.connect(self._isosurfaceRemoved) - - if isinstance(volume, items.ComplexMixIn): - self.addRow(ComplexModeRow(volume, "Complex Mode")) - - for item in volume.getIsosurfaces(): - self.addRow(nodeFromItem(item)) - - self.addRow(AddIsosurfaceRow(volume)) - - def volume(self): - """Returns the controlled volume item - - :rtype: Union[ScalarField3D,ComplexField3D] - """ - return self._volume() - - def _isosurfaceAdded(self, item): - """Handle isosurface addition - - :param Isosurface item: added isosurface - """ - volume = self.volume() - if volume is None: - return - - row = volume.getIsosurfaces().index(item) - if isinstance(volume, items.ComplexMixIn): - row += 1 # Offset for the ComplexModeRow - self.addRow(nodeFromItem(item), row) - - def _isosurfaceRemoved(self, item): - """Handle isosurface removal - - :param Isosurface item: removed isosurface - """ - volume = self.volume() - if volume is None: - return - - # Find item - for row in self.children(): - if isinstance(row, IsosurfaceRow) and 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, ItemProxyRow): - """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 - ItemProxyRow.__init__(self, - item=item, - name='Line width', - fget=item.getLineWidth, - fset=item.setLineWidth, - events=items.ItemChangedType.LINE_WIDTH, - 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(ItemProxyRow( - item=item, - name='Mode', - fget=item.getVisualization, - fset=item.setVisualization, - events=items.ItemChangedType.VISUALIZATION_MODE, - editorHint=[m.value.title() for m in item.supportedVisualizations()], - toModelData=lambda data: data.value.title(), - fromModelData=lambda data: data.lower())) - - node.addRow(ItemProxyRow( - item=item, - name='Height map', - fget=item.isHeightMap, - fset=item.setHeightMap, - events=items.Item3DChangedType.HEIGHT_MAP)) - - node.addRow(ColormapRow(item)) - - node.addRow(Scatter2DSymbolRow(item)) - node.addRow(Scatter2DSymbolSizeRow(item)) - - node.addRow(Scatter2DLineWidth(item)) - - -def initVolumeNode(node, item): - """Specific node init for volume items - - :param Item3DRow node: The model node to setup - :param Union[ScalarField3D,ComplexField3D] item: - The volume item represented by the node - """ - node.addRow(nodeFromItem(item.getCutPlanes()[0])) # Add cut plane - node.addRow(VolumeIsoSurfacesRow(item)) - - -def initVolumeCutPlaneNode(node, item): - """Specific node init for volume CutPlane - - :param Item3DRow node: The model node to setup - :param CutPlane item: The CutPlane the node is representing - """ - if isinstance(item, items.ComplexMixIn): - node.addRow(ComplexModeRow(item)) - - node.addRow(PlaneRow(item)) - - node.addRow(ColormapRow(item)) - - node.addRow(ItemProxyRow( - item=item, - name='Show <=Min', - fget=item.getDisplayValuesBelowMin, - fset=item.setDisplayValuesBelowMin, - events=items.ItemChangedType.ALPHA)) - - node.addRow(InterpolationRow(item)) - - -NODE_SPECIFIC_INIT = [ # class, init(node, item) - (items.Scatter2D, initScatter2DNode), - (items.ScalarField3D, initVolumeNode), - (CutPlane, initVolumeCutPlaneNode), -] -"""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, ComplexIsosurface): - return ComplexIsosurfaceRow(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 |