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/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,6 @@
1
+ #version 430
2
+ uniform vec4 color;
3
+ out vec4 f_color;
4
+ void main() {
5
+ f_color = color;
6
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ #version 430
2
+ in vec2 in_vert;
3
+ out vec2 uv;
4
+ void main() {
5
+ uv = in_vert * 0.5 + 0.5;
6
+ gl_Position = vec4(in_vert, 0.0, 1.0);
7
+ }