"""Textured quad drawing for Draw2D.
Handles texture registration, textured quad emission, and 9-patch rendering.
"""
from __future__ import annotations
import math
[docs]
class Draw2DTextureMixin:
"""Mixin providing textured quad drawing for Draw2D."""
# Textured quads: list of (texture_id, verts, indices)
_textured_quads: list[tuple[int, list[tuple], list[int]]] = []
# CPU-side texture data for streaming: texture_id -> PNG bytes
_texture_data: dict[int, bytes] = {}
_next_cpu_texture_id: int = 1
[docs]
@classmethod
def register_texture(cls, png_data: bytes) -> int:
"""Register a CPU-side texture for streaming. Returns a texture ID.
Use this in CPU-only mode (web editor) where there's no GPU TextureManager.
The returned ID can be passed to ``draw_texture()`` / ``draw_texture_region()``.
"""
tex_id = cls._next_cpu_texture_id
cls._next_cpu_texture_id += 1
cls._texture_data[tex_id] = png_data
return tex_id
[docs]
@classmethod
def register_texture_with_id(cls, texture_id: int, png_data: bytes) -> None:
"""Register PNG data for a known texture ID (for streaming sync)."""
cls._texture_data[texture_id] = png_data
[docs]
@classmethod
def draw_texture(
cls,
texture_id: int,
x: float,
y: float,
w: float,
h: float,
colour: tuple[float, ...] | None = None,
rotation: float = 0.0,
):
"""Draw a textured quad at (x, y) with size (w, h).
Args:
texture_id: Bindless texture index from TextureManager.
x, y: Top-left position in screen pixels.
w, h: Width and height in screen pixels.
colour: Optional RGBA tint (0.0-1.0 floats). Default white.
rotation: Rotation in radians around the quad center.
"""
cls.draw_texture_region(texture_id, x, y, w, h, 0.0, 0.0, 1.0, 1.0, colour, rotation)
[docs]
@classmethod
def draw_texture_region(
cls,
texture_id: int,
x: float,
y: float,
w: float,
h: float,
u0: float = 0.0,
v0: float = 0.0,
u1: float = 1.0,
v1: float = 1.0,
colour: tuple[float, ...] | None = None,
rotation: float = 0.0,
):
"""Draw a sub-rectangle of a texture as a quad.
Args:
texture_id: Bindless texture index.
x, y: Top-left position in screen pixels.
w, h: Width and height in screen pixels.
u0, v0, u1, v1: UV coordinates for the source region (0-1).
colour: Optional RGBA tint (0.0-1.0 floats). Default white.
rotation: Rotation in radians around the quad center.
"""
c = colour if colour else (1.0, 1.0, 1.0, 1.0)
if len(c) == 3:
c = (*c, 1.0)
# Corner positions
if abs(rotation) < 1e-6:
x0, y0 = x, y
x1, y1 = x + w, y
x2, y2 = x + w, y + h
x3, y3 = x, y + h
else:
cx, cy = x + w * 0.5, y + h * 0.5
cos_r, sin_r = math.cos(rotation), math.sin(rotation)
hw, hh = w * 0.5, h * 0.5
corners = [(-hw, -hh), (hw, -hh), (hw, hh), (-hw, hh)]
rotated = [(cx + px * cos_r - py * sin_r, cy + px * sin_r + py * cos_r) for px, py in corners]
x0, y0 = rotated[0]
x1, y1 = rotated[1]
x2, y2 = rotated[2]
x3, y3 = rotated[3]
if cls._has_xf:
x0, y0 = cls._xf_pt(x0, y0)
x1, y1 = cls._xf_pt(x1, y1)
x2, y2 = cls._xf_pt(x2, y2)
x3, y3 = cls._xf_pt(x3, y3)
verts = [
(x0, y0, u0, v0, *c),
(x1, y1, u1, v0, *c),
(x2, y2, u1, v1, *c),
(x3, y3, u0, v1, *c),
]
indices = [0, 1, 2, 0, 2, 3]
cls._textured_quads.append((texture_id, verts, indices))
[docs]
@classmethod
def draw_nine_patch(
cls,
texture_id: int,
x: float,
y: float,
w: float,
h: float,
tex_w: float,
tex_h: float,
margin_left: float = 0.0,
margin_right: float = 0.0,
margin_top: float = 0.0,
margin_bottom: float = 0.0,
draw_centre: bool = True,
colour: tuple[float, ...] | None = None,
):
"""Draw a 9-slice textured rectangle.
Divides the source texture into 9 regions using the margin values.
Corners remain fixed-size, edges stretch in one direction, and the
centre fills the remaining space. This is the standard 9-patch / 9-slice
technique for scalable UI panels, buttons, and speech bubbles.
Args:
texture_id: Bindless texture index from TextureManager.
x, y: Top-left position in screen pixels.
w, h: Display width and height in screen pixels.
tex_w, tex_h: Source texture dimensions in pixels.
margin_left, margin_right: Left/right border widths (pixels in source texture).
margin_top, margin_bottom: Top/bottom border heights (pixels in source texture).
draw_centre: Whether to draw the centre region (default True).
colour: Optional RGBA tint (0.0-1.0 floats). Default white.
"""
if tex_w <= 0 or tex_h <= 0:
return
# Clamp margins to texture and display bounds
ml = min(margin_left, tex_w, w)
mr = min(margin_right, tex_w - ml, max(0, w - ml))
mt = min(margin_top, tex_h, h)
mb = min(margin_bottom, tex_h - mt, max(0, h - mt))
# Source (texture-space) and destination (screen-space) column/row boundaries
sx = [0.0, ml, tex_w - mr, tex_w]
sy = [0.0, mt, tex_h - mb, tex_h]
dx = [x, x + ml, x + w - mr, x + w]
dy = [y, y + mt, y + h - mb, y + h]
for row in range(3):
for col in range(3):
if row == 1 and col == 1 and not draw_centre:
continue
sw = sx[col + 1] - sx[col]
sh = sy[row + 1] - sy[row]
dw = dx[col + 1] - dx[col]
dh = dy[row + 1] - dy[row]
if sw <= 0 or sh <= 0 or dw <= 0 or dh <= 0:
continue
cls.draw_texture_region(
texture_id,
dx[col],
dy[row],
dw,
dh,
sx[col] / tex_w,
sy[row] / tex_h,
sx[col + 1] / tex_w,
sy[row + 1] / tex_h,
colour=colour,
)