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/plots.py
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
import numpy as np
|
|
3
|
+
import moderngl
|
|
4
|
+
import struct
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
class ShaderManager:
|
|
10
|
+
"""Cache and manage shader files for the plots module."""
|
|
11
|
+
_cache = {}
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def load_shader(path: str) -> str:
|
|
15
|
+
"""Load a shader file with caching."""
|
|
16
|
+
if path not in ShaderManager._cache:
|
|
17
|
+
# Get the directory where this file is located
|
|
18
|
+
module_dir = os.path.dirname(__file__)
|
|
19
|
+
full_path = os.path.join(module_dir, path)
|
|
20
|
+
|
|
21
|
+
if not os.path.exists(full_path):
|
|
22
|
+
raise FileNotFoundError(f"Shader file not found: {full_path}")
|
|
23
|
+
with open(full_path, 'r', encoding='utf-8') as f:
|
|
24
|
+
ShaderManager._cache[path] = f.read()
|
|
25
|
+
return ShaderManager._cache[path]
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def create_program(ctx: moderngl.Context, vertex_path: str, fragment_path: str) -> moderngl.Program:
|
|
29
|
+
"""Create a program from shader files."""
|
|
30
|
+
vertex_shader = ShaderManager.load_shader(vertex_path)
|
|
31
|
+
fragment_shader = ShaderManager.load_shader(fragment_path)
|
|
32
|
+
return ctx.program(vertex_shader=vertex_shader, fragment_shader=fragment_shader)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def create_compute(ctx: moderngl.Context, compute_path: str) -> moderngl.ComputeShader:
|
|
36
|
+
"""Create a compute shader from file."""
|
|
37
|
+
compute_shader = ShaderManager.load_shader(compute_path)
|
|
38
|
+
return ctx.compute_shader(compute_shader)
|
|
39
|
+
|
|
40
|
+
class View2D:
|
|
41
|
+
"""
|
|
42
|
+
Manages coordinate space (World <-> Clip) via UBO.
|
|
43
|
+
Binding point: 0
|
|
44
|
+
Layout std140:
|
|
45
|
+
vec2 resolution; // 0
|
|
46
|
+
vec2 center; // 8
|
|
47
|
+
vec2 scale; // 16 (zoom_x, zoom_y)
|
|
48
|
+
float aspect; // 24
|
|
49
|
+
float _pad; // 28
|
|
50
|
+
"""
|
|
51
|
+
def __init__(self, ctx: moderngl.Context, binding: int = 0) -> None:
|
|
52
|
+
self.ctx = ctx
|
|
53
|
+
self.binding = binding
|
|
54
|
+
self.center = np.array([0.0, 0.0], dtype='f4')
|
|
55
|
+
self.zoom = 1.0
|
|
56
|
+
self.aspect = 1.0
|
|
57
|
+
self.resolution = np.array([1920.0, 1080.0], dtype='f4')
|
|
58
|
+
|
|
59
|
+
self.buffer = self.ctx.buffer(reserve=32)
|
|
60
|
+
self.buffer.bind_to_uniform_block(self.binding)
|
|
61
|
+
self.update_buffer()
|
|
62
|
+
|
|
63
|
+
def update_win_size(self, width: int, height: int) -> None:
|
|
64
|
+
self.resolution = np.array([width, height], dtype='f4')
|
|
65
|
+
self.aspect = width / height if height > 0 else 1.0
|
|
66
|
+
self.update_buffer()
|
|
67
|
+
|
|
68
|
+
def pan(self, dx: float, dy: float) -> None:
|
|
69
|
+
world_scale = 1.0 / self.zoom
|
|
70
|
+
self.center[0] -= dx * world_scale * self.aspect
|
|
71
|
+
self.center[1] -= dy * world_scale
|
|
72
|
+
self.update_buffer()
|
|
73
|
+
|
|
74
|
+
def zoom_step(self, factor: float) -> None:
|
|
75
|
+
self.zoom *= factor
|
|
76
|
+
self.update_buffer()
|
|
77
|
+
|
|
78
|
+
def zoom_at(self, factor: float, ndc_x: float, ndc_y: float) -> None:
|
|
79
|
+
"""Zooms by factor, keeping the point at (ndc_x, ndc_y) stationary."""
|
|
80
|
+
prev_zoom = self.zoom
|
|
81
|
+
self.zoom *= factor
|
|
82
|
+
|
|
83
|
+
diff_scale = (1.0/prev_zoom - 1.0/self.zoom)
|
|
84
|
+
self.center[0] += ndc_x * self.aspect * diff_scale
|
|
85
|
+
self.center[1] += ndc_y * diff_scale
|
|
86
|
+
|
|
87
|
+
self.update_buffer()
|
|
88
|
+
|
|
89
|
+
def update_buffer(self) -> None:
|
|
90
|
+
data = struct.pack(
|
|
91
|
+
'2f2f2f1f1f',
|
|
92
|
+
self.resolution[0], self.resolution[1],
|
|
93
|
+
self.center[0], self.center[1],
|
|
94
|
+
self.zoom, self.zoom,
|
|
95
|
+
self.aspect,
|
|
96
|
+
0.0
|
|
97
|
+
)
|
|
98
|
+
self.buffer.write(data)
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class PlotSettings:
|
|
102
|
+
bg_color: tuple = (0.1, 0.1, 0.1, 1.0)
|
|
103
|
+
show_axis: bool = True
|
|
104
|
+
axis_color: tuple = (0.5, 0.5, 0.5, 1.0)
|
|
105
|
+
axis_width: float = 2.0
|
|
106
|
+
show_grid: bool = True
|
|
107
|
+
grid_color: tuple = (0.2, 0.2, 0.2, 1.0)
|
|
108
|
+
grid_spacing: float = 1.0
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class CurveSettings:
|
|
112
|
+
color: tuple = (1.0, 1.0, 1.0, 1.0)
|
|
113
|
+
width: float = 2.0
|
|
114
|
+
count: int = 1024
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class ImplicitSettings:
|
|
118
|
+
color: tuple = (0.4, 0.6, 1.0, 1.0)
|
|
119
|
+
thickness: float = 2.0
|
|
120
|
+
|
|
121
|
+
class LineType(Enum):
|
|
122
|
+
NONE = 0
|
|
123
|
+
DIRECT = 1
|
|
124
|
+
BEZIER_QUADRATIC = 2
|
|
125
|
+
BEZIER_CUBIC = 3
|
|
126
|
+
SMOOTH = 4 # Catmull-Rom
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class StreamSettings:
|
|
130
|
+
point_color: tuple = (1.0, 0.0, 0.0, 1.0)
|
|
131
|
+
point_radius: float = 5.0
|
|
132
|
+
show_points: bool = True
|
|
133
|
+
round_points: bool = True
|
|
134
|
+
line_type: LineType = LineType.DIRECT
|
|
135
|
+
line_color: tuple = (1.0, 0.0, 0.0, 1.0)
|
|
136
|
+
line_width: float = 2.0
|
|
137
|
+
curve_segments: int = 10
|
|
138
|
+
|
|
139
|
+
class Plot2D:
|
|
140
|
+
"""A specific rectangular area on the screen for plotting."""
|
|
141
|
+
def __init__(self, ctx: moderngl.Context, top_left: tuple[int, int], bottom_right: tuple[int, int], settings: Optional[PlotSettings] = None) -> None:
|
|
142
|
+
self.ctx = ctx
|
|
143
|
+
self.top_left = top_left
|
|
144
|
+
self.bottom_right = bottom_right
|
|
145
|
+
self.settings = settings if settings else PlotSettings()
|
|
146
|
+
|
|
147
|
+
self.width = bottom_right[0] - top_left[0]
|
|
148
|
+
self.height = bottom_right[1] - top_left[1]
|
|
149
|
+
|
|
150
|
+
self.view = View2D(ctx)
|
|
151
|
+
self.view.update_win_size(self.width, self.height)
|
|
152
|
+
|
|
153
|
+
self.viewport = (top_left[0], 1080 - bottom_right[1], self.width, self.height)
|
|
154
|
+
self._init_grid_renderer()
|
|
155
|
+
|
|
156
|
+
self.is_dragging = False
|
|
157
|
+
self.last_mouse_pos = (0, 0)
|
|
158
|
+
|
|
159
|
+
def _init_grid_renderer(self) -> None:
|
|
160
|
+
self.grid_prog = ShaderManager.create_program(
|
|
161
|
+
self.ctx,
|
|
162
|
+
"shaders/plot_grid_vertex.glsl",
|
|
163
|
+
"shaders/plot_grid_fragment.glsl"
|
|
164
|
+
)
|
|
165
|
+
try:
|
|
166
|
+
self.grid_prog['View'].binding = 0 # type: ignore
|
|
167
|
+
except:
|
|
168
|
+
pass
|
|
169
|
+
self.grid_quad = self.ctx.buffer(np.array([-1,-1, 1,-1, -1,1, 1,1], dtype='f4'))
|
|
170
|
+
self.grid_vao = self.ctx.simple_vertex_array(self.grid_prog, self.grid_quad, "in_vert")
|
|
171
|
+
|
|
172
|
+
def set_rect(self, top_left: tuple[int, int], bottom_right: tuple[int, int]):
|
|
173
|
+
self.top_left = top_left
|
|
174
|
+
self.bottom_right = bottom_right
|
|
175
|
+
self.width = bottom_right[0] - top_left[0]
|
|
176
|
+
self.height = bottom_right[1] - top_left[1]
|
|
177
|
+
self.view.update_win_size(self.width, self.height)
|
|
178
|
+
|
|
179
|
+
def update_window_size(self, win_width: int, win_height: int):
|
|
180
|
+
x = self.top_left[0]
|
|
181
|
+
w = self.width
|
|
182
|
+
h = self.height
|
|
183
|
+
y = win_height - self.bottom_right[1]
|
|
184
|
+
self.viewport = (x, y, w, h)
|
|
185
|
+
|
|
186
|
+
def render(self, draw_callback):
|
|
187
|
+
self.ctx.viewport = self.viewport
|
|
188
|
+
self.ctx.scissor = self.viewport
|
|
189
|
+
self.ctx.clear(*self.settings.bg_color)
|
|
190
|
+
|
|
191
|
+
self.view.buffer.bind_to_uniform_block(0)
|
|
192
|
+
|
|
193
|
+
if self.settings.show_grid or self.settings.show_axis:
|
|
194
|
+
self.grid_prog['grid_color'] = self.settings.grid_color
|
|
195
|
+
self.grid_prog['axis_color'] = self.settings.axis_color
|
|
196
|
+
self.grid_prog['spacing'] = self.settings.grid_spacing
|
|
197
|
+
self.grid_prog['show_grid'] = self.settings.show_grid
|
|
198
|
+
self.grid_prog['show_axis'] = self.settings.show_axis
|
|
199
|
+
self.grid_vao.render(moderngl.TRIANGLE_STRIP)
|
|
200
|
+
|
|
201
|
+
draw_callback()
|
|
202
|
+
self.ctx.scissor = None
|
|
203
|
+
|
|
204
|
+
def contains(self, x, y) -> bool:
|
|
205
|
+
return (self.top_left[0] <= x <= self.bottom_right[0] and
|
|
206
|
+
self.top_left[1] <= y <= self.bottom_right[1])
|
|
207
|
+
|
|
208
|
+
def on_mouse_drag(self, dx, dy) -> None:
|
|
209
|
+
ndc_dx = (dx / self.width) * 2.0
|
|
210
|
+
ndc_dy = (dy / self.height) * 2.0
|
|
211
|
+
self.view.pan(ndc_dx, -ndc_dy)
|
|
212
|
+
|
|
213
|
+
def on_scroll(self, yoffset, mouse_x, mouse_y) -> None:
|
|
214
|
+
factor = 1.1 if yoffset > 0 else 0.9
|
|
215
|
+
rel_x = mouse_x - self.top_left[0]
|
|
216
|
+
rel_y = mouse_y - self.top_left[1]
|
|
217
|
+
ndc_x = (rel_x / self.width) * 2.0 - 1.0
|
|
218
|
+
ndc_y = 1.0 - (rel_y / self.height) * 2.0
|
|
219
|
+
self.view.zoom_at(factor, ndc_x, ndc_y)
|
|
220
|
+
|
|
221
|
+
class GpuStream:
|
|
222
|
+
"""Ring-buffer on GPU for high-performance point streaming."""
|
|
223
|
+
def __init__(self, ctx: moderngl.Context, capacity: int = 100000, settings: Optional[StreamSettings] = None) -> None:
|
|
224
|
+
self.ctx = ctx
|
|
225
|
+
self.capacity = capacity
|
|
226
|
+
self.settings = settings if settings else StreamSettings()
|
|
227
|
+
self.head = 0
|
|
228
|
+
self.size = 0
|
|
229
|
+
|
|
230
|
+
# Initialize buffer with zeros to prevent garbage data
|
|
231
|
+
self.buffer = self.ctx.buffer(data=np.zeros(capacity * 2, dtype='f4').tobytes())
|
|
232
|
+
self.buffer.bind_to_storage_buffer(binding=1)
|
|
233
|
+
|
|
234
|
+
self.prog = ShaderManager.create_program(
|
|
235
|
+
ctx,
|
|
236
|
+
"shaders/stream_vertex.glsl",
|
|
237
|
+
"shaders/stream_fragment.glsl"
|
|
238
|
+
)
|
|
239
|
+
try:
|
|
240
|
+
self.prog['View'].binding = 0 # type: ignore
|
|
241
|
+
except:
|
|
242
|
+
pass
|
|
243
|
+
self.vao = ctx.vertex_array(self.prog, [])
|
|
244
|
+
|
|
245
|
+
# Smooth line shader (Catmull-Rom)
|
|
246
|
+
self.smooth_prog = ctx.program(
|
|
247
|
+
vertex_shader="""
|
|
248
|
+
#version 430
|
|
249
|
+
layout(std140, binding=0) uniform View {
|
|
250
|
+
vec2 resolution;
|
|
251
|
+
vec2 center;
|
|
252
|
+
vec2 scale;
|
|
253
|
+
float aspect;
|
|
254
|
+
} view;
|
|
255
|
+
|
|
256
|
+
layout(std430, binding=1) buffer PointBuffer {
|
|
257
|
+
vec2 points[];
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
uniform int start_index;
|
|
261
|
+
uniform int capacity;
|
|
262
|
+
uniform int size;
|
|
263
|
+
uniform int segments;
|
|
264
|
+
uniform int type;
|
|
265
|
+
|
|
266
|
+
vec2 get_point(int i) {
|
|
267
|
+
int idx = clamp(i, 0, size - 1);
|
|
268
|
+
int real_idx = (start_index + idx) % capacity;
|
|
269
|
+
return points[real_idx];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
void main() {
|
|
273
|
+
int segment_id = gl_VertexID / segments;
|
|
274
|
+
float t = float(gl_VertexID % segments) / float(segments);
|
|
275
|
+
|
|
276
|
+
if (segment_id >= size - 1) {
|
|
277
|
+
gl_Position = vec4(2.0, 2.0, 0.0, 1.0);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
vec2 p0 = get_point(max(segment_id - 1, 0));
|
|
282
|
+
vec2 p1 = get_point(segment_id);
|
|
283
|
+
vec2 p2 = get_point(segment_id + 1);
|
|
284
|
+
vec2 p3 = get_point(min(segment_id + 2, size - 1));
|
|
285
|
+
|
|
286
|
+
vec2 pos;
|
|
287
|
+
if (type == 4) {
|
|
288
|
+
float t2 = t * t;
|
|
289
|
+
float t3 = t2 * t;
|
|
290
|
+
pos = 0.5 * ((2.0 * p1) +
|
|
291
|
+
(-p0 + p2) * t +
|
|
292
|
+
(2.0*p0 - 5.0*p1 + 4.0*p2 - p3) * t2 +
|
|
293
|
+
(-p0 + 3.0*p1 - 3.0*p2 + p3) * t3);
|
|
294
|
+
} else {
|
|
295
|
+
pos = mix(p1, p2, t);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
vec2 diff = pos - view.center;
|
|
299
|
+
vec2 norm = diff * view.scale;
|
|
300
|
+
norm.x /= view.aspect;
|
|
301
|
+
gl_Position = vec4(norm, 0.0, 1.0);
|
|
302
|
+
}
|
|
303
|
+
""",
|
|
304
|
+
fragment_shader="""
|
|
305
|
+
#version 430
|
|
306
|
+
uniform vec4 color;
|
|
307
|
+
out vec4 f_color;
|
|
308
|
+
void main() {
|
|
309
|
+
f_color = color;
|
|
310
|
+
}
|
|
311
|
+
"""
|
|
312
|
+
)
|
|
313
|
+
try:
|
|
314
|
+
self.smooth_prog['View'].binding = 0 # type: ignore
|
|
315
|
+
except:
|
|
316
|
+
pass
|
|
317
|
+
self.smooth_vao = ctx.vertex_array(self.smooth_prog, [])
|
|
318
|
+
|
|
319
|
+
def push(self, points: np.ndarray) -> None:
|
|
320
|
+
if points.shape[0] == 0:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
count = points.shape[0]
|
|
324
|
+
if count > self.capacity:
|
|
325
|
+
points = points[-self.capacity:]
|
|
326
|
+
count = self.capacity
|
|
327
|
+
|
|
328
|
+
offset = self.head * 8
|
|
329
|
+
data = points.tobytes()
|
|
330
|
+
|
|
331
|
+
if self.head + count <= self.capacity:
|
|
332
|
+
self.buffer.write(data, offset=offset)
|
|
333
|
+
else:
|
|
334
|
+
first_part = self.capacity - self.head
|
|
335
|
+
self.buffer.write(data[:first_part*8], offset=offset)
|
|
336
|
+
self.buffer.write(data[first_part*8:], offset=0)
|
|
337
|
+
|
|
338
|
+
self.head = (self.head + count) % self.capacity
|
|
339
|
+
self.size = min(self.size + count, self.capacity)
|
|
340
|
+
|
|
341
|
+
def draw(self) -> None:
|
|
342
|
+
if self.size == 0:
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
start_index = (self.head - self.size + self.capacity) % self.capacity
|
|
346
|
+
|
|
347
|
+
# Draw lines
|
|
348
|
+
if self.settings.line_type != LineType.NONE and self.size >= 2:
|
|
349
|
+
if self.settings.line_type == LineType.SMOOTH and self.size >= 2:
|
|
350
|
+
self.smooth_prog['start_index'] = start_index
|
|
351
|
+
self.smooth_prog['capacity'] = self.capacity
|
|
352
|
+
self.smooth_prog['size'] = self.size
|
|
353
|
+
self.smooth_prog['segments'] = self.settings.curve_segments
|
|
354
|
+
self.smooth_prog['type'] = 4
|
|
355
|
+
self.smooth_prog['color'] = self.settings.line_color
|
|
356
|
+
self.ctx.line_width = self.settings.line_width
|
|
357
|
+
|
|
358
|
+
num_vertices = (self.size - 1) * self.settings.curve_segments + 1
|
|
359
|
+
self.smooth_vao.render(moderngl.LINE_STRIP, vertices=num_vertices)
|
|
360
|
+
else:
|
|
361
|
+
self.prog['start_index'] = start_index
|
|
362
|
+
self.prog['capacity'] = self.capacity
|
|
363
|
+
self.prog['color'] = self.settings.line_color
|
|
364
|
+
self.ctx.line_width = self.settings.line_width
|
|
365
|
+
self.vao.render(moderngl.LINE_STRIP, vertices=self.size)
|
|
366
|
+
|
|
367
|
+
# Draw points
|
|
368
|
+
if self.settings.show_points:
|
|
369
|
+
self.prog['start_index'] = start_index
|
|
370
|
+
self.prog['capacity'] = self.capacity
|
|
371
|
+
self.prog['color'] = self.settings.point_color
|
|
372
|
+
self.prog['point_size'] = self.settings.point_radius
|
|
373
|
+
if 'round_points' in self.prog:
|
|
374
|
+
self.prog['round_points'] = self.settings.round_points
|
|
375
|
+
self.vao.render(moderngl.POINTS, vertices=self.size)
|
|
376
|
+
|
|
377
|
+
def shift_points(self, offset: tuple[float, float]) -> None:
|
|
378
|
+
"""Shifts all existing points by the given offset using a Compute Shader."""
|
|
379
|
+
if not hasattr(self, 'shift_prog'):
|
|
380
|
+
self.shift_prog = ShaderManager.create_compute(
|
|
381
|
+
self.ctx,
|
|
382
|
+
"shaders/stream_shift_compute.glsl"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
self.buffer.bind_to_storage_buffer(binding=1)
|
|
386
|
+
self.shift_prog['offset'] = offset
|
|
387
|
+
self.shift_prog['capacity'] = self.capacity
|
|
388
|
+
|
|
389
|
+
group_size = 64
|
|
390
|
+
num_groups = (self.capacity + group_size - 1) // group_size
|
|
391
|
+
self.shift_prog.run(num_groups)
|
|
392
|
+
|
|
393
|
+
class ComputeCurve:
|
|
394
|
+
"""Parametric curve p(t) evaluated entirely on GPU."""
|
|
395
|
+
def __init__(self, ctx: moderngl.Context, func_body: str, t_range: tuple, count: int = 1024, settings: Optional[CurveSettings] = None):
|
|
396
|
+
self.ctx = ctx
|
|
397
|
+
self.count = count
|
|
398
|
+
self.t_range = t_range
|
|
399
|
+
self.settings = settings if settings else CurveSettings()
|
|
400
|
+
|
|
401
|
+
self.vbo = self.ctx.buffer(reserve=count * 8)
|
|
402
|
+
|
|
403
|
+
cs_src = f"""
|
|
404
|
+
#version 430
|
|
405
|
+
layout(local_size_x=64) in;
|
|
406
|
+
|
|
407
|
+
layout(std430, binding=2) buffer Dest {{
|
|
408
|
+
vec2 vertices[];
|
|
409
|
+
}};
|
|
410
|
+
|
|
411
|
+
uniform float t0;
|
|
412
|
+
uniform float t1;
|
|
413
|
+
uniform int count;
|
|
414
|
+
|
|
415
|
+
void main() {{
|
|
416
|
+
uint id = gl_GlobalInvocationID.x;
|
|
417
|
+
if (id >= count) return;
|
|
418
|
+
|
|
419
|
+
float t_norm = float(id) / float(count - 1);
|
|
420
|
+
float t = t0 + t_norm * (t1 - t0);
|
|
421
|
+
|
|
422
|
+
float x, y;
|
|
423
|
+
{func_body}
|
|
424
|
+
vertices[id] = vec2(x, y);
|
|
425
|
+
}}
|
|
426
|
+
"""
|
|
427
|
+
self.compute_prog = ctx.compute_shader(cs_src)
|
|
428
|
+
|
|
429
|
+
self.render_prog = ShaderManager.create_program(
|
|
430
|
+
ctx,
|
|
431
|
+
"shaders/curve_vertex.glsl",
|
|
432
|
+
"shaders/curve_fragment.glsl"
|
|
433
|
+
)
|
|
434
|
+
try:
|
|
435
|
+
self.render_prog['View'].binding = 0 # type: ignore
|
|
436
|
+
except:
|
|
437
|
+
pass
|
|
438
|
+
self.vao = ctx.simple_vertex_array(self.render_prog, self.vbo, "in_pos")
|
|
439
|
+
|
|
440
|
+
def update(self):
|
|
441
|
+
self.vbo.bind_to_storage_buffer(binding=2)
|
|
442
|
+
self.compute_prog['t0'] = self.t_range[0]
|
|
443
|
+
self.compute_prog['t1'] = self.t_range[1]
|
|
444
|
+
self.compute_prog['count'] = self.count
|
|
445
|
+
|
|
446
|
+
group_size = 64
|
|
447
|
+
num_groups = (self.count + group_size - 1) // group_size
|
|
448
|
+
self.compute_prog.run(num_groups)
|
|
449
|
+
|
|
450
|
+
def draw(self):
|
|
451
|
+
self.render_prog['color'] = self.settings.color
|
|
452
|
+
self.ctx.line_width = self.settings.width
|
|
453
|
+
self.vao.render(moderngl.LINE_STRIP)
|
|
454
|
+
|
|
455
|
+
class ImplicitPlot:
|
|
456
|
+
"""Rendering of f(x,y)=0 via Fragment Shader and SDF."""
|
|
457
|
+
def __init__(self, ctx: moderngl.Context, func_body: str, settings: Optional[ImplicitSettings] = None):
|
|
458
|
+
self.ctx = ctx
|
|
459
|
+
self.settings = settings if settings else ImplicitSettings()
|
|
460
|
+
|
|
461
|
+
self.quad = self.ctx.buffer(np.array([-1,-1, 1,-1, -1,1, 1,1], dtype='f4'))
|
|
462
|
+
|
|
463
|
+
fs_src = f"""
|
|
464
|
+
#version 430
|
|
465
|
+
layout(std140, binding=0) uniform View {{
|
|
466
|
+
vec2 resolution;
|
|
467
|
+
vec2 center;
|
|
468
|
+
vec2 scale;
|
|
469
|
+
float aspect;
|
|
470
|
+
}} view;
|
|
471
|
+
|
|
472
|
+
uniform vec4 color;
|
|
473
|
+
uniform float thickness;
|
|
474
|
+
|
|
475
|
+
in vec2 uv;
|
|
476
|
+
out vec4 f_color;
|
|
477
|
+
|
|
478
|
+
void main() {{
|
|
479
|
+
vec2 ndc = uv * 2.0 - 1.0;
|
|
480
|
+
ndc.x *= view.aspect;
|
|
481
|
+
vec2 p = (ndc / view.scale) + view.center;
|
|
482
|
+
|
|
483
|
+
float x = p.x;
|
|
484
|
+
float y = p.y;
|
|
485
|
+
float val;
|
|
486
|
+
|
|
487
|
+
{func_body}
|
|
488
|
+
|
|
489
|
+
float dist = abs(val) / length(vec2(dFdx(val), dFdy(val)));
|
|
490
|
+
float alpha = 1.0 - smoothstep(thickness - 1.0, thickness, dist);
|
|
491
|
+
|
|
492
|
+
if (alpha <= 0.0) discard;
|
|
493
|
+
f_color = vec4(color.rgb, color.a * alpha);
|
|
494
|
+
}}
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
vs_src = """
|
|
498
|
+
#version 430
|
|
499
|
+
in vec2 in_vert;
|
|
500
|
+
out vec2 uv;
|
|
501
|
+
void main() {
|
|
502
|
+
uv = in_vert * 0.5 + 0.5;
|
|
503
|
+
gl_Position = vec4(in_vert, 0.0, 1.0);
|
|
504
|
+
}
|
|
505
|
+
"""
|
|
506
|
+
|
|
507
|
+
self.prog = ctx.program(vertex_shader=vs_src, fragment_shader=fs_src)
|
|
508
|
+
try:
|
|
509
|
+
self.prog['View'].binding = 0 # type: ignore
|
|
510
|
+
except:
|
|
511
|
+
pass
|
|
512
|
+
self.vao = ctx.simple_vertex_array(self.prog, self.quad, "in_vert")
|
|
513
|
+
|
|
514
|
+
def draw(self):
|
|
515
|
+
self.prog['color'] = self.settings.color
|
|
516
|
+
self.prog['thickness'] = self.settings.thickness
|
|
517
|
+
self.vao.render(moderngl.TRIANGLE_STRIP)
|
|
518
|
+
|
|
519
|
+
class SegmentDisplay:
|
|
520
|
+
"""Simple 7-segment display renderer for numbers."""
|
|
521
|
+
def __init__(self, ctx: moderngl.Context):
|
|
522
|
+
self.ctx = ctx
|
|
523
|
+
self.prog = ShaderManager.create_program(
|
|
524
|
+
ctx,
|
|
525
|
+
"shaders/segment_vertex.glsl",
|
|
526
|
+
"shaders/segment_fragment.glsl"
|
|
527
|
+
)
|
|
528
|
+
self.vbo = ctx.buffer(reserve=4096)
|
|
529
|
+
self.vao = ctx.simple_vertex_array(self.prog, self.vbo, 'in_pos')
|
|
530
|
+
|
|
531
|
+
# 7-segment definitions (0-9)
|
|
532
|
+
self.digits = {
|
|
533
|
+
'0': [0, 1, 2, 4, 5, 6],
|
|
534
|
+
'1': [2, 5],
|
|
535
|
+
'2': [0, 2, 3, 4, 6],
|
|
536
|
+
'3': [0, 2, 3, 5, 6],
|
|
537
|
+
'4': [1, 2, 3, 5],
|
|
538
|
+
'5': [0, 1, 3, 5, 6],
|
|
539
|
+
'6': [0, 1, 3, 4, 5, 6],
|
|
540
|
+
'7': [0, 2, 5],
|
|
541
|
+
'8': [0, 1, 2, 3, 4, 5, 6],
|
|
542
|
+
'9': [0, 1, 2, 3, 5, 6],
|
|
543
|
+
'.': [7]
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
def draw_number(self, text: str, x: float, y: float, size: float = 20.0, color: tuple = (1.0, 1.0, 1.0, 1.0)):
|
|
547
|
+
vertices = []
|
|
548
|
+
cursor_x = x
|
|
549
|
+
w = size * 0.5
|
|
550
|
+
h = size
|
|
551
|
+
|
|
552
|
+
for char in str(text):
|
|
553
|
+
if char not in self.digits:
|
|
554
|
+
cursor_x += size * 0.5
|
|
555
|
+
continue
|
|
556
|
+
|
|
557
|
+
segs = self.digits[char]
|
|
558
|
+
lines = []
|
|
559
|
+
if 0 in segs: lines.extend([(0,0), (w,0)])
|
|
560
|
+
if 1 in segs: lines.extend([(0,0), (0,h/2)])
|
|
561
|
+
if 2 in segs: lines.extend([(w,0), (w,h/2)])
|
|
562
|
+
if 3 in segs: lines.extend([(0,h/2), (w,h/2)])
|
|
563
|
+
if 4 in segs: lines.extend([(0,h/2), (0,h)])
|
|
564
|
+
if 5 in segs: lines.extend([(w,h/2), (w,h)])
|
|
565
|
+
if 6 in segs: lines.extend([(0,h), (w,h)])
|
|
566
|
+
if 7 in segs: lines.extend([(w/2, h-size*0.1), (w/2, h)])
|
|
567
|
+
|
|
568
|
+
for lx, ly in lines:
|
|
569
|
+
vertices.append(cursor_x + lx)
|
|
570
|
+
vertices.append(y + ly)
|
|
571
|
+
|
|
572
|
+
cursor_x += size * 0.8
|
|
573
|
+
|
|
574
|
+
if not vertices:
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
data = np.array(vertices, dtype='f4')
|
|
578
|
+
self.vbo.write(data.tobytes())
|
|
579
|
+
|
|
580
|
+
fb_size = self.ctx.viewport[2:]
|
|
581
|
+
self.prog['resolution'] = fb_size
|
|
582
|
+
self.prog['color'] = color
|
|
583
|
+
|
|
584
|
+
self.vao.render(moderngl.LINES, vertices=len(vertices)//2)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#version 430
|
|
2
|
+
layout(std140, binding=0) uniform View {
|
|
3
|
+
vec2 resolution;
|
|
4
|
+
vec2 center;
|
|
5
|
+
vec2 scale;
|
|
6
|
+
float aspect;
|
|
7
|
+
} view;
|
|
8
|
+
|
|
9
|
+
in vec2 in_pos;
|
|
10
|
+
|
|
11
|
+
void main() {
|
|
12
|
+
vec2 diff = in_pos - view.center;
|
|
13
|
+
vec2 norm = diff * view.scale;
|
|
14
|
+
norm.x /= view.aspect;
|
|
15
|
+
gl_Position = vec4(norm, 0.0, 1.0);
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#version 430
|
|
2
|
+
|
|
3
|
+
// Per-vertex attributes (quad corners: 4 vertices per instance)
|
|
4
|
+
in vec2 in_quad_pos; // (-1,-1), (1,-1), (1,1), (-1,1)
|
|
5
|
+
|
|
6
|
+
// Per-instance attributes
|
|
7
|
+
in vec2 in_start;
|
|
8
|
+
in vec2 in_end;
|
|
9
|
+
in float in_width;
|
|
10
|
+
in vec4 in_color;
|
|
11
|
+
|
|
12
|
+
out vec4 v_color;
|
|
13
|
+
|
|
14
|
+
uniform vec2 resolution;
|
|
15
|
+
|
|
16
|
+
void main() {
|
|
17
|
+
// Calculate line direction and perpendicular
|
|
18
|
+
vec2 line_vec = in_end - in_start;
|
|
19
|
+
float line_length = length(line_vec);
|
|
20
|
+
vec2 line_dir = line_vec / line_length;
|
|
21
|
+
vec2 line_perp = vec2(-line_dir.y, line_dir.x);
|
|
22
|
+
|
|
23
|
+
// Calculate half-width
|
|
24
|
+
float half_width = in_width * 0.5;
|
|
25
|
+
|
|
26
|
+
// Expand quad along line direction and perpendicular
|
|
27
|
+
vec2 world_pos = in_start +
|
|
28
|
+
line_dir * (in_quad_pos.x * line_length * 0.5 + line_length * 0.5) +
|
|
29
|
+
line_perp * (in_quad_pos.y * half_width);
|
|
30
|
+
|
|
31
|
+
// Convert to NDC
|
|
32
|
+
vec2 ndc = (world_pos / resolution) * 2.0 - 1.0;
|
|
33
|
+
ndc.y = -ndc.y;
|
|
34
|
+
|
|
35
|
+
gl_Position = vec4(ndc, 0.0, 1.0);
|
|
36
|
+
v_color = in_color;
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#version 430
|
|
2
|
+
layout(std140, binding=0) uniform View {
|
|
3
|
+
vec2 resolution;
|
|
4
|
+
vec2 center;
|
|
5
|
+
vec2 scale;
|
|
6
|
+
float aspect;
|
|
7
|
+
} view;
|
|
8
|
+
|
|
9
|
+
uniform vec4 grid_color;
|
|
10
|
+
uniform vec4 axis_color;
|
|
11
|
+
uniform float spacing;
|
|
12
|
+
uniform bool show_grid;
|
|
13
|
+
uniform bool show_axis;
|
|
14
|
+
|
|
15
|
+
in vec2 uv;
|
|
16
|
+
out vec4 color;
|
|
17
|
+
|
|
18
|
+
void main() {
|
|
19
|
+
// Calculate world position
|
|
20
|
+
vec2 ndc = uv * 2.0 - 1.0;
|
|
21
|
+
ndc.x *= view.aspect;
|
|
22
|
+
vec2 world_pos = (ndc / view.scale) + view.center;
|
|
23
|
+
|
|
24
|
+
vec4 final_color = vec4(0.0);
|
|
25
|
+
|
|
26
|
+
// Grid
|
|
27
|
+
if (show_grid) {
|
|
28
|
+
vec2 grid = abs(fract(world_pos / spacing - 0.5) - 0.5) / (length(vec2(dFdx(world_pos.x/spacing), dFdy(world_pos.y/spacing))));
|
|
29
|
+
float line = min(grid.x, grid.y);
|
|
30
|
+
float alpha = 1.0 - smoothstep(0.0, 1.5, line);
|
|
31
|
+
if (alpha > 0.0) {
|
|
32
|
+
final_color = mix(final_color, grid_color, alpha);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Axis
|
|
37
|
+
if (show_axis) {
|
|
38
|
+
vec2 axis = abs(world_pos) / (length(vec2(dFdx(world_pos.x), dFdy(world_pos.y))));
|
|
39
|
+
float axis_line = min(axis.x, axis.y);
|
|
40
|
+
float axis_alpha = 1.0 - smoothstep(0.0, 2.0, axis_line);
|
|
41
|
+
if (axis_alpha > 0.0) {
|
|
42
|
+
final_color = mix(final_color, axis_color, axis_alpha);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (final_color.a <= 0.0) discard;
|
|
47
|
+
color = final_color;
|
|
48
|
+
}
|