Source code for simvx.core.nodes_2d.ninepatch

"""PatchRect and NinePatchRect -- 9-slice scalable bordered texture."""

from __future__ import annotations

from typing import Any, NamedTuple

from ..descriptors import Property
from ..math.types import Vec2
from .node2d import Node2D


[docs] class PatchRect(NamedTuple): """A rectangle defined by position and size.""" x: float y: float w: float h: float
[docs] class NinePatchRect(Node2D): """9-slice scalable bordered texture for UI panels, speech bubbles, and HUD elements. Divides a source texture into 9 regions using margin values. Corners stay fixed-size, edges stretch along one axis, and the centre fills the remainder. """ texture = Property(None, hint="Source texture/image path") texture_size = Property(None, hint="Source texture dimensions (w, h)") size = Property(None, hint="Display size (Vec2). None = texture size") patch_margin_left = Property(0, range=(0, 1000), hint="Left border width") patch_margin_top = Property(0, range=(0, 1000), hint="Top border height") patch_margin_right = Property(0, range=(0, 1000), hint="Right border width") patch_margin_bottom = Property(0, range=(0, 1000), hint="Bottom border height") draw_center = Property(True, hint="Whether to draw the centre region") axis_stretch_horizontal = Property("stretch", enum=["stretch", "tile", "tile_fit"], hint="Horizontal stretch mode") axis_stretch_vertical = Property("stretch", enum=["stretch", "tile", "tile_fit"], hint="Vertical stretch mode") modulate = Property((1.0, 1.0, 1.0, 1.0), hint="Colour tint") def __init__(self, **kwargs: Any): self._texture_id: int = -1 # Set by renderer super().__init__(**kwargs)
[docs] def get_rect(self) -> PatchRect: """Bounding rectangle at global position with current display size.""" sz = self._display_size() p = self.world_position return PatchRect(p.x, p.y, sz[0], sz[1])
[docs] def get_nine_patch_rects(self) -> list[tuple[PatchRect, PatchRect]]: """Compute (source_uv_rect, dest_position_rect) pairs for the 9 patches. Returns up to 9 pairs (8 if draw_center is False). Patches with zero width or height are omitted. """ tex = self.texture_size if tex is None or (tex[0] <= 0 and tex[1] <= 0): return [] tw, th = float(tex[0]), float(tex[1]) dw, dh = self._display_size() ml = min(float(self.patch_margin_left), tw, dw) mr = min(float(self.patch_margin_right), tw - ml, max(0, dw - ml)) mt = min(float(self.patch_margin_top), th, dh) mb = min(float(self.patch_margin_bottom), th - mt, max(0, dh - mt)) sx = [0, ml, tw - mr, tw] sy = [0, mt, th - mb, th] dx = [0, ml, dw - mr, dw] dy = [0, mt, dh - mb, dh] ox, oy = float(self.world_position.x), float(self.world_position.y) patches: list[tuple[PatchRect, PatchRect]] = [] for row in range(3): for col in range(3): if row == 1 and col == 1 and not self.draw_center: continue sw = sx[col + 1] - sx[col] sh = sy[row + 1] - sy[row] ddw = dx[col + 1] - dx[col] ddh = dy[row + 1] - dy[row] if sw <= 0 or sh <= 0 or ddw <= 0 or ddh <= 0: continue src = PatchRect(sx[col], sy[row], sw, sh) dst = PatchRect(ox + dx[col], oy + dy[row], ddw, ddh) patches.append((src, dst)) return patches
[docs] def draw(self, renderer) -> None: """Draw the 9-patch texture via renderer.draw_texture_region().""" if self._texture_id < 0 or not self.visible: return tex = self.texture_size if tex is None: return tw, th = float(tex[0]), float(tex[1]) if tw <= 0 or th <= 0: return patches = self.get_nine_patch_rects() colour = self.modulate if self.modulate else None for src, dst in patches: renderer.draw_texture_region( self._texture_id, dst.x, dst.y, dst.w, dst.h, src.x / tw, src.y / th, (src.x + src.w) / tw, (src.y + src.h) / th, colour=colour, )
def _display_size(self) -> tuple[float, float]: if self.size is not None: return (float(self.size[0]), float(self.size[1])) if self.texture_size is not None: return (float(self.texture_size[0]), float(self.texture_size[1])) return (0.0, 0.0)