msspec_python3/msspec/msspecgui/scenegraph2d/scenegraph/element/__init__.py

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