"""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)