"""Scissor clipping and draw batch management for Draw2D.
Groups per-frame geometry into batches separated by clip-rect changes,
then converts raw tuple lists to structured numpy arrays for GPU upload.
"""
from __future__ import annotations
import numpy as np
from .draw2d_vertex import UI_VERTEX_DTYPE
[docs]
class Draw2DBatchMixin:
"""Mixin providing scissor clipping and batch management for Draw2D."""
# Scissor clipping
_clip_stack: list[tuple[int, int, int, int]] = []
# Batches: list of (clip, fill_verts, fill_indices, line_verts, text_verts, text_indices, tex_quads)
_batches: list[tuple] = []
_current_clip: tuple[int, int, int, int] | None = None
[docs]
@classmethod
def push_clip(cls, x: int, y: int, w: int, h: int):
"""Push a scissor clip rect. Content outside is not drawn.
Nested clips are intersected with the current clip.
"""
# Flush current geometry into a batch before changing clip
cls._flush_batch()
new_clip = (int(x), int(y), int(w), int(h))
if cls._current_clip:
# Intersect with current clip
cx, cy, cw, ch = cls._current_clip
nx = max(cx, new_clip[0])
ny = max(cy, new_clip[1])
nx2 = min(cx + cw, new_clip[0] + new_clip[2])
ny2 = min(cy + ch, new_clip[1] + new_clip[3])
new_clip = (nx, ny, max(0, nx2 - nx), max(0, ny2 - ny))
cls._clip_stack.append(cls._current_clip)
cls._current_clip = new_clip
[docs]
@classmethod
def pop_clip(cls):
"""Pop the last clip rect, restoring the previous one."""
cls._flush_batch()
if cls._clip_stack:
cls._current_clip = cls._clip_stack.pop()
else:
cls._current_clip = None
[docs]
@classmethod
def new_layer(cls):
"""Force a batch break so subsequent draws render on top of all prior geometry."""
cls._flush_batch()
[docs]
@classmethod
def reset_clip(cls):
"""Clear the clip stack and current clip, restoring full-screen drawing."""
cls._flush_batch()
cls._clip_stack.clear()
cls._current_clip = None
@classmethod
def _flush_batch(cls):
"""Save current geometry as a batch with the current clip rect."""
if cls._fill_verts or cls._line_verts or cls._text_verts or cls._textured_quads:
cls._batches.append(
(
cls._current_clip,
list(cls._fill_verts),
list(cls._fill_indices),
list(cls._line_verts),
list(cls._text_verts),
list(cls._text_indices),
list(cls._textured_quads),
)
)
cls._fill_verts.clear()
cls._fill_indices.clear()
cls._line_verts.clear()
cls._text_verts.clear()
cls._text_indices.clear()
cls._textured_quads.clear()
@classmethod
def _verts_from_tuples(cls, vert_list):
"""Convert list of 8-float tuples to structured vertex array (vectorized)."""
arr = np.array(vert_list, dtype=np.float32) # (N, 8)
verts = np.zeros(len(vert_list), dtype=UI_VERTEX_DTYPE)
verts["position"] = arr[:, :2]
verts["uv"] = arr[:, 2:4]
verts["colour"] = arr[:, 4:8]
return verts
@classmethod
def _get_batches(cls):
"""Get all batches for rendering.
Each batch: (clip, fill_data, line_data, text_data, tex_draws)
where tex_draws is a list of (texture_id, verts_array, indices_array).
"""
cls._flush_batch()
result = []
for batch in cls._batches:
clip = batch[0]
fill_v, fill_i = batch[1], batch[2]
line_v = batch[3]
text_v, text_i = batch[4], batch[5]
tex_quads = batch[6] if len(batch) > 6 else []
fill_data = None
if fill_v:
verts = cls._verts_from_tuples(fill_v)
indices = np.array(fill_i, dtype=np.uint32)
fill_data = (verts, indices)
line_data = None
if line_v:
line_data = cls._verts_from_tuples(line_v)
text_data = None
if text_v:
verts = cls._verts_from_tuples(text_v)
indices = np.array(text_i, dtype=np.uint32)
text_data = (verts, indices)
# Convert textured quads: group by texture_id, build arrays
tex_draws = []
if tex_quads:
by_tex: dict[int, tuple[list[tuple], list[int]]] = {}
for tex_id, vlist, ilist in tex_quads:
if tex_id not in by_tex:
by_tex[tex_id] = ([], [])
base = len(by_tex[tex_id][0])
by_tex[tex_id][0].extend(vlist)
by_tex[tex_id][1].extend(i + base for i in ilist)
for tex_id, (vlist, ilist) in by_tex.items():
verts = cls._verts_from_tuples(vlist)
indices = np.array(ilist, dtype=np.uint32)
tex_draws.append((tex_id, verts, indices))
result.append((clip, fill_data, line_data, text_data, tex_draws))
return result