e2D 2.0.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- e2D/__init__.py +461 -0
- e2D/commons.py +56 -0
- e2D/cvectors.c +27800 -0
- e2D/cvectors.cp313-win_amd64.pyd +0 -0
- e2D/cvectors.pxd +56 -0
- e2D/cvectors.pyx +561 -0
- e2D/devices.py +74 -0
- e2D/plots.py +584 -0
- e2D/shaders/curve_fragment.glsl +6 -0
- e2D/shaders/curve_vertex.glsl +16 -0
- e2D/shaders/line_instanced_vertex.glsl +37 -0
- e2D/shaders/plot_grid_fragment.glsl +48 -0
- e2D/shaders/plot_grid_vertex.glsl +7 -0
- e2D/shaders/segment_fragment.glsl +6 -0
- e2D/shaders/segment_vertex.glsl +9 -0
- e2D/shaders/stream_fragment.glsl +11 -0
- e2D/shaders/stream_shift_compute.glsl +16 -0
- e2D/shaders/stream_vertex.glsl +27 -0
- e2D/shapes.py +1081 -0
- e2D/text_renderer.py +491 -0
- e2D/vectors.py +247 -0
- e2d-2.0.0.dist-info/METADATA +260 -0
- e2d-2.0.0.dist-info/RECORD +26 -0
- e2d-2.0.0.dist-info/WHEEL +5 -0
- e2d-2.0.0.dist-info/licenses/LICENSE +21 -0
- e2d-2.0.0.dist-info/top_level.txt +1 -0
e2D/text_renderer.py
ADDED
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from PIL import Image, ImageFont
|
|
4
|
+
from attr import dataclass
|
|
5
|
+
import numpy as np
|
|
6
|
+
import moderngl
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class TextStyle:
|
|
10
|
+
font: str = "arial.ttf"
|
|
11
|
+
font_size: int = 32
|
|
12
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)
|
|
13
|
+
bg_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.9)
|
|
14
|
+
bg_margin: float | tuple[float, float, float, float] = 15.0
|
|
15
|
+
bg_border_radius: float | tuple[float, float, float, float] = 15.0
|
|
16
|
+
|
|
17
|
+
class Pivots(Enum):
|
|
18
|
+
TOP_LEFT = 0
|
|
19
|
+
TOP_MIDDLE = 1
|
|
20
|
+
TOP_RIGHT = 2
|
|
21
|
+
LEFT = 3
|
|
22
|
+
CENTER = 4
|
|
23
|
+
RIGHT = 5
|
|
24
|
+
BOTTOM_LEFT = 6
|
|
25
|
+
BOTTOM_MIDDLE = 7
|
|
26
|
+
BOTTOM_RIGHT = 8
|
|
27
|
+
|
|
28
|
+
DEFAULT_TEXT_STYLE = TextStyle()
|
|
29
|
+
|
|
30
|
+
class TextLabel:
|
|
31
|
+
def __init__(self, ctx: moderngl.Context, prog: moderngl.Program, texture: moderngl.Texture, vertices: list,
|
|
32
|
+
bg_prog: Optional[moderngl.Program] = None, bg_vertices: Optional[list] = None) -> None:
|
|
33
|
+
"""
|
|
34
|
+
A pre-rendered text label for efficient drawing.
|
|
35
|
+
To generate select a option below:
|
|
36
|
+
- use TextRenderer.create_label()
|
|
37
|
+
- rootEnv.print(..., save_cache = True) will return a TextLabel.
|
|
38
|
+
"""
|
|
39
|
+
self.ctx = ctx
|
|
40
|
+
self.prog = prog
|
|
41
|
+
self.texture = texture
|
|
42
|
+
self.vertices = vertices
|
|
43
|
+
self.vbo = self.ctx.buffer(np.array(vertices, dtype='f4').tobytes())
|
|
44
|
+
self.vao = self.ctx.vertex_array(self.prog, [
|
|
45
|
+
(self.vbo, '2f 2f 4f', 'in_pos', 'in_uv', 'in_color')
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
# Background rendering
|
|
49
|
+
self.bg_prog = bg_prog
|
|
50
|
+
self.bg_vertices = bg_vertices
|
|
51
|
+
if bg_prog and bg_vertices:
|
|
52
|
+
self.bg_vbo = self.ctx.buffer(np.array(bg_vertices, dtype='f4').tobytes())
|
|
53
|
+
self.bg_vao = self.ctx.vertex_array(self.bg_prog, [
|
|
54
|
+
(self.bg_vbo, '2f 4f 4f 4f', 'in_pos', 'in_color', 'in_rect', 'in_radius')
|
|
55
|
+
])
|
|
56
|
+
else:
|
|
57
|
+
self.bg_vbo = None
|
|
58
|
+
self.bg_vao = None
|
|
59
|
+
|
|
60
|
+
def draw(self) -> None:
|
|
61
|
+
self.ctx.enable(moderngl.BLEND)
|
|
62
|
+
|
|
63
|
+
# Draw background first if exists
|
|
64
|
+
if self.bg_vao and self.bg_prog:
|
|
65
|
+
self.bg_prog['resolution'] = self.ctx.viewport[2:]
|
|
66
|
+
self.bg_vao.render(moderngl.TRIANGLES)
|
|
67
|
+
|
|
68
|
+
# Draw text
|
|
69
|
+
self.prog['resolution'] = self.ctx.viewport[2:]
|
|
70
|
+
self.texture.use(0)
|
|
71
|
+
self.vao.render(moderngl.TRIANGLES)
|
|
72
|
+
|
|
73
|
+
class TextRenderer:
|
|
74
|
+
"""
|
|
75
|
+
Renders text using a texture atlas generated from a TTF font via Pillow.
|
|
76
|
+
Supports multiple fonts and sizes with caching for optimization.
|
|
77
|
+
"""
|
|
78
|
+
def __init__(self, ctx: moderngl.Context) -> None:
|
|
79
|
+
self.ctx = ctx
|
|
80
|
+
|
|
81
|
+
# Cache for font atlases: (font_path, font_size) -> {font, char_data, texture}
|
|
82
|
+
self.font_cache = {}
|
|
83
|
+
|
|
84
|
+
# Character set to render
|
|
85
|
+
self.chars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
|
|
86
|
+
|
|
87
|
+
# Background shader for rounded rectangles
|
|
88
|
+
self.bg_prog = self.ctx.program(
|
|
89
|
+
vertex_shader="""
|
|
90
|
+
#version 430
|
|
91
|
+
uniform vec2 resolution;
|
|
92
|
+
|
|
93
|
+
in vec2 in_pos;
|
|
94
|
+
in vec4 in_color;
|
|
95
|
+
in vec4 in_rect; // x, y, width, height
|
|
96
|
+
in vec4 in_radius; // top-left, top-right, bottom-right, bottom-left
|
|
97
|
+
|
|
98
|
+
out vec4 v_color;
|
|
99
|
+
out vec2 v_pos;
|
|
100
|
+
out vec4 v_rect;
|
|
101
|
+
out vec4 v_radius;
|
|
102
|
+
|
|
103
|
+
void main() {
|
|
104
|
+
vec2 ndc = (in_pos / resolution) * 2.0 - 1.0;
|
|
105
|
+
ndc.y = -ndc.y;
|
|
106
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
107
|
+
v_color = in_color;
|
|
108
|
+
v_pos = in_pos;
|
|
109
|
+
v_rect = in_rect;
|
|
110
|
+
v_radius = in_radius;
|
|
111
|
+
}
|
|
112
|
+
""",
|
|
113
|
+
fragment_shader="""
|
|
114
|
+
#version 430
|
|
115
|
+
|
|
116
|
+
in vec4 v_color;
|
|
117
|
+
in vec2 v_pos;
|
|
118
|
+
in vec4 v_rect;
|
|
119
|
+
in vec4 v_radius;
|
|
120
|
+
out vec4 f_color;
|
|
121
|
+
|
|
122
|
+
float roundedBoxSDF(vec2 center, vec2 size, vec4 radius) {
|
|
123
|
+
vec2 q = abs(center) - size + vec2(radius.x);
|
|
124
|
+
|
|
125
|
+
// Select the appropriate corner radius
|
|
126
|
+
float r = radius.x;
|
|
127
|
+
if (center.x > 0.0 && center.y > 0.0) r = radius.z; // bottom-right
|
|
128
|
+
else if (center.x > 0.0 && center.y < 0.0) r = radius.y; // top-right
|
|
129
|
+
else if (center.x < 0.0 && center.y > 0.0) r = radius.w; // bottom-left
|
|
130
|
+
else r = radius.x; // top-left
|
|
131
|
+
|
|
132
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void main() {
|
|
136
|
+
vec2 rect_center = v_rect.xy + v_rect.zw * 0.5;
|
|
137
|
+
vec2 rect_size = v_rect.zw * 0.5;
|
|
138
|
+
vec2 pos_from_center = v_pos - rect_center;
|
|
139
|
+
|
|
140
|
+
float dist = roundedBoxSDF(pos_from_center, rect_size, v_radius);
|
|
141
|
+
float alpha = 1.0 - smoothstep(-1.0, 1.0, dist);
|
|
142
|
+
|
|
143
|
+
f_color = vec4(v_color.rgb, v_color.a * alpha);
|
|
144
|
+
}
|
|
145
|
+
"""
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Background VBO
|
|
149
|
+
self.bg_vbo = self.ctx.buffer(reserve=4096)
|
|
150
|
+
self.bg_vao = self.ctx.vertex_array(self.bg_prog, [
|
|
151
|
+
(self.bg_vbo, '2f 4f 4f 4f', 'in_pos', 'in_color', 'in_rect', 'in_radius')
|
|
152
|
+
])
|
|
153
|
+
|
|
154
|
+
# Shader
|
|
155
|
+
self.prog = self.ctx.program(
|
|
156
|
+
vertex_shader="""
|
|
157
|
+
#version 430
|
|
158
|
+
uniform vec2 resolution;
|
|
159
|
+
|
|
160
|
+
in vec2 in_pos;
|
|
161
|
+
in vec2 in_uv;
|
|
162
|
+
in vec4 in_color;
|
|
163
|
+
|
|
164
|
+
out vec2 v_uv;
|
|
165
|
+
out vec4 v_color;
|
|
166
|
+
|
|
167
|
+
void main() {
|
|
168
|
+
// Pixel to NDC
|
|
169
|
+
vec2 ndc = (in_pos / resolution) * 2.0 - 1.0;
|
|
170
|
+
ndc.y = -ndc.y; // Flip Y
|
|
171
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
172
|
+
v_uv = in_uv;
|
|
173
|
+
v_color = in_color;
|
|
174
|
+
}
|
|
175
|
+
""",
|
|
176
|
+
fragment_shader="""
|
|
177
|
+
#version 430
|
|
178
|
+
uniform sampler2D tex;
|
|
179
|
+
|
|
180
|
+
in vec2 v_uv;
|
|
181
|
+
in vec4 v_color;
|
|
182
|
+
out vec4 f_color;
|
|
183
|
+
|
|
184
|
+
void main() {
|
|
185
|
+
float alpha = texture(tex, v_uv).a;
|
|
186
|
+
f_color = vec4(v_color.rgb, v_color.a * alpha);
|
|
187
|
+
}
|
|
188
|
+
"""
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Dynamic VBO for immediate mode
|
|
192
|
+
self.vbo = self.ctx.buffer(reserve=65536) # 64KB
|
|
193
|
+
self.vao = self.ctx.vertex_array(self.prog, [
|
|
194
|
+
(self.vbo, '2f 2f 4f', 'in_pos', 'in_uv', 'in_color')
|
|
195
|
+
])
|
|
196
|
+
|
|
197
|
+
def _get_or_create_font_atlas(self, font_path: str, font_size: int) -> dict:
|
|
198
|
+
"""Get or create a cached font atlas for the given font and size."""
|
|
199
|
+
cache_key = (font_path, font_size)
|
|
200
|
+
|
|
201
|
+
if cache_key in self.font_cache:
|
|
202
|
+
return self.font_cache[cache_key]
|
|
203
|
+
|
|
204
|
+
# Load font
|
|
205
|
+
try:
|
|
206
|
+
font = ImageFont.truetype(font_path, font_size)
|
|
207
|
+
except IOError:
|
|
208
|
+
print(f"Warning: Could not load font '{font_path}'. Using default.")
|
|
209
|
+
font = ImageFont.load_default()
|
|
210
|
+
|
|
211
|
+
# Generate Atlas
|
|
212
|
+
char_data = {}
|
|
213
|
+
atlas_w, atlas_h = 1024, 1024
|
|
214
|
+
atlas_img = Image.new('RGBA', (atlas_w, atlas_h), (0, 0, 0, 0))
|
|
215
|
+
|
|
216
|
+
x, y = 0, 0
|
|
217
|
+
max_h = 0
|
|
218
|
+
|
|
219
|
+
for char in self.chars:
|
|
220
|
+
mask = font.getmask(char)
|
|
221
|
+
w, h = mask.size
|
|
222
|
+
|
|
223
|
+
if x + w >= atlas_w:
|
|
224
|
+
x = 0
|
|
225
|
+
y += max_h + 2
|
|
226
|
+
max_h = 0
|
|
227
|
+
|
|
228
|
+
# Create char image
|
|
229
|
+
char_img = Image.new('RGBA', (w, h), (255, 255, 255, 255))
|
|
230
|
+
mask_img = Image.new('L', (w, h))
|
|
231
|
+
mask_img.im.paste(mask, (0, 0, w, h))
|
|
232
|
+
atlas_img.paste(char_img, (x, y), mask_img)
|
|
233
|
+
|
|
234
|
+
char_data[char] = {
|
|
235
|
+
'x': x, 'y': y, 'w': w, 'h': h,
|
|
236
|
+
'uv': (x/atlas_w, y/atlas_h, w/atlas_w, h/atlas_h)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
x += w + 2
|
|
240
|
+
max_h = max(max_h, h)
|
|
241
|
+
|
|
242
|
+
# Create texture
|
|
243
|
+
texture = self.ctx.texture(atlas_img.size, 4, atlas_img.tobytes())
|
|
244
|
+
texture.filter = (moderngl.LINEAR, moderngl.LINEAR)
|
|
245
|
+
|
|
246
|
+
# Cache it
|
|
247
|
+
font_atlas = {
|
|
248
|
+
'font': font,
|
|
249
|
+
'char_data': char_data,
|
|
250
|
+
'texture': texture
|
|
251
|
+
}
|
|
252
|
+
self.font_cache[cache_key] = font_atlas
|
|
253
|
+
|
|
254
|
+
return font_atlas
|
|
255
|
+
|
|
256
|
+
def get_text_width(self, text: str, scale: float = 1.0, style: TextStyle = DEFAULT_TEXT_STYLE) -> float:
|
|
257
|
+
"""Calculate the width of the text."""
|
|
258
|
+
font_atlas = self._get_or_create_font_atlas(style.font, style.font_size)
|
|
259
|
+
char_data = font_atlas['char_data']
|
|
260
|
+
|
|
261
|
+
total_w = 0
|
|
262
|
+
for char in text:
|
|
263
|
+
if char in char_data:
|
|
264
|
+
data = char_data[char]
|
|
265
|
+
total_w += (data['w'] * scale) + (2 * scale)
|
|
266
|
+
return total_w
|
|
267
|
+
|
|
268
|
+
def _get_text_bounds(self, text: str, scale: float = 1.0, style: TextStyle = DEFAULT_TEXT_STYLE) -> tuple[float, float]:
|
|
269
|
+
"""Calculate the bounding box dimensions (width, height) of the text."""
|
|
270
|
+
font_atlas = self._get_or_create_font_atlas(style.font, style.font_size)
|
|
271
|
+
char_data = font_atlas['char_data']
|
|
272
|
+
|
|
273
|
+
total_w = 0
|
|
274
|
+
ref_char = 'M' if 'M' in char_data else self.chars[0]
|
|
275
|
+
line_height = char_data[ref_char]['h'] * scale
|
|
276
|
+
|
|
277
|
+
for char in text:
|
|
278
|
+
if char in char_data:
|
|
279
|
+
data = char_data[char]
|
|
280
|
+
total_w += (data['w'] * scale) + (2 * scale)
|
|
281
|
+
|
|
282
|
+
return total_w, line_height
|
|
283
|
+
|
|
284
|
+
def _normalize_margin(self, margin: float | tuple[float, float, float, float]) -> tuple[float, float, float, float]:
|
|
285
|
+
"""Normalize margin to (top, right, bottom, left)."""
|
|
286
|
+
if isinstance(margin, (int, float)):
|
|
287
|
+
return (margin, margin, margin, margin)
|
|
288
|
+
return margin
|
|
289
|
+
|
|
290
|
+
def _normalize_radius(self, radius: float | tuple[float, float, float, float]) -> tuple[float, float, float, float]:
|
|
291
|
+
"""Normalize border radius to (top-left, top-right, bottom-right, bottom-left)."""
|
|
292
|
+
if isinstance(radius, (int, float)):
|
|
293
|
+
return (radius, radius, radius, radius)
|
|
294
|
+
return radius
|
|
295
|
+
|
|
296
|
+
def _generate_background_vertices(self, x: float, y: float, width: float, height: float,
|
|
297
|
+
bg_color: tuple, margin: tuple[float, float, float, float],
|
|
298
|
+
radius: tuple[float, float, float, float]) -> list[float]:
|
|
299
|
+
"""Generate vertices for a rounded rectangle background."""
|
|
300
|
+
# Apply margin
|
|
301
|
+
bg_x = x - margin[3] # left
|
|
302
|
+
bg_y = y - margin[0] # top
|
|
303
|
+
bg_w = width + margin[1] + margin[3] # right + left
|
|
304
|
+
bg_h = height + margin[0] + margin[2] # top + bottom
|
|
305
|
+
|
|
306
|
+
vertices = []
|
|
307
|
+
rect_data = [bg_x, bg_y, bg_w, bg_h]
|
|
308
|
+
|
|
309
|
+
# Two triangles forming a quad
|
|
310
|
+
# Each vertex: (x, y, r, g, b, a, rect_x, rect_y, rect_w, rect_h, radius_tl, radius_tr, radius_br, radius_bl)
|
|
311
|
+
|
|
312
|
+
# Triangle 1
|
|
313
|
+
vertices.extend([bg_x, bg_y, *bg_color, *rect_data, *radius]) # top-left
|
|
314
|
+
vertices.extend([bg_x + bg_w, bg_y, *bg_color, *rect_data, *radius]) # top-right
|
|
315
|
+
vertices.extend([bg_x, bg_y + bg_h, *bg_color, *rect_data, *radius]) # bottom-left
|
|
316
|
+
|
|
317
|
+
# Triangle 2
|
|
318
|
+
vertices.extend([bg_x + bg_w, bg_y, *bg_color, *rect_data, *radius]) # top-right
|
|
319
|
+
vertices.extend([bg_x, bg_y + bg_h, *bg_color, *rect_data, *radius]) # bottom-left
|
|
320
|
+
vertices.extend([bg_x + bg_w, bg_y + bg_h, *bg_color, *rect_data, *radius]) # bottom-right
|
|
321
|
+
|
|
322
|
+
return vertices
|
|
323
|
+
|
|
324
|
+
def _generate_vertices(self, text: str, pos: tuple[float, float], scale: float = 1.0,
|
|
325
|
+
color: tuple = (1.0, 1.0, 1.0, 1.0), pivot: Pivots = Pivots.TOP_LEFT, char_data: dict = {} ) -> list[float]:
|
|
326
|
+
|
|
327
|
+
if not char_data:
|
|
328
|
+
raise ValueError("char_data is required for _generate_vertices")
|
|
329
|
+
# Calculate text size first for pivoting
|
|
330
|
+
total_w = 0
|
|
331
|
+
max_h = 0
|
|
332
|
+
|
|
333
|
+
# Determine max height for proper vertical alignment instead of per-glyph height
|
|
334
|
+
# This fixes the "jittery" baseline issue by ensuring all chars use the same vertical metric
|
|
335
|
+
# Use 'M' or 'H' or '|' as a reference for height if available, otherwise use max found
|
|
336
|
+
ref_char = 'M' if 'M' in char_data else self.chars[0]
|
|
337
|
+
line_height = char_data[ref_char]['h'] * scale
|
|
338
|
+
|
|
339
|
+
for char in text:
|
|
340
|
+
if char in char_data:
|
|
341
|
+
data = char_data[char]
|
|
342
|
+
total_w += (data['w'] * scale) + (2 * scale)
|
|
343
|
+
max_h = max(max_h, data['h'] * scale)
|
|
344
|
+
|
|
345
|
+
# Use consistent line height for vertical alignment
|
|
346
|
+
max_h = line_height if line_height > 0 else max_h
|
|
347
|
+
|
|
348
|
+
# Adjust start position based on pivot
|
|
349
|
+
start_x, start_y = pos
|
|
350
|
+
if pivot == Pivots.TOP_RIGHT:
|
|
351
|
+
start_x -= total_w
|
|
352
|
+
elif pivot == Pivots.BOTTOM_LEFT:
|
|
353
|
+
start_y -= max_h
|
|
354
|
+
elif pivot == Pivots.BOTTOM_RIGHT:
|
|
355
|
+
start_x -= total_w
|
|
356
|
+
start_y -= max_h
|
|
357
|
+
elif pivot == Pivots.CENTER:
|
|
358
|
+
start_x -= total_w / 2
|
|
359
|
+
start_y -= max_h / 2
|
|
360
|
+
|
|
361
|
+
vertices = []
|
|
362
|
+
cursor_x = start_x
|
|
363
|
+
cursor_y = start_y
|
|
364
|
+
|
|
365
|
+
for char in text:
|
|
366
|
+
if char not in char_data:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
data = char_data[char]
|
|
370
|
+
w = data['w'] * scale
|
|
371
|
+
h = data['h'] * scale
|
|
372
|
+
|
|
373
|
+
# Align characters to bottom of line (baseline alignment)
|
|
374
|
+
# Offset smaller characters down so they sit on the same baseline as taller ones
|
|
375
|
+
y_offset = max_h - h
|
|
376
|
+
|
|
377
|
+
# Quad vertices (x, y, u, v, r, g, b, a)
|
|
378
|
+
# TL
|
|
379
|
+
vertices.extend([cursor_x, cursor_y + y_offset, data['uv'][0], data['uv'][1], *color])
|
|
380
|
+
|
|
381
|
+
# TR
|
|
382
|
+
vertices.extend([cursor_x + w, cursor_y + y_offset, data['uv'][0] + data['uv'][2], data['uv'][1], *color])
|
|
383
|
+
|
|
384
|
+
# BL
|
|
385
|
+
vertices.extend([cursor_x, cursor_y + y_offset + h, data['uv'][0], data['uv'][1] + data['uv'][3], *color])
|
|
386
|
+
|
|
387
|
+
# Triangle 2
|
|
388
|
+
# TR
|
|
389
|
+
vertices.extend([cursor_x + w, cursor_y + y_offset, data['uv'][0] + data['uv'][2], data['uv'][1], *color])
|
|
390
|
+
# BL
|
|
391
|
+
vertices.extend([cursor_x, cursor_y + y_offset + h, data['uv'][0], data['uv'][1] + data['uv'][3], *color])
|
|
392
|
+
# BR
|
|
393
|
+
vertices.extend([cursor_x + w, cursor_y + y_offset + h, data['uv'][0] + data['uv'][2], data['uv'][1] + data['uv'][3], *color])
|
|
394
|
+
|
|
395
|
+
cursor_x += w + (2 * scale) # Spacing
|
|
396
|
+
|
|
397
|
+
return vertices
|
|
398
|
+
|
|
399
|
+
def draw_text(self, text: str, pos: tuple[float, float], scale: float = 1.0, style: TextStyle = DEFAULT_TEXT_STYLE, pivot: Pivots = Pivots.TOP_LEFT) -> None:
|
|
400
|
+
if not text:
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
# Get font atlas for this style
|
|
404
|
+
font_atlas = self._get_or_create_font_atlas(style.font, style.font_size)
|
|
405
|
+
char_data = font_atlas['char_data']
|
|
406
|
+
texture = font_atlas['texture']
|
|
407
|
+
|
|
408
|
+
# Get text dimensions for background
|
|
409
|
+
text_width, text_height = self._get_text_bounds(text, scale, style)
|
|
410
|
+
|
|
411
|
+
# Adjust position based on pivot for background calculation
|
|
412
|
+
bg_x, bg_y = pos
|
|
413
|
+
if pivot == Pivots.TOP_RIGHT:
|
|
414
|
+
bg_x -= text_width
|
|
415
|
+
elif pivot == Pivots.BOTTOM_LEFT:
|
|
416
|
+
bg_y -= text_height
|
|
417
|
+
elif pivot == Pivots.BOTTOM_RIGHT:
|
|
418
|
+
bg_x -= text_width
|
|
419
|
+
bg_y -= text_height
|
|
420
|
+
elif pivot == Pivots.CENTER:
|
|
421
|
+
bg_x -= text_width / 2
|
|
422
|
+
bg_y -= text_height / 2
|
|
423
|
+
|
|
424
|
+
# Draw background if specified
|
|
425
|
+
if style.bg_color[3] > 0: # Only draw if alpha > 0
|
|
426
|
+
margin = self._normalize_margin(style.bg_margin)
|
|
427
|
+
radius = self._normalize_radius(style.bg_border_radius)
|
|
428
|
+
bg_vertices = self._generate_background_vertices(bg_x, bg_y, text_width, text_height,
|
|
429
|
+
style.bg_color, margin, radius)
|
|
430
|
+
|
|
431
|
+
bg_data = np.array(bg_vertices, dtype='f4').tobytes()
|
|
432
|
+
self.bg_vbo.write(bg_data)
|
|
433
|
+
self.bg_prog['resolution'] = self.ctx.viewport[2:]
|
|
434
|
+
self.ctx.enable(moderngl.BLEND)
|
|
435
|
+
self.bg_vao.render(moderngl.TRIANGLES, vertices=len(bg_vertices)//14)
|
|
436
|
+
|
|
437
|
+
# Draw text
|
|
438
|
+
vertices = self._generate_vertices(text, pos, scale, style.color, pivot, char_data)
|
|
439
|
+
if not vertices:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
# Update VBO
|
|
443
|
+
data_bytes = np.array(vertices, dtype='f4').tobytes()
|
|
444
|
+
self.vbo.write(data_bytes)
|
|
445
|
+
|
|
446
|
+
# Update Uniforms
|
|
447
|
+
self.prog['resolution'] = self.ctx.viewport[2:]
|
|
448
|
+
texture.use(0)
|
|
449
|
+
|
|
450
|
+
# Draw
|
|
451
|
+
self.ctx.enable(moderngl.BLEND)
|
|
452
|
+
self.vao.render(moderngl.TRIANGLES, vertices=len(vertices)//8)
|
|
453
|
+
|
|
454
|
+
def create_label(self, text: str, x: float, y: float, scale: float = 1.0, style: TextStyle = DEFAULT_TEXT_STYLE, pivot: Pivots = Pivots.TOP_LEFT) -> TextLabel:
|
|
455
|
+
if not text:
|
|
456
|
+
# Return empty label with default texture
|
|
457
|
+
font_atlas = self._get_or_create_font_atlas(style.font, style.font_size)
|
|
458
|
+
return TextLabel(self.ctx, self.prog, font_atlas['texture'], [])
|
|
459
|
+
|
|
460
|
+
# Get font atlas for this style
|
|
461
|
+
font_atlas = self._get_or_create_font_atlas(style.font, style.font_size)
|
|
462
|
+
char_data = font_atlas['char_data']
|
|
463
|
+
texture = font_atlas['texture']
|
|
464
|
+
|
|
465
|
+
# Generate text vertices
|
|
466
|
+
vertices = self._generate_vertices(text, (x, y), scale, style.color, pivot, char_data)
|
|
467
|
+
|
|
468
|
+
# Generate background vertices if needed
|
|
469
|
+
bg_vertices = None
|
|
470
|
+
if style.bg_color[3] > 0:
|
|
471
|
+
text_width, text_height = self._get_text_bounds(text, scale, style)
|
|
472
|
+
|
|
473
|
+
# Adjust position based on pivot
|
|
474
|
+
bg_x, bg_y = x, y
|
|
475
|
+
if pivot == Pivots.TOP_RIGHT:
|
|
476
|
+
bg_x -= text_width
|
|
477
|
+
elif pivot == Pivots.BOTTOM_LEFT:
|
|
478
|
+
bg_y -= text_height
|
|
479
|
+
elif pivot == Pivots.BOTTOM_RIGHT:
|
|
480
|
+
bg_x -= text_width
|
|
481
|
+
bg_y -= text_height
|
|
482
|
+
elif pivot == Pivots.CENTER:
|
|
483
|
+
bg_x -= text_width / 2
|
|
484
|
+
bg_y -= text_height / 2
|
|
485
|
+
|
|
486
|
+
margin = self._normalize_margin(style.bg_margin)
|
|
487
|
+
radius = self._normalize_radius(style.bg_border_radius)
|
|
488
|
+
bg_vertices = self._generate_background_vertices(bg_x, bg_y, text_width, text_height,
|
|
489
|
+
style.bg_color, margin, radius)
|
|
490
|
+
|
|
491
|
+
return TextLabel(self.ctx, self.prog, texture, vertices, self.bg_prog, bg_vertices)
|