summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot3d/scene
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot3d/scene')
-rw-r--r--src/silx/gui/plot3d/scene/axes.py70
-rw-r--r--src/silx/gui/plot3d/scene/camera.py117
-rw-r--r--src/silx/gui/plot3d/scene/core.py36
-rw-r--r--src/silx/gui/plot3d/scene/cutplane.py137
-rw-r--r--src/silx/gui/plot3d/scene/event.py41
-rw-r--r--src/silx/gui/plot3d/scene/function.py152
-rw-r--r--src/silx/gui/plot3d/scene/interaction.py340
-rw-r--r--src/silx/gui/plot3d/scene/primitives.py991
-rw-r--r--src/silx/gui/plot3d/scene/test/test_transform.py39
-rw-r--r--src/silx/gui/plot3d/scene/test/test_utils.py168
-rw-r--r--src/silx/gui/plot3d/scene/text.py241
-rw-r--r--src/silx/gui/plot3d/scene/transform.py304
-rw-r--r--src/silx/gui/plot3d/scene/utils.py112
-rw-r--r--src/silx/gui/plot3d/scene/viewport.py147
-rw-r--r--src/silx/gui/plot3d/scene/window.py107
15 files changed, 1665 insertions, 1337 deletions
diff --git a/src/silx/gui/plot3d/scene/axes.py b/src/silx/gui/plot3d/scene/axes.py
index 9f6ac6c..9102732 100644
--- a/src/silx/gui/plot3d/scene/axes.py
+++ b/src/silx/gui/plot3d/scene/axes.py
@@ -40,40 +40,37 @@ _logger = logging.getLogger(__name__)
class LabelledAxes(primitives.GroupBBox):
- """A group displaying a bounding box with axes labels around its children.
- """
+ """A group displaying a bounding box with axes labels around its children."""
def __init__(self):
super(LabelledAxes, self).__init__()
self._ticksForBounds = None
- self._font = text.Font()
+ self._font = text.Font(size=10)
self._boxVisibility = True
# TODO offset labels from anchor in pixels
self._xlabel = text.Text2D(font=self._font)
- self._xlabel.align = 'center'
- self._xlabel.transforms = [self._boxTransforms,
- transform.Translate(tx=0.5)]
+ self._xlabel.align = "center"
+ self._xlabel.transforms = [self._boxTransforms, transform.Translate(tx=0.5)]
self._children.insert(-1, self._xlabel)
self._ylabel = text.Text2D(font=self._font)
- self._ylabel.align = 'center'
- self._ylabel.transforms = [self._boxTransforms,
- transform.Translate(ty=0.5)]
+ self._ylabel.align = "center"
+ self._ylabel.transforms = [self._boxTransforms, transform.Translate(ty=0.5)]
self._children.insert(-1, self._ylabel)
self._zlabel = text.Text2D(font=self._font)
- self._zlabel.align = 'center'
- self._zlabel.transforms = [self._boxTransforms,
- transform.Translate(tz=0.5)]
+ self._zlabel.align = "center"
+ self._zlabel.transforms = [self._boxTransforms, transform.Translate(tz=0.5)]
self._children.insert(-1, self._zlabel)
# Init tick lines with dummy pos
self._tickLines = primitives.DashedLines(
- positions=((0., 0., 0.), (0., 0., 0.)))
+ positions=((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))
+ )
self._tickLines.dash = 5, 10
self._tickLines.visible = False
self._children.insert(-1, self._tickLines)
@@ -82,7 +79,7 @@ class LabelledAxes(primitives.GroupBBox):
self._children.insert(-1, self._tickLabels)
# Sync color
- self.tickColor = 1., 1., 1., 1.
+ self.tickColor = 1.0, 1.0, 1.0, 1.0
def _updateBoxAndAxes(self):
"""Update bbox and axes position and size according to children.
@@ -93,7 +90,7 @@ class LabelledAxes(primitives.GroupBBox):
bounds = self._group.bounds(dataBounds=True)
if bounds is not None:
- tx, ty, tz = (bounds[1] - bounds[0]) / 2.
+ tx, ty, tz = (bounds[1] - bounds[0]) / 2.0
else:
tx, ty, tz = 0.5, 0.5, 0.5
@@ -116,7 +113,7 @@ class LabelledAxes(primitives.GroupBBox):
self._ylabel.foreground = color
self._zlabel.foreground = color
transparentColor = color[0], color[1], color[2], color[3] * 0.6
- self._tickLines.setAttribute('color', transparentColor)
+ self._tickLines.setAttribute("color", transparentColor)
for label in self._tickLabels.children:
label.foreground = color
@@ -185,8 +182,9 @@ class LabelledAxes(primitives.GroupBBox):
self._tickLines.visible = False
self._tickLabels.children = [] # Reset previous labels
- elif (self._ticksForBounds is None or
- not numpy.all(numpy.equal(bounds, self._ticksForBounds))):
+ elif self._ticksForBounds is None or not numpy.all(
+ numpy.equal(bounds, self._ticksForBounds)
+ ):
self._ticksForBounds = bounds
# Update ticks
@@ -198,21 +196,21 @@ class LabelledAxes(primitives.GroupBBox):
# Update tick lines
coords = numpy.empty(
- ((len(xticks) + len(yticks) + len(zticks)), 4, 3),
- dtype=numpy.float32)
+ ((len(xticks) + len(yticks) + len(zticks)), 4, 3), dtype=numpy.float32
+ )
coords[:, :, :] = bounds[0, :] # account for offset from origin
- xcoords = coords[:len(xticks)]
+ xcoords = coords[: len(xticks)]
xcoords[:, :, 0] = numpy.asarray(xticks)[:, numpy.newaxis]
xcoords[:, 1, 1] += ticklength[1] # X ticks on XY plane
xcoords[:, 3, 2] += ticklength[2] # X ticks on XZ plane
- ycoords = coords[len(xticks):len(xticks) + len(yticks)]
+ ycoords = coords[len(xticks) : len(xticks) + len(yticks)]
ycoords[:, :, 1] = numpy.asarray(yticks)[:, numpy.newaxis]
ycoords[:, 1, 0] += ticklength[0] # Y ticks on XY plane
ycoords[:, 3, 2] += ticklength[2] # Y ticks on YZ plane
- zcoords = coords[len(xticks) + len(yticks):]
+ zcoords = coords[len(xticks) + len(yticks) :]
zcoords[:, :, 2] = numpy.asarray(zticks)[:, numpy.newaxis]
zcoords[:, 1, 0] += ticklength[0] # Z ticks on XZ plane
zcoords[:, 3, 1] += ticklength[1] # Z ticks on YZ plane
@@ -222,30 +220,36 @@ class LabelledAxes(primitives.GroupBBox):
# Update labels
color = self.tickColor
- offsets = bounds[0] - ticklength / 20.
+ offsets = bounds[0] - ticklength / 20.0
labels = []
for tick, label in zip(xticks, xlabels):
text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
+ text2d.align = "center"
+ text2d.valign = "center"
text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=tick, ty=offsets[1], tz=offsets[2])]
+ text2d.transforms = [
+ transform.Translate(tx=tick, ty=offsets[1], tz=offsets[2])
+ ]
labels.append(text2d)
for tick, label in zip(yticks, ylabels):
text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
+ text2d.align = "center"
+ text2d.valign = "center"
text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=offsets[0], ty=tick, tz=offsets[2])]
+ text2d.transforms = [
+ transform.Translate(tx=offsets[0], ty=tick, tz=offsets[2])
+ ]
labels.append(text2d)
for tick, label in zip(zticks, zlabels):
text2d = text.Text2D(text=label, font=self.font)
- text2d.align = 'center'
+ text2d.align = "center"
+ text2d.valign = "center"
text2d.foreground = color
- text2d.transforms = [transform.Translate(
- tx=offsets[0], ty=offsets[1], tz=tick)]
+ text2d.transforms = [
+ transform.Translate(tx=offsets[0], ty=offsets[1], tz=tick)
+ ]
labels.append(text2d)
self._tickLabels.children = labels # Reset previous labels
diff --git a/src/silx/gui/plot3d/scene/camera.py b/src/silx/gui/plot3d/scene/camera.py
index a6bc642..5248c39 100644
--- a/src/silx/gui/plot3d/scene/camera.py
+++ b/src/silx/gui/plot3d/scene/camera.py
@@ -35,6 +35,7 @@ from . import transform
# CameraExtrinsic #############################################################
+
class CameraExtrinsic(transform.Transform):
"""Transform matrix to handle camera position and orientation.
@@ -46,21 +47,19 @@ class CameraExtrinsic(transform.Transform):
:type up: numpy.ndarray-like of 3 float32.
"""
- def __init__(self, position=(0., 0., 0.),
- direction=(0., 0., -1.),
- up=(0., 1., 0.)):
-
+ def __init__(
+ self, position=(0.0, 0.0, 0.0), direction=(0.0, 0.0, -1.0), up=(0.0, 1.0, 0.0)
+ ):
super(CameraExtrinsic, self).__init__()
self._position = None
self.position = position # set _position
- self._side = 1., 0., 0.
- self._up = 0., 1., 0.
- self._direction = 0., 0., -1.
+ self._side = 1.0, 0.0, 0.0
+ self._up = 0.0, 1.0, 0.0
+ self._direction = 0.0, 0.0, -1.0
self.setOrientation(direction=direction, up=up) # set _direction, _up
def _makeMatrix(self):
- return transform.mat4LookAtDir(self._position,
- self._direction, self._up)
+ return transform.mat4LookAtDir(self._position, self._direction, self._up)
def copy(self):
"""Return an independent copy"""
@@ -93,8 +92,8 @@ class CameraExtrinsic(transform.Transform):
# Update side and up to make sure they are perpendicular and normalized
side = numpy.cross(direction, up)
sidenormal = numpy.linalg.norm(side)
- if sidenormal == 0.:
- raise RuntimeError('direction and up vectors are parallel.')
+ if sidenormal == 0.0:
+ raise RuntimeError("direction and up vectors are parallel.")
# Alternative: when one of the input parameter is None, it is
# possible to guess correct vectors using previous direction and up
side /= sidenormal
@@ -128,8 +127,7 @@ class CameraExtrinsic(transform.Transform):
@property
def up(self):
- """Vector pointing upward in the image plane (ndarray of 3 float32).
- """
+ """Vector pointing upward in the image plane (ndarray of 3 float32)."""
return self._up.copy()
@up.setter
@@ -143,7 +141,7 @@ class CameraExtrinsic(transform.Transform):
ndarray of 3 float32"""
return self._side.copy()
- def move(self, direction, step=1.):
+ def move(self, direction, step=1.0):
"""Move the camera relative to the image plane.
:param str direction: Direction relative to image plane.
@@ -152,35 +150,35 @@ class CameraExtrinsic(transform.Transform):
:param float step: The step of the pan to perform in the coordinate
in which the camera position is defined.
"""
- if direction in ('up', 'down'):
- vector = self.up * (1. if direction == 'up' else -1.)
- elif direction in ('left', 'right'):
- vector = self.side * (1. if direction == 'right' else -1.)
- elif direction in ('forward', 'backward'):
- vector = self.direction * (1. if direction == 'forward' else -1.)
+ if direction in ("up", "down"):
+ vector = self.up * (1.0 if direction == "up" else -1.0)
+ elif direction in ("left", "right"):
+ vector = self.side * (1.0 if direction == "right" else -1.0)
+ elif direction in ("forward", "backward"):
+ vector = self.direction * (1.0 if direction == "forward" else -1.0)
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
self.position += step * vector
- def rotate(self, direction, angle=1.):
+ def rotate(self, direction, angle=1.0):
"""First-person rotation of the camera towards the direction.
:param str direction: Direction of movement relative to image plane.
In: 'up', 'down', 'left', 'right'.
:param float angle: The angle in degrees of the rotation.
"""
- if direction in ('up', 'down'):
- axis = self.side * (1. if direction == 'up' else -1.)
- elif direction in ('left', 'right'):
- axis = self.up * (1. if direction == 'left' else -1.)
+ if direction in ("up", "down"):
+ axis = self.side * (1.0 if direction == "up" else -1.0)
+ elif direction in ("left", "right"):
+ axis = self.up * (1.0 if direction == "left" else -1.0)
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
matrix = transform.mat4RotateFromAngleAxis(numpy.radians(angle), *axis)
newdir = numpy.dot(matrix[:3, :3], self.direction)
- if direction in ('up', 'down'):
+ if direction in ("up", "down"):
# Rotate up to avoid up and new direction to be (almost) co-linear
newup = numpy.dot(matrix[:3, :3], self.up)
self.setOrientation(newdir, newup)
@@ -188,7 +186,7 @@ class CameraExtrinsic(transform.Transform):
# No need to rotate up here as it is the rotation axis
self.direction = newdir
- def orbit(self, direction, center=(0., 0., 0.), angle=1.):
+ def orbit(self, direction, center=(0.0, 0.0, 0.0), angle=1.0):
"""Rotate the camera around a point.
:param str direction: Direction of movement relative to image plane.
@@ -197,33 +195,32 @@ class CameraExtrinsic(transform.Transform):
:type center: numpy.ndarray-like of 3 float32.
:param float angle: he angle in degrees of the rotation.
"""
- if direction in ('up', 'down'):
- axis = self.side * (1. if direction == 'down' else -1.)
- elif direction in ('left', 'right'):
- axis = self.up * (1. if direction == 'right' else -1.)
+ if direction in ("up", "down"):
+ axis = self.side * (1.0 if direction == "down" else -1.0)
+ elif direction in ("left", "right"):
+ axis = self.up * (1.0 if direction == "right" else -1.0)
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
# Rotate viewing direction
- rotmatrix = transform.mat4RotateFromAngleAxis(
- numpy.radians(angle), *axis)
+ rotmatrix = transform.mat4RotateFromAngleAxis(numpy.radians(angle), *axis)
self.direction = numpy.dot(rotmatrix[:3, :3], self.direction)
# Rotate position around center
center = numpy.array(center, copy=False, dtype=numpy.float32)
matrix = numpy.dot(transform.mat4Translate(*center), rotmatrix)
matrix = numpy.dot(matrix, transform.mat4Translate(*(-center)))
- position = numpy.append(self.position, 1.)
+ position = numpy.append(self.position, 1.0)
self.position = numpy.dot(matrix, position)[:3]
_RESET_CAMERA_ORIENTATIONS = {
- 'side': ((-1., -1., -1.), (0., 1., 0.)),
- 'front': ((0., 0., -1.), (0., 1., 0.)),
- 'back': ((0., 0., 1.), (0., 1., 0.)),
- 'top': ((0., -1., 0.), (0., 0., -1.)),
- 'bottom': ((0., 1., 0.), (0., 0., 1.)),
- 'right': ((-1., 0., 0.), (0., 1., 0.)),
- 'left': ((1., 0., 0.), (0., 1., 0.))
+ "side": ((-1.0, -1.0, -1.0), (0.0, 1.0, 0.0)),
+ "front": ((0.0, 0.0, -1.0), (0.0, 1.0, 0.0)),
+ "back": ((0.0, 0.0, 1.0), (0.0, 1.0, 0.0)),
+ "top": ((0.0, -1.0, 0.0), (0.0, 0.0, -1.0)),
+ "bottom": ((0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
+ "right": ((-1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
+ "left": ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
}
def reset(self, face=None):
@@ -233,12 +230,12 @@ class CameraExtrinsic(transform.Transform):
side, front, back, top, bottom, right, left.
"""
if face not in self._RESET_CAMERA_ORIENTATIONS:
- raise ValueError('Unsupported face: %s' % face)
+ raise ValueError("Unsupported face: %s" % face)
distance = numpy.linalg.norm(self.position)
direction, up = self._RESET_CAMERA_ORIENTATIONS[face]
self.setOrientation(direction, up)
- self.position = - self.direction * distance
+ self.position = -self.direction * distance
class Camera(transform.Transform):
@@ -260,9 +257,16 @@ class Camera(transform.Transform):
:type up: numpy.ndarray-like of 3 float32.
"""
- def __init__(self, fovy=30., near=0.1, far=1., size=(1., 1.),
- position=(0., 0., 0.),
- direction=(0., 0., -1.), up=(0., 1., 0.)):
+ def __init__(
+ self,
+ fovy=30.0,
+ near=0.1,
+ far=1.0,
+ size=(1.0, 1.0),
+ position=(0.0, 0.0, 0.0),
+ direction=(0.0, 0.0, -1.0),
+ up=(0.0, 1.0, 0.0),
+ ):
super(Camera, self).__init__()
self._intrinsic = transform.Perspective(fovy, near, far, size)
self._intrinsic.addListener(self._transformChanged)
@@ -289,8 +293,8 @@ class Camera(transform.Transform):
center = 0.5 * (bounds[0] + bounds[1])
radius = numpy.linalg.norm(0.5 * (bounds[1] - bounds[0]))
- if radius == 0.: # bounds are all collapsed
- radius = 1.
+ if radius == 0.0: # bounds are all collapsed
+ radius = 1.0
if isinstance(self.intrinsic, transform.Perspective):
# Get the viewpoint distance from the bounds center
@@ -302,8 +306,7 @@ class Camera(transform.Transform):
offset = radius / numpy.sin(0.5 * minfov)
# Update camera
- self.extrinsic.position = \
- center - offset * self.extrinsic.direction
+ self.extrinsic.position = center - offset * self.extrinsic.direction
self.intrinsic.setDepthExtent(offset - radius, offset + radius)
elif isinstance(self.intrinsic, transform.Orthographic):
@@ -312,14 +315,14 @@ class Camera(transform.Transform):
left=center[0] - radius,
right=center[0] + radius,
bottom=center[1] - radius,
- top=center[1] + radius)
+ top=center[1] + radius,
+ )
# Update camera
self.extrinsic.position = 0, 0, 0
- self.intrinsic.setDepthExtent(center[2] - radius,
- center[2] + radius)
+ self.intrinsic.setDepthExtent(center[2] - radius, center[2] + radius)
else:
- raise RuntimeError('Unsupported camera: %s' % self.intrinsic)
+ raise RuntimeError("Unsupported camera: %s" % self.intrinsic)
@property
def intrinsic(self):
diff --git a/src/silx/gui/plot3d/scene/core.py b/src/silx/gui/plot3d/scene/core.py
index c32a2c1..8773301 100644
--- a/src/silx/gui/plot3d/scene/core.py
+++ b/src/silx/gui/plot3d/scene/core.py
@@ -49,6 +49,7 @@ from .viewport import Viewport
# Nodes #######################################################################
+
class Base(event.Notifier):
"""A scene node with common features."""
@@ -64,10 +65,8 @@ class Base(event.Notifier):
# notifying properties
- visible = event.notifyProperty('_visible',
- doc="Visibility flag of the node")
- pickable = event.notifyProperty('_pickable',
- doc="True to make node pickable")
+ visible = event.notifyProperty("_visible", doc="Visibility flag of the node")
+ pickable = event.notifyProperty("_pickable", doc="True to make node pickable")
# Access to tree path
@@ -84,7 +83,7 @@ class Base(event.Notifier):
:param Base parent: The parent.
"""
if parent is not None and self._parentRef is not None:
- raise RuntimeError('Trying to add a node at two places.')
+ raise RuntimeError("Trying to add a node at two places.")
# Alternative: remove it from previous children list
self._parentRef = None if parent is None else weakref.ref(parent)
@@ -96,11 +95,11 @@ class Base(event.Notifier):
then the :class:`Viewport` is the first element of path.
"""
if self.parent is None:
- return self,
+ return (self,)
elif isinstance(self.parent, Viewport):
return self.parent, self
else:
- return self.parent.path + (self, )
+ return self.parent.path + (self,)
@property
def viewport(self):
@@ -154,7 +153,7 @@ class Base(event.Notifier):
# If it is a TransformList, do not create one to enable sharing.
self._transforms = iterable
else:
- assert hasattr(iterable, '__iter__')
+ assert hasattr(iterable, "__iter__")
self._transforms = transform.TransformList(iterable)
self._transforms.addListener(self._transformChanged)
@@ -163,8 +162,9 @@ class Base(event.Notifier):
# Bounds
- _CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
- dtype=numpy.float32)
+ _CUBE_CORNERS = numpy.array(
+ list(itertools.product((0.0, 1.0), repeat=3)), dtype=numpy.float32
+ )
"""Unit cube corners used to transform bounds"""
def _bounds(self, dataBounds=False):
@@ -256,7 +256,8 @@ class PrivateGroup(Base):
def _listWillChangeHook(self, methodName, *args, **kwargs):
super(PrivateGroup.ChildrenList, self)._listWillChangeHook(
- methodName, *args, **kwargs)
+ methodName, *args, **kwargs
+ )
for item in self:
item._setParent(None)
@@ -264,7 +265,8 @@ class PrivateGroup(Base):
for item in self:
item._setParent(self._parentRef())
super(PrivateGroup.ChildrenList, self)._listWasChangedHook(
- methodName, *args, **kwargs)
+ methodName, *args, **kwargs
+ )
def __init__(self, parent, children):
self._parentRef = weakref.ref(parent)
@@ -303,8 +305,7 @@ class PrivateGroup(Base):
bounds = []
for child in self._children:
if child.visible:
- childBounds = child.bounds(
- transformed=True, dataBounds=dataBounds)
+ childBounds = child.bounds(transformed=True, dataBounds=dataBounds)
if childBounds is not None:
bounds.append(childBounds)
@@ -312,9 +313,10 @@ class PrivateGroup(Base):
return None
else:
bounds = numpy.array(bounds, dtype=numpy.float32)
- return numpy.array((bounds[:, 0, :].min(axis=0),
- bounds[:, 1, :].max(axis=0)),
- dtype=numpy.float32)
+ return numpy.array(
+ (bounds[:, 0, :].min(axis=0), bounds[:, 1, :].max(axis=0)),
+ dtype=numpy.float32,
+ )
def prepareGL2(self, ctx):
pass
diff --git a/src/silx/gui/plot3d/scene/cutplane.py b/src/silx/gui/plot3d/scene/cutplane.py
index bfd578f..f3b7494 100644
--- a/src/silx/gui/plot3d/scene/cutplane.py
+++ b/src/silx/gui/plot3d/scene/cutplane.py
@@ -42,7 +42,8 @@ from . import transform, utils
class ColormapMesh3D(Geometry):
"""A 3D mesh with color from a 3D texture."""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
@@ -67,7 +68,8 @@ class ColormapMesh3D(Geometry):
gl_Position = matrix * vec4(position, 1.0);
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -91,32 +93,41 @@ class ColormapMesh3D(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self, position, normal, data, copy=True,
- mode='triangles', indices=None, colormap=None):
+ """
+ ),
+ )
+
+ def __init__(
+ self,
+ position,
+ normal,
+ data,
+ copy=True,
+ mode="triangles",
+ indices=None,
+ colormap=None,
+ ):
assert mode in self._TRIANGLE_MODES
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
self._data = data
self._texture = None
self._update_texture = True
self._update_texture_filter = False
- self._alpha = 1.
+ self._alpha = 1.0
self._colormap = colormap or Colormap() # Default colormap
self._colormap.addListener(self._cmapChanged)
- self._interpolation = 'linear'
- super(ColormapMesh3D, self).__init__(mode,
- indices,
- position=position,
- normal=normal)
+ self._interpolation = "linear"
+ super(ColormapMesh3D, self).__init__(
+ mode, indices, position=position, normal=normal
+ )
self.isBackfaceVisible = True
- self.textureOffset = 0., 0., 0.
+ self.textureOffset = 0.0, 0.0, 0.0
"""Offset to add to texture coordinates"""
def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
self._data = data
self._update_texture = True
@@ -131,7 +142,7 @@ class ColormapMesh3D(Geometry):
@interpolation.setter
def interpolation(self, interpolation):
- assert interpolation in ('linear', 'nearest')
+ assert interpolation in ("linear", "nearest")
self._interpolation = interpolation
self._update_texture_filter = True
self.notify()
@@ -159,21 +170,24 @@ class ColormapMesh3D(Geometry):
if self._texture is not None:
self._texture.discard()
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
self._update_texture = False
self._update_texture_filter = False
self._texture = _glutils.Texture(
- gl.GL_R32F, self._data, gl.GL_RED,
+ gl.GL_R32F,
+ self._data,
+ gl.GL_RED,
minFilter=filter_,
magFilter=filter_,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
if self._update_texture_filter:
self._update_texture_filter = False
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
@@ -190,8 +204,8 @@ class ColormapMesh3D(Geometry):
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call
- )
+ colormapCall=self.colormap.call,
+ )
program = ctx.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -202,18 +216,16 @@ class ColormapMesh3D(Geometry):
gl.glCullFace(gl.GL_BACK)
gl.glEnable(gl.GL_CULL_FACE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ program.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
+ gl.glUniform1f(program.uniforms["alpha"], self._alpha)
shape = self._data.shape
- scales = 1./shape[2], 1./shape[1], 1./shape[0]
- gl.glUniform3f(program.uniforms['dataScale'], *scales)
- gl.glUniform3f(program.uniforms['texCoordsOffset'], *self.textureOffset)
+ scales = 1.0 / shape[2], 1.0 / shape[1], 1.0 / shape[0]
+ gl.glUniform3f(program.uniforms["dataScale"], *scales)
+ gl.glUniform3f(program.uniforms["texCoordsOffset"], *self.textureOffset)
- gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["data"], self._texture.texUnit)
ctx.setupProgram(program)
@@ -227,11 +239,11 @@ class ColormapMesh3D(Geometry):
class CutPlane(PlaneInGroup):
"""A cutting plane in a 3D texture"""
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0)):
self._data = None
self._mesh = None
- self._alpha = 1.
- self._interpolation = 'linear'
+ self._alpha = 1.0
+ self._interpolation = "linear"
self._colormap = Colormap()
super(CutPlane, self).__init__(point, normal)
@@ -243,7 +255,7 @@ class CutPlane(PlaneInGroup):
self._mesh = None
else:
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
self._data = data
if self._mesh is not None:
@@ -273,7 +285,7 @@ class CutPlane(PlaneInGroup):
@interpolation.setter
def interpolation(self, interpolation):
- assert interpolation in ('nearest', 'linear')
+ assert interpolation in ("nearest", "linear")
if interpolation != self.interpolation:
self._interpolation = interpolation
if self._mesh is not None:
@@ -282,45 +294,47 @@ class CutPlane(PlaneInGroup):
def prepareGL2(self, ctx):
if self.isValid:
-
contourVertices = self.contourVertices
if self._mesh is None and self._data is not None:
- self._mesh = ColormapMesh3D(contourVertices,
- normal=self.plane.normal,
- data=self._data,
- copy=False,
- mode='fan',
- colormap=self.colormap)
+ self._mesh = ColormapMesh3D(
+ contourVertices,
+ normal=self.plane.normal,
+ data=self._data,
+ copy=False,
+ mode="fan",
+ colormap=self.colormap,
+ )
self._mesh.alpha = self._alpha
self._mesh.interpolation = self.interpolation
self._children.insert(0, self._mesh)
if self._mesh is not None:
- if (contourVertices is None or
- len(contourVertices) == 0):
+ if contourVertices is None or len(contourVertices) == 0:
self._mesh.visible = False
else:
self._mesh.visible = True
- self._mesh.setAttribute('normal', self.plane.normal)
- self._mesh.setAttribute('position', contourVertices)
+ self._mesh.setAttribute("normal", self.plane.normal)
+ self._mesh.setAttribute("position", contourVertices)
needTextureOffset = False
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
# If cut plane is co-linear with array bin edges add texture offset
planePt = self.plane.point
- for index, normal in enumerate(((1., 0., 0.),
- (0., 1., 0.),
- (0., 0., 1.))):
- if (numpy.all(numpy.equal(self.plane.normal, normal)) and
- int(planePt[index]) == planePt[index]):
+ for index, normal in enumerate(
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
+ ):
+ if (
+ numpy.all(numpy.equal(self.plane.normal, normal))
+ and int(planePt[index]) == planePt[index]
+ ):
needTextureOffset = True
break
if needTextureOffset:
self._mesh.textureOffset = self.plane.normal * 1e-6
else:
- self._mesh.textureOffset = 0., 0., 0.
+ self._mesh.textureOffset = 0.0, 0.0, 0.0
super(CutPlane, self).prepareGL2(ctx)
@@ -333,8 +347,8 @@ class CutPlane(PlaneInGroup):
vertices = self.contourVertices
if vertices is not None:
return numpy.array(
- (vertices.min(axis=0), vertices.max(axis=0)),
- dtype=numpy.float32)
+ (vertices.min(axis=0), vertices.max(axis=0)), dtype=numpy.float32
+ )
else:
return None # Plane in not slicing the data volume
else:
@@ -342,9 +356,9 @@ class CutPlane(PlaneInGroup):
return None
else:
depth, height, width = self._data.shape
- return numpy.array(((0., 0., 0.),
- (width, height, depth)),
- dtype=numpy.float32)
+ return numpy.array(
+ ((0.0, 0.0, 0.0), (width, height, depth)), dtype=numpy.float32
+ )
@property
def contourVertices(self):
@@ -364,7 +378,8 @@ class CutPlane(PlaneInGroup):
boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
lineIndices = Box.getLineIndices(copy=False)
vertices = utils.boxPlaneIntersect(
- boxVertices, lineIndices, self.plane.normal, self.plane.point)
+ boxVertices, lineIndices, self.plane.normal, self.plane.point
+ )
self._cache = bounds, vertices if len(vertices) != 0 else None
@@ -382,6 +397,6 @@ class CutPlane(PlaneInGroup):
# If it is a TransformList, do not create one to enable sharing.
self._transforms = iterable
else:
- assert hasattr(iterable, '__iter__')
+ assert hasattr(iterable, "__iter__")
self._transforms = transform.TransformList(iterable)
self._transforms.addListener(self._transformChanged)
diff --git a/src/silx/gui/plot3d/scene/event.py b/src/silx/gui/plot3d/scene/event.py
index 637eddf..4c6dd47 100644
--- a/src/silx/gui/plot3d/scene/event.py
+++ b/src/silx/gui/plot3d/scene/event.py
@@ -37,6 +37,7 @@ _logger = logging.getLogger(__name__)
# Notifier ####################################################################
+
class Notifier(object):
"""Base class for object with notification mechanism."""
@@ -53,7 +54,7 @@ class Notifier(object):
if listener not in self._listeners:
self._listeners.append(listener)
else:
- _logger.warning('Ignoring addition of an already registered listener')
+ _logger.warning("Ignoring addition of an already registered listener")
def removeListener(self, listener):
"""Remove a previously registered listener.
@@ -63,7 +64,7 @@ class Notifier(object):
try:
self._listeners.remove(listener)
except ValueError:
- _logger.warning('Trying to remove a listener that is not registered')
+ _logger.warning("Trying to remove a listener that is not registered")
def notify(self, *args, **kwargs):
"""Notify all registered listeners with the given parameters.
@@ -89,19 +90,24 @@ def notifyProperty(attrName, copy=False, converter=None, doc=None):
:return: A property with getter and setter
"""
if copy:
+
def getter(self):
return getattr(self, attrName).copy()
+
else:
+
def getter(self):
return getattr(self, attrName)
if converter is None:
+
def setter(self, value):
if getattr(self, attrName) != value:
setattr(self, attrName, value)
self.notify()
else:
+
def setter(self, value):
value = converter(value)
if getattr(self, attrName) != value:
@@ -117,7 +123,7 @@ class HookList(list):
def __init__(self, iterable):
super(HookList, self).__init__(iterable)
- self._listWasChangedHook('__init__', iterable)
+ self._listWasChangedHook("__init__", iterable)
def _listWillChangeHook(self, methodName, *args, **kwargs):
"""To override. Called before modifying the list.
@@ -140,57 +146,56 @@ class HookList(list):
def _wrapper(self, methodName, *args, **kwargs):
"""Generic wrapper of list methods calling the hooks."""
self._listWillChangeHook(methodName, *args, **kwargs)
- result = getattr(super(HookList, self),
- methodName)(*args, **kwargs)
+ result = getattr(super(HookList, self), methodName)(*args, **kwargs)
self._listWasChangedHook(methodName, *args, **kwargs)
return result
# Add methods
def __iadd__(self, *args, **kwargs):
- return self._wrapper('__iadd__', *args, **kwargs)
+ return self._wrapper("__iadd__", *args, **kwargs)
def __imul__(self, *args, **kwargs):
- return self._wrapper('__imul__', *args, **kwargs)
+ return self._wrapper("__imul__", *args, **kwargs)
def append(self, *args, **kwargs):
- return self._wrapper('append', *args, **kwargs)
+ return self._wrapper("append", *args, **kwargs)
def extend(self, *args, **kwargs):
- return self._wrapper('extend', *args, **kwargs)
+ return self._wrapper("extend", *args, **kwargs)
def insert(self, *args, **kwargs):
- return self._wrapper('insert', *args, **kwargs)
+ return self._wrapper("insert", *args, **kwargs)
# Remove methods
def __delitem__(self, *args, **kwargs):
- return self._wrapper('__delitem__', *args, **kwargs)
+ return self._wrapper("__delitem__", *args, **kwargs)
def __delslice__(self, *args, **kwargs):
- return self._wrapper('__delslice__', *args, **kwargs)
+ return self._wrapper("__delslice__", *args, **kwargs)
def remove(self, *args, **kwargs):
- return self._wrapper('remove', *args, **kwargs)
+ return self._wrapper("remove", *args, **kwargs)
def pop(self, *args, **kwargs):
- return self._wrapper('pop', *args, **kwargs)
+ return self._wrapper("pop", *args, **kwargs)
# Set methods
def __setitem__(self, *args, **kwargs):
- return self._wrapper('__setitem__', *args, **kwargs)
+ return self._wrapper("__setitem__", *args, **kwargs)
def __setslice__(self, *args, **kwargs):
- return self._wrapper('__setslice__', *args, **kwargs)
+ return self._wrapper("__setslice__", *args, **kwargs)
# In place methods
def sort(self, *args, **kwargs):
- return self._wrapper('sort', *args, **kwargs)
+ return self._wrapper("sort", *args, **kwargs)
def reverse(self, *args, **kwargs):
- return self._wrapper('reverse', *args, **kwargs)
+ return self._wrapper("reverse", *args, **kwargs)
class NotifierList(HookList, Notifier):
diff --git a/src/silx/gui/plot3d/scene/function.py b/src/silx/gui/plot3d/scene/function.py
index 3d0a62f..cde7cad 100644
--- a/src/silx/gui/plot3d/scene/function.py
+++ b/src/silx/gui/plot3d/scene/function.py
@@ -44,8 +44,7 @@ _logger = logging.getLogger(__name__)
class ProgramFunction(object):
- """Class providing a function to add to a GLProgram shaders.
- """
+ """Class providing a function to add to a GLProgram shaders."""
def setupProgram(self, context, program):
"""Sets-up uniforms of a program using this shader function.
@@ -63,6 +62,7 @@ class Fog(event.Notifier, ProgramFunction):
The background of the viewport is used as fog color,
otherwise it defaults to white.
"""
+
# TODO: add more controls (set fog range), add more fog modes
_fragDecl = """
@@ -120,26 +120,29 @@ class Fog(event.Notifier, ProgramFunction):
"""
# Provide scene z extent in camera coords
bounds = viewport.camera.extrinsic.transformBounds(
- viewport.scene.bounds(transformed=True, dataBounds=True))
+ viewport.scene.bounds(transformed=True, dataBounds=True)
+ )
return bounds[:, 2]
def setupProgram(self, context, program):
if not self.isOn:
return
- far, near = context.cache(key='zExtentCamera',
- factory=self._zExtentCamera,
- viewport=context.viewport)
+ far, near = context.cache(
+ key="zExtentCamera", factory=self._zExtentCamera, viewport=context.viewport
+ )
extent = far - near
- gl.glUniform2f(program.uniforms['fogExtentInfo'],
- 0.9/extent if extent != 0. else 0.,
- near)
+ gl.glUniform2f(
+ program.uniforms["fogExtentInfo"],
+ 0.9 / extent if extent != 0.0 else 0.0,
+ near,
+ )
# Use background color as fog color
bgColor = context.viewport.background
if bgColor is None:
- bgColor = 1., 1., 1.
- gl.glUniform3f(program.uniforms['fogColor'], *bgColor[:3])
+ bgColor = 1.0, 1.0, 1.0
+ gl.glUniform3f(program.uniforms["fogColor"], *bgColor[:3])
class ClippingPlane(ProgramFunction):
@@ -183,7 +186,7 @@ class ClippingPlane(ProgramFunction):
void clipping(vec4 position) {}
"""
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 0.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 0.0)):
self._plane = utils.Plane(point, normal)
@property
@@ -209,7 +212,7 @@ class ClippingPlane(ProgramFunction):
It MUST be in use and using this function.
"""
if self.plane.isPlane:
- gl.glUniform4f(program.uniforms['planeEq'], *self.plane.parameters)
+ gl.glUniform4f(program.uniforms["planeEq"], *self.plane.parameters)
class DirectionalLight(event.Notifier, ProgramFunction):
@@ -279,9 +282,14 @@ class DirectionalLight(event.Notifier, ProgramFunction):
}
"""
- def __init__(self, direction=None,
- ambient=(1., 1., 1.), diffuse=(0., 0., 0.),
- specular=(1., 1., 1.), shininess=0):
+ def __init__(
+ self,
+ direction=None,
+ ambient=(1.0, 1.0, 1.0),
+ diffuse=(0.0, 0.0, 0.0),
+ specular=(1.0, 1.0, 1.0),
+ shininess=0,
+ ):
super(DirectionalLight, self).__init__()
self._direction = None
self.direction = direction # Set _direction
@@ -291,10 +299,10 @@ class DirectionalLight(event.Notifier, ProgramFunction):
self._specular = specular
self._shininess = shininess
- ambient = event.notifyProperty('_ambient')
- diffuse = event.notifyProperty('_diffuse')
- specular = event.notifyProperty('_specular')
- shininess = event.notifyProperty('_shininess')
+ ambient = event.notifyProperty("_ambient")
+ diffuse = event.notifyProperty("_diffuse")
+ specular = event.notifyProperty("_specular")
+ shininess = event.notifyProperty("_shininess")
@property
def isOn(self):
@@ -359,28 +367,29 @@ class DirectionalLight(event.Notifier, ProgramFunction):
if self.isOn and self._direction is not None:
# Transform light direction from camera space to object coords
lightdir = context.objectToCamera.transformDir(
- self._direction, direct=False)
+ self._direction, direct=False
+ )
lightdir /= numpy.linalg.norm(lightdir)
- gl.glUniform3f(program.uniforms['dLight.lightDir'], *lightdir)
+ gl.glUniform3f(program.uniforms["dLight.lightDir"], *lightdir)
# Convert view position to object coords
viewpos = context.objectToCamera.transformPoint(
- numpy.array((0., 0., 0., 1.), dtype=numpy.float32),
+ numpy.array((0.0, 0.0, 0.0, 1.0), dtype=numpy.float32),
direct=False,
- perspectiveDivide=True)[:3]
- gl.glUniform3f(program.uniforms['dLight.viewPos'], *viewpos)
+ perspectiveDivide=True,
+ )[:3]
+ gl.glUniform3f(program.uniforms["dLight.viewPos"], *viewpos)
- gl.glUniform3f(program.uniforms['dLight.ambient'], *self.ambient)
- gl.glUniform3f(program.uniforms['dLight.diffuse'], *self.diffuse)
- gl.glUniform3f(program.uniforms['dLight.specular'], *self.specular)
- gl.glUniform1f(program.uniforms['dLight.shininess'],
- self.shininess)
+ gl.glUniform3f(program.uniforms["dLight.ambient"], *self.ambient)
+ gl.glUniform3f(program.uniforms["dLight.diffuse"], *self.diffuse)
+ gl.glUniform3f(program.uniforms["dLight.specular"], *self.specular)
+ gl.glUniform1f(program.uniforms["dLight.shininess"], self.shininess)
class Colormap(event.Notifier, ProgramFunction):
-
- _declTemplate = string.Template("""
+ _declTemplate = string.Template(
+ """
uniform sampler2D cmap_texture;
uniform int cmap_normalization;
uniform float cmap_parameter;
@@ -429,7 +438,8 @@ class Colormap(event.Notifier, ProgramFunction):
}
return color;
}
- """)
+ """
+ )
_discardCode = """
if (value == 0.) {
@@ -439,13 +449,13 @@ class Colormap(event.Notifier, ProgramFunction):
call = "colormap"
- NORMS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'
+ NORMS = "linear", "log", "sqrt", "gamma", "arcsinh"
"""Tuple of supported normalizations."""
_COLORMAP_TEXTURE_UNIT = 1
"""Texture unit to use for storing the colormap"""
- def __init__(self, colormap=None, norm='linear', gamma=0., range_=(1., 10.)):
+ def __init__(self, colormap=None, norm="linear", gamma=0.0, range_=(1.0, 10.0)):
"""Shader function to apply a colormap to a value.
:param colormap: RGB(A) color look-up table (default: gray)
@@ -459,11 +469,11 @@ class Colormap(event.Notifier, ProgramFunction):
# Init privates to default
self._colormap = None
- self._norm = 'linear'
- self._gamma = -1.
- self._range = 1., 10.
+ self._norm = "linear"
+ self._gamma = -1.0
+ self._range = 1.0, 10.0
self._displayValuesBelowMin = True
- self._nancolor = numpy.array((1., 1., 1., 0.), dtype=numpy.float32)
+ self._nancolor = numpy.array((1.0, 1.0, 1.0, 0.0), dtype=numpy.float32)
self._texture = None
self._textureToDiscard = None
@@ -471,8 +481,7 @@ class Colormap(event.Notifier, ProgramFunction):
if colormap is None:
# default colormap
colormap = numpy.empty((256, 3), dtype=numpy.uint8)
- colormap[:] = numpy.arange(256,
- dtype=numpy.uint8)[:, numpy.newaxis]
+ colormap[:] = numpy.arange(256, dtype=numpy.uint8)[:, numpy.newaxis]
# Set to values through properties to perform asserts and updates
self.colormap = colormap
@@ -484,7 +493,8 @@ class Colormap(event.Notifier, ProgramFunction):
def decl(self):
"""Source code of the function declaration"""
return self._declTemplate.substitute(
- discard="" if self.displayValuesBelowMin else self._discardCode)
+ discard="" if self.displayValuesBelowMin else self._discardCode
+ )
@property
def colormap(self):
@@ -503,17 +513,21 @@ class Colormap(event.Notifier, ProgramFunction):
data = numpy.empty(
(16, self._colormap.shape[0], self._colormap.shape[1]),
- dtype=self._colormap.dtype)
+ dtype=self._colormap.dtype,
+ )
data[:] = self._colormap
format_ = gl.GL_RGBA if data.shape[-1] == 4 else gl.GL_RGB
self._texture = _glutils.Texture(
- format_, data, format_,
+ format_,
+ data,
+ format_,
texUnit=self._COLORMAP_TEXTURE_UNIT,
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
self.notify()
@@ -524,7 +538,7 @@ class Colormap(event.Notifier, ProgramFunction):
@nancolor.setter
def nancolor(self, color):
- color = numpy.clip(numpy.array(color, dtype=numpy.float32), 0., 1.)
+ color = numpy.clip(numpy.array(color, dtype=numpy.float32), 0.0, 1.0)
assert color.ndim == 1
assert len(color) == 4
if not numpy.array_equal(self._nancolor, color):
@@ -545,7 +559,7 @@ class Colormap(event.Notifier, ProgramFunction):
if norm != self._norm:
assert norm in self.NORMS
self._norm = norm
- if norm in ('log', 'sqrt'):
+ if norm in ("log", "sqrt"):
self.range_ = self.range_ # To test for positive range_
self.notify()
@@ -557,7 +571,7 @@ class Colormap(event.Notifier, ProgramFunction):
@gamma.setter
def gamma(self, gamma):
if gamma != self._gamma:
- assert gamma >= 0.
+ assert gamma >= 0.0
self._gamma = gamma
self.notify()
@@ -577,15 +591,13 @@ class Colormap(event.Notifier, ProgramFunction):
assert len(range_) == 2
range_ = float(range_[0]), float(range_[1])
- if self.norm == 'log' and (range_[0] <= 0. or range_[1] <= 0.):
- _logger.warning(
- "Log normalization and negative range: updating range.")
+ if self.norm == "log" and (range_[0] <= 0.0 or range_[1] <= 0.0):
+ _logger.warning("Log normalization and negative range: updating range.")
minPos = numpy.finfo(numpy.float32).tiny
range_ = max(range_[0], minPos), max(range_[1], minPos)
- elif self.norm == 'sqrt' and (range_[0] < 0. or range_[1] < 0.):
- _logger.warning(
- "Sqrt normalization and negative range: updating range.")
- range_ = max(range_[0], 0.), max(range_[1], 0.)
+ elif self.norm == "sqrt" and (range_[0] < 0.0 or range_[1] < 0.0):
+ _logger.warning("Sqrt normalization and negative range: updating range.")
+ range_ = max(range_[0], 0.0), max(range_[1], 0.0)
if range_ != self._range:
self._range = range_
@@ -593,8 +605,7 @@ class Colormap(event.Notifier, ProgramFunction):
@property
def displayValuesBelowMin(self):
- """True to display values below colormap min, False to discard them.
- """
+ """True to display values below colormap min, False to discard them."""
return self._displayValuesBelowMin
@displayValuesBelowMin.setter
@@ -615,33 +626,34 @@ class Colormap(event.Notifier, ProgramFunction):
self._texture.bind()
- gl.glUniform1i(program.uniforms['cmap_texture'],
- self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["cmap_texture"], self._texture.texUnit)
min_, max_ = self.range_
- param = 0.
- if self._norm == 'log':
+ param = 0.0
+ if self._norm == "log":
min_, max_ = numpy.log10(min_), numpy.log10(max_)
normID = 1
- elif self._norm == 'sqrt':
+ elif self._norm == "sqrt":
min_, max_ = numpy.sqrt(min_), numpy.sqrt(max_)
normID = 2
- elif self._norm == 'gamma':
+ elif self._norm == "gamma":
# Keep min_, max_ as is
param = self._gamma
normID = 3
- elif self._norm == 'arcsinh':
+ elif self._norm == "arcsinh":
min_, max_ = numpy.arcsinh(min_), numpy.arcsinh(max_)
normID = 4
else: # Linear
normID = 0
- gl.glUniform1i(program.uniforms['cmap_normalization'], normID)
- gl.glUniform1f(program.uniforms['cmap_parameter'], param)
- gl.glUniform1f(program.uniforms['cmap_min'], min_)
- gl.glUniform1f(program.uniforms['cmap_oneOverRange'],
- (1. / (max_ - min_)) if max_ != min_ else 0.)
- gl.glUniform4f(program.uniforms['nancolor'], *self._nancolor)
+ gl.glUniform1i(program.uniforms["cmap_normalization"], normID)
+ gl.glUniform1f(program.uniforms["cmap_parameter"], param)
+ gl.glUniform1f(program.uniforms["cmap_min"], min_)
+ gl.glUniform1f(
+ program.uniforms["cmap_oneOverRange"],
+ (1.0 / (max_ - min_)) if max_ != min_ else 0.0,
+ )
+ gl.glUniform4f(program.uniforms["nancolor"], *self._nancolor)
def prepareGL2(self, context):
if self._textureToDiscard is not None:
diff --git a/src/silx/gui/plot3d/scene/interaction.py b/src/silx/gui/plot3d/scene/interaction.py
index 91fab23..debf670 100644
--- a/src/silx/gui/plot3d/scene/interaction.py
+++ b/src/silx/gui/plot3d/scene/interaction.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 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
@@ -31,8 +31,12 @@ import logging
import numpy
from silx.gui import qt
-from silx.gui.plot.Interaction import \
- StateMachine, State, LEFT_BTN, RIGHT_BTN # , MIDDLE_BTN
+from silx.gui.plot.Interaction import (
+ StateMachine,
+ State,
+ LEFT_BTN,
+ RIGHT_BTN,
+) # , MIDDLE_BTN
from . import transform
@@ -41,35 +45,32 @@ _logger = logging.getLogger(__name__)
class ClickOrDrag(StateMachine):
- """Click or drag interaction for a given button.
+ """Click or drag interaction for a given button."""
- """
- #TODO: merge this class with silx.gui.plot.Interaction.ClickOrDrag
+ # TODO: merge this class with silx.gui.plot.Interaction.ClickOrDrag
- DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
+ DRAG_THRESHOLD_SQUARE_DIST = 5**2
class Idle(State):
def onPress(self, x, y, btn):
if btn == self.machine.button:
- self.goto('clickOrDrag', x, y)
+ self.goto("clickOrDrag", x, y)
return True
class ClickOrDrag(State):
def enterState(self, x, y):
self.initPos = x, y
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
def onMove(self, x, y):
dx = (x - self.initPos[0]) ** 2
dy = (y - self.initPos[1]) ** 2
- if (dx ** 2 + dy ** 2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('drag', self.initPos, (x, y))
+ if (dx**2 + dy**2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
+ self.goto("drag", self.initPos, (x, y))
def onRelease(self, x, y, btn):
if btn == self.machine.button:
self.machine.click(x, y)
- self.goto('idle')
+ self.goto("idle")
class Drag(State):
def enterState(self, initPos, curPos):
@@ -77,24 +78,22 @@ class ClickOrDrag(StateMachine):
self.machine.beginDrag(*initPos)
self.machine.drag(*curPos)
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
def onMove(self, x, y):
self.machine.drag(x, y)
def onRelease(self, x, y, btn):
if btn == self.machine.button:
self.machine.endDrag(self.initPos, (x, y))
- self.goto('idle')
+ self.goto("idle")
def __init__(self, button=LEFT_BTN):
self.button = button
states = {
- 'idle': ClickOrDrag.Idle,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
+ "idle": ClickOrDrag.Idle,
+ "clickOrDrag": ClickOrDrag.ClickOrDrag,
+ "drag": ClickOrDrag.Drag,
}
- super(ClickOrDrag, self).__init__(states, 'idle')
+ super(ClickOrDrag, self).__init__(states, "idle")
def click(self, x, y):
"""Called upon a left or right button click.
@@ -126,8 +125,9 @@ class ClickOrDrag(StateMachine):
class CameraSelectRotate(ClickOrDrag):
"""Camera rotation using an arcball-like interaction."""
- def __init__(self, viewport, orbitAroundCenter=True, button=RIGHT_BTN,
- selectCB=None):
+ def __init__(
+ self, viewport, orbitAroundCenter=True, button=RIGHT_BTN, selectCB=None
+ ):
self._viewport = viewport
self._orbitAroundCenter = orbitAroundCenter
self._selectCB = selectCB
@@ -144,7 +144,7 @@ class CameraSelectRotate(ClickOrDrag):
position = self._viewport._getXZYGL(x, y)
# This assume no object lie on the far plane
# Alternative, change the depth range so that far is < 1
- if ndcZ != 1. and position is not None:
+ if ndcZ != 1.0 and position is not None:
self._selectCB((x, y, ndcZ), position)
def beginDrag(self, x, y):
@@ -152,7 +152,7 @@ class CameraSelectRotate(ClickOrDrag):
if not self._orbitAroundCenter:
# Try to use picked object position as center of rotation
ndcZ = self._viewport._pickNdcZGL(x, y)
- if ndcZ != 1.:
+ if ndcZ != 1.0:
# Hit an object, use picked point as center
centerPos = self._viewport._getXZYGL(x, y) # Can return None
@@ -177,12 +177,11 @@ class CameraSelectRotate(ClickOrDrag):
position = self._startExtrinsic.position
else:
minsize = min(self._viewport.size)
- distance = numpy.sqrt(dx ** 2 + dy ** 2)
+ distance = numpy.sqrt(dx**2 + dy**2)
angle = distance / minsize * numpy.pi
# Take care of y inversion
- direction = dx * self._startExtrinsic.side - \
- dy * self._startExtrinsic.up
+ direction = dx * self._startExtrinsic.side - dy * self._startExtrinsic.up
direction /= numpy.linalg.norm(direction)
axis = numpy.cross(direction, self._startExtrinsic.direction)
axis /= numpy.linalg.norm(axis)
@@ -194,10 +193,9 @@ class CameraSelectRotate(ClickOrDrag):
up = rotation.transformDir(self._startExtrinsic.up)
# Rotate position around center
- trlist = transform.StaticTransformList((
- self._center,
- rotation,
- self._center.inverse()))
+ trlist = transform.StaticTransformList(
+ (self._center, rotation, self._center.inverse())
+ )
position = trlist.transformPoint(self._startExtrinsic.position)
camerapos = self._viewport.camera.extrinsic
@@ -223,7 +221,7 @@ class CameraSelectPan(ClickOrDrag):
position = self._viewport._getXZYGL(x, y)
# This assume no object lie on the far plane
# Alternative, change the depth range so that far is < 1
- if ndcZ != 1. and position is not None:
+ if ndcZ != 1.0 and position is not None:
self._selectCB((x, y, ndcZ), position)
def beginDrag(self, x, y):
@@ -231,8 +229,9 @@ class CameraSelectPan(ClickOrDrag):
ndcZ = self._viewport._pickNdcZGL(x, y)
# ndcZ is the panning plane
if ndc is not None and ndcZ is not None:
- self._lastPosNdc = numpy.array((ndc[0], ndc[1], ndcZ, 1.),
- dtype=numpy.float32)
+ self._lastPosNdc = numpy.array(
+ (ndc[0], ndc[1], ndcZ, 1.0), dtype=numpy.float32
+ )
else:
self._lastPosNdc = None
@@ -240,14 +239,17 @@ class CameraSelectPan(ClickOrDrag):
if self._lastPosNdc is not None:
ndc = self._viewport.windowToNdc(x, y)
if ndc is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], self._lastPosNdc[2], 1.),
- dtype=numpy.float32)
+ ndcPos = numpy.array(
+ (ndc[0], ndc[1], self._lastPosNdc[2], 1.0), dtype=numpy.float32
+ )
# Convert last and current NDC positions to scene coords
scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
+ ndcPos, direct=False, perspectiveDivide=True
+ )
lastScenePos = self._viewport.camera.transformPoint(
- self._lastPosNdc, direct=False, perspectiveDivide=True)
+ self._lastPosNdc, direct=False, perspectiveDivide=True
+ )
# Get translation in scene coords
translation = scenePos[:3] - lastScenePos[:3]
@@ -264,21 +266,21 @@ class CameraWheel(object):
"""StateMachine like class, just handling wheel events."""
# TODO choose scale of motion? Translation or Scale?
- def __init__(self, viewport, mode='center', scaleTransform=None):
- assert mode in ('center', 'position', 'scale')
+ def __init__(self, viewport, mode="center", scaleTransform=None):
+ assert mode in ("center", "position", "scale")
self._viewport = viewport
- if mode == 'center':
+ if mode == "center":
self._zoomTo = self._zoomToCenter
- elif mode == 'position':
+ elif mode == "position":
self._zoomTo = self._zoomToPosition
- elif mode == 'scale':
+ elif mode == "scale":
self._zoomTo = self._zoomByScale
self._scale = scaleTransform
else:
- raise ValueError('Unsupported mode: %s' % mode)
+ raise ValueError("Unsupported mode: %s" % mode)
def handleEvent(self, eventName, *args, **kwargs):
- if eventName == 'wheel':
+ if eventName == "wheel":
return self._zoomTo(*args, **kwargs)
def _zoomToCenter(self, x, y, angleInDegrees):
@@ -286,7 +288,7 @@ class CameraWheel(object):
Only works with perspective camera.
"""
- direction = 'forward' if angleInDegrees > 0 else 'backward'
+ direction = "forward" if angleInDegrees > 0 else "backward"
self._viewport.camera.move(direction)
return True
@@ -297,20 +299,22 @@ class CameraWheel(object):
"""
ndc = self._viewport.windowToNdc(x, y)
if ndc is not None:
- near = numpy.array((ndc[0], ndc[1], -1., 1.), dtype=numpy.float32)
+ near = numpy.array((ndc[0], ndc[1], -1.0, 1.0), dtype=numpy.float32)
nearscene = self._viewport.camera.transformPoint(
- near, direct=False, perspectiveDivide=True)
+ near, direct=False, perspectiveDivide=True
+ )
- far = numpy.array((ndc[0], ndc[1], 1., 1.), dtype=numpy.float32)
+ far = numpy.array((ndc[0], ndc[1], 1.0, 1.0), dtype=numpy.float32)
farscene = self._viewport.camera.transformPoint(
- far, direct=False, perspectiveDivide=True)
+ far, direct=False, perspectiveDivide=True
+ )
dirscene = farscene[:3] - nearscene[:3]
dirscene /= numpy.linalg.norm(dirscene)
if angleInDegrees < 0:
- dirscene *= -1.
+ dirscene *= -1.0
# TODO which scale
self._viewport.camera.extrinsic.position += dirscene
@@ -327,43 +331,43 @@ class CameraWheel(object):
if ndc is not None:
ndcz = self._viewport._pickNdcZGL(x, y)
- position = numpy.array((ndc[0], ndc[1], ndcz),
- dtype=numpy.float32)
+ position = numpy.array((ndc[0], ndc[1], ndcz), dtype=numpy.float32)
positionscene = self._viewport.camera.transformPoint(
- position, direct=False, perspectiveDivide=True)
+ position, direct=False, perspectiveDivide=True
+ )
camtopos = extrinsic.position - positionscene
- step = 0.2 * (1. if angleInDegrees < 0 else -1.)
+ step = 0.2 * (1.0 if angleInDegrees < 0 else -1.0)
extrinsic.position += step * camtopos
elif isinstance(projection, transform.Orthographic):
# For orthographic projection, change projection borders
ndcx, ndcy = self._viewport.windowToNdc(x, y, checkInside=False)
- step = 0.2 * (1. if angleInDegrees < 0 else -1.)
+ step = 0.2 * (1.0 if angleInDegrees < 0 else -1.0)
- dx = (ndcx + 1) / 2.
+ dx = (ndcx + 1) / 2.0
stepwidth = step * (projection.right - projection.left)
left = projection.left - dx * stepwidth
- right = projection.right + (1. - dx) * stepwidth
+ right = projection.right + (1.0 - dx) * stepwidth
- dy = (ndcy + 1) / 2.
+ dy = (ndcy + 1) / 2.0
stepheight = step * (projection.top - projection.bottom)
bottom = projection.bottom - dy * stepheight
- top = projection.top + (1. - dy) * stepheight
+ top = projection.top + (1.0 - dy) * stepheight
projection.setClipping(left, right, bottom, top)
else:
- raise RuntimeError('Unsupported camera', projection)
+ raise RuntimeError("Unsupported camera", projection)
return True
def _zoomByScale(self, x, y, angleInDegrees):
"""Zoom by scaling scene (do not keep pixel under mouse invariant)."""
scalefactor = 1.1
- if angleInDegrees < 0.:
- scalefactor = 1. / scalefactor
+ if angleInDegrees < 0.0:
+ scalefactor = 1.0 / scalefactor
self._scale.scale = scalefactor * self._scale.scale
self._viewport.adjustCameraDepthExtent()
@@ -376,12 +380,13 @@ class FocusManager(StateMachine):
On press an event handler can acquire focus.
By default it looses focus when all buttons are released.
"""
+
class Idle(State):
def onPress(self, x, y, btn):
for eventHandler in self.machine.currentEventHandler:
- requestFocus = eventHandler.handleEvent('press', x, y, btn)
+ requestFocus = eventHandler.handleEvent("press", x, y, btn)
if requestFocus:
- self.goto('focus', eventHandler, btn)
+ self.goto("focus", eventHandler, btn)
break
def _processEvent(self, *args):
@@ -391,47 +396,42 @@ class FocusManager(StateMachine):
break
def onMove(self, x, y):
- self._processEvent('move', x, y)
+ self._processEvent("move", x, y)
def onRelease(self, x, y, btn):
- self._processEvent('release', x, y, btn)
+ self._processEvent("release", x, y, btn)
def onWheel(self, x, y, angle):
- self._processEvent('wheel', x, y, angle)
+ self._processEvent("wheel", x, y, angle)
class Focus(State):
def enterState(self, eventHandler, btn):
self.eventHandler = eventHandler
self.focusBtns = {btn} # Set
- enter = enterState # silx v.0.3 support, remove when 0.4 out
-
def onPress(self, x, y, btn):
self.focusBtns.add(btn)
- self.eventHandler.handleEvent('press', x, y, btn)
+ self.eventHandler.handleEvent("press", x, y, btn)
def onMove(self, x, y):
- self.eventHandler.handleEvent('move', x, y)
+ self.eventHandler.handleEvent("move", x, y)
def onRelease(self, x, y, btn):
self.focusBtns.discard(btn)
- requestfocus = self.eventHandler.handleEvent('release', x, y, btn)
+ requestfocus = self.eventHandler.handleEvent("release", x, y, btn)
if len(self.focusBtns) == 0 and not requestfocus:
- self.goto('idle')
+ self.goto("idle")
def onWheel(self, x, y, angleInDegrees):
- self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
+ self.eventHandler.handleEvent("wheel", x, y, angleInDegrees)
def __init__(self, eventHandlers=(), ctrlEventHandlers=None):
self.defaultEventHandlers = eventHandlers
self.ctrlEventHandlers = ctrlEventHandlers
self.currentEventHandler = self.defaultEventHandlers
- states = {
- 'idle': FocusManager.Idle,
- 'focus': FocusManager.Focus
- }
- super(FocusManager, self).__init__(states, 'idle')
+ states = {"idle": FocusManager.Idle, "focus": FocusManager.Focus}
+ super(FocusManager, self).__init__(states, "idle")
def onKeyPress(self, key):
if key == qt.Qt.Key_Control and self.ctrlEventHandlers is not None:
@@ -450,43 +450,65 @@ class RotateCameraControl(FocusManager):
"""Combine wheel and rotate state machine for left button
and pan when ctrl is pressed
"""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN, selectCB))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB))
+
+ def __init__(
+ self,
+ viewport,
+ orbitAroundCenter=False,
+ mode="center",
+ scaleTransform=None,
+ selectCB=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectRotate(viewport, orbitAroundCenter, LEFT_BTN, selectCB),
+ )
+ ctrlHandlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB),
+ )
super(RotateCameraControl, self).__init__(handlers, ctrlHandlers)
class PanCameraControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine for left button
and rotate when ctrl is pressed"""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN, selectCB))
+
+ def __init__(
+ self,
+ viewport,
+ orbitAroundCenter=False,
+ mode="center",
+ scaleTransform=None,
+ selectCB=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB),
+ )
+ ctrlHandlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectRotate(viewport, orbitAroundCenter, LEFT_BTN, selectCB),
+ )
super(PanCameraControl, self).__init__(handlers, ctrlHandlers)
class CameraControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine."""
- def __init__(self, viewport,
- orbitAroundCenter=False,
- mode='center', scaleTransform=None,
- selectCB=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectPan(viewport, LEFT_BTN, selectCB),
- CameraSelectRotate(
- viewport, orbitAroundCenter, RIGHT_BTN, selectCB))
+
+ def __init__(
+ self,
+ viewport,
+ orbitAroundCenter=False,
+ mode="center",
+ scaleTransform=None,
+ selectCB=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectPan(viewport, LEFT_BTN, selectCB),
+ CameraSelectRotate(viewport, orbitAroundCenter, RIGHT_BTN, selectCB),
+ )
super(CameraControl, self).__init__(handlers)
@@ -532,14 +554,14 @@ class PlaneRotate(ClickOrDrag):
# Normalize x and y on a unit circle
spherecoords = (position - center) / float(radius)
- squarelength = numpy.sum(spherecoords ** 2)
+ squarelength = numpy.sum(spherecoords**2)
# Project on the unit sphere and compute z coordinates
if squarelength > 1.0: # Outside sphere: project
spherecoords /= numpy.sqrt(squarelength)
zsphere = 0.0
else: # In sphere: compute z
- zsphere = numpy.sqrt(1. - squarelength)
+ zsphere = numpy.sqrt(1.0 - squarelength)
spherecoords = numpy.append(spherecoords, zsphere)
return spherecoords
@@ -552,8 +574,7 @@ class PlaneRotate(ClickOrDrag):
# Store the plane normal
self._beginNormal = self._plane.plane.normal
- _logger.debug(
- 'Begin arcball, plane center %s', str(self._plane.center))
+ _logger.debug("Begin arcball, plane center %s", str(self._plane.center))
# Do the arcball on the screen
radius = min(self._viewport.size)
@@ -562,12 +583,15 @@ class PlaneRotate(ClickOrDrag):
else:
center = self._plane.objectToNDCTransform.transformPoint(
- self._plane.center, perspectiveDivide=True)
+ self._plane.center, perspectiveDivide=True
+ )
self._beginCenter = self._viewport.ndcToWindow(
- center[0], center[1], checkInside=False)
+ center[0], center[1], checkInside=False
+ )
self._startVector = self._sphereUnitVector(
- radius, self._beginCenter, (x, y))
+ radius, self._beginCenter, (x, y)
+ )
def drag(self, x, y):
if self._beginCenter is None:
@@ -575,24 +599,21 @@ class PlaneRotate(ClickOrDrag):
# Compute rotation: this is twice the rotation of the arcball
radius = min(self._viewport.size)
- currentvector = self._sphereUnitVector(
- radius, self._beginCenter, (x, y))
+ currentvector = self._sphereUnitVector(radius, self._beginCenter, (x, y))
crossprod = numpy.cross(self._startVector, currentvector)
dotprod = numpy.dot(self._startVector, currentvector)
quaternion = numpy.append(crossprod, dotprod)
# Rotation was computed with Y downward, but apply in NDC, invert Y
- quaternion[1] *= -1.
+ quaternion[1] *= -1.0
rotation = transform.Rotate()
rotation.quaternion = quaternion
# Convert to NDC, rotate, convert back to object
- normal = self._plane.objectToNDCTransform.transformNormal(
- self._beginNormal)
+ normal = self._plane.objectToNDCTransform.transformNormal(self._beginNormal)
normal = rotation.transformNormal(normal)
- normal = self._plane.objectToNDCTransform.transformNormal(
- normal, direct=False)
+ normal = self._plane.objectToNDCTransform.transformNormal(normal, direct=False)
self._plane.plane.normal = normal
def endDrag(self, x, y):
@@ -607,7 +628,7 @@ class PlanePan(ClickOrDrag):
self._viewport = viewport
self._beginPlanePoint = None
self._beginPos = None
- self._dragNdcZ = 0.
+ self._dragNdcZ = 0.0
super(PlanePan, self).__init__(button)
def click(self, x, y):
@@ -618,16 +639,17 @@ class PlanePan(ClickOrDrag):
ndcZ = self._viewport._pickNdcZGL(x, y)
# ndcZ is the panning plane
if ndc is not None and ndcZ is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], ndcZ, 1.),
- dtype=numpy.float32)
+ ndcPos = numpy.array((ndc[0], ndc[1], ndcZ, 1.0), dtype=numpy.float32)
scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
+ ndcPos, direct=False, perspectiveDivide=True
+ )
self._beginPos = self._plane.objectToSceneTransform.transformPoint(
- scenePos, direct=False)
+ scenePos, direct=False
+ )
self._dragNdcZ = ndcZ
else:
self._beginPos = None
- self._dragNdcZ = 0.
+ self._dragNdcZ = 0.0
self._beginPlanePoint = self._plane.plane.point
@@ -635,14 +657,17 @@ class PlanePan(ClickOrDrag):
if self._beginPos is not None:
ndc = self._viewport.windowToNdc(x, y)
if ndc is not None:
- ndcPos = numpy.array((ndc[0], ndc[1], self._dragNdcZ, 1.),
- dtype=numpy.float32)
+ ndcPos = numpy.array(
+ (ndc[0], ndc[1], self._dragNdcZ, 1.0), dtype=numpy.float32
+ )
# Convert last and current NDC positions to scene coords
scenePos = self._viewport.camera.transformPoint(
- ndcPos, direct=False, perspectiveDivide=True)
+ ndcPos, direct=False, perspectiveDivide=True
+ )
curPos = self._plane.objectToSceneTransform.transformPoint(
- scenePos, direct=False)
+ scenePos, direct=False
+ )
# Get translation in scene coords
translation = curPos[:3] - self._beginPos[:3]
@@ -652,8 +677,7 @@ class PlanePan(ClickOrDrag):
# Keep plane point in bounds
bounds = self._plane.parent.bounds(dataBounds=True)
if bounds is not None:
- newPoint = numpy.clip(
- newPoint, a_min=bounds[0], a_max=bounds[1])
+ newPoint = numpy.clip(newPoint, a_min=bounds[0], a_max=bounds[1])
# Only update plane if it is in some bounds
self._plane.plane.point = newPoint
@@ -664,35 +688,45 @@ class PlanePan(ClickOrDrag):
class PlaneControl(FocusManager):
"""Combine wheel, selectPan and rotate state machine for plane control."""
- def __init__(self, viewport, plane,
- mode='center', scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN),
- PlaneRotate(viewport, plane, RIGHT_BTN))
+
+ def __init__(self, viewport, plane, mode="center", scaleTransform=None):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ PlanePan(viewport, plane, LEFT_BTN),
+ PlaneRotate(viewport, plane, RIGHT_BTN),
+ )
super(PlaneControl, self).__init__(handlers)
class PanPlaneRotateCameraControl(FocusManager):
"""Combine wheel, pan plane and camera rotate state machine."""
- def __init__(self, viewport, plane,
- mode='center', scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN),
- CameraSelectRotate(viewport,
- orbitAroundCenter=False,
- button=RIGHT_BTN))
+
+ def __init__(self, viewport, plane, mode="center", scaleTransform=None):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ PlanePan(viewport, plane, LEFT_BTN),
+ CameraSelectRotate(viewport, orbitAroundCenter=False, button=RIGHT_BTN),
+ )
super(PanPlaneRotateCameraControl, self).__init__(handlers)
class PanPlaneZoomOnWheelControl(FocusManager):
"""Combine zoom on wheel and pan plane state machines."""
- def __init__(self, viewport, plane,
- mode='center',
- orbitAroundCenter=False,
- scaleTransform=None):
- handlers = (CameraWheel(viewport, mode, scaleTransform),
- PlanePan(viewport, plane, LEFT_BTN))
- ctrlHandlers = (CameraWheel(viewport, mode, scaleTransform),
- CameraSelectRotate(
- viewport, orbitAroundCenter, LEFT_BTN))
+
+ def __init__(
+ self,
+ viewport,
+ plane,
+ mode="center",
+ orbitAroundCenter=False,
+ scaleTransform=None,
+ ):
+ handlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ PlanePan(viewport, plane, LEFT_BTN),
+ )
+ ctrlHandlers = (
+ CameraWheel(viewport, mode, scaleTransform),
+ CameraSelectRotate(viewport, orbitAroundCenter, LEFT_BTN),
+ )
super(PanPlaneZoomOnWheelControl, self).__init__(handlers, ctrlHandlers)
diff --git a/src/silx/gui/plot3d/scene/primitives.py b/src/silx/gui/plot3d/scene/primitives.py
index 6d3c4ff..93070c3 100644
--- a/src/silx/gui/plot3d/scene/primitives.py
+++ b/src/silx/gui/plot3d/scene/primitives.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2015-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2023 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
@@ -26,10 +26,7 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "24/04/2018"
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
+from collections import abc
import ctypes
from functools import reduce
import logging
@@ -53,6 +50,7 @@ _logger = logging.getLogger(__name__)
# Geometry ####################################################################
+
class Geometry(core.Elem):
"""Set of vertices with normals and colors.
@@ -65,39 +63,36 @@ class Geometry(core.Elem):
"""
_ATTR_INFO = {
- 'position': {'dims': (1, 2), 'lastDim': (2, 3, 4)},
- 'normal': {'dims': (1, 2), 'lastDim': (3,)},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
+ "position": {"dims": (1, 2), "lastDim": (2, 3, 4)},
+ "normal": {"dims": (1, 2), "lastDim": (3,)},
+ "color": {"dims": (1, 2), "lastDim": (3, 4)},
}
_MODE_CHECKS = { # Min, Modulo
- 'lines': (2, 2), 'line_strip': (2, 0), 'loop': (2, 0),
- 'points': (1, 0),
- 'triangles': (3, 3), 'triangle_strip': (3, 0), 'fan': (3, 0)
+ "lines": (2, 2),
+ "line_strip": (2, 0),
+ "loop": (2, 0),
+ "points": (1, 0),
+ "triangles": (3, 3),
+ "triangle_strip": (3, 0),
+ "fan": (3, 0),
}
_MODES = {
- 'lines': gl.GL_LINES,
- 'line_strip': gl.GL_LINE_STRIP,
- 'loop': gl.GL_LINE_LOOP,
-
- 'points': gl.GL_POINTS,
-
- 'triangles': gl.GL_TRIANGLES,
- 'triangle_strip': gl.GL_TRIANGLE_STRIP,
- 'fan': gl.GL_TRIANGLE_FAN
+ "lines": gl.GL_LINES,
+ "line_strip": gl.GL_LINE_STRIP,
+ "loop": gl.GL_LINE_LOOP,
+ "points": gl.GL_POINTS,
+ "triangles": gl.GL_TRIANGLES,
+ "triangle_strip": gl.GL_TRIANGLE_STRIP,
+ "fan": gl.GL_TRIANGLE_FAN,
}
- _LINE_MODES = 'lines', 'line_strip', 'loop'
+ _LINE_MODES = "lines", "line_strip", "loop"
- _TRIANGLE_MODES = 'triangles', 'triangle_strip', 'fan'
+ _TRIANGLE_MODES = "triangles", "triangle_strip", "fan"
- def __init__(self,
- mode,
- indices=None,
- copy=True,
- attrib0='position',
- **attributes):
+ def __init__(self, mode, indices=None, copy=True, attrib0="position", **attributes):
super(Geometry, self).__init__()
self._attrib0 = str(attrib0)
@@ -146,26 +141,26 @@ class Geometry(core.Elem):
"""
# Convert single value (int, float, numpy types) to tuple
if not isinstance(array, abc.Iterable):
- array = (array, )
+ array = (array,)
# Makes sure it is an array
array = numpy.array(array, copy=False)
dtype = None
- if array.dtype.kind == 'f' and array.dtype.itemsize != 4:
+ if array.dtype.kind == "f" and array.dtype.itemsize != 4:
# Cast to float32
- _logger.info('Cast array to float32')
+ _logger.info("Cast array to float32")
dtype = numpy.float32
elif array.dtype.itemsize > 4:
# Cast (u)int64 to (u)int32
- if array.dtype.kind == 'i':
- _logger.info('Cast array to int32')
+ if array.dtype.kind == "i":
+ _logger.info("Cast array to int32")
dtype = numpy.int32
- elif array.dtype.kind == 'u':
- _logger.info('Cast array to uint32')
+ elif array.dtype.kind == "u":
+ _logger.info("Cast array to uint32")
dtype = numpy.uint32
- return numpy.array(array, dtype=dtype, order='C', copy=copy)
+ return numpy.array(array, dtype=dtype, order="C", copy=copy)
@property
def nbVertices(self):
@@ -200,17 +195,16 @@ class Geometry(core.Elem):
array = self._glReadyArray(array, copy=copy)
if name not in self._ATTR_INFO:
- _logger.debug('Not checking attribute %s dimensions', name)
+ _logger.debug("Not checking attribute %s dimensions", name)
else:
checks = self._ATTR_INFO[name]
- if (array.ndim == 1 and checks['lastDim'] == (1,) and
- len(array) > 1):
+ if array.ndim == 1 and checks["lastDim"] == (1,) and len(array) > 1:
array = array.reshape((len(array), 1))
# Checks
- assert array.ndim in checks['dims'], "Attr %s" % name
- assert array.shape[-1] in checks['lastDim'], "Attr %s" % name
+ assert array.ndim in checks["dims"], "Attr %s" % name
+ assert array.shape[-1] in checks["lastDim"], "Attr %s" % name
# Makes sure attrib0 is considered as an array of values
if name == self.attrib0 and array.ndim == 1:
@@ -277,7 +271,8 @@ class Geometry(core.Elem):
assert len(array) in (1, 2, 3, 4)
gl.glDisableVertexAttribArray(attribute)
_glVertexAttribFunc = getattr(
- _glutils.gl, 'glVertexAttrib{}f'.format(len(array)))
+ _glutils.gl, "glVertexAttrib{}f".format(len(array))
+ )
_glVertexAttribFunc(attribute, *array)
else:
# TODO As is this is a never event, remove?
@@ -288,7 +283,8 @@ class Geometry(core.Elem):
_glutils.numpyToGLType(array.dtype),
gl.GL_FALSE,
0,
- array)
+ array,
+ )
def setIndices(self, indices, copy=True):
"""Set the primitive indices to use.
@@ -297,13 +293,13 @@ class Geometry(core.Elem):
:param bool copy: True (default) to copy the data, False to use as is
"""
# Trigger garbage collection of previous indices VBO if any
- self._vbos.pop('__indices__', None)
+ self._vbos.pop("__indices__", None)
if indices is None:
self._indices = None
else:
indices = self._glReadyArray(indices, copy=copy).ravel()
- assert indices.dtype.name in ('uint8', 'uint16', 'uint32')
+ assert indices.dtype.name in ("uint8", "uint16", "uint32")
if _logger.getEffectiveLevel() <= logging.DEBUG:
# This might be a costy check
assert indices.max() < self.nbVertices
@@ -364,19 +360,22 @@ class Geometry(core.Elem):
min_ = numpy.nanmin(attribute, axis=0)
max_ = numpy.nanmax(attribute, axis=0)
else:
- min_, max_ = numpy.zeros((2, attribute.shape[1]), dtype=numpy.float32)
+ min_, max_ = numpy.zeros(
+ (2, attribute.shape[1]), dtype=numpy.float32
+ )
- toCopy = min(len(min_), 3-index)
+ toCopy = min(len(min_), 3 - index)
if toCopy != len(min_):
- _logger.error("Attribute defining bounds"
- " has too many dimensions")
+ _logger.error(
+ "Attribute defining bounds" " has too many dimensions"
+ )
- self.__bounds[0, index:index+toCopy] = min_[:toCopy]
- self.__bounds[1, index:index+toCopy] = max_[:toCopy]
+ self.__bounds[0, index : index + toCopy] = min_[:toCopy]
+ self.__bounds[1, index : index + toCopy] = max_[:toCopy]
index += toCopy
- self.__bounds[numpy.isnan(self.__bounds)] = 0. # Avoid NaNs
+ self.__bounds[numpy.isnan(self.__bounds)] = 0.0 # Avoid NaNs
return self.__bounds.copy()
@@ -389,11 +388,13 @@ class Geometry(core.Elem):
self._vbos[name] = ctx.glCtx.makeVboAttrib(array)
self._unsyncAttributes = []
- if self._indices is not None and '__indices__' not in self._vbos:
- vbo = ctx.glCtx.makeVbo(self._indices,
- usage=gl.GL_STATIC_DRAW,
- target=gl.GL_ELEMENT_ARRAY_BUFFER)
- self._vbos['__indices__'] = vbo
+ if self._indices is not None and "__indices__" not in self._vbos:
+ vbo = ctx.glCtx.makeVbo(
+ self._indices,
+ usage=gl.GL_STATIC_DRAW,
+ target=gl.GL_ELEMENT_ARRAY_BUFFER,
+ )
+ self._vbos["__indices__"] = vbo
def _draw(self, program=None, nbVertices=None):
"""Perform OpenGL draw calls.
@@ -413,18 +414,23 @@ class Geometry(core.Elem):
else:
if nbVertices is None:
nbVertices = self._indices.size
- with self._vbos['__indices__']:
- gl.glDrawElements(self._MODES[self._mode],
- nbVertices,
- _glutils.numpyToGLType(self._indices.dtype),
- ctypes.c_void_p(0))
+ with self._vbos["__indices__"]:
+ gl.glDrawElements(
+ self._MODES[self._mode],
+ nbVertices,
+ _glutils.numpyToGLType(self._indices.dtype),
+ ctypes.c_void_p(0),
+ )
# Lines #######################################################################
+
class Lines(Geometry):
"""A set of segments"""
- _shaders = ("""
+
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
@@ -446,7 +452,8 @@ class Lines(Geometry):
vColor = color;
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -461,33 +468,43 @@ class Lines(Geometry):
gl_FragColor = $lightingCall(vColor, vPosition, vNormal);
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self, positions, normals=None, colors=(1., 1., 1., 1.),
- indices=None, mode='lines', width=1.):
- if mode == 'strip':
- mode = 'line_strip'
+ """
+ ),
+ )
+
+ def __init__(
+ self,
+ positions,
+ normals=None,
+ colors=(1.0, 1.0, 1.0, 1.0),
+ indices=None,
+ mode="lines",
+ width=1.0,
+ ):
+ if mode == "strip":
+ mode = "line_strip"
assert mode in self._LINE_MODES
self._width = width
self._smooth = True
- super(Lines, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors)
+ super(Lines, self).__init__(
+ mode, indices, position=positions, normal=normals, color=colors
+ )
- width = event.notifyProperty('_width', converter=float,
- doc="Width of the line in pixels.")
+ width = event.notifyProperty(
+ "_width", converter=float, doc="Width of the line in pixels."
+ )
smooth = event.notifyProperty(
- '_smooth',
+ "_smooth",
converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
+ doc="Smooth line rendering enabled (bool, default: True)",
+ )
def renderGL2(self, ctx):
# Prepare program
- isnormals = 'normal' in self._attributes
+ isnormals = "normal" in self._attributes
if isnormals:
fraglightfunction = ctx.viewport.light.fragmentDef
else:
@@ -498,7 +515,8 @@ class Lines(Geometry):
scenePreCall=ctx.fragCallPre,
scenePostCall=ctx.fragCallPost,
lightingFunction=fraglightfunction,
- lightingCall=ctx.viewport.light.fragmentCall)
+ lightingCall=ctx.viewport.light.fragmentCall,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -507,10 +525,8 @@ class Lines(Geometry):
gl.glLineWidth(self.width)
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
@@ -524,7 +540,8 @@ class DashedLines(Lines):
This MUST be defined as a set of lines (no strip or loop).
"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 origin;
attribute vec3 normal;
@@ -554,7 +571,8 @@ class DashedLines(Lines):
vOriginFragCoord = (ndcOrigin.xy + vec2(1.0, 1.0)) * 0.5 * viewportSize + vec2(0.5, 0.5);
}
""", # noqa
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -579,16 +597,19 @@ class DashedLines(Lines):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
- def __init__(self, positions, colors=(1., 1., 1., 1.),
- indices=None, width=1.):
+ def __init__(self, positions, colors=(1.0, 1.0, 1.0, 1.0), indices=None, width=1.0):
self._dash = 1, 0
- super(DashedLines, self).__init__(positions=positions,
- colors=colors,
- indices=indices,
- mode='lines',
- width=width)
+ super(DashedLines, self).__init__(
+ positions=positions,
+ colors=colors,
+ indices=indices,
+ mode="lines",
+ width=width,
+ )
@property
def dash(self):
@@ -609,7 +630,7 @@ class DashedLines(Lines):
:returns: Coordinates of lines
:rtype: numpy.ndarray of float32 of shape (N, 2, Ndim)
"""
- return self.getAttribute('position', copy=copy)
+ return self.getAttribute("position", copy=copy)
def setPositions(self, positions, copy=True):
"""Set line coordinates.
@@ -617,27 +638,27 @@ class DashedLines(Lines):
:param positions: Array of line coordinates
:param bool copy: True to copy input array, False to use as is
"""
- self.setAttribute('position', positions, copy=copy)
+ self.setAttribute("position", positions, copy=copy)
# Update line origins from given positions
- origins = numpy.array(positions, copy=True, order='C')
+ origins = numpy.array(positions, copy=True, order="C")
origins[1::2] = origins[::2]
- self.setAttribute('origin', origins, copy=False)
+ self.setAttribute("origin", origins, copy=False)
def renderGL2(self, context):
# Prepare program
- isnormals = 'normal' in self._attributes
+ isnormals = "normal" in self._attributes
if isnormals:
fraglightfunction = context.viewport.light.fragmentDef
else:
- fraglightfunction = \
- context.viewport.light.fragmentShaderFunctionNoop
+ fraglightfunction = context.viewport.light.fragmentShaderFunctionNoop
fragment = self._shaders[1].substitute(
sceneDecl=context.fragDecl,
scenePreCall=context.fragCallPre,
scenePostCall=context.fragCallPost,
lightingFunction=fraglightfunction,
- lightingCall=context.viewport.light.fragmentCall)
+ lightingCall=context.viewport.light.fragmentCall,
+ )
program = context.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -646,14 +667,13 @@ class DashedLines(Lines):
gl.glLineWidth(self.width)
- program.setUniformMatrix('matrix', context.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- context.objectToCamera.matrix,
- safe=True)
+ program.setUniformMatrix("matrix", context.objectToNDC.matrix)
+ program.setUniformMatrix(
+ "transformMat", context.objectToCamera.matrix, safe=True
+ )
- gl.glUniform2f(
- program.uniforms['viewportSize'], *context.viewport.size)
- gl.glUniform2f(program.uniforms['dash'], *self.dash)
+ gl.glUniform2f(program.uniforms["viewportSize"], *context.viewport.size)
+ gl.glUniform2f(program.uniforms["dash"], *self.dash)
context.setupProgram(program)
@@ -663,42 +683,64 @@ class DashedLines(Lines):
class Box(core.PrivateGroup):
"""Rectangular box"""
- _lineIndices = numpy.array((
- (0, 1), (1, 2), (2, 3), (3, 0), # Lines with z=0
- (0, 4), (1, 5), (2, 6), (3, 7), # Lines from z=0 to z=1
- (4, 5), (5, 6), (6, 7), (7, 4)), # Lines with z=1
- dtype=numpy.uint8)
+ _lineIndices = numpy.array(
+ (
+ (0, 1),
+ (1, 2),
+ (2, 3),
+ (3, 0), # Lines with z=0
+ (0, 4),
+ (1, 5),
+ (2, 6),
+ (3, 7), # Lines from z=0 to z=1
+ (4, 5),
+ (5, 6),
+ (6, 7),
+ (7, 4),
+ ), # Lines with z=1
+ dtype=numpy.uint8,
+ )
_faceIndices = numpy.array(
- (0, 3, 1, 2, 5, 6, 4, 7, 7, 6, 6, 2, 7, 3, 4, 0, 5, 1),
- dtype=numpy.uint8)
-
- _vertices = numpy.array((
- # Corners with z=0
- (0., 0., 0.), (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- def __init__(self, stroke=(1., 1., 1., 1.), fill=(1., 1., 1., 0.)):
+ (0, 3, 1, 2, 5, 6, 4, 7, 7, 6, 6, 2, 7, 3, 4, 0, 5, 1), dtype=numpy.uint8
+ )
+
+ _vertices = numpy.array(
+ (
+ # Corners with z=0
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (1.0, 1.0, 0.0),
+ (0.0, 1.0, 0.0),
+ # Corners with z=1
+ (0.0, 0.0, 1.0),
+ (1.0, 0.0, 1.0),
+ (1.0, 1.0, 1.0),
+ (0.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ def __init__(self, stroke=(1.0, 1.0, 1.0, 1.0), fill=(1.0, 1.0, 1.0, 0.0)):
super(Box, self).__init__()
- self._fill = Mesh3D(self._vertices,
- colors=rgba(fill),
- mode='triangle_strip',
- indices=self._faceIndices)
- self._fill.visible = self.fillColor[-1] != 0.
+ self._fill = Mesh3D(
+ self._vertices,
+ colors=rgba(fill),
+ mode="triangle_strip",
+ indices=self._faceIndices,
+ )
+ self._fill.visible = self.fillColor[-1] != 0.0
- self._stroke = Lines(self._vertices,
- indices=self._lineIndices,
- colors=rgba(stroke),
- mode='lines')
- self._stroke.visible = self.strokeColor[-1] != 0.
- self.strokeWidth = 1.
+ self._stroke = Lines(
+ self._vertices, indices=self._lineIndices, colors=rgba(stroke), mode="lines"
+ )
+ self._stroke.visible = self.strokeColor[-1] != 0.0
+ self.strokeWidth = 1.0
self._children = [self._stroke, self._fill]
- self._size = 1., 1., 1.
+ self._size = 1.0, 1.0, 1.0
@classmethod
def getLineIndices(cls, copy=True):
@@ -732,11 +774,11 @@ class Box(core.PrivateGroup):
if size != self.size:
self._size = size
self._fill.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self._stroke.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self.notify()
@property
@@ -766,29 +808,29 @@ class Box(core.PrivateGroup):
@property
def strokeColor(self):
"""RGBA color of the box lines (4-tuple of float in [0, 1])"""
- return tuple(self._stroke.getAttribute('color', copy=False))
+ return tuple(self._stroke.getAttribute("color", copy=False))
@strokeColor.setter
def strokeColor(self, color):
color = rgba(color)
if color != self.strokeColor:
- self._stroke.setAttribute('color', color)
+ self._stroke.setAttribute("color", color)
# Fully transparent = hidden
- self._stroke.visible = color[-1] != 0.
+ self._stroke.visible = color[-1] != 0.0
self.notify()
@property
def fillColor(self):
"""RGBA color of the box faces (4-tuple of float in [0, 1])"""
- return tuple(self._fill.getAttribute('color', copy=False))
+ return tuple(self._fill.getAttribute("color", copy=False))
@fillColor.setter
def fillColor(self, color):
color = rgba(color)
if color != self.fillColor:
- self._fill.setAttribute('color', color)
+ self._fill.setAttribute("color", color)
# Fully transparent = hidden
- self._fill.visible = color[-1] != 0.
+ self._fill.visible = color[-1] != 0.0
self.notify()
@property
@@ -802,21 +844,34 @@ class Box(core.PrivateGroup):
class Axes(Lines):
"""3D RGB orthogonal axes"""
- _vertices = numpy.array(((0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.)),
- dtype=numpy.float32)
- _colors = numpy.array(((255, 0, 0, 255), (255, 0, 0, 255),
- (0, 255, 0, 255), (0, 255, 0, 255),
- (0, 0, 255, 255), (0, 0, 255, 255)),
- dtype=numpy.uint8)
+ _vertices = numpy.array(
+ (
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _colors = numpy.array(
+ (
+ (255, 0, 0, 255),
+ (255, 0, 0, 255),
+ (0, 255, 0, 255),
+ (0, 255, 0, 255),
+ (0, 0, 255, 255),
+ (0, 0, 255, 255),
+ ),
+ dtype=numpy.uint8,
+ )
def __init__(self):
- super(Axes, self).__init__(self._vertices,
- colors=self._colors,
- width=3.)
- self._size = 1., 1., 1.
+ super(Axes, self).__init__(self._vertices, colors=self._colors, width=3.0)
+ self._size = 1.0, 1.0, 1.0
@property
def size(self):
@@ -830,8 +885,8 @@ class Axes(Lines):
if size != self.size:
self._size = size
self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self.notify()
@@ -841,39 +896,67 @@ class BoxWithAxes(Lines):
:param color: RGBA color of the box
"""
- _vertices = numpy.array((
- # Axes corners
- (0., 0., 0.), (1., 0., 0.),
- (0., 0., 0.), (0., 1., 0.),
- (0., 0., 0.), (0., 0., 1.),
- # Box corners with z=0
- (1., 0., 0.), (1., 1., 0.), (0., 1., 0.),
- # Box corners with z=1
- (0., 0., 1.), (1., 0., 1.), (1., 1., 1.), (0., 1., 1.)),
- dtype=numpy.float32)
-
- _axesColors = numpy.array(((1., 0., 0., 1.), (1., 0., 0., 1.),
- (0., 1., 0., 1.), (0., 1., 0., 1.),
- (0., 0., 1., 1.), (0., 0., 1., 1.)),
- dtype=numpy.float32)
-
- _lineIndices = numpy.array((
- (0, 1), (2, 3), (4, 5), # Axes lines
- (6, 7), (7, 8), # Box lines with z=0
- (6, 10), (7, 11), (8, 12), # Box lines from z=0 to z=1
- (9, 10), (10, 11), (11, 12), (12, 9)), # Box lines with z=1
- dtype=numpy.uint8)
-
- def __init__(self, color=(1., 1., 1., 1.)):
- self._color = (1., 1., 1., 1.)
+ _vertices = numpy.array(
+ (
+ # Axes corners
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0),
+ # Box corners with z=0
+ (1.0, 0.0, 0.0),
+ (1.0, 1.0, 0.0),
+ (0.0, 1.0, 0.0),
+ # Box corners with z=1
+ (0.0, 0.0, 1.0),
+ (1.0, 0.0, 1.0),
+ (1.0, 1.0, 1.0),
+ (0.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _axesColors = numpy.array(
+ (
+ (1.0, 0.0, 0.0, 1.0),
+ (1.0, 0.0, 0.0, 1.0),
+ (0.0, 1.0, 0.0, 1.0),
+ (0.0, 1.0, 0.0, 1.0),
+ (0.0, 0.0, 1.0, 1.0),
+ (0.0, 0.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _lineIndices = numpy.array(
+ (
+ (0, 1),
+ (2, 3),
+ (4, 5), # Axes lines
+ (6, 7),
+ (7, 8), # Box lines with z=0
+ (6, 10),
+ (7, 11),
+ (8, 12), # Box lines from z=0 to z=1
+ (9, 10),
+ (10, 11),
+ (11, 12),
+ (12, 9),
+ ), # Box lines with z=1
+ dtype=numpy.uint8,
+ )
+
+ def __init__(self, color=(1.0, 1.0, 1.0, 1.0)):
+ self._color = (1.0, 1.0, 1.0, 1.0)
colors = numpy.ones((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
+ colors[: len(self._axesColors), :] = self._axesColors
- super(BoxWithAxes, self).__init__(self._vertices,
- indices=self._lineIndices,
- colors=colors,
- width=2.)
- self._size = 1., 1., 1.
+ super(BoxWithAxes, self).__init__(
+ self._vertices, indices=self._lineIndices, colors=colors, width=2.0
+ )
+ self._size = 1.0, 1.0, 1.0
self.color = color
@property
@@ -887,9 +970,9 @@ class BoxWithAxes(Lines):
if color != self._color:
self._color = color
colors = numpy.empty((len(self._vertices), 4), dtype=numpy.float32)
- colors[:len(self._axesColors), :] = self._axesColors
- colors[len(self._axesColors):, :] = self._color
- self.setAttribute('color', colors) # Do the notification
+ colors[: len(self._axesColors), :] = self._axesColors
+ colors[len(self._axesColors) :, :] = self._color
+ self.setAttribute("color", colors) # Do the notification
@property
def size(self):
@@ -903,8 +986,8 @@ class BoxWithAxes(Lines):
if size != self.size:
self._size = size
self.setAttribute(
- 'position',
- self._vertices * numpy.array(size, dtype=numpy.float32))
+ "position", self._vertices * numpy.array(size, dtype=numpy.float32)
+ )
self.notify()
@@ -916,29 +999,29 @@ class PlaneInGroup(core.PrivateGroup):
Cannot set the transform attribute of this primitive.
This primitive never has any bounds.
"""
+
# TODO inherit from Lines directly?, make sure the plane remains visible?
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0)):
super(PlaneInGroup, self).__init__()
self._cache = None, None # Store bounds, vertices
self._outline = None
self._color = None
- self.color = 1., 1., 1., 1. # Set _color
- self._width = 2.
+ self.color = 1.0, 1.0, 1.0, 1.0 # Set _color
+ self._width = 2.0
self._strokeVisible = True
self._plane = utils.Plane(point, normal)
self._plane.addListener(self._planeChanged)
def moveToCenter(self):
- """Place the plane at the center of the data, not changing orientation.
- """
+ """Place the plane at the center of the data, not changing orientation."""
if self.parent is not None:
bounds = self.parent.bounds(dataBounds=True)
if bounds is not None:
- center = (bounds[0] + bounds[1]) / 2.
- _logger.debug('Moving plane to center: %s', str(center))
+ center = (bounds[0] + bounds[1]) / 2.0
+ _logger.debug("Moving plane to center: %s", str(center))
self.plane.point = center
@property
@@ -950,7 +1033,7 @@ class PlaneInGroup(core.PrivateGroup):
def color(self, color):
self._color = numpy.array(color, copy=True, dtype=numpy.float32)
if self._outline is not None:
- self._outline.setAttribute('color', self._color)
+ self._outline.setAttribute("color", self._color)
self.notify() # This is OK as Lines are rebuild for each rendering
@property
@@ -1019,7 +1102,8 @@ class PlaneInGroup(core.PrivateGroup):
boxVertices = bounds[0] + boxVertices * (bounds[1] - bounds[0])
lineIndices = Box.getLineIndices(copy=False)
vertices = utils.boxPlaneIntersect(
- boxVertices, lineIndices, self.plane.normal, self.plane.point)
+ boxVertices, lineIndices, self.plane.normal, self.plane.point
+ )
self._cache = bounds, vertices if len(vertices) != 0 else None
@@ -1041,15 +1125,15 @@ class PlaneInGroup(core.PrivateGroup):
def prepareGL2(self, ctx):
if self.isValid:
if self._outline is None: # Init outline
- self._outline = Lines(self.contourVertices,
- mode='loop',
- colors=self.color)
+ self._outline = Lines(
+ self.contourVertices, mode="loop", colors=self.color
+ )
self._outline.width = self._width
self._outline.visible = self._strokeVisible
self._children.append(self._outline)
# Update vertices, TODO only when necessary
- self._outline.setAttribute('position', self.contourVertices)
+ self._outline.setAttribute("position", self.contourVertices)
super(PlaneInGroup, self).prepareGL2(ctx)
@@ -1094,28 +1178,36 @@ class BoundedGroup(core.Group):
def _bounds(self, dataBounds=False):
if dataBounds and self.size is not None:
- return numpy.array(((0., 0., 0.), self.size),
- dtype=numpy.float32)
+ return numpy.array(((0.0, 0.0, 0.0), self.size), dtype=numpy.float32)
else:
return super(BoundedGroup, self)._bounds(dataBounds)
# Points ######################################################################
+
class _Points(Geometry):
"""Base class to render a set of points."""
- DIAMOND = 'd'
- CIRCLE = 'o'
- SQUARE = 's'
- PLUS = '+'
- X_MARKER = 'x'
- ASTERISK = '*'
- H_LINE = '_'
- V_LINE = '|'
-
- SUPPORTED_MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS,
- X_MARKER, ASTERISK, H_LINE, V_LINE)
+ DIAMOND = "d"
+ CIRCLE = "o"
+ SQUARE = "s"
+ PLUS = "+"
+ X_MARKER = "x"
+ ASTERISK = "*"
+ H_LINE = "_"
+ V_LINE = "|"
+
+ SUPPORTED_MARKERS = (
+ DIAMOND,
+ CIRCLE,
+ SQUARE,
+ PLUS,
+ X_MARKER,
+ ASTERISK,
+ H_LINE,
+ V_LINE,
+ )
"""List of supported markers:
- 'd' diamond
@@ -1204,10 +1296,12 @@ class _Points(Geometry):
return 0.0;
}
}
- """
+ """,
}
- _shaders = (string.Template("""
+ _shaders = (
+ string.Template(
+ """
#version 120
attribute float x;
@@ -1234,8 +1328,10 @@ class _Points(Geometry):
gl_PointSize = size;
vSize = size;
}
- """),
- string.Template("""
+ """
+ ),
+ string.Template(
+ """
#version 120
varying vec4 vCameraPosition;
@@ -1260,25 +1356,23 @@ class _Points(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
_ATTR_INFO = {
- 'x': {'dims': (1, 2), 'lastDim': (1,)},
- 'y': {'dims': (1, 2), 'lastDim': (1,)},
- 'z': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)},
+ "x": {"dims": (1, 2), "lastDim": (1,)},
+ "y": {"dims": (1, 2), "lastDim": (1,)},
+ "z": {"dims": (1, 2), "lastDim": (1,)},
+ "size": {"dims": (1, 2), "lastDim": (1,)},
}
- def __init__(self, x, y, z, value, size=1., indices=None):
- super(_Points, self).__init__('points', indices,
- x=x,
- y=y,
- z=z,
- value=value,
- size=size,
- attrib0='x')
- self.boundsAttributeNames = 'x', 'y', 'z'
- self._marker = 'o'
+ def __init__(self, x, y, z, value, size=1.0, indices=None):
+ super(_Points, self).__init__(
+ "points", indices, x=x, y=y, z=z, value=value, size=size, attrib0="x"
+ )
+ self.boundsAttributeNames = "x", "y", "z"
+ self._marker = "o"
@property
def marker(self):
@@ -1297,20 +1391,16 @@ class _Points(Geometry):
self.notify()
def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- raise NotImplementedError(
- "This method must be implemented in subclass")
+ """Type definition, fragment shader declaration, fragment shader call"""
+ raise NotImplementedError("This method must be implemented in subclass")
def _renderGL2PreDrawHook(self, ctx, program):
"""Override in subclass to run code before calling gl draw"""
pass
def renderGL2(self, ctx):
- valueType, valueToColorDecl, valueToColorCall = \
- self._shaderValueDefinition()
- vertexShader = self._shaders[0].substitute(
- valueType=valueType)
+ valueType, valueToColorDecl, valueToColorCall = self._shaderValueDefinition()
+ vertexShader = self._shaders[0].substitute(valueType=valueType)
fragmentShader = self._shaders[1].substitute(
sceneDecl=ctx.fragDecl,
scenePreCall=ctx.fragCallPre,
@@ -1318,19 +1408,17 @@ class _Points(Geometry):
valueType=valueType,
valueToColorDecl=valueToColorDecl,
valueToColorCall=valueToColorCall,
- alphaSymbolDecl=self._MARKER_FUNCTIONS[self.marker])
- program = ctx.glCtx.prog(vertexShader, fragmentShader,
- attrib0=self.attrib0)
+ alphaSymbolDecl=self._MARKER_FUNCTIONS[self.marker],
+ )
+ program = ctx.glCtx.prog(vertexShader, fragmentShader, attrib0=self.attrib0)
program.use()
gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
# gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ program.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(program)
@@ -1343,16 +1431,12 @@ class Points(_Points):
"""A set of data points with an associated value and size."""
_ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (1,)}})
+ _ATTR_INFO.update({"value": {"dims": (1, 2), "lastDim": (1,)}})
- def __init__(self, x, y, z, value=0., size=1.,
- indices=None, colormap=None):
- super(Points, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=value)
+ def __init__(self, x, y, z, value=0.0, size=1.0, indices=None, colormap=None):
+ super(Points, self).__init__(
+ x=x, y=y, z=z, indices=indices, size=size, value=value
+ )
self._colormap = colormap or Colormap() # Default colormap
self._colormap.addListener(self._cmapChanged)
@@ -1367,9 +1451,8 @@ class Points(_Points):
self.notify(*args, **kwargs)
def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'float', self.colormap.decl, self.colormap.call
+ """Type definition, fragment shader declaration, fragment shader call"""
+ return "float", self.colormap.decl, self.colormap.call
def _renderGL2PreDrawHook(self, ctx, program):
"""Set-up colormap before calling gl draw"""
@@ -1380,21 +1463,16 @@ class ColorPoints(_Points):
"""A set of points with an associated color and size."""
_ATTR_INFO = _Points._ATTR_INFO.copy()
- _ATTR_INFO.update({'value': {'dims': (1, 2), 'lastDim': (3, 4)}})
+ _ATTR_INFO.update({"value": {"dims": (1, 2), "lastDim": (3, 4)}})
- def __init__(self, x, y, z, color=(1., 1., 1., 1.), size=1.,
- indices=None):
- super(ColorPoints, self).__init__(x=x,
- y=y,
- z=z,
- indices=indices,
- size=size,
- value=color)
+ def __init__(self, x, y, z, color=(1.0, 1.0, 1.0, 1.0), size=1.0, indices=None):
+ super(ColorPoints, self).__init__(
+ x=x, y=y, z=z, indices=indices, size=size, value=color
+ )
def _shaderValueDefinition(self):
- """Type definition, fragment shader declaration, fragment shader call
- """
- return 'vec4', '', ''
+ """Type definition, fragment shader declaration, fragment shader call"""
+ return "vec4", "", ""
def setColor(self, color, copy=True):
"""Set colors
@@ -1404,7 +1482,7 @@ class ColorPoints(_Points):
:param bool copy: True to copy colors (default),
False to use provided array (Do not modify!)
"""
- self.setAttribute('value', color, copy=copy)
+ self.setAttribute("value", color, copy=copy)
def getColor(self, copy=True):
"""Returns the color or array of colors of the points.
@@ -1414,13 +1492,14 @@ class ColorPoints(_Points):
:return: Color or array of colors
:rtype: numpy.ndarray
"""
- return self.getAttribute('value', copy=copy)
+ return self.getAttribute("value", copy=copy)
class GridPoints(Geometry):
# GLSL 1.30 !
"""Data points on a regular grid with an associated value and size."""
- _shaders = ("""
+ _shaders = (
+ """
#version 130
in float value;
@@ -1478,7 +1557,8 @@ class GridPoints(Geometry):
gl_PointSize = size;
}
""",
- string.Template("""
+ string.Template(
+ """
#version 130
in vec4 vCameraPosition;
@@ -1495,18 +1575,27 @@ class GridPoints(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
_ATTR_INFO = {
- 'value': {'dims': (1, 2), 'lastDim': (1,)},
- 'size': {'dims': (1, 2), 'lastDim': (1,)}
+ "value": {"dims": (1, 2), "lastDim": (1,)},
+ "size": {"dims": (1, 2), "lastDim": (1,)},
}
# TODO Add colormap, shape?
# TODO could also use a texture to store values
- def __init__(self, values=0., shape=None, sizes=1., indices=None,
- minValue=None, maxValue=None):
+ def __init__(
+ self,
+ values=0.0,
+ shape=None,
+ sizes=1.0,
+ indices=None,
+ minValue=None,
+ maxValue=None,
+ ):
if isinstance(values, abc.Iterable):
values = numpy.array(values, copy=False)
@@ -1522,16 +1611,14 @@ class GridPoints(Geometry):
assert len(self._shape) in (1, 2, 3)
- super(GridPoints, self).__init__('points', indices,
- value=values,
- size=sizes)
+ super(GridPoints, self).__init__("points", indices, value=values, size=sizes)
- data = self.getAttribute('value', copy=False)
+ data = self.getAttribute("value", copy=False)
self._minValue = data.min() if minValue is None else minValue
self._maxValue = data.max() if maxValue is None else maxValue
- minValue = event.notifyProperty('_minValue')
- maxValue = event.notifyProperty('_maxValue')
+ minValue = event.notifyProperty("_minValue")
+ maxValue = event.notifyProperty("_maxValue")
def _bounds(self, dataBounds=False):
# Get bounds from values shape
@@ -1544,7 +1631,8 @@ class GridPoints(Geometry):
fragment = self._shaders[1].substitute(
sceneDecl=ctx.fragDecl,
scenePreCall=ctx.fragCallPre,
- scenePostCall=ctx.fragCallPost)
+ scenePostCall=ctx.fragCallPost,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -1552,25 +1640,26 @@ class GridPoints(Geometry):
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
# gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
- gl.glUniform3i(prog.uniforms['gridDims'],
- self._shape[2] if len(self._shape) == 3 else 1,
- self._shape[1] if len(self._shape) >= 2 else 1,
- self._shape[0])
+ gl.glUniform3i(
+ prog.uniforms["gridDims"],
+ self._shape[2] if len(self._shape) == 3 else 1,
+ self._shape[1] if len(self._shape) >= 2 else 1,
+ self._shape[0],
+ )
- gl.glUniform2f(prog.uniforms['valRange'], self.minValue, self.maxValue)
+ gl.glUniform2f(prog.uniforms["valRange"], self.minValue, self.maxValue)
self._draw(prog, nbVertices=reduce(lambda a, b: a * b, self._shape))
# Spheres #####################################################################
+
class Spheres(Geometry):
"""A set of spheres.
@@ -1581,6 +1670,7 @@ class Spheres(Geometry):
- Do not render distorion by perspective projection.
- If the sphere center is clipped, the whole sphere is not displayed.
"""
+
# TODO check those links
# Accounting for perspective projection
# http://iquilezles.org/www/articles/sphereproj/sphereproj.htm
@@ -1593,7 +1683,8 @@ class Spheres(Geometry):
# TODO some issues with small scaling and regular grid or due to sampling
- _shaders = ("""
+ _shaders = (
+ """
#version 120
attribute vec3 position;
@@ -1632,7 +1723,8 @@ class Spheres(Geometry):
vViewDepth = vCameraPosition.z;
}
""",
- string.Template("""
+ string.Template(
+ """
# version 120
uniform mat4 projMat;
@@ -1672,20 +1764,21 @@ class Spheres(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
_ATTR_INFO = {
- 'position': {'dims': (2, ), 'lastDim': (2, 3, 4)},
- 'radius': {'dims': (1, 2), 'lastDim': (1, )},
- 'color': {'dims': (1, 2), 'lastDim': (3, 4)},
+ "position": {"dims": (2,), "lastDim": (2, 3, 4)},
+ "radius": {"dims": (1, 2), "lastDim": (1,)},
+ "color": {"dims": (1, 2), "lastDim": (3, 4)},
}
- def __init__(self, positions, radius=1., colors=(1., 1., 1., 1.)):
+ def __init__(self, positions, radius=1.0, colors=(1.0, 1.0, 1.0, 1.0)):
self.__bounds = None
- super(Spheres, self).__init__('points', None,
- position=positions,
- radius=radius,
- color=colors)
+ super(Spheres, self).__init__(
+ "points", None, position=positions, radius=radius, color=colors
+ )
def renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
@@ -1693,7 +1786,8 @@ class Spheres(Geometry):
scenePreCall=ctx.fragCallPre,
scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
- lightingCall=ctx.viewport.light.fragmentCall)
+ lightingCall=ctx.viewport.light.fragmentCall,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -1703,14 +1797,12 @@ class Spheres(Geometry):
gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
# gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
- prog.setUniformMatrix('projMat', ctx.projection.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("projMat", ctx.projection.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
- gl.glUniform2f(prog.uniforms['screenSize'], *ctx.viewport.size)
+ gl.glUniform2f(prog.uniforms["screenSize"], *ctx.viewport.size)
self._draw(prog)
@@ -1718,21 +1810,25 @@ class Spheres(Geometry):
if self.__bounds is None:
self.__bounds = numpy.zeros((2, 3), dtype=numpy.float32)
# Support vertex with to 2 to 4 coordinates
- positions = self._attributes['position']
- radius = self._attributes['radius']
- self.__bounds[0, :positions.shape[1]] = \
- (positions - radius).min(axis=0)[:3]
- self.__bounds[1, :positions.shape[1]] = \
- (positions + radius).max(axis=0)[:3]
+ positions = self._attributes["position"]
+ radius = self._attributes["radius"]
+ self.__bounds[0, : positions.shape[1]] = (positions - radius).min(axis=0)[
+ :3
+ ]
+ self.__bounds[1, : positions.shape[1]] = (positions + radius).max(axis=0)[
+ :3
+ ]
return self.__bounds.copy()
# Meshes ######################################################################
+
class Mesh3D(Geometry):
"""A conventional 3D mesh"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
@@ -1756,7 +1852,8 @@ class Mesh3D(Geometry):
gl_Position = matrix * vec4(position, 1.0);
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec3 vNormal;
@@ -1773,21 +1870,17 @@ class Mesh3D(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self,
- positions,
- colors,
- normals=None,
- mode='triangles',
- indices=None,
- copy=True):
+ """
+ ),
+ )
+
+ def __init__(
+ self, positions, colors, normals=None, mode="triangles", indices=None, copy=True
+ ):
assert mode in self._TRIANGLE_MODES
- super(Mesh3D, self).__init__(mode, indices,
- position=positions,
- normal=normals,
- color=colors,
- copy=copy)
+ super(Mesh3D, self).__init__(
+ mode, indices, position=positions, normal=normals, color=colors, copy=copy
+ )
self._culling = None
@@ -1801,13 +1894,13 @@ class Mesh3D(Geometry):
@culling.setter
def culling(self, culling):
- assert culling in ('back', 'front', None)
+ assert culling in ("back", "front", None)
if culling != self._culling:
self._culling = culling
self.notify()
def renderGL2(self, ctx):
- isnormals = 'normal' in self._attributes
+ isnormals = "normal" in self._attributes
if isnormals:
fragLightFunction = ctx.viewport.light.fragmentDef
else:
@@ -1818,7 +1911,8 @@ class Mesh3D(Geometry):
scenePreCall=ctx.fragCallPre,
scenePostCall=ctx.fragCallPost,
lightingFunction=fragLightFunction,
- lightingCall=ctx.viewport.light.fragmentCall)
+ lightingCall=ctx.viewport.light.fragmentCall,
+ )
prog = ctx.glCtx.prog(self._shaders[0], fragment)
prog.use()
@@ -1826,14 +1920,12 @@ class Mesh3D(Geometry):
ctx.viewport.light.setupProgram(ctx, prog)
if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
+ cullFace = gl.GL_FRONT if self.culling == "front" else gl.GL_BACK
gl.glCullFace(cullFace)
gl.glEnable(gl.GL_CULL_FACE)
- prog.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- prog.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
+ prog.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ prog.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
ctx.setupProgram(prog)
@@ -1846,7 +1938,8 @@ class Mesh3D(Geometry):
class ColormapMesh3D(Geometry):
"""A 3D mesh with color computed from a colormap"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec3 position;
attribute vec3 normal;
attribute float value;
@@ -1870,7 +1963,8 @@ class ColormapMesh3D(Geometry):
gl_Position = matrix * vec4(position, 1.0);
}
""",
- string.Template("""
+ string.Template(
+ """
uniform float alpha;
varying vec4 vCameraPosition;
@@ -1892,21 +1986,23 @@ class ColormapMesh3D(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
-
- def __init__(self,
- position,
- value,
- colormap=None,
- normal=None,
- mode='triangles',
- indices=None,
- copy=True):
- super(ColormapMesh3D, self).__init__(mode, indices,
- position=position,
- normal=normal,
- value=value,
- copy=copy)
+ """
+ ),
+ )
+
+ def __init__(
+ self,
+ position,
+ value,
+ colormap=None,
+ normal=None,
+ mode="triangles",
+ indices=None,
+ copy=True,
+ ):
+ super(ColormapMesh3D, self).__init__(
+ mode, indices, position=position, normal=normal, value=value, copy=copy
+ )
self._alpha = 1.0
self._lineWidth = 1.0
@@ -1915,17 +2011,19 @@ class ColormapMesh3D(Geometry):
self._colormap = colormap or Colormap() # Default colormap
self._colormap.addListener(self._cmapChanged)
- lineWidth = event.notifyProperty('_lineWidth', converter=float,
- doc="Width of the line in pixels.")
+ lineWidth = event.notifyProperty(
+ "_lineWidth", converter=float, doc="Width of the line in pixels."
+ )
lineSmooth = event.notifyProperty(
- '_lineSmooth',
+ "_lineSmooth",
converter=bool,
- doc="Smooth line rendering enabled (bool, default: True)")
+ doc="Smooth line rendering enabled (bool, default: True)",
+ )
alpha = event.notifyProperty(
- '_alpha', converter=float,
- doc="Transparency of the mesh, float in [0, 1]")
+ "_alpha", converter=float, doc="Transparency of the mesh, float in [0, 1]"
+ )
@property
def culling(self):
@@ -1937,7 +2035,7 @@ class ColormapMesh3D(Geometry):
@culling.setter
def culling(self, culling):
- assert culling in ('back', 'front', None)
+ assert culling in ("back", "front", None)
if culling != self._culling:
self._culling = culling
self.notify()
@@ -1952,7 +2050,7 @@ class ColormapMesh3D(Geometry):
self.notify(*args, **kwargs)
def renderGL2(self, ctx):
- if 'normal' in self._attributes:
+ if "normal" in self._attributes:
self._renderGL2(ctx)
else: # Disable lighting
with self.viewport.light.turnOff():
@@ -1966,7 +2064,8 @@ class ColormapMesh3D(Geometry):
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
+ colormapCall=self.colormap.call,
+ )
program = ctx.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -1975,15 +2074,13 @@ class ColormapMesh3D(Geometry):
self.colormap.setupProgram(ctx, program)
if self.culling is not None:
- cullFace = gl.GL_FRONT if self.culling == 'front' else gl.GL_BACK
+ cullFace = gl.GL_FRONT if self.culling == "front" else gl.GL_BACK
gl.glCullFace(cullFace)
gl.glEnable(gl.GL_CULL_FACE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ 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)
@@ -1998,10 +2095,12 @@ class ColormapMesh3D(Geometry):
# ImageData ##################################################################
+
class _Image(Geometry):
"""Base class for ImageData and ImageRgba"""
- _shaders = ("""
+ _shaders = (
+ """
attribute vec2 position;
uniform mat4 matrix;
@@ -2022,7 +2121,8 @@ class _Image(Geometry):
gl_Position = matrix * positionVec4;
}
""",
- string.Template("""
+ string.Template(
+ """
varying vec4 vCameraPosition;
varying vec3 vPosition;
varying vec2 vTexCoords;
@@ -2048,22 +2148,24 @@ class _Image(Geometry):
$scenePostCall(vCameraPosition);
}
- """))
+ """
+ ),
+ )
- _UNIT_SQUARE = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32)
+ _UNIT_SQUARE = numpy.array(
+ ((0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (1.0, 1.0)), dtype=numpy.float32
+ )
def __init__(self, data, copy=True):
- super(_Image, self).__init__(mode='triangle_strip',
- position=self._UNIT_SQUARE)
+ super(_Image, self).__init__(mode="triangle_strip", position=self._UNIT_SQUARE)
self._texture = None
self._update_texture = True
self._update_texture_filter = False
self._data = None
self.setData(data, copy)
- self._alpha = 1.
- self._interpolation = 'linear'
+ self._alpha = 1.0
+ self._interpolation = "linear"
self.isBackfaceVisible = True
@@ -2077,7 +2179,9 @@ class _Image(Geometry):
self._update_texture = True
# By updating the position rather than always using a unit square
# we benefit from Geometry bounds handling
- self.setAttribute('position', self._UNIT_SQUARE * (self._data.shape[1], self._data.shape[0]))
+ self.setAttribute(
+ "position", self._UNIT_SQUARE * (self._data.shape[1], self._data.shape[0])
+ )
self.notify()
def getData(self, copy=True):
@@ -2090,7 +2194,7 @@ class _Image(Geometry):
@interpolation.setter
def interpolation(self, interpolation):
- assert interpolation in ('linear', 'nearest')
+ assert interpolation in ("linear", "nearest")
self._interpolation = interpolation
self._update_texture_filter = True
self.notify()
@@ -2110,15 +2214,14 @@ class _Image(Geometry):
:return: 2-tuple of gl flags (internalFormat, format)
"""
- raise NotImplementedError(
- "This method must be implemented in a subclass")
+ raise NotImplementedError("This method must be implemented in a subclass")
def prepareGL2(self, ctx):
if self._texture is None or self._update_texture:
if self._texture is not None:
self._texture.discard()
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
@@ -2134,11 +2237,12 @@ class _Image(Geometry):
format_,
minFilter=filter_,
magFilter=filter_,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
if self._update_texture_filter and self._texture is not None:
self._update_texture_filter = False
- if self.interpolation == 'nearest':
+ if self.interpolation == "nearest":
filter_ = gl.GL_NEAREST
else:
filter_ = gl.GL_LINEAR
@@ -2160,8 +2264,7 @@ class _Image(Geometry):
def _shaderImageColorDecl(self):
"""Returns fragment shader imageColor function declaration"""
- raise NotImplementedError(
- "This method must be implemented in a subclass")
+ raise NotImplementedError("This method must be implemented in a subclass")
def _renderGL2(self, ctx):
fragment = self._shaders[1].substitute(
@@ -2170,8 +2273,8 @@ class _Image(Geometry):
scenePostCall=ctx.fragCallPost,
lightingFunction=ctx.viewport.light.fragmentDef,
lightingCall=ctx.viewport.light.fragmentCall,
- imageDecl=self._shaderImageColorDecl()
- )
+ imageDecl=self._shaderImageColorDecl(),
+ )
program = ctx.glCtx.prog(self._shaders[0], fragment)
program.use()
@@ -2181,16 +2284,14 @@ class _Image(Geometry):
gl.glCullFace(gl.GL_BACK)
gl.glEnable(gl.GL_CULL_FACE)
- program.setUniformMatrix('matrix', ctx.objectToNDC.matrix)
- program.setUniformMatrix('transformMat',
- ctx.objectToCamera.matrix,
- safe=True)
- gl.glUniform1f(program.uniforms['alpha'], self._alpha)
+ program.setUniformMatrix("matrix", ctx.objectToNDC.matrix)
+ program.setUniformMatrix("transformMat", ctx.objectToCamera.matrix, safe=True)
+ gl.glUniform1f(program.uniforms["alpha"], self._alpha)
shape = self._data.shape
- gl.glUniform2f(program.uniforms['dataScale'], 1./shape[1], 1./shape[0])
+ gl.glUniform2f(program.uniforms["dataScale"], 1.0 / shape[1], 1.0 / shape[0])
- gl.glUniform1i(program.uniforms['data'], self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["data"], self._texture.texUnit)
ctx.setupProgram(program)
@@ -2207,7 +2308,8 @@ class _Image(Geometry):
class ImageData(_Image):
"""Display a 2x2 data array with a texture."""
- _imageDecl = string.Template("""
+ _imageDecl = string.Template(
+ """
$colormapDecl
vec4 imageColor(sampler2D data, vec2 texCoords) {
@@ -2215,7 +2317,8 @@ class ImageData(_Image):
vec4 color = $colormapCall(value);
return color;
}
- """)
+ """
+ )
def __init__(self, data, copy=True, colormap=None):
super(ImageData, self).__init__(data, copy=copy)
@@ -2224,7 +2327,7 @@ class ImageData(_Image):
self._colormap.addListener(self._cmapChanged)
def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C', dtype=numpy.float32)
+ data = numpy.array(data, copy=copy, order="C", dtype=numpy.float32)
# TODO support (u)int8|16
assert data.ndim == 2
@@ -2247,12 +2350,13 @@ class ImageData(_Image):
def _shaderImageColorDecl(self):
return self._imageDecl.substitute(
- colormapDecl=self.colormap.decl,
- colormapCall=self.colormap.call)
+ colormapDecl=self.colormap.decl, colormapCall=self.colormap.call
+ )
# ImageRgba ##################################################################
+
class ImageRgba(_Image):
"""Display a 2x2 RGBA image with a texture.
@@ -2270,10 +2374,10 @@ class ImageRgba(_Image):
super(ImageRgba, self).__init__(data, copy=copy)
def setData(self, data, copy=True):
- data = numpy.array(data, copy=copy, order='C')
+ data = numpy.array(data, copy=copy, order="C")
assert data.ndim == 3
assert data.shape[2] in (3, 4)
- if data.dtype.kind == 'f':
+ if data.dtype.kind == "f":
if data.dtype != numpy.dtype(numpy.float32):
_logger.warning("Converting image data to float32")
data = numpy.array(data, dtype=numpy.float32, copy=False)
@@ -2295,6 +2399,7 @@ class ImageRgba(_Image):
# TODO lighting, clipping as groups?
# group composition?
+
class GroupDepthOffset(core.Group):
"""A group using 2-pass rendering and glDepthRange to avoid Z-fighting"""
@@ -2306,7 +2411,7 @@ class GroupDepthOffset(core.Group):
def prepareGL2(self, ctx):
if self._epsilon is None:
depthbits = gl.glGetInteger(gl.GL_DEPTH_BITS)
- self._epsilon = 1. / (1 << (depthbits - 1))
+ self._epsilon = 1.0 / (1 << (depthbits - 1))
def renderGL2(self, ctx):
if self.isDepthRangeOn:
@@ -2319,38 +2424,34 @@ class GroupDepthOffset(core.Group):
with gl.enabled(gl.GL_CULL_FACE):
gl.glCullFace(gl.GL_BACK)
for child in self.children:
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
+ gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
+ gl.glDepthRange(self._epsilon, 1.0)
child.render(ctx)
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
+ gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
+ gl.glDepthRange(0.0, 1.0 - self._epsilon)
child.render(ctx)
gl.glCullFace(gl.GL_FRONT)
for child in reversed(self.children):
- gl.glColorMask(
- gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
+ gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(self._epsilon, 1.)
+ gl.glDepthRange(self._epsilon, 1.0)
child.render(ctx)
- gl.glColorMask(
- gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
+ gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_FALSE)
- gl.glDepthRange(0., 1. - self._epsilon)
+ gl.glDepthRange(0.0, 1.0 - self._epsilon)
child.render(ctx)
gl.glDepthMask(gl.GL_TRUE)
- gl.glDepthRange(0., 1.)
+ gl.glDepthRange(0.0, 1.0)
# gl.glDepthFunc(gl.GL_LEQUAL)
# TODO use epsilon for all rendering?
# TODO issue with picking in depth buffer!
@@ -2382,7 +2483,7 @@ class GroupNoDepth(core.Group):
class GroupBBox(core.PrivateGroup):
"""A group displaying a bounding box around the children."""
- def __init__(self, children=(), color=(1., 1., 1., 1.)):
+ def __init__(self, children=(), color=(1.0, 1.0, 1.0, 1.0)):
super(GroupBBox, self).__init__()
self._group = core.Group(children)
@@ -2394,7 +2495,7 @@ class GroupBBox(core.PrivateGroup):
self._boxWithAxes.smooth = False
self._boxWithAxes.transforms = self._boxTransforms
- self._box = Box(stroke=color, fill=(1., 1., 1., 0.))
+ self._box = Box(stroke=color, fill=(1.0, 1.0, 1.0, 0.0))
self._box.strokeSmooth = False
self._box.transforms = self._boxTransforms
self._box.visible = False
@@ -2404,7 +2505,7 @@ class GroupBBox(core.PrivateGroup):
self._axes.transforms = self._boxTransforms
self._axes.visible = False
- self.strokeWidth = 2.
+ self.strokeWidth = 2.0
self._children = [self._boxWithAxes, self._box, self._axes, self._group]
@@ -2415,7 +2516,7 @@ class GroupBBox(core.PrivateGroup):
origin = bounds[0]
size = bounds[1] - bounds[0]
else:
- origin, size = (0., 0., 0.), (1., 1., 1.)
+ origin, size = (0.0, 0.0, 0.0), (1.0, 1.0, 1.0)
self._boxTransforms[0].translation = origin
@@ -2484,8 +2585,9 @@ class GroupBBox(core.PrivateGroup):
@axesVisible.setter
def axesVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=bool(visible),
- boxVisible=self.boxVisible)
+ self._updateBoxAndAxesVisibility(
+ axesVisible=bool(visible), boxVisible=self.boxVisible
+ )
@property
def boxVisible(self):
@@ -2494,12 +2596,14 @@ class GroupBBox(core.PrivateGroup):
@boxVisible.setter
def boxVisible(self, visible):
- self._updateBoxAndAxesVisibility(axesVisible=self.axesVisible,
- boxVisible=bool(visible))
+ self._updateBoxAndAxesVisibility(
+ axesVisible=self.axesVisible, boxVisible=bool(visible)
+ )
# Clipping Plane ##############################################################
+
class ClipPlane(PlaneInGroup):
"""A clipping plane attached to a box"""
@@ -2510,8 +2614,9 @@ class ClipPlane(PlaneInGroup):
# Set-up clipping plane for following brothers
# No need of perspective divide, no projection
- point = ctx.objectToCamera.transformPoint(self.plane.point,
- perspectiveDivide=False)
+ point = ctx.objectToCamera.transformPoint(
+ self.plane.point, perspectiveDivide=False
+ )
normal = ctx.objectToCamera.transformNormal(self.plane.normal)
ctx.setClipPlane(point, normal)
diff --git a/src/silx/gui/plot3d/scene/test/test_transform.py b/src/silx/gui/plot3d/scene/test/test_transform.py
index 2998c65..cba384d 100644
--- a/src/silx/gui/plot3d/scene/test/test_transform.py
+++ b/src/silx/gui/plot3d/scene/test/test_transform.py
@@ -34,7 +34,6 @@ from silx.gui.plot3d.scene import transform
class TestTransformList(unittest.TestCase):
-
def assertSameArrays(self, a, b):
return self.assertTrue(numpy.allclose(a, b, atol=1e-06))
@@ -45,25 +44,36 @@ class TestTransformList(unittest.TestCase):
self.assertSameArrays(refmatrix, transforms.matrix)
# Append translate
- transforms.append(transform.Translate(1., 1., 1.))
- refmatrix = numpy.array(((1., 0., 0., 1.),
- (0., 1., 0., 1.),
- (0., 0., 1., 1.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ transforms.append(transform.Translate(1.0, 1.0, 1.0))
+ refmatrix = numpy.array(
+ (
+ (1.0, 0.0, 0.0, 1.0),
+ (0.0, 1.0, 0.0, 1.0),
+ (0.0, 0.0, 1.0, 1.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
self.assertSameArrays(refmatrix, transforms.matrix)
# Extend scale
- transforms.extend([transform.Scale(0.1, 2., 1.)])
- refmatrix = numpy.dot(refmatrix,
- numpy.array(((0.1, 0., 0., 0.),
- (0., 2., 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)),
- dtype=numpy.float32))
+ transforms.extend([transform.Scale(0.1, 2.0, 1.0)])
+ refmatrix = numpy.dot(
+ refmatrix,
+ numpy.array(
+ (
+ (0.1, 0.0, 0.0, 0.0),
+ (0.0, 2.0, 0.0, 0.0),
+ (0.0, 0.0, 1.0, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ ),
+ )
self.assertSameArrays(refmatrix, transforms.matrix)
# Insert rotate
- transforms.insert(0, transform.Rotate(360.))
+ transforms.insert(0, transform.Rotate(360.0))
self.assertSameArrays(refmatrix, transforms.matrix)
# Update translate and check for listener called
@@ -71,6 +81,7 @@ class TestTransformList(unittest.TestCase):
def listener(source):
self._callCount += 1
+
transforms.addListener(listener)
transforms[1].tx += 1
diff --git a/src/silx/gui/plot3d/scene/test/test_utils.py b/src/silx/gui/plot3d/scene/test/test_utils.py
index a9ba6bc..81f99d6 100644
--- a/src/silx/gui/plot3d/scene/test/test_utils.py
+++ b/src/silx/gui/plot3d/scene/test/test_utils.py
@@ -27,7 +27,6 @@ __license__ = "MIT"
__date__ = "17/01/2018"
-import unittest
from silx.utils.testutils import ParametricTestCase
import numpy
@@ -37,34 +36,35 @@ from silx.gui.plot3d.scene import utils
# angleBetweenVectors #########################################################
-class TestAngleBetweenVectors(ParametricTestCase):
+class TestAngleBetweenVectors(ParametricTestCase):
TESTS = { # name: (refvector, vectors, norm, refangles)
- 'single vector':
- ((1., 0., 0.), (1., 0., 0.), (0., 0., 1.), 0.),
- 'single vector, no norm':
- ((1., 0., 0.), (1., 0., 0.), None, 0.),
-
- 'with orthogonal norm':
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- (0., 0., 1.),
- (0., 90., 180., 270.)),
-
- 'with coplanar norm': # = similar to no norm
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- (1., 0., 0.),
- (0., 90., 180., 90.)),
-
- 'without norm':
- ((1., 0., 0.),
- ((1., 0., 0.), (0., 1., 0.), (-1., 0., 0.), (0., -1., 0.)),
- None,
- (0., 90., 180., 90.)),
-
- 'not unit vectors':
- ((2., 2., 0.), ((1., 1., 0.), (1., -1., 0.)), None, (0., 90.)),
+ "single vector": ((1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 1.0), 0.0),
+ "single vector, no norm": ((1.0, 0.0, 0.0), (1.0, 0.0, 0.0), None, 0.0),
+ "with orthogonal norm": (
+ (1.0, 0.0, 0.0),
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
+ (0.0, 0.0, 1.0),
+ (0.0, 90.0, 180.0, 270.0),
+ ),
+ "with coplanar norm": ( # = similar to no norm
+ (1.0, 0.0, 0.0),
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
+ (1.0, 0.0, 0.0),
+ (0.0, 90.0, 180.0, 90.0),
+ ),
+ "without norm": (
+ (1.0, 0.0, 0.0),
+ ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (-1.0, 0.0, 0.0), (0.0, -1.0, 0.0)),
+ None,
+ (0.0, 90.0, 180.0, 90.0),
+ ),
+ "not unit vectors": (
+ (2.0, 2.0, 0.0),
+ ((1.0, 1.0, 0.0), (1.0, -1.0, 0.0)),
+ None,
+ (0.0, 90.0),
+ ),
}
def testAngleBetweenVectorsFunction(self):
@@ -78,15 +78,14 @@ class TestAngleBetweenVectors(ParametricTestCase):
if norm is not None:
norm = numpy.array(norm)
- testangles = utils.angleBetweenVectors(
- refvector, vectors, norm)
+ testangles = utils.angleBetweenVectors(refvector, vectors, norm)
- self.assertTrue(
- numpy.allclose(testangles, refangles, atol=1e-5))
+ self.assertTrue(numpy.allclose(testangles, refangles, atol=1e-5))
# Plane #######################################################################
+
class AssertNotificationContext(object):
"""Context that checks if an event.Notifier is sending events."""
@@ -118,9 +117,9 @@ class TestPlaneParameters(ParametricTestCase):
"""Test Plane.parameters read/write and notifications."""
PARAMETERS = {
- 'unit normal': (1., 0., 0., 1.),
- 'not unit normal': (1., 1., 0., 1.),
- 'd = 0': (1., 0., 0., 0.)
+ "unit normal": (1.0, 0.0, 0.0, 1.0),
+ "not unit normal": (1.0, 1.0, 0.0, 1.0),
+ "d = 0": (1.0, 0.0, 0.0, 0.0),
}
def testParameters(self):
@@ -136,12 +135,9 @@ class TestPlaneParameters(ParametricTestCase):
normparams = parameters / numpy.linalg.norm(parameters[:3])
self.assertTrue(numpy.allclose(plane.parameters, normparams))
- ZEROS_PARAMETERS = (
- (0., 0., 0., 0.),
- (0., 0., 0., 1.)
- )
+ ZEROS_PARAMETERS = ((0.0, 0.0, 0.0, 0.0), (0.0, 0.0, 0.0, 1.0))
- ZEROS = 0., 0., 0., 0.
+ ZEROS = 0.0, 0.0, 0.0, 0.0
def testParametersNoPlane(self):
"""Test Plane.parameters with ||normal|| == 0 ."""
@@ -152,24 +148,25 @@ class TestPlaneParameters(ParametricTestCase):
with self.subTest(parameters=parameters):
with AssertNotificationContext(plane, count=0):
plane.parameters = parameters
- self.assertTrue(
- numpy.allclose(plane.parameters, self.ZEROS, 0., 0.))
+ self.assertTrue(numpy.allclose(plane.parameters, self.ZEROS, 0.0, 0.0))
# unindexArrays ###############################################################
+
class TestUnindexArrays(ParametricTestCase):
"""Test unindexArrays function."""
def testBasicModes(self):
"""Test for modes: points, lines and triangles"""
indices = numpy.array((1, 2, 0))
- arrays = (numpy.array((0., 1., 2.)),
- numpy.array(((0, 0), (1, 1), (2, 2))))
- refresults = (numpy.array((1., 2., 0.)),
- numpy.array(((1, 1), (2, 2), (0, 0))))
+ arrays = (numpy.array((0.0, 1.0, 2.0)), numpy.array(((0, 0), (1, 1), (2, 2))))
+ refresults = (
+ numpy.array((1.0, 2.0, 0.0)),
+ numpy.array(((1, 1), (2, 2), (0, 0))),
+ )
- for mode in ('points', 'lines', 'triangles'):
+ for mode in ("points", "lines", "triangles"):
with self.subTest(mode=mode):
testresults = utils.unindexArrays(mode, indices, *arrays)
for ref, test in zip(refresults, testresults):
@@ -178,15 +175,16 @@ class TestUnindexArrays(ParametricTestCase):
def testPackedLines(self):
"""Test for modes: line_strip, loop"""
indices = numpy.array((1, 2, 0))
- arrays = (numpy.array((0., 1., 2.)),
- numpy.array(((0, 0), (1, 1), (2, 2))))
+ arrays = (numpy.array((0.0, 1.0, 2.0)), numpy.array(((0, 0), (1, 1), (2, 2))))
results = {
- 'line_strip': (
- numpy.array((1., 2., 2., 0.)),
- numpy.array(((1, 1), (2, 2), (2, 2), (0, 0)))),
- 'loop': (
- numpy.array((1., 2., 2., 0., 0., 1.)),
- numpy.array(((1, 1), (2, 2), (2, 2), (0, 0), (0, 0), (1, 1)))),
+ "line_strip": (
+ numpy.array((1.0, 2.0, 2.0, 0.0)),
+ numpy.array(((1, 1), (2, 2), (2, 2), (0, 0))),
+ ),
+ "loop": (
+ numpy.array((1.0, 2.0, 2.0, 0.0, 0.0, 1.0)),
+ numpy.array(((1, 1), (2, 2), (2, 2), (0, 0), (0, 0), (1, 1))),
+ ),
}
for mode, refresults in results.items():
@@ -198,15 +196,19 @@ class TestUnindexArrays(ParametricTestCase):
def testPackedTriangles(self):
"""Test for modes: triangle_strip, fan"""
indices = numpy.array((1, 2, 0, 3))
- arrays = (numpy.array((0., 1., 2., 3.)),
- numpy.array(((0, 0), (1, 1), (2, 2), (3, 3))))
+ arrays = (
+ numpy.array((0.0, 1.0, 2.0, 3.0)),
+ numpy.array(((0, 0), (1, 1), (2, 2), (3, 3))),
+ )
results = {
- 'triangle_strip': (
- numpy.array((1., 2., 0., 2., 0., 3.)),
- numpy.array(((1, 1), (2, 2), (0, 0), (2, 2), (0, 0), (3, 3)))),
- 'fan': (
- numpy.array((1., 2., 0., 1., 0., 3.)),
- numpy.array(((1, 1), (2, 2), (0, 0), (1, 1), (0, 0), (3, 3)))),
+ "triangle_strip": (
+ numpy.array((1.0, 2.0, 0.0, 2.0, 0.0, 3.0)),
+ numpy.array(((1, 1), (2, 2), (0, 0), (2, 2), (0, 0), (3, 3))),
+ ),
+ "fan": (
+ numpy.array((1.0, 2.0, 0.0, 1.0, 0.0, 3.0)),
+ numpy.array(((1, 1), (2, 2), (0, 0), (1, 1), (0, 0), (3, 3))),
+ ),
}
for mode, refresults in results.items():
@@ -221,35 +223,49 @@ class TestUnindexArrays(ParametricTestCase):
# negative indices
with self.assertRaises(AssertionError):
- utils.unindexArrays('points', (-1, 0), *arrays)
+ utils.unindexArrays("points", (-1, 0), *arrays)
# Too high indices
with self.assertRaises(AssertionError):
- utils.unindexArrays('points', (0, 10), *arrays)
+ utils.unindexArrays("points", (0, 10), *arrays)
# triangleNormals #############################################################
+
class TestTriangleNormals(ParametricTestCase):
"""Test triangleNormals function."""
def test(self):
"""Test for modes: points, lines and triangles"""
positions = numpy.array(
- ((0., 0., 0.), (1., 0., 0.), (0., 1., 0.), # normal = Z
- (1., 1., 1.), (1., 2., 3.), (4., 5., 6.), # Random triangle
- # Degenerated triangles:
- (0., 0., 0.), (1., 0., 0.), (2., 0., 0.), # Colinear points
- (1., 1., 1.), (1., 1., 1.), (1., 1., 1.), # All same point
- ),
- dtype='float32')
+ (
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (0.0, 1.0, 0.0), # normal = Z
+ (1.0, 1.0, 1.0),
+ (1.0, 2.0, 3.0),
+ (4.0, 5.0, 6.0), # Random triangle
+ # Degenerated triangles:
+ (0.0, 0.0, 0.0),
+ (1.0, 0.0, 0.0),
+ (2.0, 0.0, 0.0), # Colinear points
+ (1.0, 1.0, 1.0),
+ (1.0, 1.0, 1.0),
+ (1.0, 1.0, 1.0), # All same point
+ ),
+ dtype="float32",
+ )
normals = numpy.array(
- ((0., 0., 1.),
- (-0.40824829, 0.81649658, -0.40824829),
- (0., 0., 0.),
- (0., 0., 0.)),
- dtype='float32')
+ (
+ (0.0, 0.0, 1.0),
+ (-0.40824829, 0.81649658, -0.40824829),
+ (0.0, 0.0, 0.0),
+ (0.0, 0.0, 0.0),
+ ),
+ dtype="float32",
+ )
testnormals = utils.trianglesNormal(positions)
self.assertTrue(numpy.allclose(testnormals, normals))
diff --git a/src/silx/gui/plot3d/scene/text.py b/src/silx/gui/plot3d/scene/text.py
index 3c4e692..79cdb13 100644
--- a/src/silx/gui/plot3d/scene/text.py
+++ b/src/silx/gui/plot3d/scene/text.py
@@ -33,7 +33,7 @@ import numpy
from silx.gui.colors import rgba
-from ... import _glutils
+from ... import _glutils, qt
from ..._glutils import gl
from ..._glutils import font as _font
@@ -62,24 +62,18 @@ class Font(event.Notifier):
super(Font, self).__init__()
name = event.notifyProperty(
- '_name',
- doc="""Name of the font (str)""",
- converter=str)
+ "_name", doc="""Name of the font (str)""", converter=str
+ )
size = event.notifyProperty(
- '_size',
- doc="""Font size in points (int)""",
- converter=int)
+ "_size", doc="""Font size in points (int)""", converter=int
+ )
- weight = event.notifyProperty(
- '_weight',
- doc="""Font size in points (int)""",
- converter=int)
+ weight = event.notifyProperty("_weight", doc="""Font weight (int)""", converter=int)
italic = event.notifyProperty(
- '_italic',
- doc="""True for italic (bool)""",
- converter=bool)
+ "_italic", doc="""True for italic (bool)""", converter=bool
+ )
class Text2D(primitives.Geometry):
@@ -90,14 +84,14 @@ class Text2D(primitives.Geometry):
"""
# Text anchor values
- CENTER = 'center'
+ CENTER = "center"
- LEFT = 'left'
- RIGHT = 'right'
+ LEFT = "left"
+ RIGHT = "right"
- TOP = 'top'
- BASELINE = 'baseline'
- BOTTOM = 'bottom'
+ TOP = "top"
+ BASELINE = "baseline"
+ BOTTOM = "bottom"
_ALIGN = LEFT, CENTER, RIGHT
_VALIGN = TOP, BASELINE, CENTER, BOTTOM
@@ -106,30 +100,31 @@ class Text2D(primitives.Geometry):
"""Internal cache storing already rasterized text"""
# TODO limit cache size and discard least recent used
- def __init__(self, text='', font=None):
+ def __init__(self, text="", font=None):
self._dirtyTexture = True
self._dirtyAlign = True
self._baselineOffset = 0
self._text = text
self._font = font if font is not None else Font()
- self._foreground = 1., 1., 1., 1.
- self._background = 0., 0., 0., 0.
+ self._foreground = 1.0, 1.0, 1.0, 1.0
+ self._background = 0.0, 0.0, 0.0, 0.0
self._overlay = False
- self._align = 'left'
- self._valign = 'baseline'
- self._devicePixelRatio = 1.0 # Store it to check for changes
+ self._align = "left"
+ self._valign = "baseline"
+ self._dotsPerInch = 96.0 # Store it to check for changes
self._texture = None
self._textureDirty = True
super(Text2D, self).__init__(
- 'triangle_strip',
+ "triangle_strip",
copy=False,
# Keep an array for position as it is bound to attr 0 and MUST
# be active and an array at least on Mac OS X
position=numpy.zeros((4, 3), dtype=numpy.float32),
- vertexID=numpy.arange(4., dtype=numpy.float32).reshape(4, 1),
- offsetInViewportCoords=(0., 0.))
+ vertexID=numpy.arange(4.0, dtype=numpy.float32).reshape(4, 1),
+ offsetInViewportCoords=(0.0, 0.0),
+ )
@property
def text(self):
@@ -162,18 +157,22 @@ class Text2D(primitives.Geometry):
self.notify()
foreground = event.notifyProperty(
- '_foreground', doc="""RGBA color of the text: 4 float in [0, 1]""",
- converter=rgba)
+ "_foreground",
+ doc="""RGBA color of the text: 4 float in [0, 1]""",
+ converter=rgba,
+ )
background = event.notifyProperty(
- '_background',
+ "_background",
doc="RGBA background color of the text field: 4 float in [0, 1]",
- converter=rgba)
+ converter=rgba,
+ )
overlay = event.notifyProperty(
- '_overlay',
+ "_overlay",
doc="True to always display text on top of the scene (default: False)",
- converter=bool)
+ converter=bool,
+ )
def _setAlign(self, align):
assert align in self._ALIGN
@@ -186,7 +185,8 @@ class Text2D(primitives.Geometry):
_setAlign,
doc="""Horizontal anchor position of the text field (str).
- Either 'left' (default), 'center' or 'right'.""")
+ Either 'left' (default), 'center' or 'right'.""",
+ )
def _setVAlign(self, valign):
assert valign in self._VALIGN
@@ -199,37 +199,45 @@ class Text2D(primitives.Geometry):
_setVAlign,
doc="""Vertical anchor position of the text field (str).
- Either 'top', 'baseline' (default), 'center' or 'bottom'""")
+ Either 'top', 'baseline' (default), 'center' or 'bottom'""",
+ )
- def _raster(self, devicePixelRatio):
+ def _raster(self, dotsPerInch: float):
"""Raster current primitive to a bitmap
- :param float devicePixelRatio:
- The ratio between device and device-independent pixels
+ :param dotsPerInch: Screen resolution in pixels per inch
:return: Corresponding image in grayscale and baseline offset from top
:rtype: (HxW numpy.ndarray of uint8, int)
"""
- params = (self.text,
- self.font.name,
- self.font.size,
- self.font.weight,
- self.font.italic,
- devicePixelRatio)
-
- if params not in self._rasterTextCache: # Add to cache
- self._rasterTextCache[params] = _font.rasterText(*params)
-
- array, offset = self._rasterTextCache[params]
+ key = (
+ self.text,
+ self.font.name,
+ self.font.size,
+ self.font.weight,
+ self.font.italic,
+ dotsPerInch,
+ )
+
+ if key not in self._rasterTextCache: # Add to cache
+ font = qt.QFont(
+ self.font.name,
+ self.font.size,
+ self.font.weight,
+ self.font.italic,
+ )
+ self._rasterTextCache[key] = _font.rasterText(self.text, font, dotsPerInch)
+
+ array, offset = self._rasterTextCache[key]
return array.copy(), offset
def _bounds(self, dataBounds=False):
return None
def prepareGL2(self, context):
- # Check if devicePixelRatio has changed since last rendering
- devicePixelRatio = context.glCtx.devicePixelRatio
- if self._devicePixelRatio != devicePixelRatio:
- self._devicePixelRatio = devicePixelRatio
+ # Check if dotsPerInch has changed since last rendering
+ dotsPerInch = context.glCtx.dotsPerInch
+ if self._dotsPerInch != dotsPerInch:
+ self._dotsPerInch = dotsPerInch
self._dirtyTexture = True
if self._dirtyTexture:
@@ -241,13 +249,15 @@ class Text2D(primitives.Geometry):
self._baselineOffset = 0
if self.text:
- image, self._baselineOffset = self._raster(
- self._devicePixelRatio)
+ image, self._baselineOffset = self._raster(dotsPerInch)
self._texture = _glutils.Texture(
- gl.GL_R8, image, gl.GL_RED,
+ gl.GL_R8,
+ image,
+ gl.GL_RED,
minFilter=gl.GL_NEAREST,
magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
self._texture.prepare()
self._dirtyAlign = True # To force update of offset
@@ -257,32 +267,33 @@ class Text2D(primitives.Geometry):
if self._texture is not None:
height, width = self._texture.shape
- if self._align == 'left':
- ox = 0.
- elif self._align == 'center':
- ox = - width // 2
- elif self._align == 'right':
- ox = - width
+ if self._align == "left":
+ ox = 0.0
+ elif self._align == "center":
+ ox = -width // 2
+ elif self._align == "right":
+ ox = -width
else:
_logger.error("Unsupported align: %s", self._align)
- ox = 0.
+ ox = 0.0
- if self._valign == 'top':
- oy = 0.
- elif self._valign == 'baseline':
+ if self._valign == "top":
+ oy = 0.0
+ elif self._valign == "baseline":
oy = self._baselineOffset
- elif self._valign == 'center':
+ elif self._valign == "center":
oy = height // 2
- elif self._valign == 'bottom':
+ elif self._valign == "bottom":
oy = height
else:
_logger.error("Unsupported valign: %s", self._valign)
- oy = 0.
+ oy = 0.0
offsets = (ox, oy) + numpy.array(
- ((0., 0.), (width, 0.), (0., -height), (width, -height)),
- dtype=numpy.float32)
- self.setAttribute('offsetInViewportCoords', offsets)
+ ((0.0, 0.0), (width, 0.0), (0.0, -height), (width, -height)),
+ dtype=numpy.float32,
+ )
+ self.setAttribute("offsetInViewportCoords", offsets)
super(Text2D, self).prepareGL2(context)
@@ -293,14 +304,12 @@ class Text2D(primitives.Geometry):
program = context.glCtx.prog(*self._shaders)
program.use()
- program.setUniformMatrix('matrix', context.objectToNDC.matrix)
- gl.glUniform2f(
- program.uniforms['viewportSize'], *context.viewport.size)
- gl.glUniform4f(program.uniforms['foreground'], *self.foreground)
- gl.glUniform4f(program.uniforms['background'], *self.background)
- gl.glUniform1i(program.uniforms['texture'], self._texture.texUnit)
- gl.glUniform1i(program.uniforms['isOverlay'],
- 1 if self._overlay else 0)
+ program.setUniformMatrix("matrix", context.objectToNDC.matrix)
+ gl.glUniform2f(program.uniforms["viewportSize"], *context.viewport.size)
+ gl.glUniform4f(program.uniforms["foreground"], *self.foreground)
+ gl.glUniform4f(program.uniforms["background"], *self.background)
+ gl.glUniform1i(program.uniforms["texture"], self._texture.texUnit)
+ gl.glUniform1i(program.uniforms["isOverlay"], 1 if self._overlay else 0)
self._texture.bind()
@@ -351,7 +360,6 @@ class Text2D(primitives.Geometry):
vertexID < 1.5 ? 0.0 : 1.0);
}
""", # noqa
-
"""
varying vec2 texCoords;
@@ -373,12 +381,12 @@ class Text2D(primitives.Geometry):
}
}
}
- """)
+ """,
+ )
class LabelledAxes(primitives.GroupBBox):
- """A group displaying a bounding box with axes labels around its children.
- """
+ """A group displaying a bounding box with axes labels around its children."""
def __init__(self):
super(LabelledAxes, self).__init__()
@@ -389,26 +397,23 @@ class LabelledAxes(primitives.GroupBBox):
# TODO offset labels from anchor in pixels
self._xlabel = Text2D(font=self._font)
- self._xlabel.align = 'center'
- self._xlabel.transforms = [self._boxTransforms,
- transform.Translate(tx=0.5)]
+ self._xlabel.align = "center"
+ self._xlabel.transforms = [self._boxTransforms, transform.Translate(tx=0.5)]
self._children.append(self._xlabel)
self._ylabel = Text2D(font=self._font)
- self._ylabel.align = 'center'
- self._ylabel.transforms = [self._boxTransforms,
- transform.Translate(ty=0.5)]
+ self._ylabel.align = "center"
+ self._ylabel.transforms = [self._boxTransforms, transform.Translate(ty=0.5)]
self._children.append(self._ylabel)
self._zlabel = Text2D(font=self._font)
- self._zlabel.align = 'center'
- self._zlabel.transforms = [self._boxTransforms,
- transform.Translate(tz=0.5)]
+ self._zlabel.align = "center"
+ self._zlabel.transforms = [self._boxTransforms, transform.Translate(tz=0.5)]
self._children.append(self._zlabel)
self._tickLines = primitives.Lines( # Init tick lines with dummy pos
- positions=((0., 0., 0.), (0., 0., 0.)),
- mode='lines')
+ positions=((0.0, 0.0, 0.0), (0.0, 0.0, 0.0)), mode="lines"
+ )
self._tickLines.visible = False
self._children.append(self._tickLines)
@@ -465,13 +470,14 @@ class LabelledAxes(primitives.GroupBBox):
self._tickLines.visible = False
self._tickLabels.children = [] # Reset previous labels
- elif (self._ticksForBounds is None or
- not numpy.all(numpy.equal(bounds, self._ticksForBounds))):
+ elif self._ticksForBounds is None or not numpy.all(
+ numpy.equal(bounds, self._ticksForBounds)
+ ):
self._ticksForBounds = bounds
# Update ticks
# TODO make ticks having a constant length on the screen
- ticklength = numpy.abs(bounds[1] - bounds[0]) / 20.
+ ticklength = numpy.abs(bounds[1] - bounds[0]) / 20.0
xticks, xlabels = ticklayout.ticks(*bounds[:, 0])
yticks, ylabels = ticklayout.ticks(*bounds[:, 1])
@@ -479,26 +485,26 @@ class LabelledAxes(primitives.GroupBBox):
# Update tick lines
coords = numpy.empty(
- ((len(xticks) + len(yticks) + len(zticks)), 4, 3),
- dtype=numpy.float32)
+ ((len(xticks) + len(yticks) + len(zticks)), 4, 3), dtype=numpy.float32
+ )
coords[:, :, :] = bounds[0, :] # account for offset from origin
- xcoords = coords[:len(xticks)]
+ xcoords = coords[: len(xticks)]
xcoords[:, :, 0] = numpy.asarray(xticks)[:, numpy.newaxis]
xcoords[:, 1, 1] += ticklength[1] # X ticks on XY plane
xcoords[:, 3, 2] += ticklength[2] # X ticks on XZ plane
- ycoords = coords[len(xticks):len(xticks) + len(yticks)]
+ ycoords = coords[len(xticks) : len(xticks) + len(yticks)]
ycoords[:, :, 1] = numpy.asarray(yticks)[:, numpy.newaxis]
ycoords[:, 1, 0] += ticklength[0] # Y ticks on XY plane
ycoords[:, 3, 2] += ticklength[2] # Y ticks on YZ plane
- zcoords = coords[len(xticks) + len(yticks):]
+ zcoords = coords[len(xticks) + len(yticks) :]
zcoords[:, :, 2] = numpy.asarray(zticks)[:, numpy.newaxis]
zcoords[:, 1, 0] += ticklength[0] # Z ticks on XZ plane
zcoords[:, 3, 1] += ticklength[1] # Z ticks on YZ plane
- self._tickLines.setAttribute('position', coords.reshape(-1, 3))
+ self._tickLines.setAttribute("position", coords.reshape(-1, 3))
self._tickLines.visible = True
# Update labels
@@ -506,23 +512,26 @@ class LabelledAxes(primitives.GroupBBox):
labels = []
for tick, label in zip(xticks, xlabels):
text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=tick, ty=offsets[1], tz=offsets[2])]
+ text.align = "center"
+ text.transforms = [
+ transform.Translate(tx=tick, ty=offsets[1], tz=offsets[2])
+ ]
labels.append(text)
for tick, label in zip(yticks, ylabels):
text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=offsets[0], ty=tick, tz=offsets[2])]
+ text.align = "center"
+ text.transforms = [
+ transform.Translate(tx=offsets[0], ty=tick, tz=offsets[2])
+ ]
labels.append(text)
for tick, label in zip(zticks, zlabels):
text = Text2D(text=label, font=self.font)
- text.align = 'center'
- text.transforms = [transform.Translate(
- tx=offsets[0], ty=offsets[1], tz=tick)]
+ text.align = "center"
+ text.transforms = [
+ transform.Translate(tx=offsets[0], ty=offsets[1], tz=tick)
+ ]
labels.append(text)
self._tickLabels.children = labels # Reset previous labels
diff --git a/src/silx/gui/plot3d/scene/transform.py b/src/silx/gui/plot3d/scene/transform.py
index 5c2cbb3..20e2453 100644
--- a/src/silx/gui/plot3d/scene/transform.py
+++ b/src/silx/gui/plot3d/scene/transform.py
@@ -38,6 +38,7 @@ from . import event
# Projections
+
def mat4LookAtDir(position, direction, up):
"""Creates matrix to look in direction from position.
@@ -54,24 +55,22 @@ def mat4LookAtDir(position, direction, up):
direction = numpy.array(direction, copy=True, dtype=numpy.float32)
dirnorm = numpy.linalg.norm(direction)
- assert dirnorm != 0.
+ assert dirnorm != 0.0
direction /= dirnorm
- side = numpy.cross(direction,
- numpy.array(up, copy=False, dtype=numpy.float32))
+ side = numpy.cross(direction, numpy.array(up, copy=False, dtype=numpy.float32))
sidenorm = numpy.linalg.norm(side)
- assert sidenorm != 0.
+ assert sidenorm != 0.0
up = numpy.cross(side / sidenorm, direction)
upnorm = numpy.linalg.norm(up)
- assert upnorm != 0.
+ assert upnorm != 0.0
up /= upnorm
matrix = numpy.identity(4, dtype=numpy.float32)
matrix[0, :3] = side
matrix[1, :3] = up
matrix[2, :3] = -direction
- return numpy.dot(matrix,
- mat4Translate(-position[0], -position[1], -position[2]))
+ return numpy.dot(matrix, mat4Translate(-position[0], -position[1], -position[2]))
def mat4LookAt(position, center, up):
@@ -97,11 +96,15 @@ def mat4Frustum(left, right, bottom, top, near, far):
See glFrustum.
"""
- return numpy.array((
- (2.*near / (right-left), 0., (right+left) / (right-left), 0.),
- (0., 2.*near / (top-bottom), (top+bottom) / (top-bottom), 0.),
- (0., 0., -(far+near) / (far-near), -2.*far*near / (far-near)),
- (0., 0., -1., 0.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (2.0 * near / (right - left), 0.0, (right + left) / (right - left), 0.0),
+ (0.0, 2.0 * near / (top - bottom), (top + bottom) / (top - bottom), 0.0),
+ (0.0, 0.0, -(far + near) / (far - near), -2.0 * far * near / (far - near)),
+ (0.0, 0.0, -1.0, 0.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4Perspective(fovy, width, height, near, far):
@@ -120,15 +123,19 @@ def mat4Perspective(fovy, width, height, near, far):
assert fovy != 0
assert height != 0
assert width != 0
- assert near > 0.
+ assert near > 0.0
assert far > near
aspectratio = width / height
- f = 1. / numpy.tan(numpy.radians(fovy) / 2.)
- return numpy.array((
- (f / aspectratio, 0., 0., 0.),
- (0., f, 0., 0.),
- (0., 0., (far + near) / (near - far), 2. * far * near / (near - far)),
- (0., 0., -1., 0.)), dtype=numpy.float32)
+ f = 1.0 / numpy.tan(numpy.radians(fovy) / 2.0)
+ return numpy.array(
+ (
+ (f / aspectratio, 0.0, 0.0, 0.0),
+ (0.0, f, 0.0, 0.0),
+ (0.0, 0.0, (far + near) / (near - far), 2.0 * far * near / (near - far)),
+ (0.0, 0.0, -1.0, 0.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4Orthographic(left, right, bottom, top, near, far):
@@ -136,34 +143,47 @@ def mat4Orthographic(left, right, bottom, top, near, far):
See glOrtho.
"""
- return numpy.array((
- (2. / (right - left), 0., 0., - (right + left) / (right - left)),
- (0., 2. / (top - bottom), 0., - (top + bottom) / (top - bottom)),
- (0., 0., -2. / (far - near), - (far + near) / (far - near)),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (2.0 / (right - left), 0.0, 0.0, -(right + left) / (right - left)),
+ (0.0, 2.0 / (top - bottom), 0.0, -(top + bottom) / (top - bottom)),
+ (0.0, 0.0, -2.0 / (far - near), -(far + near) / (far - near)),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
# Affine
+
def mat4Translate(tx, ty, tz):
"""4x4 translation matrix."""
- return numpy.array((
- (1., 0., 0., tx),
- (0., 1., 0., ty),
- (0., 0., 1., tz),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (1.0, 0.0, 0.0, tx),
+ (0.0, 1.0, 0.0, ty),
+ (0.0, 0.0, 1.0, tz),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4Scale(sx, sy, sz):
"""4x4 scale matrix."""
- return numpy.array((
- (sx, 0., 0., 0.),
- (0., sy, 0., 0.),
- (0., 0., sz, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4RotateFromAngleAxis(angle, x=0., y=0., z=1.):
+ return numpy.array(
+ (
+ (sx, 0.0, 0.0, 0.0),
+ (0.0, sy, 0.0, 0.0),
+ (0.0, 0.0, sz, 0.0),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+
+def mat4RotateFromAngleAxis(angle, x=0.0, y=0.0, z=1.0):
"""4x4 rotation matrix from angle and axis.
:param float angle: The rotation angle in radians.
@@ -173,11 +193,30 @@ def mat4RotateFromAngleAxis(angle, x=0., y=0., z=1.):
"""
ca = numpy.cos(angle)
sa = numpy.sin(angle)
- return numpy.array((
- ((1.-ca) * x*x + ca, (1.-ca) * x*y - sa*z, (1.-ca) * x*z + sa*y, 0.),
- ((1.-ca) * x*y + sa*z, (1.-ca) * y*y + ca, (1.-ca) * y*z - sa*x, 0.),
- ((1.-ca) * x*z - sa*y, (1.-ca) * y*z + sa*x, (1.-ca) * z*z + ca, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
+ return numpy.array(
+ (
+ (
+ (1.0 - ca) * x * x + ca,
+ (1.0 - ca) * x * y - sa * z,
+ (1.0 - ca) * x * z + sa * y,
+ 0.0,
+ ),
+ (
+ (1.0 - ca) * x * y + sa * z,
+ (1.0 - ca) * y * y + ca,
+ (1.0 - ca) * y * z - sa * x,
+ 0.0,
+ ),
+ (
+ (1.0 - ca) * x * z - sa * y,
+ (1.0 - ca) * y * z + sa * x,
+ (1.0 - ca) * z * z + ca,
+ 0.0,
+ ),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
def mat4RotateFromQuaternion(quaternion):
@@ -189,14 +228,33 @@ def mat4RotateFromQuaternion(quaternion):
quaternion /= numpy.linalg.norm(quaternion)
qx, qy, qz, qw = quaternion
- return numpy.array((
- (1. - 2.*(qy**2 + qz**2), 2.*(qx*qy - qw*qz), 2.*(qx*qz + qw*qy), 0.),
- (2.*(qx*qy + qw*qz), 1. - 2.*(qx**2 + qz**2), 2.*(qy*qz - qw*qx), 0.),
- (2.*(qx*qz - qw*qy), 2.*(qy*qz + qw*qx), 1. - 2.*(qx**2 + qy**2), 0.),
- (0., 0., 0., 1.)), dtype=numpy.float32)
-
-
-def mat4Shear(axis, sx=0., sy=0., sz=0.):
+ return numpy.array(
+ (
+ (
+ 1.0 - 2.0 * (qy**2 + qz**2),
+ 2.0 * (qx * qy - qw * qz),
+ 2.0 * (qx * qz + qw * qy),
+ 0.0,
+ ),
+ (
+ 2.0 * (qx * qy + qw * qz),
+ 1.0 - 2.0 * (qx**2 + qz**2),
+ 2.0 * (qy * qz - qw * qx),
+ 0.0,
+ ),
+ (
+ 2.0 * (qx * qz - qw * qy),
+ 2.0 * (qy * qz + qw * qx),
+ 1.0 - 2.0 * (qx**2 + qy**2),
+ 0.0,
+ ),
+ (0.0, 0.0, 0.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+
+def mat4Shear(axis, sx=0.0, sy=0.0, sz=0.0):
"""4x4 shear matrix: Skew two axes relative to a third fixed one.
shearFactor = tan(shearAngle)
@@ -207,22 +265,22 @@ def mat4Shear(axis, sx=0., sy=0., sz=0.):
:param float sy: The shear factor for the Y axis relative to axis.
:param float sz: The shear factor for the Z axis relative to axis.
"""
- assert axis in ('x', 'y', 'z')
+ assert axis in ("x", "y", "z")
matrix = numpy.identity(4, dtype=numpy.float32)
# Make the shear column
- index = 'xyz'.find(axis)
- shearcolumn = numpy.array((sx, sy, sz, 0.), dtype=numpy.float32)
- shearcolumn[index] = 1.
+ index = "xyz".find(axis)
+ shearcolumn = numpy.array((sx, sy, sz, 0.0), dtype=numpy.float32)
+ shearcolumn[index] = 1.0
matrix[:, index] = shearcolumn
return matrix
# Transforms ##################################################################
-class Transform(event.Notifier):
+class Transform(event.Notifier):
def __init__(self, static=False):
"""Base class for (row-major) 4x4 matrix transforms.
@@ -236,8 +294,7 @@ class Transform(event.Notifier):
self.addListener(self._changed) # Listening self for changes
def __repr__(self):
- return '%s(%s)' % (self.__class__.__init__,
- repr(self.getMatrix(copy=False)))
+ return "%s(%s)" % (self.__class__.__init__, repr(self.getMatrix(copy=False)))
def inverse(self):
"""Return the Transform of the inverse.
@@ -290,8 +347,8 @@ class Transform(event.Notifier):
return self._inverse
inverseMatrix = property(
- getInverseMatrix,
- doc="The 4x4 matrix of the inverse of this transform.")
+ getInverseMatrix, doc="The 4x4 matrix of the inverse of this transform."
+ )
# Listener
@@ -328,14 +385,13 @@ class Transform(event.Notifier):
if dimension == 3: # Add 4th coordinate
points = numpy.append(
- points,
- numpy.ones((1, points.shape[1]), dtype=points.dtype),
- axis=0)
+ points, numpy.ones((1, points.shape[1]), dtype=points.dtype), axis=0
+ )
result = numpy.transpose(numpy.dot(matrix, points))
if perspectiveDivide:
- mask = result[:, 3] != 0.
+ mask = result[:, 3] != 0.0
result[mask] /= result[mask, 3][:, numpy.newaxis]
return result[:, :3] if dimension == 3 else result
@@ -364,9 +420,9 @@ class Transform(event.Notifier):
matrix = self.getMatrix(copy=False)
else:
matrix = self.getInverseMatrix(copy=False)
- result = numpy.dot(matrix, self._prepareVector(point, 1.))
+ result = numpy.dot(matrix, self._prepareVector(point, 1.0))
- if perspectiveDivide and result[3] != 0.:
+ if perspectiveDivide and result[3] != 0.0:
result /= result[3]
if len(point) == 3:
@@ -404,8 +460,9 @@ class Transform(event.Notifier):
matrix = self.getMatrix(copy=False).T
return numpy.dot(matrix[:3, :3], normal[:3])
- _CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
- dtype=numpy.float32)
+ _CUBE_CORNERS = numpy.array(
+ list(itertools.product((0.0, 1.0), repeat=3)), dtype=numpy.float32
+ )
"""Unit cube corners used by :meth:`transformBounds`"""
def transformBounds(self, bounds, direct=True):
@@ -419,8 +476,7 @@ class Transform(event.Notifier):
:rtype: 2x3 numpy.ndarray of float32
"""
corners = numpy.ones((8, 4), dtype=numpy.float32)
- corners[:, :3] = bounds[0] + \
- self._CUBE_CORNERS * (bounds[1] - bounds[0])
+ corners[:, :3] = bounds[0] + self._CUBE_CORNERS * (bounds[1] - bounds[0])
if direct:
matrix = self.getMatrix(copy=False)
@@ -502,8 +558,8 @@ class StaticTransformList(Transform):
# Affine ######################################################################
-class Matrix(Transform):
+class Matrix(Transform):
def __init__(self, matrix=None):
"""4x4 Matrix.
@@ -528,16 +584,17 @@ class Matrix(Transform):
self.notify()
# Redefined here to add a setter
- matrix = property(Transform.getMatrix, setMatrix,
- doc="The 4x4 matrix of this transform.")
+ matrix = property(
+ Transform.getMatrix, setMatrix, doc="The 4x4 matrix of this transform."
+ )
class Translate(Transform):
"""4x4 translation matrix."""
- def __init__(self, tx=0., ty=0., tz=0.):
+ def __init__(self, tx=0.0, ty=0.0, tz=0.0):
super(Translate, self).__init__()
- self._tx, self._ty, self._tz = 0., 0., 0.
+ self._tx, self._ty, self._tz = 0.0, 0.0, 0.0
self.setTranslate(tx, ty, tz)
def _makeMatrix(self):
@@ -592,16 +649,16 @@ class Translate(Transform):
class Scale(Transform):
"""4x4 scale matrix."""
- def __init__(self, sx=1., sy=1., sz=1.):
+ def __init__(self, sx=1.0, sy=1.0, sz=1.0):
super(Scale, self).__init__()
- self._sx, self._sy, self._sz = 0., 0., 0.
+ self._sx, self._sy, self._sz = 0.0, 0.0, 0.0
self.setScale(sx, sy, sz)
def _makeMatrix(self):
return mat4Scale(self.sx, self.sy, self.sz)
def _makeInverse(self):
- return mat4Scale(1. / self.sx, 1. / self.sy, 1. / self.sz)
+ return mat4Scale(1.0 / self.sx, 1.0 / self.sy, 1.0 / self.sz)
@property
def sx(self):
@@ -638,20 +695,19 @@ class Scale(Transform):
def setScale(self, sx=None, sy=None, sz=None):
if sx is not None:
- assert sx != 0.
+ assert sx != 0.0
self._sx = sx
if sy is not None:
- assert sy != 0.
+ assert sy != 0.0
self._sy = sy
if sz is not None:
- assert sz != 0.
+ assert sz != 0.0
self._sz = sz
self.notify()
class Rotate(Transform):
-
- def __init__(self, angle=0., ax=0., ay=0., az=1.):
+ def __init__(self, angle=0.0, ax=0.0, ay=0.0, az=1.0):
"""4x4 rotation matrix.
:param float angle: The rotation angle in degrees.
@@ -660,7 +716,7 @@ class Rotate(Transform):
:param float az: The z coordinate of the rotation axis.
"""
super(Rotate, self).__init__()
- self._angle = 0.
+ self._angle = 0.0
self._axis = None
self.setAngleAxis(angle, (ax, ay, az))
@@ -695,9 +751,9 @@ class Rotate(Transform):
axis = numpy.array(axis, copy=True, dtype=numpy.float32)
assert axis.size == 3
norm = numpy.linalg.norm(axis)
- if norm == 0.: # No axis, set rotation angle to 0.
- self._angle = 0.
- self._axis = numpy.array((0., 0., 1.), dtype=numpy.float32)
+ if norm == 0.0: # No axis, set rotation angle to 0.
+ self._angle = 0.0
+ self._axis = numpy.array((0.0, 0.0, 1.0), dtype=numpy.float32)
else:
self._axis = axis / norm
@@ -710,8 +766,8 @@ class Rotate(Transform):
Where: ||(x, y, z)|| = sin(angle/2), w = cos(angle/2).
"""
- if numpy.linalg.norm(self._axis) == 0.:
- return numpy.array((0., 0., 0., 1.), dtype=numpy.float32)
+ if numpy.linalg.norm(self._axis) == 0.0:
+ return numpy.array((0.0, 0.0, 0.0, 1.0), dtype=numpy.float32)
else:
quaternion = numpy.empty((4,), dtype=numpy.float32)
@@ -731,7 +787,7 @@ class Rotate(Transform):
# Get angle
sinhalfangle = numpy.linalg.norm(quaternion[0:3])
coshalfangle = quaternion[3]
- angle = 2. * numpy.arctan2(sinhalfangle, coshalfangle)
+ angle = 2.0 * numpy.arctan2(sinhalfangle, coshalfangle)
# Axis will be normalized in setAngleAxis
self.setAngleAxis(numpy.degrees(angle), quaternion[0:3])
@@ -741,14 +797,16 @@ class Rotate(Transform):
return mat4RotateFromAngleAxis(angle, *self.axis)
def _makeInverse(self):
- return numpy.array(self.getMatrix(copy=False).transpose(),
- copy=True, order='C',
- dtype=numpy.float32)
+ return numpy.array(
+ self.getMatrix(copy=False).transpose(),
+ copy=True,
+ order="C",
+ dtype=numpy.float32,
+ )
class Shear(Transform):
-
- def __init__(self, axis, sx=0., sy=0., sz=0.):
+ def __init__(self, axis, sx=0.0, sy=0.0, sz=0.0):
"""4x4 shear/skew matrix of 2 axes relative to the third one.
:param str axis: The axis to keep fixed, in 'x', 'y', 'z'
@@ -756,7 +814,7 @@ class Shear(Transform):
:param float sy: The shear factor for the y axis.
:param float sz: The shear factor for the z axis.
"""
- assert axis in ('x', 'y', 'z')
+ assert axis in ("x", "y", "z")
super(Shear, self).__init__()
self._axis = axis
self._factors = sx, sy, sz
@@ -781,6 +839,7 @@ class Shear(Transform):
# Projection ##################################################################
+
class _Projection(Transform):
"""Base class for projection matrix.
@@ -795,12 +854,12 @@ class _Projection(Transform):
:type size: 2-tuple of float
"""
- def __init__(self, near, far, checkDepthExtent=False, size=(1., 1.)):
+ def __init__(self, near, far, checkDepthExtent=False, size=(1.0, 1.0)):
super(_Projection, self).__init__()
self._checkDepthExtent = checkDepthExtent
self._depthExtent = 1, 10
self.setDepthExtent(near, far) # set _depthExtent
- self._size = 1., 1.
+ self._size = 1.0, 1.0
self.size = size # set _size
def setDepthExtent(self, near=None, far=None):
@@ -813,7 +872,7 @@ class _Projection(Transform):
far = float(far) if far is not None else self._depthExtent[1]
if self._checkDepthExtent:
- assert near > 0.
+ assert near > 0.0
assert far > near
self._depthExtent = near, far
@@ -874,18 +933,27 @@ class Orthographic(_Projection):
True (default) to keep aspect ratio, False otherwise.
"""
- def __init__(self, left=0., right=1., bottom=1., top=0., near=-1., far=1.,
- size=(1., 1.), keepaspect=True):
+ def __init__(
+ self,
+ left=0.0,
+ right=1.0,
+ bottom=1.0,
+ top=0.0,
+ near=-1.0,
+ far=1.0,
+ size=(1.0, 1.0),
+ keepaspect=True,
+ ):
self._left, self._right = left, right
self._bottom, self._top = bottom, top
self._keepaspect = bool(keepaspect)
- super(Orthographic, self).__init__(near, far, checkDepthExtent=False,
- size=size)
+ super(Orthographic, self).__init__(near, far, checkDepthExtent=False, size=size)
# _update called when setting size
def _makeMatrix(self):
return mat4Orthographic(
- self.left, self.right, self.bottom, self.top, self.near, self.far)
+ self.left, self.right, self.bottom, self.top, self.near, self.far
+ )
def _update(self, left, right, bottom, top):
if self.keepaspect:
@@ -895,14 +963,12 @@ class Orthographic(_Projection):
orthoaspect = abs(left - right) / abs(bottom - top)
if orthoaspect >= aspect: # Keep width, enlarge height
- newheight = \
- numpy.sign(top - bottom) * abs(left - right) / aspect
+ newheight = numpy.sign(top - bottom) * abs(left - right) / aspect
bottom = 0.5 * (bottom + top) - 0.5 * newheight
top = bottom + newheight
else: # Keep height, enlarge width
- newwidth = \
- numpy.sign(right - left) * abs(bottom - top) * aspect
+ newwidth = numpy.sign(right - left) * abs(bottom - top) * aspect
left = 0.5 * (left + right) - 0.5 * newwidth
right = left + newwidth
@@ -929,17 +995,15 @@ class Orthographic(_Projection):
self._update(left, right, bottom, top)
self.notify()
- left = property(lambda self: self._left,
- doc="Coord of the left clipping plane.")
+ left = property(lambda self: self._left, doc="Coord of the left clipping plane.")
- right = property(lambda self: self._right,
- doc="Coord of the right clipping plane.")
+ right = property(lambda self: self._right, doc="Coord of the right clipping plane.")
- bottom = property(lambda self: self._bottom,
- doc="Coord of the bottom clipping plane.")
+ bottom = property(
+ lambda self: self._bottom, doc="Coord of the bottom clipping plane."
+ )
- top = property(lambda self: self._top,
- doc="Coord of the top clipping plane.")
+ top = property(lambda self: self._top, doc="Coord of the top clipping plane.")
@property
def size(self):
@@ -982,13 +1046,12 @@ class Ortho2DWidget(_Projection):
:type size: 2-tuple of float
"""
- def __init__(self, near=-1., far=1., size=(1., 1.)):
-
+ def __init__(self, near=-1.0, far=1.0, size=(1.0, 1.0)):
super(Ortho2DWidget, self).__init__(near, far, size)
def _makeMatrix(self):
width, height = self.size
- return mat4Orthographic(0., width, height, 0., self.near, self.far)
+ return mat4Orthographic(0.0, width, height, 0.0, self.near, self.far)
class Perspective(_Projection):
@@ -1002,10 +1065,9 @@ class Perspective(_Projection):
:type size: 2-tuple of float
"""
- def __init__(self, fovy=90., near=0.1, far=1., size=(1., 1.)):
-
+ def __init__(self, fovy=90.0, near=0.1, far=1.0, size=(1.0, 1.0)):
super(Perspective, self).__init__(near, far, checkDepthExtent=True)
- self._fovy = 90.
+ self._fovy = 90.0
self.fovy = fovy # Set _fovy
self.size = size # Set _ size
diff --git a/src/silx/gui/plot3d/scene/utils.py b/src/silx/gui/plot3d/scene/utils.py
index 48fc2f5..c856f15 100644
--- a/src/silx/gui/plot3d/scene/utils.py
+++ b/src/silx/gui/plot3d/scene/utils.py
@@ -42,6 +42,7 @@ _logger = logging.getLogger(__name__)
# numpy #######################################################################
+
def _uniqueAlongLastAxis(a):
"""Numpy unique on the last axis of a 2D array
@@ -57,12 +58,12 @@ def _uniqueAlongLastAxis(a):
assert len(a.shape) == 2
# Construct a type over last array dimension to run unique on a 1D array
- if a.dtype.char in numpy.typecodes['AllInteger']:
+ if a.dtype.char in numpy.typecodes["AllInteger"]:
# Bit-wise comparison of the 2 indices of a line at once
# Expect a C contiguous array of shape N, 2
uniquedt = numpy.dtype((numpy.void, a.itemsize * a.shape[-1]))
- elif a.dtype.char in numpy.typecodes['Float']:
- uniquedt = [('f{i}'.format(i=i), a.dtype) for i in range(a.shape[-1])]
+ elif a.dtype.char in numpy.typecodes["Float"]:
+ uniquedt = [("f{i}".format(i=i), a.dtype) for i in range(a.shape[-1])]
else:
raise TypeError("Unsupported type {dtype}".format(dtype=a.dtype))
@@ -72,6 +73,7 @@ def _uniqueAlongLastAxis(a):
# conversions #################################################################
+
def triangleToLineIndices(triangleIndices, unicity=False):
"""Generates lines indices from triangle indices.
@@ -88,8 +90,7 @@ def triangleToLineIndices(triangleIndices, unicity=False):
triangleIndices = triangleIndices.reshape(-1, 3)
# Pack line indices by triangle and by edge
- lineindices = numpy.empty((len(triangleIndices), 3, 2),
- dtype=triangleIndices.dtype)
+ lineindices = numpy.empty((len(triangleIndices), 3, 2), dtype=triangleIndices.dtype)
lineindices[:, 0] = triangleIndices[:, :2] # edge = t0, t1
lineindices[:, 1] = triangleIndices[:, 1:] # edge =t1, t2
lineindices[:, 2] = triangleIndices[:, ::2] # edge = t0, t2
@@ -103,7 +104,7 @@ def triangleToLineIndices(triangleIndices, unicity=False):
return lineindices
-def verticesNormalsToLines(vertices, normals, scale=1.):
+def verticesNormalsToLines(vertices, normals, scale=1.0):
"""Return vertices of lines representing normals at given positions.
:param vertices: Positions of the points.
@@ -137,13 +138,19 @@ def unindexArrays(mode, indices, *arrays):
"""
indices = numpy.array(indices, copy=False)
- assert mode in ('points',
- 'lines', 'line_strip', 'loop',
- 'triangles', 'triangle_strip', 'fan')
-
- if mode in ('lines', 'line_strip', 'loop'):
+ assert mode in (
+ "points",
+ "lines",
+ "line_strip",
+ "loop",
+ "triangles",
+ "triangle_strip",
+ "fan",
+ )
+
+ if mode in ("lines", "line_strip", "loop"):
assert len(indices) >= 2
- elif mode in ('triangles', 'triangle_strip', 'fan'):
+ elif mode in ("triangles", "triangle_strip", "fan"):
assert len(indices) >= 3
assert indices.min() >= 0
@@ -151,27 +158,27 @@ def unindexArrays(mode, indices, *arrays):
for data in arrays:
assert len(data) >= max_index
- if mode == 'line_strip':
+ if mode == "line_strip":
unpacked = numpy.empty((2 * (len(indices) - 1),), dtype=indices.dtype)
unpacked[0::2] = indices[:-1]
unpacked[1::2] = indices[1:]
indices = unpacked
- elif mode == 'loop':
+ elif mode == "loop":
unpacked = numpy.empty((2 * len(indices),), dtype=indices.dtype)
unpacked[0::2] = indices
unpacked[1:-1:2] = indices[1:]
unpacked[-1] = indices[0]
indices = unpacked
- elif mode == 'triangle_strip':
+ elif mode == "triangle_strip":
unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
unpacked[0::3] = indices[:-2]
unpacked[1::3] = indices[1:-1]
unpacked[2::3] = indices[2:]
indices = unpacked
- elif mode == 'fan':
+ elif mode == "fan":
unpacked = numpy.empty((3 * (len(indices) - 2),), dtype=indices.dtype)
unpacked[0::3] = indices[0]
unpacked[1::3] = indices[1:-1]
@@ -220,8 +227,9 @@ def trianglesNormal(positions):
positions = numpy.array(positions, copy=False).reshape(-1, 3, 3)
- normals = numpy.cross(positions[:, 1] - positions[:, 0],
- positions[:, 2] - positions[:, 0])
+ normals = numpy.cross(
+ positions[:, 1] - positions[:, 0], positions[:, 2] - positions[:, 0]
+ )
# Normalize normals
norms = numpy.linalg.norm(normals, axis=1)
@@ -232,6 +240,7 @@ def trianglesNormal(positions):
# grid ########################################################################
+
def gridVertices(dim0Array, dim1Array, dtype):
"""Generate an array of 2D positions from 2 arrays of 1D coordinates.
@@ -308,29 +317,28 @@ def linesGridIndices(dim0, dim1):
nbsegmentalongdim1 = 2 * (dim1 - 1)
nbsegmentalongdim0 = 2 * (dim0 - 1)
- indices = numpy.empty(nbsegmentalongdim1 * dim0 +
- nbsegmentalongdim0 * dim1,
- dtype=numpy.uint32)
+ indices = numpy.empty(
+ nbsegmentalongdim1 * dim0 + nbsegmentalongdim0 * dim1, dtype=numpy.uint32
+ )
# Line indices over dim0
- onedim1line = (numpy.arange(nbsegmentalongdim1,
- dtype=numpy.uint32) + 1) // 2
- indices[:dim0 * nbsegmentalongdim1] = \
- (dim1 * numpy.arange(dim0, dtype=numpy.uint32)[:, None] +
- onedim1line[None, :]).ravel()
+ onedim1line = (numpy.arange(nbsegmentalongdim1, dtype=numpy.uint32) + 1) // 2
+ indices[: dim0 * nbsegmentalongdim1] = (
+ dim1 * numpy.arange(dim0, dtype=numpy.uint32)[:, None] + onedim1line[None, :]
+ ).ravel()
# Line indices over dim1
- onedim0line = (numpy.arange(nbsegmentalongdim0,
- dtype=numpy.uint32) + 1) // 2
- indices[dim0 * nbsegmentalongdim1:] = \
- (numpy.arange(dim1, dtype=numpy.uint32)[:, None] +
- dim1 * onedim0line[None, :]).ravel()
+ onedim0line = (numpy.arange(nbsegmentalongdim0, dtype=numpy.uint32) + 1) // 2
+ indices[dim0 * nbsegmentalongdim1 :] = (
+ numpy.arange(dim1, dtype=numpy.uint32)[:, None] + dim1 * onedim0line[None, :]
+ ).ravel()
return indices
# intersection ################################################################
+
def angleBetweenVectors(refVector, vectors, norm=None):
"""Return the angle between 2 vectors.
@@ -357,10 +365,10 @@ def angleBetweenVectors(refVector, vectors, norm=None):
vectors = numpy.array([v / numpy.linalg.norm(v) for v in vectors])
dots = numpy.sum(refVector * vectors, axis=-1)
- angles = numpy.arccos(numpy.clip(dots, -1., 1.))
+ angles = numpy.arccos(numpy.clip(dots, -1.0, 1.0))
if norm is not None:
- signs = numpy.sum(norm * numpy.cross(refVector, vectors), axis=-1) < 0.
- angles[signs] = numpy.pi * 2. - angles[signs]
+ signs = numpy.sum(norm * numpy.cross(refVector, vectors), axis=-1) < 0.0
+ angles[signs] = numpy.pi * 2.0 - angles[signs]
return angles[0] if singlevector else angles
@@ -391,8 +399,8 @@ def segmentPlaneIntersect(s0, s1, planeNorm, planePt):
else: # No intersection
return []
- alpha = - numpy.dot(planeNorm, s0 - planePt) / dotnormseg
- if 0. <= alpha <= 1.: # Intersection with segment
+ alpha = -numpy.dot(planeNorm, s0 - planePt) / dotnormseg
+ if 0.0 <= alpha <= 1.0: # Intersection with segment
return [s0 + alpha * segdir]
else: # intersection outside segment
return []
@@ -459,8 +467,9 @@ def clipSegmentToBounds(segment, bounds):
points.shape = -1, 3 # Set back to 2D array
# Find intersection points that are included in the volume
- mask = numpy.logical_and(numpy.all(bounds[0] <= points, axis=1),
- numpy.all(points <= bounds[1], axis=1))
+ mask = numpy.logical_and(
+ numpy.all(bounds[0] <= points, axis=1), numpy.all(points <= bounds[1], axis=1)
+ )
intersections = numpy.unique(offsets[mask])
if len(intersections) != 2:
return None
@@ -519,12 +528,12 @@ def segmentVolumeIntersect(segment, nbins):
# Get corresponding line parameters
t = []
if numpy.all(0 <= p0) and numpy.all(p0 <= nbins):
- t.append([0.]) # p0 within volume, add it
+ t.append([0.0]) # p0 within volume, add it
t += [(edgesByDim[i] - p0[i]) / delta[i] for i in range(dim) if delta[i] != 0]
if numpy.all(0 <= p1) and numpy.all(p1 <= nbins):
- t.append([1.]) # p1 within volume, add it
+ t.append([1.0]) # p1 within volume, add it
t = numpy.concatenate(t)
- t.sort(kind='mergesort')
+ t.sort(kind="mergesort")
# Remove duplicates
unique = numpy.ones((len(t),), dtype=bool)
@@ -536,13 +545,14 @@ def segmentVolumeIntersect(segment, nbins):
# bin edges/line intersection points
points = t.reshape(-1, 1) * delta + p0
- centers = (points[:-1] + points[1:]) / 2.
+ centers = (points[:-1] + points[1:]) / 2.0
bins = numpy.floor(centers).astype(numpy.int64)
return bins
# Plane #######################################################################
+
class Plane(event.Notifier):
"""Object handling a plane and notifying plane changes.
@@ -552,7 +562,7 @@ class Plane(event.Notifier):
:type normal: 3-tuple of float.
"""
- def __init__(self, point=(0., 0., 0.), normal=(0., 0., 1.)):
+ def __init__(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0)):
super(Plane, self).__init__()
assert len(point) == 3
@@ -583,7 +593,7 @@ class Plane(event.Notifier):
normal = numpy.array(normal, copy=True, dtype=numpy.float32)
norm = numpy.linalg.norm(normal)
- if norm != 0.:
+ if norm != 0.0:
normal /= norm
if not numpy.all(numpy.equal(self._normal, normal)):
@@ -591,8 +601,11 @@ class Plane(event.Notifier):
planechanged = True
if planechanged:
- _logger.debug('Plane updated:\n\tpoint: %s\n\tnormal: %s',
- str(self._point), str(self._normal))
+ _logger.debug(
+ "Plane updated:\n\tpoint: %s\n\tnormal: %s",
+ str(self._point),
+ str(self._normal),
+ )
self.notify()
@property
@@ -616,8 +629,7 @@ class Plane(event.Notifier):
@property
def parameters(self):
"""Plane equation parameters: a*x + b*y + c*z + d = 0."""
- return numpy.append(self._normal,
- - numpy.dot(self._point, self._normal))
+ return numpy.append(self._normal, -numpy.dot(self._point, self._normal))
@parameters.setter
def parameters(self, parameters):
@@ -630,13 +642,13 @@ class Plane(event.Notifier):
parameters /= norm
normal = parameters[:3]
- point = - parameters[3] * normal
+ point = -parameters[3] * normal
self.setPlane(point, normal)
@property
def isPlane(self):
"""True if a plane is defined (i.e., ||normal|| != 0)."""
- return numpy.any(self.normal != 0.)
+ return numpy.any(self.normal != 0.0)
def move(self, step):
"""Move the plane of step along the normal."""
diff --git a/src/silx/gui/plot3d/scene/viewport.py b/src/silx/gui/plot3d/scene/viewport.py
index bff77e2..c39d3ef 100644
--- a/src/silx/gui/plot3d/scene/viewport.py
+++ b/src/silx/gui/plot3d/scene/viewport.py
@@ -59,17 +59,19 @@ class RenderContext(object):
:param Context glContext: The operating system OpenGL context in use.
"""
- _FRAGMENT_SHADER_SRC = string.Template("""
+ _FRAGMENT_SHADER_SRC = string.Template(
+ """
void scene_post(vec4 cameraPosition) {
gl_FragColor = $fogCall(gl_FragColor, cameraPosition);
}
- """)
+ """
+ )
def __init__(self, viewport, glContext):
self._viewport = viewport
self._glContext = glContext
self._transformStack = [viewport.camera.extrinsic]
- self._clipPlane = ClippingPlane(normal=(0., 0., 0.))
+ self._clipPlane = ClippingPlane(normal=(0.0, 0.0, 0.0))
# cache
self.__cache = {}
@@ -118,8 +120,7 @@ class RenderContext(object):
Do not modify.
"""
- return transform.StaticTransformList(
- (self.projection, self.objectToCamera))
+ return transform.StaticTransformList((self.projection, self.objectToCamera))
def pushTransform(self, transform_, multiply=True):
"""Push a :class:`Transform` on the transform stack.
@@ -132,7 +133,8 @@ class RenderContext(object):
if multiply:
assert len(self._transformStack) >= 1
transform_ = transform.StaticTransformList(
- (self._transformStack[-1], transform_))
+ (self._transformStack[-1], transform_)
+ )
self._transformStack.append(transform_)
@@ -149,7 +151,7 @@ class RenderContext(object):
"""The current clipping plane (ClippingPlane)"""
return self._clipPlane
- def setClipPlane(self, point=(0., 0., 0.), normal=(0., 0., 0.)):
+ def setClipPlane(self, point=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 0.0)):
"""Set the clipping plane to use
For now only handles a single clipping plane.
@@ -173,11 +175,15 @@ class RenderContext(object):
@property
def fragDecl(self):
"""Fragment shader declaration for scene shader functions"""
- return '\n'.join((
- self.clipper.fragDecl,
- self.viewport.fog.fragDecl,
- self._FRAGMENT_SHADER_SRC.substitute(
- fogCall=self.viewport.fog.fragCall)))
+ return "\n".join(
+ (
+ self.clipper.fragDecl,
+ self.viewport.fog.fragDecl,
+ self._FRAGMENT_SHADER_SRC.substitute(
+ fogCall=self.viewport.fog.fragCall
+ ),
+ )
+ )
@property
def fragCallPre(self):
@@ -204,6 +210,7 @@ class Viewport(event.Notifier):
def __init__(self, framebuffer=0):
from . import Group # Here to avoid cyclic import
+
super(Viewport, self).__init__()
self._dirty = True
self._origin = 0, 0
@@ -212,15 +219,16 @@ class Viewport(event.Notifier):
self.scene = Group() # The stuff to render, add overlaid scenes?
self.scene._setParent(self)
self.scene.addListener(self._changed)
- self._background = 0., 0., 0., 1.
- self._camera = camera.Camera(fovy=30., near=1., far=100.,
- position=(0., 0., 12.))
+ self._background = 0.0, 0.0, 0.0, 1.0
+ self._camera = camera.Camera(
+ fovy=30.0, near=1.0, far=100.0, position=(0.0, 0.0, 12.0)
+ )
self._camera.addListener(self._changed)
self._transforms = transform.TransformList([self._camera])
- self._light = DirectionalLight(direction=(0., 0., -1.),
- ambient=(0.3, 0.3, 0.3),
- diffuse=(0.7, 0.7, 0.7))
+ self._light = DirectionalLight(
+ direction=(0.0, 0.0, -1.0), ambient=(0.3, 0.3, 0.3), diffuse=(0.7, 0.7, 0.7)
+ )
self._light.addListener(self._changed)
self._fog = Fog()
self._fog.isOn = False
@@ -352,7 +360,7 @@ class Viewport(event.Notifier):
gl.glEnable(gl.GL_DEPTH_TEST)
gl.glDepthFunc(gl.GL_LEQUAL)
- gl.glDepthRange(0., 1.)
+ gl.glDepthRange(0.0, 1.0)
# gl.glEnable(gl.GL_POLYGON_OFFSET_FILL)
# gl.glPolygonOffset(1., 1.)
@@ -361,15 +369,16 @@ class Viewport(event.Notifier):
gl.glEnable(gl.GL_LINE_SMOOTH)
if self.background is None:
- gl.glClear(gl.GL_STENCIL_BUFFER_BIT |
- gl.GL_DEPTH_BUFFER_BIT)
+ gl.glClear(gl.GL_STENCIL_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
else:
gl.glClearColor(*self.background)
# Prepare OpenGL
- gl.glClear(gl.GL_COLOR_BUFFER_BIT |
- gl.GL_STENCIL_BUFFER_BIT |
- gl.GL_DEPTH_BUFFER_BIT)
+ gl.glClear(
+ gl.GL_COLOR_BUFFER_BIT
+ | gl.GL_STENCIL_BUFFER_BIT
+ | gl.GL_DEPTH_BUFFER_BIT
+ )
ctx = RenderContext(self, glContext)
self.scene.render(ctx)
@@ -384,15 +393,16 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
bounds = self.camera.extrinsic.transformBounds(bounds)
if isinstance(self.camera.intrinsic, transform.Perspective):
# This needs to be reworked
- zbounds = - bounds[:, 2]
+ zbounds = -bounds[:, 2]
zextent = max(numpy.fabs(zbounds[0] - zbounds[1]), 0.0001)
- near = max(zextent / 1000., 0.95 * zbounds[1])
+ near = max(zextent / 1000.0, 0.95 * zbounds[1])
far = max(near + 0.1, 1.05 * zbounds[0])
self.camera.intrinsic.setDepthExtent(near, far)
@@ -401,7 +411,7 @@ class Viewport(event.Notifier):
border = max(abs(bounds[:, 2]))
self.camera.intrinsic.setDepthExtent(-border, border)
else:
- raise RuntimeError('Unsupported camera', self.camera.intrinsic)
+ raise RuntimeError("Unsupported camera", self.camera.intrinsic)
def resetCamera(self):
"""Change camera to have the whole scene in the viewing frustum.
@@ -411,11 +421,12 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
self.camera.resetCamera(bounds)
- def orbitCamera(self, direction, angle=1.):
+ def orbitCamera(self, direction, angle=1.0):
"""Rotate the camera around center of the scene.
:param str direction: Direction of movement relative to image plane.
@@ -424,8 +435,9 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
center = 0.5 * (bounds[0] + bounds[1])
self.camera.orbit(direction, center, angle)
@@ -439,35 +451,36 @@ class Viewport(event.Notifier):
"""
bounds = self.scene.bounds(transformed=True)
if bounds is None:
- bounds = numpy.array(((0., 0., 0.), (1., 1., 1.)),
- dtype=numpy.float32)
+ bounds = numpy.array(
+ ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)), dtype=numpy.float32
+ )
bounds = self.camera.extrinsic.transformBounds(bounds)
center = 0.5 * (bounds[0] + bounds[1])
- ndcCenter = self.camera.intrinsic.transformPoint(
- center, perspectiveDivide=True)
+ ndcCenter = self.camera.intrinsic.transformPoint(center, perspectiveDivide=True)
- step *= 2. # NDC has size 2
+ step *= 2.0 # NDC has size 2
- if direction == 'up':
+ if direction == "up":
ndcCenter[1] -= step
- elif direction == 'down':
+ elif direction == "down":
ndcCenter[1] += step
- elif direction == 'right':
+ elif direction == "right":
ndcCenter[0] -= step
- elif direction == 'left':
+ elif direction == "left":
ndcCenter[0] += step
- elif direction == 'forward':
+ elif direction == "forward":
ndcCenter[2] += step
- elif direction == 'backward':
+ elif direction == "backward":
ndcCenter[2] -= step
else:
- raise ValueError('Unsupported direction: %s' % direction)
+ raise ValueError("Unsupported direction: %s" % direction)
newCenter = self.camera.intrinsic.transformPoint(
- ndcCenter, direct=False, perspectiveDivide=True)
+ ndcCenter, direct=False, perspectiveDivide=True
+ )
self.camera.move(direction, numpy.linalg.norm(newCenter - center))
@@ -495,11 +508,11 @@ class Viewport(event.Notifier):
x, y = winX - ox, winY - oy
- if checkInside and (x < 0. or x > width or y < 0. or y > height):
+ if checkInside and (x < 0.0 or x > width or y < 0.0 or y > height):
return None # Out of viewport
- ndcx = 2. * x / float(width) - 1.
- ndcy = 1. - 2. * y / float(height)
+ ndcx = 2.0 * x / float(width) - 1.0
+ ndcy = 1.0 - 2.0 * y / float(height)
return ndcx, ndcy
def ndcToWindow(self, ndcX, ndcY, checkInside=True):
@@ -512,15 +525,14 @@ class Viewport(event.Notifier):
:return: (x, y) window coordinates or None.
Origin top-left, x to the right, y goes downward.
"""
- if (checkInside and
- (ndcX < -1. or ndcX > 1. or ndcY < -1. or ndcY > 1.)):
+ if checkInside and (ndcX < -1.0 or ndcX > 1.0 or ndcY < -1.0 or ndcY > 1.0):
return None # Outside viewport
ox, oy = self._origin
width, height = self.size
- winx = ox + width * 0.5 * (ndcX + 1.)
- winy = oy + height * 0.5 * (1. - ndcY)
+ winx = ox + width * 0.5 * (ndcX + 1.0)
+ winy = oy + height * 0.5 * (1.0 - ndcY)
return winx, winy
def _pickNdcZGL(self, x, y, offset=0):
@@ -550,20 +562,19 @@ class Viewport(event.Notifier):
if offset == 0: # Fast path
# glReadPixels is not GL|ES friendly
- depth = gl.glReadPixels(
- x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)[0]
+ depthPatch = gl.glReadPixels(x, y, 1, 1, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)
+ depth = numpy.ravel(depthPatch)[0]
else:
offset = abs(int(offset))
- size = 2*offset + 1
+ size = 2 * offset + 1
depthPatch = gl.glReadPixels(
- x - offset, y - offset,
- size, size,
- gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT)
+ x - offset, y - offset, size, size, gl.GL_DEPTH_COMPONENT, gl.GL_FLOAT
+ )
depthPatch = depthPatch.ravel() # Work in 1D
# TODO cache sortedIndices to avoid computing it each time
# Compute distance of each pixels to the center of the patch
- offsetToCenter = numpy.arange(- offset, offset + 1, dtype=numpy.float32) ** 2
+ offsetToCenter = numpy.arange(-offset, offset + 1, dtype=numpy.float32) ** 2
sqDistToCenter = numpy.add.outer(offsetToCenter, offsetToCenter)
# Use distance to center to sort values from the patch
@@ -571,26 +582,26 @@ class Viewport(event.Notifier):
sortedValues = depthPatch[sortedIndices]
# Take first depth that is not 1 in the sorted values
- hits = sortedValues[sortedValues != 1.]
- depth = 1. if len(hits) == 0 else hits[0]
+ hits = sortedValues[sortedValues != 1.0]
+ depth = 1.0 if len(hits) == 0 else hits[0]
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
# Z in NDC in [-1., 1.]
- return float(depth) * 2. - 1.
+ return float(depth) * 2.0 - 1.0
def _getXZYGL(self, x, y):
ndc = self.windowToNdc(x, y)
if ndc is None:
return None # Outside viewport
ndcz = self._pickNdcZGL(x, y)
- ndcpos = numpy.array((ndc[0], ndc[1], ndcz, 1.), dtype=numpy.float32)
+ ndcpos = numpy.array((ndc[0], ndc[1], ndcz, 1.0), dtype=numpy.float32)
camerapos = self.camera.intrinsic.transformPoint(
- ndcpos, direct=False, perspectiveDivide=True)
+ ndcpos, direct=False, perspectiveDivide=True
+ )
- scenepos = self.camera.extrinsic.transformPoint(camerapos,
- direct=False)
+ scenepos = self.camera.extrinsic.transformPoint(camerapos, direct=False)
return scenepos[:3]
def pick(self, x, y):
diff --git a/src/silx/gui/plot3d/scene/window.py b/src/silx/gui/plot3d/scene/window.py
index c8f4cee..2a6d93b 100644
--- a/src/silx/gui/plot3d/scene/window.py
+++ b/src/silx/gui/plot3d/scene/window.py
@@ -58,6 +58,7 @@ class Context(object):
self._context = glContextHandle
self._isCurrent = False
self._devicePixelRatio = 1.0
+ self._dotsPerInch = 96.0
@property
def isCurrent(self):
@@ -75,6 +76,16 @@ class Context(object):
self._isCurrent = bool(isCurrent)
@property
+ def dotsPerInch(self) -> float:
+ """Number of physical dots per inch on the screen"""
+ return self._dotsPerInch
+
+ @dotsPerInch.setter
+ def dotsPerInch(self, dpi: float):
+ assert dpi > 0.0
+ self._dotsPerInch = float(dpi)
+
+ @property
def devicePixelRatio(self):
"""Ratio between device and device independent pixels (float)
@@ -112,6 +123,7 @@ class ContextGL2(Context):
:param glContextHandle: System specific OpenGL context handle.
"""
+
def __init__(self, glContextHandle):
super(ContextGL2, self).__init__(glContextHandle)
@@ -121,7 +133,7 @@ class ContextGL2(Context):
# programs
- def prog(self, vertexShaderSrc, fragmentShaderSrc, attrib0='position'):
+ def prog(self, vertexShaderSrc, fragmentShaderSrc, attrib0="position"):
"""Cache program within context.
WARNING: No clean-up.
@@ -138,14 +150,14 @@ class ContextGL2(Context):
program = self._programs.get(key, None)
if program is None:
program = _glutils.Program(
- vertexShaderSrc, fragmentShaderSrc, attrib0=attrib0)
+ vertexShaderSrc, fragmentShaderSrc, attrib0=attrib0
+ )
self._programs[key] = program
return program
# VBOs
- def makeVbo(self, data=None, sizeInBytes=None,
- usage=None, target=None):
+ def makeVbo(self, data=None, sizeInBytes=None, usage=None, target=None):
"""Create a VBO in this context with the data.
Current limitations:
@@ -193,7 +205,8 @@ class ContextGL2(Context):
size=data.shape[0],
dimension=dimension,
offset=0,
- stride=0)
+ stride=0,
+ )
def _deadVbo(self, vboRef):
"""Callback handling dead VBOAttribs."""
@@ -228,13 +241,18 @@ class Window(event.Notifier):
update the texture only when needed.
"""
- _position = numpy.array(((-1., -1., 0., 0.),
- (1., -1., 1., 0.),
- (-1., 1., 0., 1.),
- (1., 1., 1., 1.)),
- dtype=numpy.float32)
-
- _shaders = ("""
+ _position = numpy.array(
+ (
+ (-1.0, -1.0, 0.0, 0.0),
+ (1.0, -1.0, 1.0, 0.0),
+ (-1.0, 1.0, 0.0, 1.0),
+ (1.0, 1.0, 1.0, 1.0),
+ ),
+ dtype=numpy.float32,
+ )
+
+ _shaders = (
+ """
attribute vec4 position;
varying vec2 textureCoord;
@@ -243,7 +261,7 @@ class Window(event.Notifier):
textureCoord = position.zw;
}
""",
- """
+ """
uniform sampler2D texture;
varying vec2 textureCoord;
@@ -251,9 +269,10 @@ class Window(event.Notifier):
gl_FragColor = texture2D(texture, textureCoord);
gl_FragColor.a = 1.0;
}
- """)
+ """,
+ )
- def __init__(self, mode='framebuffer'):
+ def __init__(self, mode="framebuffer"):
super(Window, self).__init__()
self._dirty = True
self._size = 0, 0
@@ -263,8 +282,8 @@ class Window(event.Notifier):
self._framebufferid = 0
self._framebuffers = {} # Cache of framebuffers
- assert mode in ('direct', 'framebuffer')
- self._isframebuffer = mode == 'framebuffer'
+ assert mode in ("direct", "framebuffer")
+ self._isframebuffer = mode == "framebuffer"
@property
def dirty(self):
@@ -316,8 +335,9 @@ class Window(event.Notifier):
self._dirty = True
self.notify(*args, **kwargs)
- framebufferid = property(lambda self: self._framebufferid,
- doc="Framebuffer ID used to perform rendering")
+ framebufferid = property(
+ lambda self: self._framebufferid, doc="Framebuffer ID used to perform rendering"
+ )
def grab(self, glcontext):
"""Returns the raster of the scene as an RGB numpy array
@@ -332,21 +352,21 @@ class Window(event.Notifier):
previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.framebufferid)
gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
- gl.glReadPixels(
- 0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, image)
+ gl.glReadPixels(0, 0, width, height, gl.GL_RGB, gl.GL_UNSIGNED_BYTE, image)
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)
# glReadPixels gives bottom to top,
# while images are stored as top to bottom
image = numpy.flipud(image)
- return numpy.array(image, copy=False, order='C')
+ return numpy.array(image, copy=False, order="C")
- def render(self, glcontext, devicePixelRatio):
+ def render(self, glcontext, dotsPerInch: float, devicePixelRatio: float):
"""Perform the rendering of attached viewports
:param glcontext: System identifier of the OpenGL context
- :param float devicePixelRatio:
+ :param dotsPerInch: Screen physical resolution in pixels per inch
+ :param devicePixelRatio:
Ratio between device and device-independent pixels
"""
if self.size == (0, 0):
@@ -356,6 +376,7 @@ class Window(event.Notifier):
self._contexts[glcontext] = ContextGL2(glcontext) # New context
with self._contexts[glcontext] as context:
+ context.dotsPerInch = dotsPerInch
context.devicePixelRatio = devicePixelRatio
if self._isframebuffer:
self._renderWithOffscreenFramebuffer(context)
@@ -384,18 +405,22 @@ class Window(event.Notifier):
if self.dirty or context not in self._framebuffers:
# Need to redraw framebuffer content
- if (context not in self._framebuffers or
- self._framebuffers[context].shape != self.shape):
+ if (
+ context not in self._framebuffers
+ or self._framebuffers[context].shape != self.shape
+ ):
# Need to rebuild framebuffer
if context in self._framebuffers:
self._framebuffers[context].discard()
- fbo = _glutils.FramebufferTexture(gl.GL_RGBA,
- shape=self.shape,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=gl.GL_CLAMP_TO_EDGE)
+ fbo = _glutils.FramebufferTexture(
+ gl.GL_RGBA,
+ shape=self.shape,
+ minFilter=gl.GL_NEAREST,
+ magFilter=gl.GL_NEAREST,
+ wrap=gl.GL_CLAMP_TO_EDGE,
+ )
self._framebuffers[context] = fbo
self._framebufferid = fbo.name
@@ -415,16 +440,18 @@ class Window(event.Notifier):
gl.glDisable(gl.GL_DEPTH_TEST)
gl.glDisable(gl.GL_SCISSOR_TEST)
# gl.glScissor(0, 0, width, height)
- gl.glClearColor(0., 0., 0., 0.)
+ gl.glClearColor(0.0, 0.0, 0.0, 0.0)
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
- gl.glUniform1i(program.uniforms['texture'], fbo.texture.texUnit)
- gl.glEnableVertexAttribArray(program.attributes['position'])
- gl.glVertexAttribPointer(program.attributes['position'],
- 4,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._position)
+ gl.glUniform1i(program.uniforms["texture"], fbo.texture.texUnit)
+ gl.glEnableVertexAttribArray(program.attributes["position"])
+ gl.glVertexAttribPointer(
+ program.attributes["position"],
+ 4,
+ gl.GL_FLOAT,
+ gl.GL_FALSE,
+ 0,
+ self._position,
+ )
fbo.texture.bind()
gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._position))
gl.glBindTexture(gl.GL_TEXTURE_2D, 0)