295 lines
9.9 KiB
Python
295 lines
9.9 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
|
||
|
"""
|
||
|
scenegraph.element
|
||
|
"""
|
||
|
|
||
|
# imports ####################################################################
|
||
|
|
||
|
from weakref import WeakValueDictionary as _weakdict
|
||
|
|
||
|
# from ...opengl.utils import OffscreenContext
|
||
|
from .._common import _Element
|
||
|
from ..paint import Color, _Texture, _MaskContext
|
||
|
from ..transform import Matrix, Translate, stretch, product
|
||
|
|
||
|
|
||
|
# element ####################################################################
|
||
|
|
||
|
_elements_by_id = _weakdict()
|
||
|
|
||
|
|
||
|
def _id(element):
|
||
|
element_id = getattr(element, "_id", "%X" % id(element))
|
||
|
_elements_by_id[element_id] = element
|
||
|
return element_id
|
||
|
|
||
|
|
||
|
def get_element_by_id(element_id):
|
||
|
return _elements_by_id[element_id]
|
||
|
|
||
|
"""
|
||
|
List of all possible attributes for svg nodes
|
||
|
"""
|
||
|
_ATTRIBUTES = [
|
||
|
"id", "href",
|
||
|
"x", "y",
|
||
|
"width", "height",
|
||
|
"r", "rx", "ry",
|
||
|
"cx", "cy",
|
||
|
"points",
|
||
|
"x1", "x2", "y1", "y2",
|
||
|
"opacity", "color",
|
||
|
"fill", "fill_opacity", "fill_rule",
|
||
|
"stroke", "stroke_opacity", "stroke_width",
|
||
|
"stroke_linecap", "stroke_linejoin", "stroke_miterlimit",
|
||
|
"stroke_dasharray", "stroke_dashoffset",
|
||
|
"font_family", "font_weight", "font_style", "font_size",
|
||
|
"text_anchor",
|
||
|
"transform",
|
||
|
"clip_path",
|
||
|
"mask",
|
||
|
"d",
|
||
|
]
|
||
|
|
||
|
_INHERITEDS = {
|
||
|
"color": Color.black,
|
||
|
"fill": Color.black,
|
||
|
"fill_opacity": 1.,
|
||
|
"fill_rule": 'nonzero',
|
||
|
"stroke": Color.none,
|
||
|
"stroke_opacity": 1.,
|
||
|
"stroke_width": 1,
|
||
|
"stroke_linecap": 'butt',
|
||
|
"stroke_linejoin": 'miter',
|
||
|
"stroke_miterlimit": 4.,
|
||
|
"stroke_dasharray": None,
|
||
|
"stroke_dashoffset": 0.,
|
||
|
"font_family": 'sans-serif',
|
||
|
"font_weight": 'normal',
|
||
|
"font_style": 'normal',
|
||
|
"font_size": 10,
|
||
|
"text_anchor": 'start',
|
||
|
}
|
||
|
|
||
|
|
||
|
class Element(_Element):
|
||
|
""" A node in the svg tree
|
||
|
"""
|
||
|
x, y = 0, 0
|
||
|
transform = None
|
||
|
|
||
|
opacity = 1.
|
||
|
clip_path = None
|
||
|
mask = None
|
||
|
|
||
|
_state_attributes = _Element._state_attributes + list(_INHERITEDS) + [
|
||
|
"x", "y", "transform",
|
||
|
"opacity", "clip_path", "mask"
|
||
|
]
|
||
|
|
||
|
def __init__(self, **attributes):
|
||
|
"""
|
||
|
:param attributes: svg attributes (eg for a circle element : { 'cx':'5.0', 'cy':'10.0', 'r':'6.0', 'transform':'translate(30,40) rotate(45)' })
|
||
|
"""
|
||
|
self._attributes = set()
|
||
|
self._inheriteds = _INHERITEDS
|
||
|
for attribute in attributes:
|
||
|
setattr(self, attribute, attributes[attribute])
|
||
|
# the transform attribute contains a list of transformations associated to this Element. It doesn't take into account its parents transforms.
|
||
|
if self.transform is None:
|
||
|
# empty list of transforms if the transform svg attribute is not present in the svg node
|
||
|
self.transform = []
|
||
|
self._parent = None # the svg group containing this element
|
||
|
|
||
|
def __setattr__(self, attribute, value):
|
||
|
if attribute in _ATTRIBUTES:
|
||
|
self._attributes.add(attribute)
|
||
|
super(Element, self).__setattr__(attribute, value)
|
||
|
|
||
|
def __delattr__(self, attribute):
|
||
|
super(Element, self).__delattr__(attribute)
|
||
|
if attribute in _ATTRIBUTES:
|
||
|
self._attributes.remove(attribute)
|
||
|
|
||
|
def __getattr__(self, attribute):
|
||
|
if attribute in _INHERITEDS:
|
||
|
return self._inheriteds[attribute]
|
||
|
try:
|
||
|
return super(Element, self).__getattr__(attribute)
|
||
|
except AttributeError:
|
||
|
return super(Element, self).__getattribute__(attribute)
|
||
|
|
||
|
def _inherit(self, inheriteds):
|
||
|
self._inheriteds = inheriteds
|
||
|
return {attr: getattr(self, attr) for attr in _INHERITEDS}
|
||
|
|
||
|
@property
|
||
|
def id(self):
|
||
|
self._attributes.add("id")
|
||
|
return _id(self)
|
||
|
|
||
|
@property
|
||
|
def attributes(self):
|
||
|
return (name for name in _ATTRIBUTES if name in self._attributes)
|
||
|
|
||
|
def __hash__(self):
|
||
|
return id(self) # hash((self.name, self.location))
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
return id(self) == id(other)
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
# Not strictly necessary, but to avoid having both x==y and x!=y
|
||
|
# True at the same time
|
||
|
return not(self == other)
|
||
|
|
||
|
@property
|
||
|
def parent(self):
|
||
|
"""
|
||
|
:rtype: scenegraph.Element
|
||
|
"""
|
||
|
return self._parent
|
||
|
|
||
|
@parent.setter
|
||
|
def parent(self, parent_group):
|
||
|
self._parent = parent_group
|
||
|
|
||
|
# transformations
|
||
|
|
||
|
def local_to_parent_matrix(self):
|
||
|
"""returns the matrix that converts coordinates in this node's space to its parent's space
|
||
|
|
||
|
:rtype: scenegraph.Matrix
|
||
|
"""
|
||
|
return product(*self.transform + [Translate(self.x, self.y)])
|
||
|
|
||
|
def parent_to_world_matrix(self):
|
||
|
"""returns the matrix that converts coordinates in this node's parent's space to the world space
|
||
|
|
||
|
:rtype: scenegraph.Matrix
|
||
|
"""
|
||
|
if self.parent is None:
|
||
|
return Matrix()
|
||
|
else:
|
||
|
return self.parent.parent_to_world_matrix() * self.parent.local_to_parent_matrix() # pylint: disable=no-member
|
||
|
|
||
|
def local_to_world_matrix(self):
|
||
|
"""returns the matrix that converts coordinates in this node's space to the world space
|
||
|
|
||
|
:rtype: scenegraph.Matrix
|
||
|
"""
|
||
|
return self.parent_to_world_matrix() * self.local_to_parent_matrix() # pylint: disable=no-member
|
||
|
|
||
|
# axis-aligned bounding box
|
||
|
|
||
|
def aabbox(self, transform=Matrix(), inheriteds=_INHERITEDS):
|
||
|
"""returns the axis-aligned bounding box of this xml element
|
||
|
"""
|
||
|
inheriteds = self._inherit(inheriteds)
|
||
|
return self._aabbox(transform * self.local_to_parent_matrix(), inheriteds)
|
||
|
|
||
|
def _aabbox(self, transform, inheriteds):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
def _units(self, elem, attr, default="userSpaceOnUse"):
|
||
|
units = getattr(elem, attr, default)
|
||
|
if units == "userSpaceOnUse":
|
||
|
transform = Matrix()
|
||
|
elif units == "objectBoundingBox":
|
||
|
(x_min, y_min), (x_max, y_max) = self.aabbox()
|
||
|
transform = stretch(x_min, y_min, x_max - x_min, y_max - y_min)
|
||
|
else:
|
||
|
raise ValueError("unknown units %s" % units)
|
||
|
return product(*self.transform) * transform
|
||
|
|
||
|
# rendering
|
||
|
|
||
|
def _color(self, color):
|
||
|
if color == Color.current:
|
||
|
return self.color
|
||
|
return color
|
||
|
|
||
|
def render(self, transform=Matrix(), inheriteds=_INHERITEDS, context=None,
|
||
|
clipping=True, masking=True, opacity=True):
|
||
|
inheriteds = self._inherit(inheriteds)
|
||
|
if context is None:
|
||
|
# context = OffscreenContext()
|
||
|
assert False
|
||
|
|
||
|
if (clipping and self.clip_path) or (masking and self.mask):
|
||
|
if clipping and self.clip_path:
|
||
|
clipping = False
|
||
|
mask, units = self.clip_path, "clipPathUnits"
|
||
|
else:
|
||
|
masking = False
|
||
|
mask, units = self.mask, "maskContentUnits"
|
||
|
|
||
|
mask_transform = self._units(mask, units)
|
||
|
with context(mask.aabbox(transform * mask_transform),
|
||
|
(0., 0., 0., 0.)) as ((x, y), (width, height),
|
||
|
mask_texture_id):
|
||
|
if not mask_texture_id:
|
||
|
return
|
||
|
mask.render(transform * mask_transform, context=context)
|
||
|
|
||
|
with _MaskContext((x, y), (width, height), mask_texture_id):
|
||
|
self.render(transform, inheriteds, context,
|
||
|
clipping=clipping, masking=masking, opacity=opacity)
|
||
|
|
||
|
elif opacity and self.opacity < 1.:
|
||
|
with context(self.aabbox(transform, inheriteds)) as \
|
||
|
((x, y), (width, height), elem_texture_id):
|
||
|
if not elem_texture_id:
|
||
|
return
|
||
|
self.render(transform, inheriteds, context,
|
||
|
clipping=clipping, masking=masking, opacity=False)
|
||
|
|
||
|
Rectangle(x=x, y=y, width=width, height=height,
|
||
|
fill=_Texture(elem_texture_id),
|
||
|
fill_opacity=self.opacity).render(context=context)
|
||
|
|
||
|
else:
|
||
|
self._render(transform * self.local_to_parent_matrix(), inheriteds, context)
|
||
|
|
||
|
def _render(self, transform, inheriteds, context):
|
||
|
raise NotImplementedError
|
||
|
|
||
|
# picking
|
||
|
|
||
|
def _hit_test(self, x, y, transform):
|
||
|
"""tests if the position (x,y) collides with this shape (not its children)
|
||
|
"""
|
||
|
return []
|
||
|
|
||
|
def pick(self, x=0, y=0, parent_to_world=Matrix()):
|
||
|
"""
|
||
|
returns the list of svg nodes that are hit when picking at position x,y
|
||
|
|
||
|
:param parent_to_world: the transformation matrix that converts coordinates from this element's parent space to the scene's world space (the space in which x and y coordinates are expressed)
|
||
|
"""
|
||
|
parent_to_world = parent_to_world * self.local_to_parent_matrix()
|
||
|
hits = self._hit_test(x, y, parent_to_world)
|
||
|
hits += [([self] + e, p) for e, p in self._pick_content(x, y, parent_to_world)]
|
||
|
return hits
|
||
|
|
||
|
def _pick_content(self, x, y, transform):
|
||
|
"""tests if the position (x,y) collides with the children shapes of this shape
|
||
|
"""
|
||
|
return []
|
||
|
|
||
|
|
||
|
# elements ###################################################################
|
||
|
|
||
|
from .use import Use # @IgnorePep8
|
||
|
from .group import Group # @IgnorePep8
|
||
|
from .rectangle import Rectangle # @IgnorePep8
|
||
|
from .circle import Circle # @IgnorePep8
|
||
|
from .ellipse import Ellipse # @IgnorePep8
|
||
|
from .line import Line # @IgnorePep8
|
||
|
from .polyline import Polyline # @IgnorePep8
|
||
|
from .polygon import Polygon # @IgnorePep8
|
||
|
from .path import Path # @IgnorePep8
|
||
|
from .text import Text # @IgnorePep8
|
||
|
from .image import Image # @IgnorePep8
|