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