Source code for simvx.graphics.text_utils

"""Text utility helpers extracted from Engine: font finding and text rasterization."""

from typing import TYPE_CHECKING, Any

import vulkan as vk

from simvx.core.text import rasterize_text

if TYPE_CHECKING:
    from .gpu.context import GPUContext

__all__ = ["TextTexture", "create_text_texture", "find_font_path", "rasterize_text"]

[docs] def find_font_path() -> str: """Find a usable system font path via the unified font finder.""" from .text_renderer import _find_font path = _find_font() if path is None: raise FileNotFoundError("No system font found. Pass font= explicitly.") return path
[docs] class TextTexture: """GPU-backed text texture for use on 3D objects. Properties ``.text`` and ``.colour`` trigger re-rasterization and GPU upload when changed. """ def __init__( self, ctx: GPUContext, register_texture: Any, texture_descriptor_set: Any, default_sampler: Any, font: str | None = None, size: int = 32, width: int = 256, height: int = 64, ) -> None: from .text_renderer import TextRenderer self._ctx = ctx self._register_texture = register_texture self._texture_descriptor_set = texture_descriptor_set self._default_sampler = default_sampler self._tr = TextRenderer() self._font = font self._size = size self._width = width self._height = height self._text = "" self._colour = (1.0, 1.0, 1.0, 1.0) self._image = None self._image_mem = None self._image_view = None self.texture_index = -1 @property def text(self) -> str: return self._text
[docs] @text.setter def text(self, value: str) -> None: if value != self._text: self._text = value self._render()
@property def colour(self) -> tuple: return self._colour
[docs] @colour.setter def colour(self, value: tuple) -> None: if value != self._colour: self._colour = value if self._text: self._render()
def _render(self) -> None: """Rasterize text from MSDF atlas into RGBA pixels, upload to GPU.""" from .gpu.descriptors import write_texture_descriptor from .gpu.memory import upload_image_data atlas = self._tr.get_atlas( self._font or find_font_path(), font_size=self._size, ) pixels = rasterize_text( self._text, atlas, self._size, self._width, self._height, self._colour, ) device = self._ctx.device # Upload as GPU texture image, mem = upload_image_data( device, self._ctx.physical_device, self._ctx.graphics_queue, self._ctx.command_pool, pixels, self._width, self._height, ) # Create image view view_ci = vk.VkImageViewCreateInfo( image=image, viewType=vk.VK_IMAGE_VIEW_TYPE_2D, format=vk.VK_FORMAT_R8G8B8A8_UNORM, subresourceRange=vk.VkImageSubresourceRange( aspectMask=vk.VK_IMAGE_ASPECT_COLOR_BIT, baseMipLevel=0, levelCount=1, baseArrayLayer=0, layerCount=1, ), ) new_view = vk.vkCreateImageView(device, view_ci, None) # Clean up previous upload: wait for GPU since old view may be in-flight if self._image_view: vk.vkDeviceWaitIdle(device) vk.vkDestroyImageView(device, self._image_view, None) if self._image: vk.vkDestroyImage(device, self._image, None) if self._image_mem: vk.vkFreeMemory(device, self._image_mem, None) self._image = image self._image_mem = mem self._image_view = new_view # Register or re-register in bindless array if self.texture_index < 0: self.texture_index = self._register_texture(new_view) else: # Update existing descriptor slot write_texture_descriptor( device, self._texture_descriptor_set, self.texture_index, new_view, self._default_sampler, )
[docs] def create_text_texture( ctx: GPUContext, register_texture: Any, texture_descriptor_set: Any, default_sampler: Any, font: str | None = None, size: int = 32, width: int = 256, height: int = 64, ) -> TextTexture: """Create a TextTexture backed by GPU resources from the given context.""" return TextTexture( ctx, register_texture, texture_descriptor_set, default_sampler, font=font, size=size, width=width, height=height, )