wolfhece.opengl.text_renderer2d =============================== .. py:module:: wolfhece.opengl.text_renderer2d .. autoapi-nested-parse:: Shared-resource text renderer using SDF glyph atlas. Renders text strings as per-glyph quads textured from a :class:`~wolfhece.opengl.glyph_atlas.GlyphAtlas`. Features: - **Pixel-size or world-size** text (same dual-mode as polyline width) - **Multiline** text (split on ``\n``) - **Rotation** and **alignment** (left / centre / right) - **Glow / outline** via SDF thresholding - **Animations**: pulse, wave, typewriter Usage:: tr = TextRenderer2D.get_instance() tr.draw_text("Hello\nWorld", x, y, mvp, viewport, font_size=16, glow_enabled=True) Author: HECE - University of Liege, Pierre Archambeau Date: 2026 Copyright (c) 2026 University of Liege. All rights reserved. Module Contents --------------- .. py:data:: SHADER_DIR .. py:data:: _STRIDE :value: 5 .. py:data:: _BYTES_PER_FLOAT :value: 4 .. py:function:: build_text_vertices(text: str, atlas: wolfhece.opengl.glyph_atlas.GlyphAtlas, x: float, y: float, scale: float, rotation: float = 0.0, alignment: str = 'left', line_spacing: float = 1.2) -> tuple[numpy.ndarray, int] Build per-glyph quad vertex data for a text string. :param text: Text with optional ``\n`` for multiline. :param atlas: :class:`GlyphAtlas` to use for metrics. :param x: Anchor X in world coordinates. :param y: Anchor Y in world coordinates. :param scale: World-space size of one em. :param rotation: Counter-clockwise rotation **in radians**. :param alignment: ``'left'``, ``'center'``, or ``'right'``. :param line_spacing: Line spacing as a multiple of *line_height*. :return: ``(vbo_data, vertex_count)`` — layout is ``[x, y, u, v, char_idx]`` per vertex, 6 vertices per visible glyph (two triangles). .. py:function:: measure_text(text: str, atlas: wolfhece.opengl.glyph_atlas.GlyphAtlas, scale: float, line_spacing: float = 1.2) -> tuple[float, float] Measure the bounding box of *text* in world units. :return: ``(width, height)`` — width of the widest line, total height including line spacing. .. py:function:: _interpolate_polyline(points: numpy.ndarray, cum_dist: numpy.ndarray, s: float) -> tuple[float, float, float, float] Interpolate position and tangent along a polyline at distance *s*. :param points: ``(N, 2)`` vertex array. :param cum_dist: ``(N,)`` cumulative distance array. :param s: Curvilinear abscissa to evaluate. :return: ``(x, y, tx, ty)`` — position and **unit** tangent. .. py:function:: build_text_along_polyline(text: str, atlas: wolfhece.opengl.glyph_atlas.GlyphAtlas, points: numpy.ndarray, cum_dist: numpy.ndarray, scale: float, offset_along: float = 0.0, offset_perp: float = 0.0, alignment: str = 'left') -> tuple[numpy.ndarray, int] Build per-glyph quad vertex data for text that follows a polyline. Each character is placed at the appropriate curvilinear distance along the polyline and rotated to match the local tangent direction. :param text: Text string (single line). :param atlas: SDF :class:`GlyphAtlas` for metrics. :param points: ``(N, 2)`` polyline vertices in world coords. :param cum_dist: ``(N,)`` cumulative distances (from :meth:`get_sz`). :param scale: World-space size of one em. :param offset_along: Shift the start of the text along the polyline (in world units). Positive = downstream. :param offset_perp: Perpendicular offset from the polyline (in world units). Positive = left side. :param alignment: ``'left'`` | ``'center'`` | ``'right'``. :return: ``(vbo_data, vertex_count)`` — same layout as :func:`build_text_vertices`. .. py:function:: snap_to_polyline(mouse_x: float, mouse_y: float, points: numpy.ndarray, cum_dist: numpy.ndarray) -> tuple[float, float, float, float, float, float] Find the closest point on a polyline to a mouse position. Uses vectorised numpy operations — no Shapely dependency. :param mouse_x: Mouse X in world coordinates. :param mouse_y: Mouse Y in world coordinates. :param points: ``(N, 2)`` polyline vertices. :param cum_dist: ``(N,)`` cumulative distances. :return: ``(snap_x, snap_y, curvi_dist, total_length, tangent_x, tangent_y)`` .. py:class:: TextRenderer2D Singleton text renderer: compiles shaders once, manages shared VAO/VBO. Call :meth:`draw_text` for each piece of text to render. .. py:attribute:: _instance :type: TextRenderer2D | None :value: None .. py:attribute:: _TEXT_GEOM_CACHE_MAX :value: 512 .. py:attribute:: _TEXT_MEASURE_CACHE_MAX :value: 256 .. py:attribute:: _program :type: int | None :value: None .. py:attribute:: _locs :type: dict | None :value: None .. py:attribute:: _vao :type: int | None :value: None .. py:attribute:: _vbo :type: int | None :value: None .. py:attribute:: _text_geom_cache :type: collections.OrderedDict .. py:attribute:: _text_measure_cache :type: collections.OrderedDict .. py:method:: get_instance() -> TextRenderer2D :classmethod: Return the singleton, creating it lazily. .. py:method:: _init_program() Compile and link vertex + fragment shaders. .. py:method:: _compile(shader_type: int, path: pathlib.Path) -> int :staticmethod: Compile a single GLSL shader from *path*. .. py:method:: _ensure_vao_vbo() Create VAO/VBO if not yet allocated. .. py:method:: _upload(vbo_data: numpy.ndarray) Upload vertex data to the shared VBO. .. py:method:: _q(val: float, ndigits: int = 7) -> float :staticmethod: Quantize a float for stable cache keys. .. py:method:: _cache_get(cache: collections.OrderedDict, key) :staticmethod: Get value from OrderedDict LRU cache and refresh recency. .. py:method:: _cache_set(cache: collections.OrderedDict, key, value, max_size: int) :staticmethod: Insert value into OrderedDict LRU cache with bounded size. .. py:method:: _measure_text_cached(text: str, atlas: wolfhece.opengl.glyph_atlas.GlyphAtlas, line_spacing: float) -> tuple[float, float] Measure text with a small LRU cache (hot path in width-priority mode). .. py:method:: _build_text_vertices_cached(text: str, atlas: wolfhece.opengl.glyph_atlas.GlyphAtlas, x: float, y: float, scale: float, rotation: float, alignment: str, line_spacing: float) -> tuple[numpy.ndarray, int] Build per-glyph vertices using an LRU cache for static labels. .. py:method:: draw_text(text: str, x: float, y: float, mvp: numpy.ndarray, viewport: tuple[int, int], *, font_name: str = 'arial.ttf', font_size: float = 14.0, color: tuple[float, Ellipsis] = (1.0, 1.0, 1.0, 1.0), size_in_pixels: bool = True, world_height: float | None = None, world_width: float | None = None, rotation: float = 0.0, alignment: str = 'left', line_spacing: float = 1.2, smoothing: float = 1.0, glow_enabled: bool = False, glow_width: float = 0.15, glow_color: tuple[float, Ellipsis] = (1.0, 1.0, 1.0, 0.5), anim_mode: int = 0, anim_phase: float = 0.0, anim_speed: float = 1.0) Render *text* at world position (x, y). :param text: Text string (supports ``\n`` for multiline). :param x: Anchor X in world coordinates. :param y: Anchor Y in world coordinates. :param mvp: 4×4 model-view-projection (column-major float32). :param viewport: ``(width_px, height_px)``. :param font_name: TrueType font file name. :param font_size: Size in pixels (if *size_in_pixels*) or world units. :param color: ``(r, g, b, a)`` each in ``[0, 1]``. :param size_in_pixels: If ``True`` *font_size* is screen pixels. :param world_height: If set, overrides *font_size* with this world-unit height. :param world_width: If set (and *world_height* is ``None``), scales the text so its measured width matches this world-unit value. :param rotation: Counter-clockwise rotation in **degrees**. :param alignment: ``'left'`` | ``'center'`` | ``'right'``. :param line_spacing: Line spacing multiplier. :param smoothing: SDF edge-width multiplier (1.0 = standard AA). :param glow_enabled: Enable glow / outline halo. :param glow_width: SDF threshold offset for glow (e.g. 0.15). :param glow_color: ``(r, g, b, a)`` for the glow. :param anim_mode: 0 = none, 1 = pulse, 2 = wave, 3 = typewriter. :param anim_phase: Phase in ``[0, 1]``. :param anim_speed: Speed multiplier (reserved for future use). .. py:method:: draw_text_along_polyline(text: str, points: numpy.ndarray, cum_dist: numpy.ndarray, mvp: numpy.ndarray, viewport: tuple[int, int], *, font_name: str = 'arial.ttf', font_size: float = 14.0, color: tuple[float, Ellipsis] = (1.0, 1.0, 1.0, 1.0), size_in_pixels: bool = True, world_height: float | None = None, world_width: float | None = None, offset_along: float = 0.0, offset_perp: float = 0.0, alignment: str = 'left', smoothing: float = 1.0, glow_enabled: bool = False, glow_width: float = 0.15, glow_color: tuple[float, Ellipsis] = (1.0, 1.0, 1.0, 0.5), anim_mode: int = 0, anim_phase: float = 0.0, anim_speed: float = 1.0) Render *text* along a polyline, each character following the tangent. :param text: Text string (single line). :param points: ``(N, 2)`` polyline vertices in world coords. :param cum_dist: ``(N,)`` cumulative distances. :param mvp: 4×4 model-view-projection (column-major float32). :param viewport: ``(width_px, height_px)``. :param font_name: TrueType font file name. :param font_size: Size in pixels (if *size_in_pixels*) or world units. :param color: ``(r, g, b, a)`` each in ``[0, 1]``. :param size_in_pixels: If ``True`` *font_size* is screen pixels. :param world_height: If set, overrides *font_size* with this world-unit height. :param world_width: If set (and *world_height* is ``None``), scales the text so its measured width matches this world-unit value. :param offset_along: Shift text start along polyline (world units). :param offset_perp: Perpendicular offset (world units, +left). :param alignment: ``'left'`` | ``'center'`` | ``'right'``. :param smoothing: SDF edge-width multiplier. :param glow_enabled: Enable glow / outline halo. :param glow_width: SDF threshold offset for glow. :param glow_color: ``(r, g, b, a)`` for the glow. :param anim_mode: 0 = none, 1 = pulse, 2 = wave, 3 = typewriter. :param anim_phase: Phase in ``[0, 1]``. :param anim_speed: Speed multiplier. .. py:method:: draw_tracking_label(mouse_x: float, mouse_y: float, points: numpy.ndarray, cum_dist: numpy.ndarray, mvp: numpy.ndarray, viewport: tuple[int, int], *, format_func: Callable[[float, float], str] | None = None, font_name: str = 'arial.ttf', font_size: float = 14.0, color: tuple[float, Ellipsis] = (1.0, 1.0, 0.0, 1.0), size_in_pixels: bool = True, world_height: float | None = None, offset_perp: float = 0.0, smoothing: float = 1.0, glow_enabled: bool = True, glow_width: float = 0.2, glow_color: tuple[float, Ellipsis] = (0.0, 0.0, 0.0, 0.8), snap_radius: float | None = None) Render a label that tracks the mouse snapped to the polyline. The label shows dynamic content (by default the curvilinear distance) and follows the mouse position projected onto the polyline. :param mouse_x: Mouse X in world coordinates. :param mouse_y: Mouse Y in world coordinates. :param points: ``(N, 2)`` polyline vertices in world coords. :param cum_dist: ``(N,)`` cumulative distances. :param mvp: 4×4 model-view-projection (column-major float32). :param viewport: ``(width_px, height_px)``. :param format_func: ``(curvi_dist, total_length) -> str``. Defaults to ``"d = {curvi:.2f} m"``. :param font_name: TrueType font file name. :param font_size: Size in pixels (if *size_in_pixels*) or world units. :param color: ``(r, g, b, a)`` — yellow by default. :param size_in_pixels: If ``True``, *font_size* is screen pixels. :param world_height: If set, overrides *font_size*. :param offset_perp: Perpendicular offset (world units, positive = left of polyline direction). :param smoothing: SDF edge-width multiplier. :param glow_enabled: Enable background halo for readability. :param glow_width: Glow SDF threshold. :param glow_color: ``(r, g, b, a)`` — dark semi-transparent default. :param snap_radius: If set, label is hidden when the mouse is farther than this distance (in world units) from the polyline. .. py:method:: destroy() Free GPU resources.