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/shapes.py
ADDED
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
import moderngl
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .commons import get_pattr, get_pattr_value, set_pattr_value
|
|
4
|
+
from typing import Optional, Sequence
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FillMode(Enum):
|
|
9
|
+
FILL = 0
|
|
10
|
+
STROKE = 1
|
|
11
|
+
FILL_STROKE = 2
|
|
12
|
+
|
|
13
|
+
class ShapeLabel:
|
|
14
|
+
"""A pre-rendered shape for efficient repeated drawing."""
|
|
15
|
+
def __init__(self, ctx: moderngl.Context, prog: moderngl.Program,
|
|
16
|
+
vbo: moderngl.Buffer, vertex_count: int, shape_type: str = 'line') -> None:
|
|
17
|
+
self.ctx = ctx
|
|
18
|
+
self.prog = prog
|
|
19
|
+
self.vbo = vbo
|
|
20
|
+
self.vertex_count = vertex_count
|
|
21
|
+
self.shape_type = shape_type
|
|
22
|
+
|
|
23
|
+
# Create VAO based on shape type
|
|
24
|
+
if shape_type == 'circle':
|
|
25
|
+
self.vao = self.ctx.vertex_array(self.prog, [
|
|
26
|
+
(self.vbo, '2f 4f 1f 4f 1f 1f 2f', 'in_pos', 'in_color', 'in_radius',
|
|
27
|
+
'in_border_color', 'in_border_width', 'in_aa', 'in_center')
|
|
28
|
+
])
|
|
29
|
+
elif shape_type == 'rect':
|
|
30
|
+
self.vao = self.ctx.vertex_array(self.prog, [
|
|
31
|
+
(self.vbo, '2f 4f 1f 4f 1f 1f 2f 2f', 'in_pos', 'in_color', 'in_radius',
|
|
32
|
+
'in_border_color', 'in_border_width', 'in_aa', 'in_size', 'in_local_pos')
|
|
33
|
+
])
|
|
34
|
+
else: # line
|
|
35
|
+
self.vao = self.ctx.vertex_array(self.prog, [
|
|
36
|
+
(self.vbo, '2f 4f', 'in_pos', 'in_color')
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
def draw(self) -> None:
|
|
40
|
+
"""Draw the cached shape."""
|
|
41
|
+
self.ctx.enable(moderngl.BLEND)
|
|
42
|
+
set_pattr_value(self.prog, 'resolution', self.ctx.viewport[2:])
|
|
43
|
+
self.vao.render(moderngl.TRIANGLES, vertices=self.vertex_count)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InstancedShapeBatch:
|
|
47
|
+
"""High-performance instanced batch for drawing thousands of shapes with minimal CPU overhead."""
|
|
48
|
+
def __init__(self, ctx: moderngl.Context, prog: moderngl.Program, shape_type: str = 'circle', max_instances: int = 100000) -> None:
|
|
49
|
+
self.ctx = ctx
|
|
50
|
+
self.prog = prog
|
|
51
|
+
self.shape_type = shape_type
|
|
52
|
+
self.max_instances = max_instances
|
|
53
|
+
self.instance_count = 0
|
|
54
|
+
|
|
55
|
+
# Per-instance data (stored once, reused for drawing)
|
|
56
|
+
if shape_type == 'circle':
|
|
57
|
+
# Per-instance: center(2f), color(4f), radius(1f), border_color(4f), border_width(1f), aa(1f) = 13 floats
|
|
58
|
+
self.floats_per_instance = 13
|
|
59
|
+
self.instance_buffer = self.ctx.buffer(reserve=max_instances * self.floats_per_instance * 4, dynamic=True)
|
|
60
|
+
|
|
61
|
+
# Template quad (6 vertices for 2 triangles) - shared by all instances
|
|
62
|
+
# These are the LOCAL positions that will be offset by instance data
|
|
63
|
+
quad_verts = np.array([
|
|
64
|
+
-1.0, -1.0,
|
|
65
|
+
1.0, -1.0,
|
|
66
|
+
-1.0, 1.0,
|
|
67
|
+
1.0, -1.0,
|
|
68
|
+
-1.0, 1.0,
|
|
69
|
+
1.0, 1.0
|
|
70
|
+
], dtype='f4')
|
|
71
|
+
self.quad_vbo = self.ctx.buffer(quad_verts.tobytes())
|
|
72
|
+
|
|
73
|
+
# Create VAO with per-vertex and per-instance attributes
|
|
74
|
+
self.vao = self.ctx.vertex_array(
|
|
75
|
+
self.prog,
|
|
76
|
+
[
|
|
77
|
+
(self.quad_vbo, '2f', 'in_vertex'), # Per-vertex (6 vertices)
|
|
78
|
+
(self.instance_buffer, '2f 4f 1f 4f 1f 1f/i', 'in_center', 'in_color', 'in_radius',
|
|
79
|
+
'in_border_color', 'in_border_width', 'in_aa') # Per-instance (divisor = 1)
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
elif shape_type == 'rect':
|
|
83
|
+
# Per-instance: center(2f), size(2f), color(4f), radius(1f), border_color(4f), border_width(1f), aa(1f), rotation(1f) = 16 floats
|
|
84
|
+
self.floats_per_instance = 16
|
|
85
|
+
self.instance_buffer = self.ctx.buffer(reserve=max_instances * self.floats_per_instance * 4, dynamic=True)
|
|
86
|
+
|
|
87
|
+
quad_verts = np.array([
|
|
88
|
+
-1.0, -1.0,
|
|
89
|
+
1.0, -1.0,
|
|
90
|
+
-1.0, 1.0,
|
|
91
|
+
1.0, -1.0,
|
|
92
|
+
-1.0, 1.0,
|
|
93
|
+
1.0, 1.0
|
|
94
|
+
], dtype='f4')
|
|
95
|
+
self.quad_vbo = self.ctx.buffer(quad_verts.tobytes())
|
|
96
|
+
|
|
97
|
+
self.vao = self.ctx.vertex_array(
|
|
98
|
+
self.prog,
|
|
99
|
+
[
|
|
100
|
+
(self.quad_vbo, '2f', 'in_vertex'),
|
|
101
|
+
(self.instance_buffer, '2f 2f 4f 1f 4f 1f 1f 1f/i', 'in_center', 'in_size', 'in_color',
|
|
102
|
+
'in_radius', 'in_border_color', 'in_border_width', 'in_aa', 'in_rotation')
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
elif shape_type == 'line':
|
|
106
|
+
# Per-instance: start(2f), end(2f), width(1f), color(4f) = 9 floats
|
|
107
|
+
self.floats_per_instance = 9
|
|
108
|
+
self.instance_buffer = self.ctx.buffer(reserve=max_instances * self.floats_per_instance * 4, dynamic=True)
|
|
109
|
+
|
|
110
|
+
# Quad template for line segment
|
|
111
|
+
quad_verts = np.array([
|
|
112
|
+
-1.0, -1.0,
|
|
113
|
+
1.0, -1.0,
|
|
114
|
+
-1.0, 1.0,
|
|
115
|
+
1.0, -1.0,
|
|
116
|
+
-1.0, 1.0,
|
|
117
|
+
1.0, 1.0
|
|
118
|
+
], dtype='f4')
|
|
119
|
+
self.quad_vbo = self.ctx.buffer(quad_verts.tobytes())
|
|
120
|
+
|
|
121
|
+
self.vao = self.ctx.vertex_array(
|
|
122
|
+
self.prog,
|
|
123
|
+
[
|
|
124
|
+
(self.quad_vbo, '2f', 'in_quad_pos'),
|
|
125
|
+
(self.instance_buffer, '2f 2f 1f 4f/i', 'in_start', 'in_end', 'in_width', 'in_color')
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.instance_data = []
|
|
130
|
+
|
|
131
|
+
def add_circle(self, center: tuple[float, float], radius: float,
|
|
132
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
133
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
134
|
+
border_width: float = 0.0,
|
|
135
|
+
antialiasing: float = 1.0) -> None:
|
|
136
|
+
"""Add a circle instance to the batch."""
|
|
137
|
+
self.instance_data.extend([*center, *color, radius, *border_color, border_width, antialiasing])
|
|
138
|
+
self.instance_count += 1
|
|
139
|
+
|
|
140
|
+
def add_circles_numpy(self, centers: np.ndarray, radii: np.ndarray,
|
|
141
|
+
colors: np.ndarray,
|
|
142
|
+
border_colors: Optional[np.ndarray] = None,
|
|
143
|
+
border_widths: Optional[np.ndarray] = None,
|
|
144
|
+
antialiasing: float = 1.0) -> None:
|
|
145
|
+
"""Add multiple circles efficiently using numpy arrays (10-50x faster than loop).
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
centers: (N, 2) array of (x, y) positions
|
|
149
|
+
radii: (N,) array of radii
|
|
150
|
+
colors: (N, 4) array of (r, g, b, a) colors
|
|
151
|
+
border_colors: (N, 4) array or None (defaults to transparent)
|
|
152
|
+
border_widths: (N,) array or None (defaults to 0)
|
|
153
|
+
antialiasing: Antialiasing width (scalar applied to all)
|
|
154
|
+
"""
|
|
155
|
+
n = len(centers)
|
|
156
|
+
if border_colors is None:
|
|
157
|
+
border_colors = np.zeros((n, 4), dtype='f4')
|
|
158
|
+
if border_widths is None:
|
|
159
|
+
border_widths = np.zeros(n, dtype='f4')
|
|
160
|
+
|
|
161
|
+
# Build interleaved data: [cx, cy, r, g, b, a, radius, br, bg, bb, ba, bw, aa] * N
|
|
162
|
+
# Format: center(2), color(4), radius(1), border_color(4), border_width(1), aa(1) = 13 floats
|
|
163
|
+
data = np.empty((n, 13), dtype='f4')
|
|
164
|
+
data[:, 0:2] = centers
|
|
165
|
+
data[:, 2:6] = colors
|
|
166
|
+
data[:, 6] = radii
|
|
167
|
+
data[:, 7:11] = border_colors
|
|
168
|
+
data[:, 11] = border_widths
|
|
169
|
+
data[:, 12] = antialiasing
|
|
170
|
+
|
|
171
|
+
self.instance_data.extend(data.ravel())
|
|
172
|
+
self.instance_count += n
|
|
173
|
+
|
|
174
|
+
def add_rect(self, center: tuple[float, float], size: tuple[float, float],
|
|
175
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
176
|
+
corner_radius: float = 0.0,
|
|
177
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
178
|
+
border_width: float = 0.0,
|
|
179
|
+
antialiasing: float = 1.0,
|
|
180
|
+
rotation: float = 0.0) -> None:
|
|
181
|
+
"""Add a rectangle instance to the batch."""
|
|
182
|
+
self.instance_data.extend([*center, *size, *color, corner_radius, *border_color, border_width, antialiasing, rotation])
|
|
183
|
+
self.instance_count += 1
|
|
184
|
+
|
|
185
|
+
def add_rects_numpy(self, centers: np.ndarray, sizes: np.ndarray,
|
|
186
|
+
colors: np.ndarray,
|
|
187
|
+
corner_radii: Optional[np.ndarray] = None,
|
|
188
|
+
border_colors: Optional[np.ndarray] = None,
|
|
189
|
+
border_widths: Optional[np.ndarray] = None,
|
|
190
|
+
antialiasing: float = 1.0,
|
|
191
|
+
rotations: Optional[np.ndarray] = None) -> None:
|
|
192
|
+
"""Add multiple rectangles efficiently using numpy arrays (10-50x faster than loop).
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
centers: (N, 2) array of (x, y) positions
|
|
196
|
+
sizes: (N, 2) array of (width, height)
|
|
197
|
+
colors: (N, 4) array of (r, g, b, a) colors
|
|
198
|
+
corner_radii: (N,) array or None (defaults to 0)
|
|
199
|
+
border_colors: (N, 4) array or None (defaults to transparent)
|
|
200
|
+
border_widths: (N,) array or None (defaults to 0)
|
|
201
|
+
antialiasing: Antialiasing width (scalar applied to all)
|
|
202
|
+
rotations: (N,) array or None (defaults to 0)
|
|
203
|
+
"""
|
|
204
|
+
n = len(centers)
|
|
205
|
+
if corner_radii is None:
|
|
206
|
+
corner_radii = np.zeros(n, dtype='f4')
|
|
207
|
+
if border_colors is None:
|
|
208
|
+
border_colors = np.zeros((n, 4), dtype='f4')
|
|
209
|
+
if border_widths is None:
|
|
210
|
+
border_widths = np.zeros(n, dtype='f4')
|
|
211
|
+
if rotations is None:
|
|
212
|
+
rotations = np.zeros(n, dtype='f4')
|
|
213
|
+
|
|
214
|
+
# Format: center(2), size(2), color(4), radius(1), border_color(4), border_width(1), aa(1), rotation(1) = 16 floats
|
|
215
|
+
data = np.empty((n, 16), dtype='f4')
|
|
216
|
+
data[:, 0:2] = centers
|
|
217
|
+
data[:, 2:4] = sizes
|
|
218
|
+
data[:, 4:8] = colors
|
|
219
|
+
data[:, 8] = corner_radii
|
|
220
|
+
data[:, 9:13] = border_colors
|
|
221
|
+
data[:, 13] = border_widths
|
|
222
|
+
data[:, 14] = antialiasing
|
|
223
|
+
data[:, 15] = rotations
|
|
224
|
+
|
|
225
|
+
self.instance_data.extend(data.ravel())
|
|
226
|
+
self.instance_count += n
|
|
227
|
+
|
|
228
|
+
def add_line(self, start: tuple[float, float], end: tuple[float, float],
|
|
229
|
+
width: float = 1.0,
|
|
230
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)) -> None:
|
|
231
|
+
"""Add a line instance to the batch."""
|
|
232
|
+
self.instance_data.extend([*start, *end, width, *color])
|
|
233
|
+
self.instance_count += 1
|
|
234
|
+
|
|
235
|
+
def add_lines_numpy(self, starts: np.ndarray, ends: np.ndarray,
|
|
236
|
+
widths: np.ndarray, colors: np.ndarray) -> None:
|
|
237
|
+
"""Add multiple lines efficiently using numpy arrays (10-50x faster than loop).
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
starts: (N, 2) array of start (x, y) positions
|
|
241
|
+
ends: (N, 2) array of end (x, y) positions
|
|
242
|
+
widths: (N,) array of line widths
|
|
243
|
+
colors: (N, 4) array of (r, g, b, a) colors
|
|
244
|
+
"""
|
|
245
|
+
n = len(starts)
|
|
246
|
+
|
|
247
|
+
# Format: start(2), end(2), width(1), color(4) = 9 floats
|
|
248
|
+
data = np.empty((n, 9), dtype='f4')
|
|
249
|
+
data[:, 0:2] = starts
|
|
250
|
+
data[:, 2:4] = ends
|
|
251
|
+
data[:, 4] = widths
|
|
252
|
+
data[:, 5:9] = colors
|
|
253
|
+
|
|
254
|
+
self.instance_data.extend(data.ravel())
|
|
255
|
+
self.instance_count += n
|
|
256
|
+
|
|
257
|
+
def flush(self) -> None:
|
|
258
|
+
"""Draw all instances in a single draw call."""
|
|
259
|
+
if self.instance_count == 0:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# Upload instance data
|
|
263
|
+
data = np.array(self.instance_data, dtype='f4')
|
|
264
|
+
self.instance_buffer.write(data.tobytes())
|
|
265
|
+
|
|
266
|
+
# Draw all instances with one call
|
|
267
|
+
self.ctx.enable(moderngl.BLEND)
|
|
268
|
+
set_pattr_value(self.prog, 'resolution', self.ctx.viewport[2:])
|
|
269
|
+
self.vao.render(moderngl.TRIANGLES, vertices=6, instances=self.instance_count)
|
|
270
|
+
|
|
271
|
+
self.instance_data.clear()
|
|
272
|
+
self.instance_count = 0
|
|
273
|
+
|
|
274
|
+
def clear(self) -> None:
|
|
275
|
+
"""Clear the batch without drawing."""
|
|
276
|
+
self.instance_data.clear()
|
|
277
|
+
self.instance_count = 0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ShapeRenderer:
|
|
281
|
+
"""
|
|
282
|
+
High-performance 2D shape renderer using SDF (Signed Distance Functions) and GPU shaders.
|
|
283
|
+
Supports immediate mode, cached drawing, and batched rendering.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def __init__(self, ctx: moderngl.Context) -> None:
|
|
287
|
+
self.ctx = ctx
|
|
288
|
+
|
|
289
|
+
# ===== INSTANCED Circle Shader (for high-performance batching) =====
|
|
290
|
+
self.circle_instanced_prog = self.ctx.program(
|
|
291
|
+
vertex_shader="""
|
|
292
|
+
#version 430
|
|
293
|
+
uniform vec2 resolution;
|
|
294
|
+
|
|
295
|
+
in vec2 in_vertex; // Template quad vertex: (-1,-1) to (1,1)
|
|
296
|
+
in vec2 in_center; // Per-instance
|
|
297
|
+
in vec4 in_color; // Per-instance
|
|
298
|
+
in float in_radius; // Per-instance
|
|
299
|
+
in vec4 in_border_color; // Per-instance
|
|
300
|
+
in float in_border_width; // Per-instance
|
|
301
|
+
in float in_aa; // Per-instance
|
|
302
|
+
|
|
303
|
+
out vec4 v_color;
|
|
304
|
+
out vec2 v_local_pos;
|
|
305
|
+
out float v_radius;
|
|
306
|
+
out vec4 v_border_color;
|
|
307
|
+
out float v_border_width;
|
|
308
|
+
out float v_aa;
|
|
309
|
+
|
|
310
|
+
void main() {
|
|
311
|
+
// Expand vertex by radius + border + aa
|
|
312
|
+
float expand = in_radius + in_border_width + in_aa * 2.0;
|
|
313
|
+
vec2 world_pos = in_center + in_vertex * expand;
|
|
314
|
+
|
|
315
|
+
vec2 ndc = (world_pos / resolution) * 2.0 - 1.0;
|
|
316
|
+
ndc.y = -ndc.y;
|
|
317
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
318
|
+
|
|
319
|
+
v_color = in_color;
|
|
320
|
+
v_local_pos = in_vertex * expand; // Local position for SDF
|
|
321
|
+
v_radius = in_radius;
|
|
322
|
+
v_border_color = in_border_color;
|
|
323
|
+
v_border_width = in_border_width;
|
|
324
|
+
v_aa = in_aa;
|
|
325
|
+
}
|
|
326
|
+
""",
|
|
327
|
+
fragment_shader="""
|
|
328
|
+
#version 430
|
|
329
|
+
|
|
330
|
+
in vec4 v_color;
|
|
331
|
+
in vec2 v_local_pos;
|
|
332
|
+
in float v_radius;
|
|
333
|
+
in vec4 v_border_color;
|
|
334
|
+
in float v_border_width;
|
|
335
|
+
in float v_aa;
|
|
336
|
+
|
|
337
|
+
out vec4 f_color;
|
|
338
|
+
|
|
339
|
+
void main() {
|
|
340
|
+
float dist = length(v_local_pos) - v_radius;
|
|
341
|
+
|
|
342
|
+
if (v_border_width > 0.0) {
|
|
343
|
+
float outer_dist = abs(dist);
|
|
344
|
+
float inner_dist = abs(dist + v_border_width);
|
|
345
|
+
float alpha_outer = 1.0 - smoothstep(0.0, v_aa, outer_dist);
|
|
346
|
+
float alpha_inner = 1.0 - smoothstep(0.0, v_aa, inner_dist);
|
|
347
|
+
float border_alpha = alpha_outer * (1.0 - alpha_inner);
|
|
348
|
+
float fill_alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
349
|
+
vec4 fill_color = vec4(v_color.rgb, v_color.a * fill_alpha);
|
|
350
|
+
vec4 border_col = vec4(v_border_color.rgb, v_border_color.a * border_alpha);
|
|
351
|
+
f_color = mix(fill_color, border_col, border_alpha / max(border_alpha + fill_alpha, 0.001));
|
|
352
|
+
} else {
|
|
353
|
+
float alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
354
|
+
f_color = vec4(v_color.rgb, v_color.a * alpha);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
"""
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# ===== INSTANCED Rectangle Shader =====
|
|
361
|
+
self.rect_instanced_prog = self.ctx.program(
|
|
362
|
+
vertex_shader="""
|
|
363
|
+
#version 430
|
|
364
|
+
uniform vec2 resolution;
|
|
365
|
+
|
|
366
|
+
in vec2 in_vertex;
|
|
367
|
+
in vec2 in_center;
|
|
368
|
+
in vec2 in_size;
|
|
369
|
+
in vec4 in_color;
|
|
370
|
+
in float in_radius;
|
|
371
|
+
in vec4 in_border_color;
|
|
372
|
+
in float in_border_width;
|
|
373
|
+
in float in_aa;
|
|
374
|
+
in float in_rotation;
|
|
375
|
+
|
|
376
|
+
out vec4 v_color;
|
|
377
|
+
out vec2 v_local_pos;
|
|
378
|
+
out float v_radius;
|
|
379
|
+
out vec4 v_border_color;
|
|
380
|
+
out float v_border_width;
|
|
381
|
+
out float v_aa;
|
|
382
|
+
out vec2 v_size;
|
|
383
|
+
|
|
384
|
+
void main() {
|
|
385
|
+
float expand = in_border_width + in_aa * 2.0;
|
|
386
|
+
vec2 expanded_size = in_size + expand;
|
|
387
|
+
|
|
388
|
+
// Apply rotation
|
|
389
|
+
float cos_a = cos(in_rotation);
|
|
390
|
+
float sin_a = sin(in_rotation);
|
|
391
|
+
vec2 rotated = vec2(
|
|
392
|
+
in_vertex.x * cos_a - in_vertex.y * sin_a,
|
|
393
|
+
in_vertex.x * sin_a + in_vertex.y * cos_a
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
vec2 world_pos = in_center + rotated * expanded_size;
|
|
397
|
+
|
|
398
|
+
vec2 ndc = (world_pos / resolution) * 2.0 - 1.0;
|
|
399
|
+
ndc.y = -ndc.y;
|
|
400
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
401
|
+
|
|
402
|
+
v_color = in_color;
|
|
403
|
+
v_local_pos = rotated * expanded_size;
|
|
404
|
+
v_radius = in_radius;
|
|
405
|
+
v_border_color = in_border_color;
|
|
406
|
+
v_border_width = in_border_width;
|
|
407
|
+
v_aa = in_aa;
|
|
408
|
+
v_size = in_size;
|
|
409
|
+
}
|
|
410
|
+
""",
|
|
411
|
+
fragment_shader="""
|
|
412
|
+
#version 430
|
|
413
|
+
|
|
414
|
+
in vec4 v_color;
|
|
415
|
+
in vec2 v_local_pos;
|
|
416
|
+
in float v_radius;
|
|
417
|
+
in vec4 v_border_color;
|
|
418
|
+
in float v_border_width;
|
|
419
|
+
in float v_aa;
|
|
420
|
+
in vec2 v_size;
|
|
421
|
+
|
|
422
|
+
out vec4 f_color;
|
|
423
|
+
|
|
424
|
+
float roundedBoxSDF(vec2 center, vec2 size, float radius) {
|
|
425
|
+
vec2 q = abs(center) - size + radius;
|
|
426
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
void main() {
|
|
430
|
+
float dist = roundedBoxSDF(v_local_pos, v_size, v_radius);
|
|
431
|
+
|
|
432
|
+
if (v_border_width > 0.0) {
|
|
433
|
+
float outer_dist = abs(dist);
|
|
434
|
+
float inner_dist = abs(dist + v_border_width);
|
|
435
|
+
float alpha_outer = 1.0 - smoothstep(0.0, v_aa, outer_dist);
|
|
436
|
+
float alpha_inner = 1.0 - smoothstep(0.0, v_aa, inner_dist);
|
|
437
|
+
float border_alpha = alpha_outer * (1.0 - alpha_inner);
|
|
438
|
+
float fill_alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
439
|
+
vec4 fill_color = vec4(v_color.rgb, v_color.a * fill_alpha);
|
|
440
|
+
vec4 border_col = vec4(v_border_color.rgb, v_border_color.a * border_alpha);
|
|
441
|
+
f_color = mix(fill_color, border_col, border_alpha / max(border_alpha + fill_alpha, 0.001));
|
|
442
|
+
} else {
|
|
443
|
+
float alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
444
|
+
f_color = vec4(v_color.rgb, v_color.a * alpha);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
"""
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
self.line_instanced_prog = self.ctx.program(
|
|
451
|
+
vertex_shader="""
|
|
452
|
+
#version 430
|
|
453
|
+
|
|
454
|
+
// Per-vertex attributes (quad corners: 4 vertices per instance)
|
|
455
|
+
in vec2 in_quad_pos; // (-1,-1), (1,-1), (1,1), (-1,1)
|
|
456
|
+
|
|
457
|
+
// Per-instance attributes
|
|
458
|
+
in vec2 in_start;
|
|
459
|
+
in vec2 in_end;
|
|
460
|
+
in float in_width;
|
|
461
|
+
in vec4 in_color;
|
|
462
|
+
|
|
463
|
+
out vec4 v_color;
|
|
464
|
+
|
|
465
|
+
uniform vec2 resolution;
|
|
466
|
+
|
|
467
|
+
void main() {
|
|
468
|
+
// Calculate line direction and perpendicular
|
|
469
|
+
vec2 line_vec = in_end - in_start;
|
|
470
|
+
float line_length = length(line_vec);
|
|
471
|
+
vec2 line_dir = line_vec / line_length;
|
|
472
|
+
vec2 line_perp = vec2(-line_dir.y, line_dir.x);
|
|
473
|
+
|
|
474
|
+
// Calculate half-width
|
|
475
|
+
float half_width = in_width * 0.5;
|
|
476
|
+
|
|
477
|
+
// Expand quad along line direction and perpendicular
|
|
478
|
+
vec2 world_pos = in_start +
|
|
479
|
+
line_dir * (in_quad_pos.x * line_length * 0.5 + line_length * 0.5) +
|
|
480
|
+
line_perp * (in_quad_pos.y * half_width);
|
|
481
|
+
|
|
482
|
+
// Convert to NDC
|
|
483
|
+
vec2 ndc = (world_pos / resolution) * 2.0 - 1.0;
|
|
484
|
+
ndc.y = -ndc.y;
|
|
485
|
+
|
|
486
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
487
|
+
v_color = in_color;
|
|
488
|
+
}
|
|
489
|
+
""",
|
|
490
|
+
fragment_shader="""
|
|
491
|
+
#version 430
|
|
492
|
+
|
|
493
|
+
in vec4 v_color;
|
|
494
|
+
out vec4 fragColor;
|
|
495
|
+
|
|
496
|
+
uniform vec2 resolution;
|
|
497
|
+
|
|
498
|
+
void main() {
|
|
499
|
+
fragColor = v_color;
|
|
500
|
+
}
|
|
501
|
+
"""
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ===== SDF Circle Shader =====
|
|
506
|
+
self.circle_prog = self.ctx.program(
|
|
507
|
+
vertex_shader="""
|
|
508
|
+
#version 430
|
|
509
|
+
uniform vec2 resolution;
|
|
510
|
+
|
|
511
|
+
in vec2 in_pos;
|
|
512
|
+
in vec4 in_color;
|
|
513
|
+
in float in_radius;
|
|
514
|
+
in vec4 in_border_color;
|
|
515
|
+
in float in_border_width;
|
|
516
|
+
in float in_aa;
|
|
517
|
+
in vec2 in_center;
|
|
518
|
+
|
|
519
|
+
out vec4 v_color;
|
|
520
|
+
out vec2 v_local_pos;
|
|
521
|
+
out float v_radius;
|
|
522
|
+
out vec4 v_border_color;
|
|
523
|
+
out float v_border_width;
|
|
524
|
+
out float v_aa;
|
|
525
|
+
|
|
526
|
+
void main() {
|
|
527
|
+
vec2 ndc = (in_pos / resolution) * 2.0 - 1.0;
|
|
528
|
+
ndc.y = -ndc.y;
|
|
529
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
530
|
+
v_color = in_color;
|
|
531
|
+
v_local_pos = in_pos - in_center;
|
|
532
|
+
v_radius = in_radius;
|
|
533
|
+
v_border_color = in_border_color;
|
|
534
|
+
v_border_width = in_border_width;
|
|
535
|
+
v_aa = in_aa;
|
|
536
|
+
}
|
|
537
|
+
""",
|
|
538
|
+
fragment_shader="""
|
|
539
|
+
#version 430
|
|
540
|
+
|
|
541
|
+
in vec4 v_color;
|
|
542
|
+
in vec2 v_local_pos;
|
|
543
|
+
in float v_radius;
|
|
544
|
+
in vec4 v_border_color;
|
|
545
|
+
in float v_border_width;
|
|
546
|
+
in float v_aa;
|
|
547
|
+
|
|
548
|
+
out vec4 f_color;
|
|
549
|
+
|
|
550
|
+
void main() {
|
|
551
|
+
float dist = length(v_local_pos) - v_radius;
|
|
552
|
+
|
|
553
|
+
if (v_border_width > 0.0) {
|
|
554
|
+
float outer_dist = abs(dist);
|
|
555
|
+
float inner_dist = abs(dist + v_border_width);
|
|
556
|
+
float alpha_outer = 1.0 - smoothstep(0.0, v_aa, outer_dist);
|
|
557
|
+
float alpha_inner = 1.0 - smoothstep(0.0, v_aa, inner_dist);
|
|
558
|
+
float border_alpha = alpha_outer * (1.0 - alpha_inner);
|
|
559
|
+
float fill_alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
560
|
+
vec4 fill_color = vec4(v_color.rgb, v_color.a * fill_alpha);
|
|
561
|
+
vec4 border_col = vec4(v_border_color.rgb, v_border_color.a * border_alpha);
|
|
562
|
+
f_color = mix(fill_color, border_col, border_alpha / max(border_alpha + fill_alpha, 0.001));
|
|
563
|
+
} else {
|
|
564
|
+
float alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
565
|
+
f_color = vec4(v_color.rgb, v_color.a * alpha);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
"""
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# ===== SDF Rectangle Shader =====
|
|
572
|
+
self.rect_prog = self.ctx.program(
|
|
573
|
+
vertex_shader="""
|
|
574
|
+
#version 430
|
|
575
|
+
uniform vec2 resolution;
|
|
576
|
+
|
|
577
|
+
in vec2 in_pos;
|
|
578
|
+
in vec4 in_color;
|
|
579
|
+
in float in_radius;
|
|
580
|
+
in vec4 in_border_color;
|
|
581
|
+
in float in_border_width;
|
|
582
|
+
in float in_aa;
|
|
583
|
+
in vec2 in_size;
|
|
584
|
+
in vec2 in_local_pos;
|
|
585
|
+
|
|
586
|
+
out vec4 v_color;
|
|
587
|
+
out vec2 v_local_pos;
|
|
588
|
+
out float v_radius;
|
|
589
|
+
out vec4 v_border_color;
|
|
590
|
+
out float v_border_width;
|
|
591
|
+
out float v_aa;
|
|
592
|
+
out vec2 v_size;
|
|
593
|
+
|
|
594
|
+
void main() {
|
|
595
|
+
vec2 ndc = (in_pos / resolution) * 2.0 - 1.0;
|
|
596
|
+
ndc.y = -ndc.y;
|
|
597
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
598
|
+
v_color = in_color;
|
|
599
|
+
v_local_pos = in_local_pos;
|
|
600
|
+
v_radius = in_radius;
|
|
601
|
+
v_border_color = in_border_color;
|
|
602
|
+
v_border_width = in_border_width;
|
|
603
|
+
v_aa = in_aa;
|
|
604
|
+
v_size = in_size;
|
|
605
|
+
}
|
|
606
|
+
""",
|
|
607
|
+
fragment_shader="""
|
|
608
|
+
#version 430
|
|
609
|
+
|
|
610
|
+
in vec4 v_color;
|
|
611
|
+
in vec2 v_local_pos;
|
|
612
|
+
in float v_radius;
|
|
613
|
+
in vec4 v_border_color;
|
|
614
|
+
in float v_border_width;
|
|
615
|
+
in float v_aa;
|
|
616
|
+
in vec2 v_size;
|
|
617
|
+
|
|
618
|
+
out vec4 f_color;
|
|
619
|
+
|
|
620
|
+
float roundedBoxSDF(vec2 center, vec2 size, float radius) {
|
|
621
|
+
vec2 q = abs(center) - size + radius;
|
|
622
|
+
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
void main() {
|
|
626
|
+
float dist = roundedBoxSDF(v_local_pos, v_size, v_radius);
|
|
627
|
+
|
|
628
|
+
if (v_border_width > 0.0) {
|
|
629
|
+
float outer_dist = abs(dist);
|
|
630
|
+
float inner_dist = abs(dist + v_border_width);
|
|
631
|
+
float alpha_outer = 1.0 - smoothstep(0.0, v_aa, outer_dist);
|
|
632
|
+
float alpha_inner = 1.0 - smoothstep(0.0, v_aa, inner_dist);
|
|
633
|
+
float border_alpha = alpha_outer * (1.0 - alpha_inner);
|
|
634
|
+
float fill_alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
635
|
+
vec4 fill_color = vec4(v_color.rgb, v_color.a * fill_alpha);
|
|
636
|
+
vec4 border_col = vec4(v_border_color.rgb, v_border_color.a * border_alpha);
|
|
637
|
+
f_color = mix(fill_color, border_col, border_alpha / max(border_alpha + fill_alpha, 0.001));
|
|
638
|
+
} else {
|
|
639
|
+
float alpha = 1.0 - smoothstep(-v_aa, v_aa, dist);
|
|
640
|
+
f_color = vec4(v_color.rgb, v_color.a * alpha);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
"""
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
# ===== Line Shader (immediate mode) =====
|
|
647
|
+
self.line_prog = self.ctx.program(
|
|
648
|
+
vertex_shader="""
|
|
649
|
+
#version 430
|
|
650
|
+
uniform vec2 resolution;
|
|
651
|
+
|
|
652
|
+
in vec2 in_pos;
|
|
653
|
+
in vec4 in_color;
|
|
654
|
+
|
|
655
|
+
out vec4 v_color;
|
|
656
|
+
|
|
657
|
+
void main() {
|
|
658
|
+
vec2 ndc = (in_pos / resolution) * 2.0 - 1.0;
|
|
659
|
+
ndc.y = -ndc.y;
|
|
660
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
661
|
+
v_color = in_color;
|
|
662
|
+
}
|
|
663
|
+
""",
|
|
664
|
+
fragment_shader="""
|
|
665
|
+
#version 430
|
|
666
|
+
|
|
667
|
+
in vec4 v_color;
|
|
668
|
+
out vec4 f_color;
|
|
669
|
+
|
|
670
|
+
void main() {
|
|
671
|
+
f_color = v_color;
|
|
672
|
+
}
|
|
673
|
+
"""
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# Dynamic VBOs for immediate mode
|
|
677
|
+
self.circle_vbo = self.ctx.buffer(reserve=65536)
|
|
678
|
+
self.rect_vbo = self.ctx.buffer(reserve=65536)
|
|
679
|
+
self.line_vbo = self.ctx.buffer(reserve=262144) # Larger for polylines
|
|
680
|
+
|
|
681
|
+
# VAOs for immediate mode
|
|
682
|
+
# Circle format: pos(2f), color(4f), radius(1f), border_color(4f), border_width(1f), aa(1f), center(2f) = 15 floats
|
|
683
|
+
self.circle_vao = self.ctx.vertex_array(self.circle_prog, [
|
|
684
|
+
(self.circle_vbo, '2f 4f 1f 4f 1f 1f 2f', 'in_pos', 'in_color', 'in_radius',
|
|
685
|
+
'in_border_color', 'in_border_width', 'in_aa', 'in_center')
|
|
686
|
+
])
|
|
687
|
+
|
|
688
|
+
# Rect format: pos(2f), color(4f), radius(1f), border_color(4f), border_width(1f), aa(1f), size(2f), local_pos(2f) = 17 floats
|
|
689
|
+
self.rect_vao = self.ctx.vertex_array(self.rect_prog, [
|
|
690
|
+
(self.rect_vbo, '2f 4f 1f 4f 1f 1f 2f 2f', 'in_pos', 'in_color', 'in_radius',
|
|
691
|
+
'in_border_color', 'in_border_width', 'in_aa', 'in_size', 'in_local_pos')
|
|
692
|
+
])
|
|
693
|
+
|
|
694
|
+
self.line_vao = self.ctx.vertex_array(self.line_prog, [
|
|
695
|
+
(self.line_vbo, '2f 4f', 'in_pos', 'in_color')
|
|
696
|
+
])
|
|
697
|
+
|
|
698
|
+
# ========== CIRCLE ==========
|
|
699
|
+
|
|
700
|
+
def _generate_circle_vertices(self, center: tuple[float, float], radius: float,
|
|
701
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
702
|
+
rotation: float = 0.0,
|
|
703
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
704
|
+
border_width: float = 0.0,
|
|
705
|
+
antialiasing: float = 1.0) -> list[float]:
|
|
706
|
+
"""Generate vertices for a circle (as a quad).
|
|
707
|
+
Format: pos(2f), color(4f), radius(1f), border_color(4f), border_width(1f), aa(1f), center(2f)
|
|
708
|
+
"""
|
|
709
|
+
cx, cy = center
|
|
710
|
+
|
|
711
|
+
# Expand for border and antialiasing
|
|
712
|
+
expand = radius + border_width + antialiasing * 2
|
|
713
|
+
|
|
714
|
+
# Quad vertices (2 triangles) in screen space
|
|
715
|
+
quad_positions = [
|
|
716
|
+
(cx - expand, cy - expand),
|
|
717
|
+
(cx + expand, cy - expand),
|
|
718
|
+
(cx - expand, cy + expand),
|
|
719
|
+
(cx + expand, cy - expand),
|
|
720
|
+
(cx - expand, cy + expand),
|
|
721
|
+
(cx + expand, cy + expand)
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
vertices = []
|
|
725
|
+
for pos in quad_positions:
|
|
726
|
+
vertices.extend([
|
|
727
|
+
*pos, # in_pos (screen position)
|
|
728
|
+
*color, # in_color
|
|
729
|
+
radius, # in_radius
|
|
730
|
+
*border_color, # in_border_color
|
|
731
|
+
border_width, # in_border_width
|
|
732
|
+
antialiasing, # in_aa
|
|
733
|
+
cx, cy # in_center (circle center)
|
|
734
|
+
])
|
|
735
|
+
|
|
736
|
+
return vertices
|
|
737
|
+
|
|
738
|
+
def draw_circle(self, center: tuple[float, float], radius: float,
|
|
739
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
740
|
+
rotation: float = 0.0,
|
|
741
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
742
|
+
border_width: float = 0.0,
|
|
743
|
+
antialiasing: float = 1.0) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Draw a circle immediately.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
center: (x, y) position in screen coordinates
|
|
749
|
+
radius: Circle radius in pixels
|
|
750
|
+
color: (r, g, b, a) fill color
|
|
751
|
+
rotation: Rotation angle in radians (not used for circles, included for API consistency)
|
|
752
|
+
border_color: (r, g, b, a) border color
|
|
753
|
+
border_width: Border width in pixels (0 = no border)
|
|
754
|
+
antialiasing: Antialiasing smoothness in pixels
|
|
755
|
+
"""
|
|
756
|
+
vertices = self._generate_circle_vertices(center, radius, color, rotation,
|
|
757
|
+
border_color, border_width, antialiasing)
|
|
758
|
+
|
|
759
|
+
data = np.array(vertices, dtype='f4')
|
|
760
|
+
self.circle_vbo.write(data.tobytes())
|
|
761
|
+
|
|
762
|
+
self.ctx.enable(moderngl.BLEND)
|
|
763
|
+
set_pattr_value(self.circle_prog, 'resolution', self.ctx.viewport[2:])
|
|
764
|
+
self.circle_vao.render(moderngl.TRIANGLES, vertices=6)
|
|
765
|
+
|
|
766
|
+
def create_circle(self, center: tuple[float, float], radius: float,
|
|
767
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
768
|
+
rotation: float = 0.0,
|
|
769
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
770
|
+
border_width: float = 0.0,
|
|
771
|
+
antialiasing: float = 1.0) -> ShapeLabel:
|
|
772
|
+
"""Create a cached circle for repeated drawing."""
|
|
773
|
+
vertices = self._generate_circle_vertices(center, radius, color, rotation,
|
|
774
|
+
border_color, border_width, antialiasing)
|
|
775
|
+
|
|
776
|
+
data = np.array(vertices, dtype='f4')
|
|
777
|
+
vbo = self.ctx.buffer(data.tobytes())
|
|
778
|
+
|
|
779
|
+
return ShapeLabel(self.ctx, self.circle_prog, vbo, 6, 'circle')
|
|
780
|
+
|
|
781
|
+
# ========== RECTANGLE ==========
|
|
782
|
+
|
|
783
|
+
def _generate_rect_vertices(self, position: tuple[float, float], size: tuple[float, float],
|
|
784
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
785
|
+
rotation: float = 0.0,
|
|
786
|
+
corner_radius: float = 0.0,
|
|
787
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
788
|
+
border_width: float = 0.0,
|
|
789
|
+
antialiasing: float = 1.0) -> list[float]:
|
|
790
|
+
"""Generate vertices for a rectangle.
|
|
791
|
+
Format: pos(2f), color(4f), radius(1f), border_color(4f), border_width(1f), aa(1f), size(2f), local_pos(2f)
|
|
792
|
+
"""
|
|
793
|
+
x, y = position
|
|
794
|
+
w, h = size
|
|
795
|
+
|
|
796
|
+
# Center of rectangle
|
|
797
|
+
cx, cy = x + w / 2, y + h / 2
|
|
798
|
+
|
|
799
|
+
# Expand for border and antialiasing
|
|
800
|
+
expand = border_width + antialiasing * 2
|
|
801
|
+
|
|
802
|
+
# Rotation matrix
|
|
803
|
+
cos_a = np.cos(rotation)
|
|
804
|
+
sin_a = np.sin(rotation)
|
|
805
|
+
|
|
806
|
+
# Half dimensions
|
|
807
|
+
hw, hh = w / 2 + expand, h / 2 + expand
|
|
808
|
+
half_size = (w / 2, h / 2)
|
|
809
|
+
|
|
810
|
+
# Local corner positions
|
|
811
|
+
local_corners = [
|
|
812
|
+
(-hw, -hh), (hw, -hh), (-hw, hh),
|
|
813
|
+
(hw, -hh), (-hw, hh), (hw, hh)
|
|
814
|
+
]
|
|
815
|
+
|
|
816
|
+
vertices = []
|
|
817
|
+
for lx, ly in local_corners:
|
|
818
|
+
# Apply rotation and translate to screen space
|
|
819
|
+
rx = lx * cos_a - ly * sin_a + cx
|
|
820
|
+
ry = lx * sin_a + ly * cos_a + cy
|
|
821
|
+
|
|
822
|
+
vertices.extend([
|
|
823
|
+
rx, ry, # in_pos (screen position)
|
|
824
|
+
*color, # in_color
|
|
825
|
+
corner_radius, # in_radius
|
|
826
|
+
*border_color, # in_border_color
|
|
827
|
+
border_width, # in_border_width
|
|
828
|
+
antialiasing, # in_aa
|
|
829
|
+
*half_size, # in_size (half-width, half-height for SDF)
|
|
830
|
+
lx, ly # in_local_pos (local position relative to center)
|
|
831
|
+
])
|
|
832
|
+
|
|
833
|
+
return vertices
|
|
834
|
+
|
|
835
|
+
def draw_rect(self, position: tuple[float, float], size: tuple[float, float],
|
|
836
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
837
|
+
rotation: float = 0.0,
|
|
838
|
+
corner_radius: float = 0.0,
|
|
839
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
840
|
+
border_width: float = 0.0,
|
|
841
|
+
antialiasing: float = 1.0) -> None:
|
|
842
|
+
"""
|
|
843
|
+
Draw a rectangle immediately.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
position: (x, y) top-left corner in screen coordinates
|
|
847
|
+
size: (width, height) in pixels
|
|
848
|
+
color: (r, g, b, a) fill color
|
|
849
|
+
rotation: Rotation angle in radians around center
|
|
850
|
+
corner_radius: Radius for rounded corners in pixels
|
|
851
|
+
border_color: (r, g, b, a) border color
|
|
852
|
+
border_width: Border width in pixels (0 = no border)
|
|
853
|
+
antialiasing: Antialiasing smoothness in pixels
|
|
854
|
+
"""
|
|
855
|
+
vertices = self._generate_rect_vertices(position, size, color, rotation,
|
|
856
|
+
corner_radius, border_color, border_width, antialiasing)
|
|
857
|
+
|
|
858
|
+
data = np.array(vertices, dtype='f4')
|
|
859
|
+
self.rect_vbo.write(data.tobytes())
|
|
860
|
+
|
|
861
|
+
self.ctx.enable(moderngl.BLEND)
|
|
862
|
+
set_pattr_value(self.rect_prog, 'resolution', self.ctx.viewport[2:])
|
|
863
|
+
self.rect_vao.render(moderngl.TRIANGLES, vertices=6)
|
|
864
|
+
|
|
865
|
+
def create_rect(self, position: tuple[float, float], size: tuple[float, float],
|
|
866
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
867
|
+
rotation: float = 0.0,
|
|
868
|
+
corner_radius: float = 0.0,
|
|
869
|
+
border_color: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0),
|
|
870
|
+
border_width: float = 0.0,
|
|
871
|
+
antialiasing: float = 1.0) -> ShapeLabel:
|
|
872
|
+
"""Create a cached rectangle for repeated drawing."""
|
|
873
|
+
vertices = self._generate_rect_vertices(position, size, color, rotation,
|
|
874
|
+
corner_radius, border_color, border_width, antialiasing)
|
|
875
|
+
|
|
876
|
+
data = np.array(vertices, dtype='f4')
|
|
877
|
+
vbo = self.ctx.buffer(data.tobytes())
|
|
878
|
+
|
|
879
|
+
return ShapeLabel(self.ctx, self.rect_prog, vbo, 6, 'rect')
|
|
880
|
+
|
|
881
|
+
# ========== LINES ==========
|
|
882
|
+
|
|
883
|
+
def _generate_line_segment_vertices(self, start: tuple[float, float], end: tuple[float, float],
|
|
884
|
+
width: float, color: tuple[float, float, float, float],
|
|
885
|
+
antialias: float = 1.0) -> list[float]:
|
|
886
|
+
"""Generate vertices for a line segment as a quad."""
|
|
887
|
+
x1, y1 = start
|
|
888
|
+
x2, y2 = end
|
|
889
|
+
|
|
890
|
+
# Calculate perpendicular direction
|
|
891
|
+
dx = x2 - x1
|
|
892
|
+
dy = y2 - y1
|
|
893
|
+
length = np.sqrt(dx * dx + dy * dy)
|
|
894
|
+
|
|
895
|
+
if length < 0.001:
|
|
896
|
+
return []
|
|
897
|
+
|
|
898
|
+
# Normalized perpendicular
|
|
899
|
+
px = -dy / length
|
|
900
|
+
py = dx / length
|
|
901
|
+
|
|
902
|
+
# Half width
|
|
903
|
+
hw = width / 2 + antialias
|
|
904
|
+
|
|
905
|
+
# Quad corners
|
|
906
|
+
vertices = []
|
|
907
|
+
quad = [
|
|
908
|
+
(x1 + px * hw, y1 + py * hw),
|
|
909
|
+
(x2 + px * hw, y2 + py * hw),
|
|
910
|
+
(x1 - px * hw, y1 - py * hw),
|
|
911
|
+
(x2 + px * hw, y2 + py * hw),
|
|
912
|
+
(x1 - px * hw, y1 - py * hw),
|
|
913
|
+
(x2 - px * hw, y2 - py * hw)
|
|
914
|
+
]
|
|
915
|
+
|
|
916
|
+
for x, y in quad:
|
|
917
|
+
vertices.extend([x, y, *color])
|
|
918
|
+
|
|
919
|
+
return vertices
|
|
920
|
+
|
|
921
|
+
def draw_line(self, start: tuple[float, float], end: tuple[float, float],
|
|
922
|
+
width: float = 1.0,
|
|
923
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
924
|
+
antialiasing: float = 1.0) -> None:
|
|
925
|
+
"""
|
|
926
|
+
Draw a single line segment.
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
start: (x, y) start point in screen coordinates
|
|
930
|
+
end: (x, y) end point in screen coordinates
|
|
931
|
+
width: Line width in pixels
|
|
932
|
+
color: (r, g, b, a) line color
|
|
933
|
+
antialiasing: Antialiasing smoothness in pixels
|
|
934
|
+
"""
|
|
935
|
+
vertices = self._generate_line_segment_vertices(start, end, width, color, antialiasing)
|
|
936
|
+
|
|
937
|
+
if not vertices:
|
|
938
|
+
return
|
|
939
|
+
|
|
940
|
+
data = np.array(vertices, dtype='f4')
|
|
941
|
+
self.line_vbo.write(data.tobytes())
|
|
942
|
+
|
|
943
|
+
self.ctx.enable(moderngl.BLEND)
|
|
944
|
+
set_pattr_value(self.line_prog, 'resolution', self.ctx.viewport[2:])
|
|
945
|
+
self.line_vao.render(moderngl.TRIANGLES, vertices=6)
|
|
946
|
+
|
|
947
|
+
def draw_lines(self, points: np.ndarray | Sequence[tuple[float, float]],
|
|
948
|
+
width: float = 1.0,
|
|
949
|
+
color: tuple[float, float, float, float] | np.ndarray = (1.0, 1.0, 1.0, 1.0),
|
|
950
|
+
antialiasing: float = 1.0,
|
|
951
|
+
closed: bool = False) -> None:
|
|
952
|
+
"""
|
|
953
|
+
Draw a polyline (connected line segments).
|
|
954
|
+
|
|
955
|
+
Args:
|
|
956
|
+
points: Array of (x, y) points, shape (N, 2) or list of tuples
|
|
957
|
+
width: Line width in pixels
|
|
958
|
+
color: Single (r, g, b, a) color or array of colors per segment
|
|
959
|
+
antialiasing: Antialiasing smoothness in pixels
|
|
960
|
+
closed: If True, connect last point to first point
|
|
961
|
+
"""
|
|
962
|
+
if isinstance(points, np.ndarray):
|
|
963
|
+
points_array = points
|
|
964
|
+
else:
|
|
965
|
+
points_array = np.array(points, dtype='f4')
|
|
966
|
+
|
|
967
|
+
if len(points_array) < 2:
|
|
968
|
+
return
|
|
969
|
+
|
|
970
|
+
# Handle color
|
|
971
|
+
if isinstance(color, np.ndarray):
|
|
972
|
+
colors = color
|
|
973
|
+
else:
|
|
974
|
+
colors = np.tile(color, (len(points_array) - 1, 1))
|
|
975
|
+
|
|
976
|
+
vertices = []
|
|
977
|
+
|
|
978
|
+
# Generate line segments
|
|
979
|
+
for i in range(len(points_array) - 1):
|
|
980
|
+
start = tuple(points_array[i])
|
|
981
|
+
end = tuple(points_array[i + 1])
|
|
982
|
+
seg_color = tuple(colors[i]) if len(colors) > 1 else tuple(colors[0])
|
|
983
|
+
|
|
984
|
+
seg_verts = self._generate_line_segment_vertices(start, end, width, seg_color, antialiasing)
|
|
985
|
+
vertices.extend(seg_verts)
|
|
986
|
+
|
|
987
|
+
# Close the loop if requested
|
|
988
|
+
if closed:
|
|
989
|
+
start = tuple(points_array[-1])
|
|
990
|
+
end = tuple(points_array[0])
|
|
991
|
+
seg_color = tuple(colors[-1]) if len(colors) > 1 else tuple(colors[0])
|
|
992
|
+
seg_verts = self._generate_line_segment_vertices(start, end, width, seg_color, antialiasing)
|
|
993
|
+
vertices.extend(seg_verts)
|
|
994
|
+
|
|
995
|
+
if not vertices:
|
|
996
|
+
return
|
|
997
|
+
|
|
998
|
+
data = np.array(vertices, dtype='f4')
|
|
999
|
+
self.line_vbo.write(data.tobytes())
|
|
1000
|
+
|
|
1001
|
+
self.ctx.enable(moderngl.BLEND)
|
|
1002
|
+
set_pattr_value(self.line_prog, 'resolution', self.ctx.viewport[2:])
|
|
1003
|
+
num_segments = (len(points_array) - 1) + (1 if closed else 0)
|
|
1004
|
+
self.line_vao.render(moderngl.TRIANGLES, vertices=num_segments * 6)
|
|
1005
|
+
|
|
1006
|
+
def create_line(self, start: tuple[float, float], end: tuple[float, float],
|
|
1007
|
+
width: float = 1.0,
|
|
1008
|
+
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0),
|
|
1009
|
+
antialiasing: float = 1.0) -> ShapeLabel:
|
|
1010
|
+
"""Create a cached line for repeated drawing."""
|
|
1011
|
+
vertices = self._generate_line_segment_vertices(start, end, width, color, antialiasing)
|
|
1012
|
+
|
|
1013
|
+
data = np.array(vertices, dtype='f4')
|
|
1014
|
+
vbo = self.ctx.buffer(data.tobytes())
|
|
1015
|
+
|
|
1016
|
+
return ShapeLabel(self.ctx, self.line_prog, vbo, 6, 'line')
|
|
1017
|
+
|
|
1018
|
+
def create_lines(self, points: np.ndarray | Sequence[tuple[float, float]],
|
|
1019
|
+
width: float = 1.0,
|
|
1020
|
+
color: tuple[float, float, float, float] | np.ndarray = (1.0, 1.0, 1.0, 1.0),
|
|
1021
|
+
antialiasing: float = 1.0,
|
|
1022
|
+
closed: bool = False) -> ShapeLabel:
|
|
1023
|
+
"""Create a cached polyline for repeated drawing."""
|
|
1024
|
+
if isinstance(points, np.ndarray):
|
|
1025
|
+
points_array = points
|
|
1026
|
+
else:
|
|
1027
|
+
points_array = np.array(points, dtype='f4')
|
|
1028
|
+
|
|
1029
|
+
if isinstance(color, np.ndarray):
|
|
1030
|
+
colors = color
|
|
1031
|
+
else:
|
|
1032
|
+
colors = np.tile(color, (len(points_array) - 1, 1))
|
|
1033
|
+
|
|
1034
|
+
vertices = []
|
|
1035
|
+
|
|
1036
|
+
for i in range(len(points_array) - 1):
|
|
1037
|
+
start = tuple(points_array[i])
|
|
1038
|
+
end = tuple(points_array[i + 1])
|
|
1039
|
+
seg_color = tuple(colors[i]) if len(colors) > 1 else tuple(colors[0])
|
|
1040
|
+
|
|
1041
|
+
seg_verts = self._generate_line_segment_vertices(start, end, width, seg_color, antialiasing)
|
|
1042
|
+
vertices.extend(seg_verts)
|
|
1043
|
+
|
|
1044
|
+
if closed:
|
|
1045
|
+
start = tuple(points_array[-1])
|
|
1046
|
+
end = tuple(points_array[0])
|
|
1047
|
+
seg_color = tuple(colors[-1]) if len(colors) > 1 else tuple(colors[0])
|
|
1048
|
+
seg_verts = self._generate_line_segment_vertices(start, end, width, seg_color, antialiasing)
|
|
1049
|
+
vertices.extend(seg_verts)
|
|
1050
|
+
|
|
1051
|
+
data = np.array(vertices, dtype='f4')
|
|
1052
|
+
vbo = self.ctx.buffer(data.tobytes())
|
|
1053
|
+
|
|
1054
|
+
num_segments = (len(points_array) - 1) + (1 if closed else 0)
|
|
1055
|
+
return ShapeLabel(self.ctx, self.line_prog, vbo, num_segments * 6, 'line')
|
|
1056
|
+
|
|
1057
|
+
# ========== BATCHING ==========
|
|
1058
|
+
|
|
1059
|
+
def create_circle_batch(self, max_shapes: int = 10000) -> InstancedShapeBatch:
|
|
1060
|
+
"""Create a batch for drawing multiple circles efficiently using GPU instancing.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
max_shapes: Maximum number of shapes in the batch
|
|
1064
|
+
"""
|
|
1065
|
+
return InstancedShapeBatch(self.ctx, self.circle_instanced_prog, 'circle', max_shapes)
|
|
1066
|
+
|
|
1067
|
+
def create_rect_batch(self, max_shapes: int = 10000) -> InstancedShapeBatch:
|
|
1068
|
+
"""Create a batch for drawing multiple rectangles efficiently using GPU instancing.
|
|
1069
|
+
|
|
1070
|
+
Args:
|
|
1071
|
+
max_shapes: Maximum number of shapes in the batch
|
|
1072
|
+
"""
|
|
1073
|
+
return InstancedShapeBatch(self.ctx, self.rect_instanced_prog, 'rect', max_shapes)
|
|
1074
|
+
|
|
1075
|
+
def create_line_batch(self, max_shapes: int = 10000) -> InstancedShapeBatch:
|
|
1076
|
+
"""Create a batch for drawing multiple lines efficiently using GPU instancing.
|
|
1077
|
+
|
|
1078
|
+
Args:
|
|
1079
|
+
max_shapes: Maximum number of lines in the batch
|
|
1080
|
+
"""
|
|
1081
|
+
return InstancedShapeBatch(self.ctx, self.line_instanced_prog, 'line', max_shapes)
|