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

213 lines
7.1 KiB
Python

# -*- coding: utf-8 -*-
"""
scenegraph.element.text
"""
# imports ####################################################################
from math import hypot, degrees, atan2, modf, log
from ...font import Face
# from ...opengl.utils import create_texture
from .._common import _u
from ..transform import Translate, Rotate, Scale
from ..paint import Color
from .rectangle import Rectangle
from .path import Path
from .group import Group
from .use import Use
from . import Element
# text #######################################################################
_ANGLE_STEPS = 360
_SCALE_DOUBLE_STEPS = 128
_SUBPIXEL_STEPS = 64
class Text(Element):
tag = "text"
_state_attributes = Element._state_attributes + [
"font_family", "font_weight", "font_style", "font_size", "text"
]
_VECTOR_L = 30
_letters_cache = {}
_faces_cache = {}
def __init__(self, text,
**attributes):
super(Text, self).__init__(**attributes)
self._text_bbox = Rectangle()
self._width = None
self._text = None
self._ws = []
self.text = text
# TODO: handle list of coordinates
if isinstance(self.x, list):
self.x = self.x[0]
if isinstance(self.y, list):
self.y = self.y[0]
def _update_text_bbox(self):
(x, y), (width, height) = self.font_face.get_bbox(self.text)
self._width = width
self._text_bbox.x, self._text_bbox.width = x, width
self._text_bbox.y, self._text_bbox.height = y, height
self._text_bbox._paths() # force bbox update
@property
def font_face(self):
key = (self.font_family, self.font_weight, self.font_style,
int(self.font_size))
try:
font_face = self._faces_cache[key]
except KeyError:
font_face = self._faces_cache[key] = Face(*key)
return font_face
def get_text(self):
return _u(self._text)
def set_text(self, text):
self._text = text.strip()
text = property(get_text, set_text)
def _anchor(self):
self._update_text_bbox()
return {
'start': 0.,
'middle': -self._width / 2., # pylint: disable=invalid-unary-operand-type
'end': -self._width, # pylint: disable=invalid-unary-operand-type
}[self.text_anchor]
def _aabbox(self, transform, inheriteds):
return self._text_bbox.aabbox(transform * Translate(self._anchor()), inheriteds)
def _render(self, transform, inheriteds, context):
font_size = self.font_size
font_face = self.font_face
_, (cosa, sina), _, (sx, sy) = transform.params()
a, b = cosa * sx, sina * sy
c, d = -b, a
scale = hypot(a, b)
angle = degrees(atan2(b, a))
vector = font_size * scale > self._VECTOR_L
vector = vector or (self.stroke is not None) or (self.fill is None)
x_anchor = self._anchor()
X0, Y0 = transform.project(x_anchor)
untransform = transform.inverse()
if vector:
X, Y = 0., 0.
else:
(X, X0), (Y, Y0) = modf(X0), modf(Y0)
letters = Group(
transform=[Translate(x_anchor), Rotate(-angle), Scale(1 / scale), Translate(-X, -Y)],
stroke_width=self.stroke_width * scale,
)
self._ws = [0]
up = None
for uc in self.text:
X += font_face.get_hkerning(up, uc)
up = uc
if vector:
(Xf, Xi), (Yf, Yi) = (0., X), (0., Y)
else:
(Xf, Xi), (Yf, Yi) = modf(X), modf(Y)
if Xf < 0:
Xf, Xi = Xf + 1, Xi - 1
if Yf < 0:
Yf, Yi = Yf + 1, Yi - 1
key = (font_face, uc, vector,
int(round(angle * _ANGLE_STEPS / 360.)),
int(log(scale, 2.) * _SCALE_DOUBLE_STEPS),
int(Xf * _SUBPIXEL_STEPS), int(Yf * _SUBPIXEL_STEPS))
try:
letter, (Xc, Yc), (W, H), (dX, dY) = self._letters_cache[key]
except KeyError:
font_face.set_transform(a, b, c, d, Xf, Yf)
if vector:
(Xc, Yc), (W, H), (dX, dY), outline = font_face.outline(uc)
letter = Path(d=outline)
else:
assert False # code requires opengl
# (Xc, Yc), (W, H), (dX, dY), data = font_face.bitmap(uc)
# letter = Rectangle(x=Xc, y=Yc, width=W, height=H, fill=_Texture(create_texture(W, H, data)))
self._letters_cache[key] = letter, (Xc, Yc), (W, H), (dX, dY)
if W > 0 and H > 0:
letters.add_child(Use(letter, x=Xi, y=Yi))
X += dX
Y += dY
x, _ = untransform.project(X + X0, Y + Y0)
self._ws.append(x - x_anchor)
if all(type(c) in [type(None), Color] for c in [self.fill, self.stroke]):
# single pass rendering
if not vector:
for letter in letters.children:
letter.element.fill.rgb = self.fill.rgb
letters.render(transform, inheriteds, context)
else:
# multi-pass rendering if a gradient or pattern is used
filler_x, filler_y = self.x, self.y - self._text_bbox.height
bbox_x = self._text_bbox.x - self.stroke_width / 2. + x_anchor
bbox_y = self._text_bbox.y - self.stroke_width / 2.
width = self._text_bbox.width + self.stroke_width,
height = self._text_bbox.height + self.stroke_width
mask = Use(letters,
transform=[Translate(filler_x - bbox_x, filler_y - bbox_y)],
fill_opacity=1., stroke_opacity=1.,)
filler = Rectangle(
x=filler_x, y=filler_y, width=width, height=height,
transform=[Translate(bbox_x - filler_x, bbox_y - filler_y)],
stroke=None,
mask=mask,)
for filler_fill, filler_opacity, masking in [
(self.fill, self.fill_opacity, (Color.white, None)),
(self.stroke, self.stroke_opacity, (None, Color.white))
]:
if filler_fill:
filler.fill, filler.fill_opacity = filler_fill, filler_opacity
mask.fill, mask.stroke = masking
filler.render(transform, inheriteds, context)
def index(self, x, y=0):
"""index of the char at x (local coordinates)."""
for i, w in enumerate(self._ws):
if x < w:
break
return i - 1
def _hit_test(self, x, y, transform):
return self._text_bbox.pick(x, y, transform * Translate(self._anchor()))
def _xml_content(self, defs):
text = self.text
for old, new in [('&', '&amp;'),
('<', '&lt;'),
('>', '&gt;')]:
text = text.replace(old, new)
return text