diff options
Diffstat (limited to 'src/silx/gui/plot3d/scene')
-rw-r--r-- | src/silx/gui/plot3d/scene/axes.py | 70 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/camera.py | 117 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/core.py | 36 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/cutplane.py | 137 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/event.py | 41 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/function.py | 152 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/interaction.py | 340 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/primitives.py | 991 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/test/test_transform.py | 39 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/test/test_utils.py | 168 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/text.py | 241 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/transform.py | 304 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/utils.py | 112 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/viewport.py | 147 | ||||
-rw-r--r-- | src/silx/gui/plot3d/scene/window.py | 107 |
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) |