Source code for pi3d.util.TextBlock

from __future__ import absolute_import, division, print_function, unicode_literals

""" Text block used to update contents of buffers used to draw gl_point font characters.
"""
import numpy as np
import math
import colorsys
import logging

LOGGER = logging.getLogger(__name__)
WRAP_ON = [" ", "-", "_"]

[docs]def getattra(obj, attr, default): if("[" in attr): splits = attr.split("[") index = int(splits[1].replace("]", "")) thearray = getattr(obj, splits[0], None) if(thearray is not None): return thearray[index] else: return default else: return getattr(obj, attr, default)
[docs]class TextBlockColour(object): def __init__(self, colour=(1.0, 1.0, 1.0, 1.0), textBlock=None): self.colour = [colour[0], colour[1], colour[2], colour[3]] self.textBlock = textBlock
[docs] def recolour(self): self.set_colour()
[docs] def set_colour(self, colour=None, alpha=None): if colour is not None: self.colour[0:len(colour)] = colour[:] # could be 3 or 4, ambiguous if alpha is not None: self.colour[3] = alpha textBlock = self.textBlock manager = textBlock._text_manager st = textBlock._buffer_index mid = st + textBlock._string_length end = st + textBlock.char_count #Reset alpha to zero for all characters. Prevents displaying old chars from longer strings manager.normals[st:end, 2] = 0 #Fill an array with the colour to copy to the manager normals #rotation is included for efficiency normal = np.zeros((3), dtype=float) normal[0] = textBlock.rot + textBlock.char_rot normal[1] = (self.colour[1] * 0.999) + (math.floor(self.colour[0] * 999)) normal[2] = (self.colour[3] * 0.999) + (math.floor(self.colour[2] * 999)) #Only set colour alpha for string length. Zero for non displayed characters manager.normals[st:mid, :] = normal
[docs]class TextBlockColourGradient(TextBlockColour): def __init__(self, colour1, colour2, textBlock=None): self.colour1 = colour1 self.colour2 = colour2 self.textBlock = textBlock
[docs] def set_colour(self, colour1=None, colour2=None): ''' Colour each character with a gradient from colour1 to colour2 Interpolate hsv instead of rgb since it is a more natural change. ''' if colour1 is not None: self.colour1 = colour1 if colour2 is not None: self.colour2 = colour2 if self.textBlock is None: return colour1 = self.colour1 colour2 = self.colour2 textBlock = self.textBlock manager = textBlock._text_manager hsv1 = colorsys.rgb_to_hsv(colour1[0], colour1[1], colour1[2]) hsv2 = colorsys.rgb_to_hsv(colour2[0], colour2[1], colour2[2]) normal = np.zeros((3), dtype=float) normal[0] = textBlock.rot + textBlock.char_rot tlen = textBlock._string_length # alias for brevity below for i in range(tlen): h = hsv1[0] + (hsv2[0] - hsv1[0]) * i / tlen s = hsv1[1] + (hsv2[1] - hsv1[1]) * i / tlen v = hsv1[2] + (hsv2[2] - hsv1[2]) * i / tlen a = colour1[3] + (colour2[3] - colour1[3]) * i / tlen rgb = colorsys.hsv_to_rgb(h, s, v) normal[1] = (rgb[1] * 0.999) + math.floor((rgb[0] * 999)) normal[2] = (a * 0.999) + math.floor((rgb[2] * 999)) #Only set colour alpha for string length. Zero for non displayed characters manager.normals[textBlock._buffer_index + i, :] = normal
[docs]class TextBlock(object): def __init__(self, x, y, z, rot, char_count, data_obj=None, attr=None, text_format="{:s}", size=0.99, spacing="C", space=1.1, colour=(1.0,1.0,1.0,1.0) , char_rot=0.0, justify=0.0): """ Arguments: *x, y, z*: As usual *rot*: rotation in degrees *char_count*: number of characters for this block (capacity it can expand into) *data_obj*: Data object to use in text format *attr*: Attribute in data object to use in text format *text_format*: Thetext format to use including any data formattings *size*: Size of the text 0 to 0.9999 *spacing*: Type of character spacing. C=Constant, M=Multiplier, F=Fixed space between chars *space*: Value to set the spacing to *colour*: drawn colour including alpha as format (0.99, 0.99, 0.99, 0.99) *char_rot*: character rotation in degrees *justify*: Justification position. 0.0=Left, 0.5=Center, 1.0=Right """ self.x = x self.y = y self.z = z self.rot = math.radians(rot) self.char_count = char_count self.data_obj = data_obj self.attr = attr self.text_format = text_format self.size = size self.spacing = spacing self.space = space self.point_size = 48 #If the colour is a tuple initialize it a plain colour #Otherwise use a TextBlockColour object and its textBlock reference to this TextBlock if isinstance(colour, tuple): self.colouring = TextBlockColour(colour, self) else: self.colouring = colour self.colouring.textBlock = self self.char_rot = math.radians(char_rot) self.justify = justify self.last_value = self # hack so that static None object get initialization self.rotation_changed = False self._buffer_index = 0 self._text_manager = None self._string_length = 0 self.char_offsets = np.zeros((char_count, 3)) # x, y, char width for wrapping alignment self._string_length = len(self.get_string(self.get_value()))
[docs] def set_text_manager(self, manager, buffer_index): self._text_manager = manager self._buffer_index = buffer_index
[docs] def get_value(self): if (self.attr is not None) and (self.data_obj is not None): return getattra(self.data_obj, self.attr, None) return None
[docs] def get_string(self, value): if value is not None: strval = self.text_format.format(value) self._string_length = len(strval) return strval return self.text_format
[docs] def set_position(self, x=None, y=None, z=None, rot=None): if x is not None: self.x = x if y is not None: self.y = y if z is not None: self.z = z if rot is not None: self.rot = math.radians(rot) size_pos, __ = math.modf(self.size) size_pos += math.trunc(self.z * 10.0) # depth has resolution of 0.1m and range of 25.5m pos = [self.x, self.y, size_pos] locations = np.zeros((self.char_count, 3), dtype=float) (c, s) = (np.cos(self.rot), np.sin(self.rot)) matrix = np.array([[c, -s], [s, c]]) locations[:,:2] = matrix.dot(self.char_offsets[:,:2].T).T locations += pos st = self._buffer_index mid = st + self._string_length end = st + self.char_count self._text_manager.locations[st:end, :] = locations self._text_manager.normals[st:mid, 0] = self.rot + self.char_rot self._text_manager.set_do_reinit()
[docs] def recolour(self): self.colouring.recolour()
[docs] def set_text(self, text_format=None, size=None, spacing=None, space=None, char_rot=None, set_pos=True, set_colour=True, wrap=None, line_space=1.0, wrap_pixels=None): """ Arguments: *text_format*: text to display or a format string for displaying value from data_obj *size*: size of text 0.0 to 0.9999 *spacing*: 'C', 'M' or 'F' *space*: additional space between character *char_rot*: character rotation in degrees *set_pos*: control whether set_position() is called - default True *set_colour*: control whether set_colour() is called - default True *wrap*: if set then line breaks insterted to wrap at this number of characters *line_space*: multiplier for line spacing if multiline TextBlock *wrap_pixels* if set to value then lines will be wrapped to new line at this distance TODO only aligns accurately with spacing type F """ if text_format is not None: self.text_format = text_format if size is not None: self.size = size if spacing is not None: self.spacing = spacing if space is not None: self.space = space if char_rot is not None: self.char_rot = math.radians(char_rot) #If there is no data object then use the simple static string value if self.data_obj is not None: value = self.get_value() strval = self.get_string(value) self.last_value = value else: strval = self.text_format self._string_length = len(strval) if self._string_length > self.char_count: LOGGER.error("'%s' needs %s characters but only space for %s", strval, self._string_length, self.char_count) return -1 if wrap is not None: strval = self.split_lines(strval, wrap) pos = 0.0 spacing = 0.0 #to stop crash if zero length string index = 0 #Now the string is updated set the correct glyphs and calculate the #character offset positions. This is not the same as the final char positions. const_width = 64.0 * self.size * self.space vari_width = 0.0 if self.spacing == "M": const_width = 0.0 vari_width = self.size * self.space if self.spacing == "F": vari_width = self.size lines = [[0, 0]] # [start slice, end slice] line_n = 0 """scale is needed if pointsize is different in Points from Font which is needed to avoid clipping or overlap of corners when rotation some truetype fonts""" g_scale = float(self._text_manager.point_size) / self._text_manager.font.height line_ht = None #need to use same line height for whole thing for glyph in self._text_manager.font.glyph_table.values(): line_ht = glyph[1] * g_scale break last_space = None # index of last space on this line for char in strval: if char == '\n': ## new line lines.append([index, index]) pos = 0.0 line_n += 1 if char in self._text_manager.font.glyph_table: # skip chars not in font glyph = self._text_manager.font.glyph_table[char] self._text_manager.uv[index+self._buffer_index] = glyph[4:] char_offset = pos char_width = glyph[0] * g_scale * vari_width if self.spacing == "F": #center character to the right char_offset += char_width * 0.5 self.char_offsets[index] = [char_offset, -line_space * line_n * line_ht, char_width] spacing = char_width + const_width index += 1 lines[line_n][1] = index pos += spacing if wrap_pixels is not None and (char in WRAP_ON or index == len(strval)): # check if last word needs wrapping if last_space is not None and pos > wrap_pixels: # wrap to new line start_c = last_space offset = (self.char_offsets[start_c - 1, 0] - self.char_offsets[0, 0] + 0.5 * (self.char_offsets[start_c - 1, 2] + self.char_offsets[0, 2])) self.char_offsets[start_c:index,:2] -= [offset, line_space * line_ht] lines[line_n][1] = last_space # revise end of line to before this space last_c = (index - 2) if char == " " else index lines.append([start_c, last_c]) pos = self.char_offsets[index - 1, 0] + self.char_offsets[index - 1, 2] line_n += 1 last_space = None else: last_space = index for i in range(index, self.char_count): # if text re-used with shorter string self._text_manager.uv[i + self._buffer_index] = [0.0, 0.0] #Justification if len(strval) > 0: # could be passed zero length string to blank previous contents of TextBlock for (i, line) in enumerate(lines): last_c = line[1] - (2 if strval[line[1] - 1] == " " else 1) last_offs = self.char_offsets[last_c] adjust_len = last_offs[0] + 0.5 * last_offs[2] self.char_offsets[line[0]:line[1], 0] += adjust_len * -self.justify if set_pos: self.set_position() if set_colour: self.recolour() self._text_manager.set_do_reinit()
[docs] def split_lines(self, txt, width): new_txt = "" line_len = 0 txt_words = txt.split() for w in txt_words: len_w = len(w) if line_len > 0: if (line_len + len_w) > width: new_txt += "\n" line_len = 0 else: new_txt += " " line_len += 1 new_txt += w line_len += len_w return new_txt