glplot 0.1.0__py3-none-any.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.
File without changes
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Tuple, Optional, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ..engine import GPULinePlot
8
+ from ..core.context import RenderContext
9
+
10
+ @dataclass
11
+ class AxisTicks:
12
+ major: np.ndarray = field(default_factory=lambda: np.array([]))
13
+ labels: List[str] = field(default_factory=list)
14
+
15
+ class AxisManager:
16
+ """
17
+ Logical manager for coordinate axes.
18
+ Generates 'nice' ticks and labels based on the visible data range.
19
+ """
20
+ def __init__(self, plot: GPULinePlot):
21
+ self.plot = plot
22
+ self.options = plot.options
23
+ self.ticks_x = AxisTicks()
24
+ self.ticks_y = AxisTicks()
25
+
26
+ def update(self, ctx: RenderContext) -> None:
27
+ """Recalculate ticks for the current view."""
28
+ # Target ~7 ticks for X, ~6 for Y as suggested by user
29
+ target_x = max(4, int(ctx.width_px / 160))
30
+ target_y = max(4, int(ctx.height_px / 120))
31
+
32
+ win = ctx.window_world
33
+ self.ticks_x = self._generate_ticks(win[0], win[1], target_x)
34
+ self.ticks_y = self._generate_ticks(win[2], win[3], target_y)
35
+
36
+ def _generate_ticks(self, vmin: float, vmax: float, target_count: int) -> AxisTicks:
37
+ if vmin >= vmax: return AxisTicks()
38
+
39
+ span = vmax - vmin
40
+ raw_step = span / max(1, target_count)
41
+
42
+ # Nice Step Algorithm (1, 2, 5 x 10^n)
43
+ p = 10 ** np.floor(np.log10(raw_step))
44
+ m = raw_step / p
45
+
46
+ if m < 1.5: step = 1.0 * p
47
+ elif m < 3.5: step = 2.0 * p
48
+ elif m < 7.5: step = 5.0 * p
49
+ else: step = 10.0 * p
50
+
51
+ # Calculate start/end
52
+ start = np.ceil(vmin / step) * step
53
+ end = np.floor(vmax / step) * step
54
+
55
+ if start > end: return AxisTicks()
56
+
57
+ major = np.arange(start, end + step/2, step)
58
+
59
+ # Generate labels
60
+ labels = []
61
+ precision = max(0, int(-np.floor(np.log10(step)))) if step < 1 else 0
62
+ fmt = f"{{:.{precision}f}}"
63
+ for v in major:
64
+ labels.append(fmt.format(v))
65
+
66
+ return AxisTicks(major=major, labels=labels)
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ import glfw
4
+ from OpenGL.GL import *
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ..utils.gl_utils import link_program
8
+ from ..utils.shaders import (
9
+ POST_FX_VS,
10
+ GRADIENT_BG_FS,
11
+ BLOOM_EXTRACT_FS,
12
+ GAUSSIAN_BLUR_FS,
13
+ POST_COMPOSITE_FS,
14
+ )
15
+ from ..renderers.base import GLOffscreenTarget
16
+
17
+ if TYPE_CHECKING:
18
+ from ..engine import GPULinePlot
19
+
20
+
21
+ class EffectManager:
22
+ """
23
+ Post-processing manager.
24
+
25
+ Design goals:
26
+ - Zero meaningful overhead when all effects are disabled.
27
+ - Lazy initialization of shaders/FBOs.
28
+ - Explicit scene render flow:
29
+ begin_scene()
30
+ draw_background()
31
+ <draw scene here>
32
+ end_scene()
33
+ """
34
+
35
+ def __init__(self, plot: "GPULinePlot"):
36
+ self.plot = plot
37
+ self.options = plot.options
38
+
39
+ # Programs
40
+ self.prog_bg = 0
41
+ self.prog_extract = 0
42
+ self.prog_blur = 0
43
+ self.prog_composite = 0
44
+
45
+ # Cached uniform locations
46
+ self.u_bg_top_color = -1
47
+ self.u_bg_bottom_color = -1
48
+
49
+ self.u_extract_tex = -1
50
+ self.u_extract_threshold = -1
51
+
52
+ self.u_blur_tex = -1
53
+ self.u_blur_horizontal = -1
54
+ self.u_blur_radius = -1
55
+
56
+ self.u_comp_scene_tex = -1
57
+ self.u_comp_bloom_tex = -1
58
+ self.u_comp_bloom_enabled = -1
59
+ self.u_comp_bloom_intensity = -1
60
+
61
+ # FBOs
62
+ self.scene_fbo = GLOffscreenTarget()
63
+ self.extract_fbo = GLOffscreenTarget()
64
+ self.ping_fbo = GLOffscreenTarget()
65
+ self.pong_fbo = GLOffscreenTarget()
66
+
67
+ # Fullscreen quad
68
+ self.quad_vao = 0
69
+
70
+ self.initialized = False
71
+
72
+ # ------------------------------------------------------------------
73
+ # Public state
74
+ # ------------------------------------------------------------------
75
+
76
+ def any_post_enabled(self) -> bool:
77
+ v = self.options.visual
78
+ return v.glow.enabled
79
+
80
+ # ------------------------------------------------------------------
81
+ # Lifecycle
82
+ # ------------------------------------------------------------------
83
+
84
+ def ensure_resources(self) -> None:
85
+ if self.initialized:
86
+ return
87
+
88
+ self.prog_bg = link_program(POST_FX_VS, GRADIENT_BG_FS)
89
+ self.prog_extract = link_program(POST_FX_VS, BLOOM_EXTRACT_FS)
90
+ self.prog_blur = link_program(POST_FX_VS, GAUSSIAN_BLUR_FS)
91
+ self.prog_composite = link_program(POST_FX_VS, POST_COMPOSITE_FS)
92
+
93
+ self.u_bg_top_color = glGetUniformLocation(self.prog_bg, "u_top_color")
94
+ self.u_bg_bottom_color = glGetUniformLocation(self.prog_bg, "u_bottom_color")
95
+
96
+ self.u_extract_tex = glGetUniformLocation(self.prog_extract, "u_tex")
97
+ self.u_extract_threshold = glGetUniformLocation(self.prog_extract, "u_threshold")
98
+
99
+ self.u_blur_tex = glGetUniformLocation(self.prog_blur, "u_tex")
100
+ self.u_blur_horizontal = glGetUniformLocation(self.prog_blur, "u_horizontal")
101
+ self.u_blur_radius = glGetUniformLocation(self.prog_blur, "u_radius")
102
+
103
+ self.u_comp_scene_tex = glGetUniformLocation(self.prog_composite, "u_scene_tex")
104
+ self.u_comp_bloom_tex = glGetUniformLocation(self.prog_composite, "u_bloom_tex")
105
+ self.u_comp_bloom_enabled = glGetUniformLocation(self.prog_composite, "u_bloom_enabled")
106
+ self.u_comp_bloom_intensity = glGetUniformLocation(self.prog_composite, "u_bloom_intensity")
107
+
108
+ self.quad_vao = glGenVertexArrays(1)
109
+ self._rebuild_targets()
110
+ self.initialized = True
111
+
112
+ def shutdown(self) -> None:
113
+ self._destroy_target(self.scene_fbo)
114
+ self._destroy_target(self.extract_fbo)
115
+ self._destroy_target(self.ping_fbo)
116
+ self._destroy_target(self.pong_fbo)
117
+
118
+ if self.quad_vao:
119
+ glDeleteVertexArrays(1, [self.quad_vao])
120
+ self.quad_vao = 0
121
+
122
+ for prog in (self.prog_bg, self.prog_extract, self.prog_blur, self.prog_composite):
123
+ if prog:
124
+ glDeleteProgram(prog)
125
+
126
+ self.prog_bg = 0
127
+ self.prog_extract = 0
128
+ self.prog_blur = 0
129
+ self.prog_composite = 0
130
+ self.initialized = False
131
+
132
+ def on_resize(self) -> None:
133
+ if self.initialized:
134
+ self._rebuild_targets()
135
+
136
+ # ------------------------------------------------------------------
137
+ # Scene flow
138
+ # ------------------------------------------------------------------
139
+
140
+ def begin_scene(self) -> None:
141
+ """
142
+ Bind the correct render target for the scene.
143
+ """
144
+ if not self.any_post_enabled():
145
+ glBindFramebuffer(GL_FRAMEBUFFER, 0)
146
+ glViewport(0, 0, self.plot.fb_width, self.plot.fb_height)
147
+ return
148
+
149
+ self.ensure_resources()
150
+ glBindFramebuffer(GL_FRAMEBUFFER, self.scene_fbo.fbo)
151
+ glViewport(0, 0, self.scene_fbo.width, self.scene_fbo.height)
152
+ glClearColor(0.0, 0.0, 0.0, 0.0)
153
+ glClear(GL_COLOR_BUFFER_BIT)
154
+
155
+ def end_scene(self) -> None:
156
+ """
157
+ Resolve post-processing if needed.
158
+ """
159
+ if self.any_post_enabled():
160
+ glBindFramebuffer(GL_FRAMEBUFFER, 0)
161
+ self.resolve()
162
+
163
+ # ------------------------------------------------------------------
164
+ # Background
165
+ # ------------------------------------------------------------------
166
+
167
+ def draw_background(self) -> None:
168
+ """
169
+ Draw the background into whichever framebuffer is currently bound.
170
+ """
171
+ v = self.options.visual.gradient_background
172
+
173
+ if not v.enabled:
174
+ c = self.options.visual.background_color
175
+ glClearColor(c[0], c[1], c[2], 1.0)
176
+ glClear(GL_COLOR_BUFFER_BIT)
177
+ return
178
+
179
+ self.ensure_resources()
180
+ glDisable(GL_BLEND)
181
+
182
+ # Disable world clipping for background pass
183
+ if self.options.enable_clipping_optimization:
184
+ for i in range(4): glDisable(GL_CLIP_DISTANCE0 + i)
185
+
186
+ glUseProgram(self.prog_bg)
187
+ glUniform3f(self.u_bg_top_color, *v.top_color)
188
+ glUniform3f(self.u_bg_bottom_color, *v.bottom_color)
189
+
190
+ glBindVertexArray(self.quad_vao)
191
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
192
+ glBindVertexArray(0)
193
+ glUseProgram(0)
194
+
195
+ # ------------------------------------------------------------------
196
+ # Post-processing
197
+ # ------------------------------------------------------------------
198
+
199
+ def resolve(self) -> None:
200
+ """
201
+ Perform post-processing and draw to the default framebuffer.
202
+ """
203
+ v = self.options.visual
204
+ if not self.any_post_enabled():
205
+ return
206
+
207
+ self.ensure_resources()
208
+
209
+ glDisable(GL_BLEND)
210
+ glBindVertexArray(self.quad_vao)
211
+
212
+ # 1) Bright-pass extraction
213
+ if v.glow.enabled:
214
+ glBindFramebuffer(GL_FRAMEBUFFER, self.extract_fbo.fbo)
215
+ glViewport(0, 0, self.extract_fbo.width, self.extract_fbo.height)
216
+ glUseProgram(self.prog_extract)
217
+
218
+ glActiveTexture(GL_TEXTURE0)
219
+ glBindTexture(GL_TEXTURE_2D, self.scene_fbo.tex)
220
+ glUniform1i(self.u_extract_tex, 0)
221
+ glUniform1f(self.u_extract_threshold, v.glow.threshold)
222
+
223
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
224
+
225
+ # 2) Horizontal blur
226
+ glBindFramebuffer(GL_FRAMEBUFFER, self.ping_fbo.fbo)
227
+ glViewport(0, 0, self.ping_fbo.width, self.ping_fbo.height)
228
+ glUseProgram(self.prog_blur)
229
+
230
+ glActiveTexture(GL_TEXTURE0)
231
+ glBindTexture(GL_TEXTURE_2D, self.extract_fbo.tex)
232
+ glUniform1i(self.u_blur_tex, 0)
233
+ glUniform1i(self.u_blur_horizontal, 1)
234
+ glUniform1f(self.u_blur_radius, v.glow.radius_px)
235
+
236
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
237
+
238
+ # 3) Vertical blur
239
+ glBindFramebuffer(GL_FRAMEBUFFER, self.pong_fbo.fbo)
240
+ glViewport(0, 0, self.pong_fbo.width, self.pong_fbo.height)
241
+
242
+ glActiveTexture(GL_TEXTURE0)
243
+ glBindTexture(GL_TEXTURE_2D, self.ping_fbo.tex)
244
+ glUniform1i(self.u_blur_tex, 0)
245
+ glUniform1i(self.u_blur_horizontal, 0)
246
+ glUniform1f(self.u_blur_radius, v.glow.radius_px)
247
+
248
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
249
+
250
+ # 4) Final composite to default framebuffer
251
+ glBindFramebuffer(GL_FRAMEBUFFER, 0)
252
+ glViewport(0, 0, self.plot.fb_width, self.plot.fb_height)
253
+ glUseProgram(self.prog_composite)
254
+
255
+ glActiveTexture(GL_TEXTURE0)
256
+ glBindTexture(GL_TEXTURE_2D, self.scene_fbo.tex)
257
+ glUniform1i(self.u_comp_scene_tex, 0)
258
+
259
+ glActiveTexture(GL_TEXTURE1)
260
+ glBindTexture(GL_TEXTURE_2D, self.pong_fbo.tex if v.glow.enabled else self.scene_fbo.tex)
261
+ glUniform1i(self.u_comp_bloom_tex, 1)
262
+
263
+ glUniform1i(self.u_comp_bloom_enabled, 1 if v.glow.enabled else 0)
264
+ glUniform1f(self.u_comp_bloom_intensity, v.glow.intensity)
265
+
266
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
267
+
268
+ if self.options.enable_clipping_optimization:
269
+ # Re-enable for subsequent exact draws if they don't explicitly handle it
270
+ for i in range(4): glEnable(GL_CLIP_DISTANCE0 + i)
271
+
272
+ glBindVertexArray(0)
273
+ glUseProgram(0)
274
+
275
+ # ------------------------------------------------------------------
276
+ # Internal helpers
277
+ # ------------------------------------------------------------------
278
+
279
+ def _rebuild_targets(self) -> None:
280
+ w, h = self.plot.fb_width, self.plot.fb_height
281
+ if w <= 0 or h <= 0:
282
+ return
283
+
284
+ self._destroy_target(self.scene_fbo)
285
+ self._destroy_target(self.extract_fbo)
286
+ self._destroy_target(self.ping_fbo)
287
+ self._destroy_target(self.pong_fbo)
288
+
289
+ # Scene target: full res, float
290
+ self.scene_fbo = self._create_target(w, h, GL_RGBA16F)
291
+
292
+ # Bloom chain: half res
293
+ bw = max(1, int(round(w * 0.5)))
294
+ bh = max(1, int(round(h * 0.5)))
295
+ self.extract_fbo = self._create_target(bw, bh, GL_RGBA8)
296
+ self.ping_fbo = self._create_target(bw, bh, GL_RGBA8)
297
+ self.pong_fbo = self._create_target(bw, bh, GL_RGBA8)
298
+
299
+ def _create_target(self, w: int, h: int, internal_format: int) -> GLOffscreenTarget:
300
+ tex = glGenTextures(1)
301
+ glBindTexture(GL_TEXTURE_2D, tex)
302
+
303
+ if internal_format == GL_RGBA16F:
304
+ fmt = GL_RGBA
305
+ dtype = GL_FLOAT
306
+ elif internal_format == GL_RGBA8:
307
+ fmt = GL_RGBA
308
+ dtype = GL_UNSIGNED_BYTE
309
+ elif internal_format == GL_R32F:
310
+ fmt = GL_RED
311
+ dtype = GL_FLOAT
312
+ elif internal_format == GL_R32I:
313
+ fmt = GL_RED_INTEGER
314
+ dtype = GL_INT
315
+ else:
316
+ raise ValueError(f"Unsupported internal format: {internal_format}")
317
+
318
+ glTexImage2D(GL_TEXTURE_2D, 0, internal_format, w, h, 0, fmt, dtype, None)
319
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
320
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
321
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
322
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
323
+
324
+ fbo = glGenFramebuffers(1)
325
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo)
326
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0)
327
+
328
+ status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
329
+ glBindFramebuffer(GL_FRAMEBUFFER, 0)
330
+ if status != GL_FRAMEBUFFER_COMPLETE:
331
+ raise RuntimeError(f"Framebuffer incomplete: {status}")
332
+
333
+ return GLOffscreenTarget(fbo=fbo, tex=tex, width=w, height=h)
334
+
335
+ def _destroy_target(self, target: GLOffscreenTarget) -> None:
336
+ if target.fbo:
337
+ glDeleteFramebuffers(1, [target.fbo])
338
+ if target.tex:
339
+ glDeleteTextures(1, [target.tex])
340
+ target.fbo = 0
341
+ target.tex = 0
342
+ target.width = 0
343
+ target.height = 0