summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d')
-rw-r--r--silx/gui/plot3d/_model/items.py96
-rw-r--r--silx/gui/plot3d/items/_pick.py39
-rw-r--r--silx/gui/plot3d/items/scatter.py3
-rw-r--r--silx/gui/plot3d/items/volume.py151
-rw-r--r--silx/gui/plot3d/scene/primitives.py9
-rw-r--r--silx/gui/plot3d/tools/PositionInfoWidget.py2
6 files changed, 230 insertions, 70 deletions
diff --git a/silx/gui/plot3d/_model/items.py b/silx/gui/plot3d/_model/items.py
index 9fe3e51..7f3921a 100644
--- a/silx/gui/plot3d/_model/items.py
+++ b/silx/gui/plot3d/_model/items.py
@@ -45,7 +45,7 @@ from ...utils.image import convertArrayToQImage
from ...colors import preferredColormaps
from ... import qt, icons
from .. import items
-from ..items.volume import Isosurface, CutPlane
+from ..items.volume import Isosurface, CutPlane, ComplexIsosurface
from ..Plot3DWidget import Plot3DWidget
@@ -867,6 +867,17 @@ class ColormapRow(_ColormapBaseProxyRow):
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
@@ -908,13 +919,9 @@ class ColormapRow(_ColormapBaseProxyRow):
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)
+ return self.getColormapImage()
+ else:
+ return super(ColormapRow, self).data(column, role)
class SymbolRow(ItemProxyRow):
@@ -1055,12 +1062,12 @@ class ComplexModeRow(ItemProxyRow):
:param Item3D item: Scene item with symbol property
"""
- def __init__(self, item):
+ def __init__(self, item, name='Mode'):
names = [m.value.replace('_', ' ').title()
for m in item.supportedComplexModes()]
super(ComplexModeRow, self).__init__(
item=item,
- name='Mode',
+ name=name,
fget=item.getComplexMode,
fset=item.setComplexMode,
events=items.ItemChangedType.COMPLEX_MODE,
@@ -1283,6 +1290,71 @@ class IsosurfaceRow(Item3DRow):
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
@@ -1358,7 +1430,7 @@ class VolumeIsoSurfacesRow(StaticRow):
volume.sigIsosurfaceRemoved.connect(self._isosurfaceRemoved)
if isinstance(volume, items.ComplexMixIn):
- self.addRow(ComplexModeRow(volume))
+ self.addRow(ComplexModeRow(volume, "Complex Mode"))
for item in volume.getIsosurfaces():
self.addRow(nodeFromItem(item))
@@ -1581,6 +1653,8 @@ def nodeFromItem(item):
# 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)
diff --git a/silx/gui/plot3d/items/_pick.py b/silx/gui/plot3d/items/_pick.py
index b35ef0d..8494723 100644
--- a/silx/gui/plot3d/items/_pick.py
+++ b/silx/gui/plot3d/items/_pick.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2019 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
@@ -34,6 +34,7 @@ __date__ = "24/09/2018"
import logging
import numpy
+from ...plot.items._pick import PickingResult as _PickingResult
from ..scene import Viewport, Base
@@ -177,9 +178,8 @@ class PickContext(object):
return rayObject
-class PickingResult(object):
- """Class to access picking information in a 3D scene.
- """
+class PickingResult(_PickingResult):
+ """Class to access picking information in a 3D scene."""
def __init__(self, item, positions, indices=None, fetchdata=None):
"""Init
@@ -194,7 +194,8 @@ class PickingResult(object):
to provide an alternative function to access item data.
Default is to use `item.getData`.
"""
- self._item = item
+ super(PickingResult, self).__init__(item, indices)
+
self._objectPositions = numpy.array(
positions, copy=False, dtype=numpy.float)
@@ -205,36 +206,8 @@ class PickingResult(object):
self._scenePositions = None
self._ndcPositions = None
- if indices is None:
- self._indices = None
- else:
- self._indices = numpy.array(indices, copy=False, dtype=numpy.int)
-
self._fetchdata = fetchdata
- def getItem(self):
- """Returns the item this results corresponds to.
-
- :rtype: ~silx.gui.plot3d.items.Item3D
- """
- return self._item
-
- def getIndices(self, copy=True):
- """Returns indices of picked data.
-
- If data is 1D, it returns a numpy.ndarray, otherwise
- it returns a tuple with as many numpy.ndarray as there are
- dimensions in the data.
-
- :param bool copy: True (default) to get a copy,
- False to return internal arrays
- :rtype: Union[None,numpy.ndarray,List[numpy.ndarray]]
- """
- if self._indices is None:
- return None
- indices = numpy.array(self._indices, copy=copy)
- return indices if indices.ndim == 1 else tuple(indices)
-
def getData(self, copy=True):
"""Returns picked data values
diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py
index e8ffee1..5fce629 100644
--- a/silx/gui/plot3d/items/scatter.py
+++ b/silx/gui/plot3d/items/scatter.py
@@ -234,6 +234,9 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
}
"""Dict {visualization mode: property names used in this mode}"""
+ _SUPPORTED_SCATTER_VISUALIZATION = tuple(_VISUALIZATION_PROPERTIES.keys())
+ """Overrides supported Visualizations"""
+
def __init__(self, parent=None):
DataItem3D.__init__(self, parent=parent)
ColormapMixIn.__init__(self)
diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py
index ae91e82..e0a2a1f 100644
--- a/silx/gui/plot3d/items/volume.py
+++ b/silx/gui/plot3d/items/volume.py
@@ -37,13 +37,14 @@ import numpy
from silx.math.combo import min_max
from silx.math.marchingcubes import MarchingCubes
+from silx.math.interpolate import interp3d
from ....utils.proxy import docstring
from ... import _glutils as glu
from ... import qt
from ...colors import rgba
-from ..scene import cutplane, primitives, transform, utils
+from ..scene import cutplane, function, primitives, transform, utils
from .core import BaseNodeItem, Item3D, ItemChangedType, Item3DChangedType
from .mixins import ColormapMixIn, ComplexMixIn, InterpolationMixIn, PlaneMixIn
@@ -301,6 +302,15 @@ class Isosurface(Item3D):
"""Return the color of this iso-surface (QColor)"""
return qt.QColor.fromRgbF(*self._color)
+ def _updateColor(self, color):
+ """Handle update of color
+
+ :param List[float] color: RGBA channels in [0, 1]
+ """
+ primitive = self._getScenePrimitive()
+ if len(primitive.children) != 0:
+ primitive.children[0].setAttribute('color', color)
+
def setColor(self, color):
"""Set the color of the iso-surface
@@ -310,15 +320,15 @@ class Isosurface(Item3D):
color = rgba(color)
if color != self._color:
self._color = color
- primitive = self._getScenePrimitive()
- if len(primitive.children) != 0:
- primitive.children[0].setAttribute('color', self._color)
+ self._updateColor(self._color)
self._updated(ItemChangedType.COLOR)
- def _updateScenePrimitive(self):
- """Update underlying mesh"""
- self._getScenePrimitive().children = []
+ def _computeIsosurface(self):
+ """Compute isosurface for current state.
+ :return: (vertices, normals, indices) arrays
+ :rtype: List[Union[None,numpy.ndarray]]
+ """
data = self.getData(copy=False)
if data is None:
@@ -349,24 +359,31 @@ class Isosurface(Item3D):
self._level = level
self._updated(Item3DChangedType.ISO_LEVEL)
- if not numpy.isfinite(self._level):
- return
+ if numpy.isfinite(self._level):
+ st = time.time()
+ vertices, normals, indices = MarchingCubes(
+ data,
+ isolevel=self._level)
+ _logger.info('Computed iso-surface in %f s.', time.time() - st)
- st = time.time()
- vertices, normals, indices = MarchingCubes(
- data,
- isolevel=self._level)
- _logger.info('Computed iso-surface in %f s.', time.time() - st)
+ if len(vertices) != 0:
+ return vertices, normals, indices
- if len(vertices) == 0:
- return
- else:
- mesh = primitives.Mesh3D(vertices,
- colors=self._color,
- normals=normals,
- mode='triangles',
- indices=indices)
- self._getScenePrimitive().children = [mesh]
+ return None, None, None
+
+ def _updateScenePrimitive(self):
+ """Update underlying mesh"""
+ self._getScenePrimitive().children = []
+
+ vertices, normals, indices = self._computeIsosurface()
+ if vertices is not None:
+ mesh = primitives.Mesh3D(vertices,
+ colors=self._color,
+ normals=normals,
+ mode='triangles',
+ indices=indices,
+ copy=False)
+ self._getScenePrimitive().children = [mesh]
def _pickFull(self, context):
"""Perform picking in this item at given widget position.
@@ -677,17 +694,39 @@ class ComplexCutPlane(CutPlane, ComplexMixIn):
super(ComplexCutPlane, self)._updated(event)
-class ComplexIsosurface(Isosurface):
+class ComplexIsosurface(Isosurface, ComplexMixIn, ColormapMixIn):
"""Class representing an iso-surface in a :class:`ComplexField3D` item.
:param parent: The DataItem3D this iso-surface belongs to
"""
+ _SUPPORTED_COMPLEX_MODES = \
+ (ComplexMixIn.ComplexMode.NONE,) + ComplexMixIn._SUPPORTED_COMPLEX_MODES
+ """Overrides supported ComplexMode"""
+
def __init__(self, parent):
- super(ComplexIsosurface, self).__init__(parent)
+ ComplexMixIn.__init__(self)
+ ColormapMixIn.__init__(self, function.Colormap())
+ Isosurface.__init__(self, parent=parent)
+ self.setComplexMode(self.ComplexMode.NONE)
+
+ def _updateColor(self, color):
+ """Handle update of color
+
+ :param List[float] color: RGBA channels in [0, 1]
+ """
+ primitive = self._getScenePrimitive()
+ if (len(primitive.children) != 0 and
+ isinstance(primitive.children[0], primitives.ColormapMesh3D)):
+ primitive.children[0].alpha = self._color[3]
+ else:
+ super(ComplexIsosurface, self)._updateColor(color)
def _syncDataWithParent(self):
"""Synchronize this instance data with that of its parent"""
+ if self.getComplexMode() != self.ComplexMode.NONE:
+ self._setRangeFromData(self.getColormappedData(copy=False))
+
parent = self.parent()
if parent is None:
self._data = None
@@ -702,6 +741,67 @@ class ComplexIsosurface(Isosurface):
self._syncDataWithParent()
super(ComplexIsosurface, self)._parentChanged(event)
+ def getColormappedData(self, copy=True):
+ """Return 3D dataset used to apply the colormap on the isosurface.
+
+ This depends on :meth:`getComplexMode`.
+
+ :param bool copy:
+ True (default) to get a copy,
+ False to get the internal data (DO NOT modify!)
+ :return: The data set (or None if not set)
+ :rtype: Union[numpy.ndarray,None]
+ """
+ if self.getComplexMode() == self.ComplexMode.NONE:
+ return None
+ else:
+ parent = self.parent()
+ if parent is None:
+ return None
+ else:
+ return parent.getData(mode=self.getComplexMode(), copy=copy)
+
+ def _updated(self, event=None):
+ """Handle update of the isosurface (and take care of mode change)
+
+ :param ItemChangedType event: The kind of update
+ """
+ if (event == ItemChangedType.COMPLEX_MODE and
+ self.getComplexMode() != self.ComplexMode.NONE):
+ self._setRangeFromData(self.getColormappedData(copy=False))
+
+ if event in (ItemChangedType.COMPLEX_MODE,
+ ItemChangedType.COLORMAP,
+ Item3DChangedType.INTERPOLATION):
+ self._updateScenePrimitive()
+ super(ComplexIsosurface, self)._updated(event)
+
+ def _updateScenePrimitive(self):
+ """Update underlying mesh"""
+ if self.getComplexMode() == self.ComplexMode.NONE:
+ super(ComplexIsosurface, self)._updateScenePrimitive()
+
+ else: # Specific display for colormapped isosurface
+ self._getScenePrimitive().children = []
+
+ values = self.getColormappedData(copy=False)
+ if values is not None:
+ vertices, normals, indices = self._computeIsosurface()
+ if vertices is not None:
+ values = interp3d(values, vertices, method='linear_omp')
+ # TODO reuse isosurface when only color changes...
+
+ mesh = primitives.ColormapMesh3D(
+ vertices,
+ value=values.reshape(-1, 1),
+ colormap=self._getSceneColormap(),
+ normal=normals,
+ mode='triangles',
+ indices=indices,
+ copy=False)
+ mesh.alpha = self._color[3]
+ self._getScenePrimitive().children = [mesh]
+
class ComplexField3D(ScalarField3D, ComplexMixIn):
"""3D complex field on a regular grid.
@@ -720,6 +820,7 @@ class ComplexField3D(ScalarField3D, ComplexMixIn):
@docstring(ComplexMixIn)
def setComplexMode(self, mode):
+ mode = ComplexMixIn.ComplexMode.from_value(mode)
if mode != self.getComplexMode():
self.clearIsosurfaces() # Reset isosurfaces
ComplexMixIn.setComplexMode(self, mode)
diff --git a/silx/gui/plot3d/scene/primitives.py b/silx/gui/plot3d/scene/primitives.py
index 08724ba..7db61e8 100644
--- a/silx/gui/plot3d/scene/primitives.py
+++ b/silx/gui/plot3d/scene/primitives.py
@@ -1874,6 +1874,8 @@ class ColormapMesh3D(Geometry):
}
""",
string.Template("""
+ uniform float alpha;
+
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -1889,6 +1891,7 @@ class ColormapMesh3D(Geometry):
vec4 color = $colormapCall(vValue);
gl_FragColor = $lightingCall(color, vPosition, vNormal);
+ gl_FragColor.a *= alpha;
$scenePostCall(vCameraPosition);
}
@@ -1908,6 +1911,7 @@ class ColormapMesh3D(Geometry):
value=value,
copy=copy)
+ self._alpha = 1.0
self._lineWidth = 1.0
self._lineSmooth = True
self._culling = None
@@ -1922,6 +1926,10 @@ class ColormapMesh3D(Geometry):
converter=bool,
doc="Smooth line rendering enabled (bool, default: True)")
+ alpha = event.notifyProperty(
+ '_alpha', converter=float,
+ doc="Transparency of the mesh, float in [0, 1]")
+
@property
def culling(self):
"""Face culling (str)
@@ -1978,6 +1986,7 @@ class ColormapMesh3D(Geometry):
program.setUniformMatrix('transformMat',
ctx.objectToCamera.matrix,
safe=True)
+ gl.glUniform1f(program.uniforms['alpha'], self._alpha)
if self.drawMode in self._LINE_MODES:
gl.glLineWidth(self.lineWidth)
diff --git a/silx/gui/plot3d/tools/PositionInfoWidget.py b/silx/gui/plot3d/tools/PositionInfoWidget.py
index fc86a7f..52a6163 100644
--- a/silx/gui/plot3d/tools/PositionInfoWidget.py
+++ b/silx/gui/plot3d/tools/PositionInfoWidget.py
@@ -189,7 +189,7 @@ class PositionInfoWidget(qt.QWidget):
return # No picked item
item = picking.getItem()
- self._itemLabel.setText(item.getLabel())
+ self._itemLabel.setText(item.getName())
positions = picking.getPositions('scene', copy=False)
x, y, z = positions[0]
self._xLabel.setText("%g" % x)