summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot3d/scene/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot3d/scene/core.py')
-rw-r--r--src/silx/gui/plot3d/scene/core.py343
1 files changed, 343 insertions, 0 deletions
diff --git a/src/silx/gui/plot3d/scene/core.py b/src/silx/gui/plot3d/scene/core.py
new file mode 100644
index 0000000..43838fe
--- /dev/null
+++ b/src/silx/gui/plot3d/scene/core.py
@@ -0,0 +1,343 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides the base scene structure.
+
+This module provides the classes for describing a tree structure with
+rendering and picking API.
+All nodes inherit from :class:`Base`.
+Nodes with children are provided with :class:`PrivateGroup` and
+:class:`Group` classes.
+Leaf rendering nodes should inherit from :class:`Elem`.
+"""
+
+from __future__ import absolute_import, division, unicode_literals
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "25/07/2016"
+
+
+import itertools
+import weakref
+
+import numpy
+
+from . import event
+from . import transform
+
+from .viewport import Viewport
+
+
+# Nodes #######################################################################
+
+class Base(event.Notifier):
+ """A scene node with common features."""
+
+ def __init__(self):
+ super(Base, self).__init__()
+ self._visible = True
+ self._pickable = False
+
+ self._parentRef = None
+
+ self._transforms = transform.TransformList()
+ self._transforms.addListener(self._transformChanged)
+
+ # notifying properties
+
+ visible = event.notifyProperty('_visible',
+ doc="Visibility flag of the node")
+ pickable = event.notifyProperty('_pickable',
+ doc="True to make node pickable")
+
+ # Access to tree path
+
+ @property
+ def parent(self):
+ """Parent or None if no parent"""
+ return None if self._parentRef is None else self._parentRef()
+
+ def _setParent(self, parent):
+ """Set the parent of this node.
+
+ For internal use.
+
+ :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.')
+ # Alternative: remove it from previous children list
+ self._parentRef = None if parent is None else weakref.ref(parent)
+
+ @property
+ def path(self):
+ """Tuple of scene nodes, from the tip of the tree down to this node.
+
+ If this tree is attached to a :class:`Viewport`,
+ then the :class:`Viewport` is the first element of path.
+ """
+ if self.parent is None:
+ return self,
+ elif isinstance(self.parent, Viewport):
+ return self.parent, self
+ else:
+ return self.parent.path + (self, )
+
+ @property
+ def viewport(self):
+ """The viewport this node is attached to or None."""
+ root = self.path[0]
+ return root if isinstance(root, Viewport) else None
+
+ @property
+ def root(self):
+ """The root node of the scene.
+
+ If attached to a :class:`Viewport`, this is the item right under it
+ """
+ path = self.path
+ return path[1] if isinstance(path[0], Viewport) else path[0]
+
+ @property
+ def objectToNDCTransform(self):
+ """Transform from object to normalized device coordinates.
+
+ Do not forget perspective divide.
+ """
+ # Using the Viewport's transforms property to proxy the camera
+ path = self.path
+ assert isinstance(path[0], Viewport)
+ return transform.StaticTransformList(elem.transforms for elem in path)
+
+ @property
+ def objectToSceneTransform(self):
+ """Transform from object to scene.
+
+ Combine transforms up to the Viewport (not including it).
+ """
+ path = self.path
+ if isinstance(path[0], Viewport):
+ path = path[1:] # Remove viewport to remove camera transforms
+ return transform.StaticTransformList(elem.transforms for elem in path)
+
+ # transform
+
+ @property
+ def transforms(self):
+ """List of transforms defining the frame of this node relative
+ to its parent."""
+ return self._transforms
+
+ @transforms.setter
+ def transforms(self, iterable):
+ self._transforms.removeListener(self._transformChanged)
+ if isinstance(iterable, transform.TransformList):
+ # If it is a TransformList, do not create one to enable sharing.
+ self._transforms = iterable
+ else:
+ assert hasattr(iterable, '__iter__')
+ self._transforms = transform.TransformList(iterable)
+ self._transforms.addListener(self._transformChanged)
+
+ def _transformChanged(self, source):
+ self.notify() # Broadcast transform notification
+
+ # Bounds
+
+ _CUBE_CORNERS = numpy.array(list(itertools.product((0., 1.), repeat=3)),
+ dtype=numpy.float32)
+ """Unit cube corners used to transform bounds"""
+
+ def _bounds(self, dataBounds=False):
+ """Override in subclass to provide bounds in object coordinates"""
+ return None
+
+ def bounds(self, transformed=False, dataBounds=False):
+ """Returns the bounds of this node aligned with the axis,
+ with or without transform applied.
+
+ :param bool transformed: False to give bounds in object coordinates
+ (the default), True to apply this object's
+ transforms.
+ :param bool dataBounds: False to give bounds of vertices (the default),
+ True to give bounds of the represented data.
+ :return: The bounds: ((xMin, yMin, zMin), (xMax, yMax, zMax)) or None
+ if no bounds.
+ :rtype: numpy.ndarray of float
+ """
+ bounds = self._bounds(dataBounds)
+
+ if transformed and bounds is not None:
+ bounds = self.transforms.transformBounds(bounds)
+
+ return bounds
+
+ # Rendering
+
+ def prepareGL2(self, ctx):
+ """Called before the rendering to prepare OpenGL resources.
+
+ Override in subclass.
+ """
+ pass
+
+ def renderGL2(self, ctx):
+ """Called to perform the OpenGL rendering.
+
+ Override in subclass.
+ """
+ pass
+
+ def render(self, ctx):
+ """Called internally to perform rendering."""
+ if self.visible:
+ ctx.pushTransform(self.transforms)
+ self.prepareGL2(ctx)
+ self.renderGL2(ctx)
+ ctx.popTransform()
+
+ def postRender(self, ctx):
+ """Hook called when parent's node render is finished.
+
+ Called in the reverse of rendering order (i.e., last child first).
+
+ Meant for nodes that modify the :class:`RenderContext` ctx to
+ reset their modifications.
+ """
+ pass
+
+ def pick(self, ctx, x, y, depth=None):
+ """True/False picking, should be fast"""
+ if self.pickable:
+ pass
+
+ def pickRay(self, ctx, ray):
+ """Picking returning list of ray intersections."""
+ if self.pickable:
+ pass
+
+
+class Elem(Base):
+ """A scene node that does some rendering."""
+
+ def __init__(self):
+ super(Elem, self).__init__()
+ # self.showBBox = False # Here or outside scene graph?
+ # self.clipPlane = None # This needs to be handled in the shader
+
+
+class PrivateGroup(Base):
+ """A scene node that renders its (private) childern.
+
+ :param iterable children: :class:`Base` nodes to add as children
+ """
+
+ class ChildrenList(event.NotifierList):
+ """List of children with notification and children's parent update."""
+
+ def _listWillChangeHook(self, methodName, *args, **kwargs):
+ super(PrivateGroup.ChildrenList, self)._listWillChangeHook(
+ methodName, *args, **kwargs)
+ for item in self:
+ item._setParent(None)
+
+ def _listWasChangedHook(self, methodName, *args, **kwargs):
+ for item in self:
+ item._setParent(self._parentRef())
+ super(PrivateGroup.ChildrenList, self)._listWasChangedHook(
+ methodName, *args, **kwargs)
+
+ def __init__(self, parent, children):
+ self._parentRef = weakref.ref(parent)
+ super(PrivateGroup.ChildrenList, self).__init__(children)
+
+ def __init__(self, children=()):
+ super(PrivateGroup, self).__init__()
+ self.__children = PrivateGroup.ChildrenList(self, children)
+ self.__children.addListener(self._updated)
+
+ @property
+ def _children(self):
+ """List of children to be rendered.
+
+ This private attribute is meant to be used by subclass.
+ """
+ return self.__children
+
+ @_children.setter
+ def _children(self, iterable):
+ self.__children.removeListener(self._updated)
+ for item in self.__children:
+ item._setParent(None)
+ del self.__children # This is needed
+ self.__children = PrivateGroup.ChildrenList(self, iterable)
+ self.__children.addListener(self._updated)
+ self.notify()
+
+ def _updated(self, source, *args, **kwargs):
+ """Listen for updates"""
+ if source is not self: # Avoid infinite recursion
+ self.notify(*args, **kwargs)
+
+ def _bounds(self, dataBounds=False):
+ """Compute the bounds from transformed children bounds"""
+ bounds = []
+ for child in self._children:
+ if child.visible:
+ childBounds = child.bounds(
+ transformed=True, dataBounds=dataBounds)
+ if childBounds is not None:
+ bounds.append(childBounds)
+
+ if len(bounds) == 0:
+ 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)
+
+ def prepareGL2(self, ctx):
+ pass
+
+ def renderGL2(self, ctx):
+ """Render all children"""
+ for child in self._children:
+ child.render(ctx)
+ for child in reversed(self._children):
+ child.postRender(ctx)
+
+
+class Group(PrivateGroup):
+ """A scene node that renders its (public) children."""
+
+ @property
+ def children(self):
+ """List of children to be rendered."""
+ return self._children
+
+ @children.setter
+ def children(self, iterable):
+ self._children = iterable