Source code for silx.gui.plot3d.scene.core

# /*##########################################################################
#
# 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`.
"""

__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 #######################################################################


[docs] 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.0, 1.0), 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
[docs] 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
[docs] def prepareGL2(self, ctx): """Called before the rendering to prepare OpenGL resources. Override in subclass. """ pass
[docs] def renderGL2(self, ctx): """Called to perform the OpenGL rendering. Override in subclass. """ pass
[docs] def render(self, ctx): """Called internally to perform rendering.""" if self.visible: ctx.pushTransform(self.transforms) self.prepareGL2(ctx) self.renderGL2(ctx) ctx.popTransform()
[docs] 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
[docs] def pick(self, ctx, x, y, depth=None): """True/False picking, should be fast""" if self.pickable: pass
[docs] def pickRay(self, ctx, ray): """Picking returning list of ray intersections.""" if self.pickable: pass
[docs] 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
[docs] class PrivateGroup(Base): """A scene node that renders its (private) childern. :param iterable children: :class:`Base` nodes to add as children """
[docs] 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, )
[docs] def prepareGL2(self, ctx): pass
[docs] def renderGL2(self, ctx): """Render all children""" for child in self._children: child.render(ctx) for child in reversed(self._children): child.postRender(ctx)
[docs] 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