glplot 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- {glplot-0.1.1 → glplot-0.1.2}/.coverage +0 -0
- glplot-0.1.2/.gitignore +1 -0
- glplot-0.1.2/.vscode/settings.json +11 -0
- glplot-0.1.2/LICENSE +21 -0
- {glplot-0.1.1 → glplot-0.1.2}/PKG-INFO +1 -1
- glplot-0.1.2/examples/ex_read_density.py +33 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/controllers.py +62 -3
- {glplot-0.1.1 → glplot-0.1.2}/glplot/engine.py +129 -29
- {glplot-0.1.1 → glplot-0.1.2}/glplot/managers/effects.py +14 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/managers/hud.py +90 -37
- {glplot-0.1.1 → glplot-0.1.2}/glplot/options.py +6 -1
- {glplot-0.1.1 → glplot-0.1.2}/glplot/policy.py +10 -2
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/axis.py +51 -7
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/density.py +64 -2
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/interaction.py +2 -1
- {glplot-0.1.1 → glplot-0.1.2}/glplot/utils/mpl_bridge.py +3 -4
- {glplot-0.1.1 → glplot-0.1.2}/glplot/utils/shaders.py +210 -5
- {glplot-0.1.1 → glplot-0.1.2}/imgui.ini +9 -4
- {glplot-0.1.1 → glplot-0.1.2}/pyproject.toml +1 -1
- {glplot-0.1.1 → glplot-0.1.2}/tests/test_backend.py +18 -0
- glplot-0.1.1/LICENSE +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/README.md +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_aspect_ratio_fix.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_density.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_density_gain.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_expert_performance.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_full_showcase.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_mpl_bridge.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_scatter.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_simple.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_v2_layers.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/ex_viewports.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/examples/verify_polyline_cmap.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/__init__.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/backend.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/core/__init__.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/core/context.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/core/layers.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/core/legacy.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/managers/__init__.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/managers/axis.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/managers/hud_state.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/managers/picking.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/managers/renderer_manager.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/pyplot.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/__init__.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/base.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/exact.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/line_family.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/patch.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/polyline.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/scatter.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/renderers/text.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/scratch/__init__.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/utils/__init__.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/utils/export.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/glplot/utils/gl_utils.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/check_gl_limits.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/check_imgui.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/check_import.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/check_runtime_math.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/diag_view.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/smoke_test.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/test_asymmetric_projection.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/test_autoscale_all.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/scratch/test_modular_export.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/tests/test_camera_anisotropy.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/tests/test_pyplot.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/tests/verify_blending_extension.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/tests/verify_density.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/tests/verify_headless.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/tests/verify_phase4.py +0 -0
- {glplot-0.1.1 → glplot-0.1.2}/tests/verify_phase5_density.py +0 -0
|
Binary file
|
glplot-0.1.2/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.DS_Store
|
glplot-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Juan Manuel Lombardi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import glplot.pyplot as gplt
|
|
3
|
+
|
|
4
|
+
def demo_read_density():
|
|
5
|
+
n = 500000
|
|
6
|
+
a = np.random.randn(n) * 0.15
|
|
7
|
+
b = np.random.randn(n) * 0.15
|
|
8
|
+
|
|
9
|
+
# Enable density mode
|
|
10
|
+
gplt.figure("Read Density Example", density=True, hud=True)
|
|
11
|
+
gplt.lines(a, b, x_range=(-1, 1), color='cyan')
|
|
12
|
+
|
|
13
|
+
# We retrieve the GPULinePlot engine instance
|
|
14
|
+
plot = gplt.gcf()
|
|
15
|
+
|
|
16
|
+
# Force a single frame draw so densities are accumulated on the GPU before readback.
|
|
17
|
+
plot._draw_exact_view()
|
|
18
|
+
|
|
19
|
+
# Retrieve the densities as a 2D numpy array
|
|
20
|
+
densities = plot.get_density_array()
|
|
21
|
+
|
|
22
|
+
print("\n--- Density Array Extracted via API ---")
|
|
23
|
+
print(f"Array shape: {densities.shape} (height, width)")
|
|
24
|
+
print(f"Data type: {densities.dtype}")
|
|
25
|
+
print(f"Min density: {densities.min()}")
|
|
26
|
+
print(f"Max density: {densities.max()}")
|
|
27
|
+
print(f"Mean density: {densities.mean()}")
|
|
28
|
+
print("----------------------------------------\n")
|
|
29
|
+
|
|
30
|
+
gplt.show()
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
demo_read_density()
|
|
@@ -28,12 +28,71 @@ class CameraController:
|
|
|
28
28
|
|
|
29
29
|
def mvp(self, width: int, height: int, window: Optional[Tuple[float, float, float, float]] = None) -> np.ndarray:
|
|
30
30
|
l, r, b, t = window if window is not None else self.world_window(width, height)
|
|
31
|
-
|
|
31
|
+
|
|
32
|
+
# Apply inset margins for axes and labels visibility
|
|
33
|
+
margin_l = 60.0
|
|
34
|
+
margin_r = 20.0
|
|
35
|
+
margin_b = 40.0
|
|
36
|
+
margin_t = 20.0
|
|
37
|
+
|
|
38
|
+
rl, tb = (r - l), (t - b)
|
|
39
|
+
|
|
40
|
+
# Calculate NDC scale and translation for margins
|
|
41
|
+
w_px = max(width, 1e-12)
|
|
42
|
+
h_px = max(height, 1e-12)
|
|
43
|
+
sx = (width - margin_l - margin_r) / w_px
|
|
44
|
+
tx = (margin_l - margin_r) / w_px
|
|
45
|
+
sy = (height - margin_t - margin_b) / h_px
|
|
46
|
+
ty = (margin_b - margin_t) / h_px
|
|
47
|
+
|
|
48
|
+
# Standard ortho values
|
|
49
|
+
m00 = 2.0 / max(rl, 1e-12)
|
|
50
|
+
m03 = -(r + l) / max(rl, 1e-12)
|
|
51
|
+
m11 = 2.0 / max(tb, 1e-12)
|
|
52
|
+
m13 = -(t + b) / max(tb, 1e-12)
|
|
53
|
+
|
|
54
|
+
# Apply transformation
|
|
55
|
+
r00 = sx * m00
|
|
56
|
+
r03 = sx * m03 + tx
|
|
57
|
+
r11 = sy * m11
|
|
58
|
+
r13 = sy * m13 + ty
|
|
59
|
+
|
|
60
|
+
return np.array([
|
|
61
|
+
[r00, 0.0, 0.0, r03],
|
|
62
|
+
[0.0, r11, 0.0, r13],
|
|
63
|
+
[0.0, 0.0, -1.0, 0.0],
|
|
64
|
+
[0.0, 0.0, 0.0, 1.0]
|
|
65
|
+
], dtype=np.float32)
|
|
32
66
|
|
|
33
67
|
def screen_to_world(self, sx: float, sy: float, width: int, height: int) -> Tuple[float, float]:
|
|
34
68
|
l, r, b, t = self.world_window(width, height)
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
|
|
70
|
+
# Apply inset margins for axes and labels visibility
|
|
71
|
+
margin_l = 60.0
|
|
72
|
+
margin_r = 20.0
|
|
73
|
+
margin_b = 40.0
|
|
74
|
+
margin_t = 20.0
|
|
75
|
+
|
|
76
|
+
w_px = max(width, 1e-12)
|
|
77
|
+
h_px = max(height, 1e-12)
|
|
78
|
+
|
|
79
|
+
# Scale and translation in NDC space
|
|
80
|
+
scale_x = (width - margin_l - margin_r) / w_px
|
|
81
|
+
trans_x = (margin_l - margin_r) / w_px
|
|
82
|
+
scale_y = (height - margin_t - margin_b) / h_px
|
|
83
|
+
trans_y = (margin_b - margin_t) / h_px
|
|
84
|
+
|
|
85
|
+
# Screen to NDC inset
|
|
86
|
+
ndc_x_inset = (sx / w_px) * 2.0 - 1.0
|
|
87
|
+
ndc_y_inset = ((height - sy) / h_px) * 2.0 - 1.0
|
|
88
|
+
|
|
89
|
+
# NDC inset to standard NDC
|
|
90
|
+
ndc_x = (ndc_x_inset - trans_x) / max(scale_x, 1e-12)
|
|
91
|
+
ndc_y = (ndc_y_inset - trans_y) / max(scale_y, 1e-12)
|
|
92
|
+
|
|
93
|
+
# Standard NDC to world
|
|
94
|
+
x = l + (ndc_x + 1.0) * 0.5 * (r - l)
|
|
95
|
+
y = b + (ndc_y + 1.0) * 0.5 * (t - b)
|
|
37
96
|
return x, y
|
|
38
97
|
|
|
39
98
|
def apply_zoom_at_cursor(self, factor: float, mx: float, my: float, width: int, height: int) -> None:
|
|
@@ -64,6 +64,7 @@ class GPULinePlot:
|
|
|
64
64
|
|
|
65
65
|
self.effects = EffectManager(self)
|
|
66
66
|
self._shim_cache: Dict[str, BaseLayer] = {}
|
|
67
|
+
self._apply_background_mode()
|
|
67
68
|
|
|
68
69
|
# --------------------------------------------------------
|
|
69
70
|
# Public API
|
|
@@ -353,6 +354,15 @@ class GPULinePlot:
|
|
|
353
354
|
fname = filename or f"plot_{int(time.time())}.png"
|
|
354
355
|
self.savefig(fname, scale=scale)
|
|
355
356
|
|
|
357
|
+
def get_density_array(self) -> np.ndarray:
|
|
358
|
+
"""
|
|
359
|
+
Read back the accumulated density values from the GPU framebuffer texture
|
|
360
|
+
and return them as a 2D numpy array of shape (height, width).
|
|
361
|
+
"""
|
|
362
|
+
if self.window:
|
|
363
|
+
glfw.make_context_current(self.window)
|
|
364
|
+
return self.density_renderer.get_density_array()
|
|
365
|
+
|
|
356
366
|
# --------------------------------------------------------
|
|
357
367
|
# Init
|
|
358
368
|
# --------------------------------------------------------
|
|
@@ -419,7 +429,29 @@ class GPULinePlot:
|
|
|
419
429
|
# --------------------------------------------------------
|
|
420
430
|
|
|
421
431
|
def _update_runtime_policy(self) -> None:
|
|
432
|
+
prev_mode = self.policy.runtime.current_mode
|
|
422
433
|
self.policy.update(self.scene, self.interaction, self.cache)
|
|
434
|
+
if prev_mode == RenderMode.INTERACTIVE and self.policy.runtime.current_mode == RenderMode.EXACT:
|
|
435
|
+
self.frame.dirty_scene = True
|
|
436
|
+
if self.hud.state.show_profiler:
|
|
437
|
+
self.policy.runtime.hud_enabled_this_frame = True
|
|
438
|
+
|
|
439
|
+
def _apply_background_mode(self) -> None:
|
|
440
|
+
"""Apply style overrides for background, axes, and grid based on light_bg_mode."""
|
|
441
|
+
if getattr(self.options, "light_bg_mode", False):
|
|
442
|
+
# Light BG mode
|
|
443
|
+
self.options.visual.background_color = (1.0, 1.0, 1.0)
|
|
444
|
+
self.options.visual.gradient_background.enabled = True
|
|
445
|
+
self.options.visual.gradient_background.top_color = (1.0, 1.0, 1.0)
|
|
446
|
+
self.options.visual.gradient_background.bottom_color = (0.95, 0.95, 0.95)
|
|
447
|
+
self.options.axis_grid_color = (0.8, 0.8, 0.8)
|
|
448
|
+
else:
|
|
449
|
+
# Dark BG mode
|
|
450
|
+
self.options.visual.background_color = (0.0, 0.0, 0.0)
|
|
451
|
+
self.options.visual.gradient_background.enabled = True
|
|
452
|
+
self.options.visual.gradient_background.top_color = (0.0, 0.0, 0.0)
|
|
453
|
+
self.options.visual.gradient_background.bottom_color = (0.08, 0.08, 0.12)
|
|
454
|
+
self.options.axis_grid_color = (0.2, 0.2, 0.2)
|
|
423
455
|
|
|
424
456
|
def _get_adaptive_alpha(self, count: int) -> float:
|
|
425
457
|
"""
|
|
@@ -543,7 +575,8 @@ class GPULinePlot:
|
|
|
543
575
|
|
|
544
576
|
if self.display_density:
|
|
545
577
|
# Modular Density Pass (Lines, Scatters)
|
|
546
|
-
|
|
578
|
+
current_fbo = glGetIntegerv(GL_FRAMEBUFFER_BINDING)
|
|
579
|
+
self.renderer_manager.draw_density(layers, ctx, target_fbo=current_fbo)
|
|
547
580
|
else:
|
|
548
581
|
# Standard Pass
|
|
549
582
|
self.renderer_manager.draw_exact(layers, ctx)
|
|
@@ -563,7 +596,8 @@ class GPULinePlot:
|
|
|
563
596
|
|
|
564
597
|
current_window = self.camera_controller.world_window(self.width, self.height)
|
|
565
598
|
if self.options.enable_cache_interaction_path and self.cache.capture_window is not None:
|
|
566
|
-
|
|
599
|
+
current_fbo = glGetIntegerv(GL_FRAMEBUFFER_BINDING)
|
|
600
|
+
self.interaction_renderer.draw_cached_impostor(self.cache.capture_window, current_window, target_fbo=current_fbo)
|
|
567
601
|
else:
|
|
568
602
|
self._draw_exact_view()
|
|
569
603
|
|
|
@@ -662,11 +696,34 @@ class GPULinePlot:
|
|
|
662
696
|
glfw.swap_interval(1)
|
|
663
697
|
|
|
664
698
|
while not glfw.window_should_close(self.window):
|
|
665
|
-
|
|
699
|
+
if self.options.reactive_rendering:
|
|
700
|
+
glfw.wait_events_timeout(0.02)
|
|
701
|
+
else:
|
|
702
|
+
glfw.poll_events()
|
|
666
703
|
|
|
667
704
|
# 1. Update Input and State
|
|
668
705
|
self.hud.process_inputs()
|
|
669
706
|
self._update_runtime_policy()
|
|
707
|
+
self._apply_background_mode()
|
|
708
|
+
|
|
709
|
+
# Check cache release deadline
|
|
710
|
+
t_now = glfw.get_time()
|
|
711
|
+
if self.cache.active and not self.interaction.drag_active and not self.interaction.right_drag_active and t_now >= self.cache.release_deadline:
|
|
712
|
+
self.cache.active = False
|
|
713
|
+
self.frame.dirty_scene = True
|
|
714
|
+
|
|
715
|
+
need_render = (
|
|
716
|
+
not self.options.reactive_rendering or
|
|
717
|
+
self.frame.dirty_scene or
|
|
718
|
+
self.frame.dirty_ui or
|
|
719
|
+
self.frame.dirty_pick or
|
|
720
|
+
self.interaction.drag_active or
|
|
721
|
+
self.interaction.right_drag_active or
|
|
722
|
+
self.hud.state.show_profiler
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
if not need_render:
|
|
726
|
+
continue
|
|
670
727
|
|
|
671
728
|
# 2. Start ImGui frame before ANY rendering/processing happens
|
|
672
729
|
self.hud.begin()
|
|
@@ -687,23 +744,28 @@ class GPULinePlot:
|
|
|
687
744
|
|
|
688
745
|
t0 = glfw.get_time()
|
|
689
746
|
|
|
690
|
-
|
|
747
|
+
t_scene_start = time.perf_counter()
|
|
748
|
+
if self.frame.dirty_scene or not self.effects.any_post_enabled():
|
|
749
|
+
self.effects.begin_scene()
|
|
691
750
|
|
|
692
|
-
|
|
693
|
-
|
|
751
|
+
self.effects.draw_background()
|
|
752
|
+
self._apply_blending_policy()
|
|
694
753
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
754
|
+
if self.policy.runtime.current_mode == RenderMode.INTERACTIVE:
|
|
755
|
+
self._draw_interaction_view()
|
|
756
|
+
else:
|
|
757
|
+
self._draw_exact_view()
|
|
699
758
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
759
|
+
# Draw zoom box if active
|
|
760
|
+
if self.interaction.right_drag_active:
|
|
761
|
+
if self.options.enable_clipping_optimization:
|
|
762
|
+
for i in range(4): glDisable(GL_CLIP_DISTANCE0 + i)
|
|
763
|
+
self._draw_zoom_box()
|
|
764
|
+
|
|
765
|
+
self.effects.end_scene()
|
|
766
|
+
else:
|
|
767
|
+
self.effects.resolve()
|
|
768
|
+
self.hud.state.gpu_timings["Render Scene"] = time.perf_counter() - t_scene_start
|
|
707
769
|
|
|
708
770
|
# Update HUD metrics and Draw
|
|
709
771
|
self._service_hud_metrics(t0)
|
|
@@ -712,15 +774,43 @@ class GPULinePlot:
|
|
|
712
774
|
if self.options.enable_clipping_optimization:
|
|
713
775
|
for i in range(4): glDisable(GL_CLIP_DISTANCE0 + i)
|
|
714
776
|
|
|
777
|
+
t_hud_start = time.perf_counter()
|
|
778
|
+
# Draw Axis Labels (Scale) on every frame so they update dynamically and persist when the scene is cached.
|
|
779
|
+
if self.options.axis_show_labels:
|
|
780
|
+
window_world = self.camera_controller.world_window(self.width, self.height)
|
|
781
|
+
mvp = self.camera_controller.mvp(self.width, self.height)
|
|
782
|
+
ndc_scale, ndc_offset = self._get_ndc_transform(window_world)
|
|
783
|
+
ctx_labels = RenderContext(
|
|
784
|
+
mvp=mvp,
|
|
785
|
+
window_world=window_world,
|
|
786
|
+
ndc_scale=ndc_scale,
|
|
787
|
+
ndc_offset=ndc_offset,
|
|
788
|
+
width_px=self.width,
|
|
789
|
+
height_px=self.height,
|
|
790
|
+
fb_width=self.fb_width,
|
|
791
|
+
fb_height=self.fb_height,
|
|
792
|
+
dpr=self.fb_width / max(self.width, 1),
|
|
793
|
+
mode=self.policy.runtime.current_mode,
|
|
794
|
+
global_alpha=1.0,
|
|
795
|
+
lod_keep_prob=1.0,
|
|
796
|
+
is_density=self.display_density,
|
|
797
|
+
time=time.perf_counter()
|
|
798
|
+
)
|
|
799
|
+
self.axis_manager.update(ctx_labels)
|
|
800
|
+
self.renderer_manager.renderers["axis"]._draw_labels(self.axis_manager, ctx_labels)
|
|
801
|
+
|
|
715
802
|
# HUD panels are only updated if HUD is enabled, but begin/end must wrap all
|
|
716
803
|
if self.policy.runtime.hud_enabled_this_frame:
|
|
717
804
|
self.hud.update()
|
|
718
805
|
|
|
719
806
|
self.hud.end()
|
|
807
|
+
self.hud.state.gpu_timings["UI Panels"] = time.perf_counter() - t_hud_start
|
|
720
808
|
|
|
721
809
|
# Note: GL state is cleaned up/reset at start of next frame or specific renderers
|
|
722
810
|
|
|
811
|
+
t_swap_start = time.perf_counter()
|
|
723
812
|
glfw.swap_buffers(self.window)
|
|
813
|
+
self.hud.state.gpu_timings["Buffer Swap"] = time.perf_counter() - t_swap_start
|
|
724
814
|
t1 = glfw.get_time()
|
|
725
815
|
|
|
726
816
|
dt = max(t1 - t0, 1e-6)
|
|
@@ -729,10 +819,6 @@ class GPULinePlot:
|
|
|
729
819
|
self.frame.dirty_scene = False
|
|
730
820
|
self.frame.dirty_ui = False
|
|
731
821
|
|
|
732
|
-
if self.cache.active and not self.interaction.drag_active and not self.interaction.right_drag_active and t1 >= self.cache.release_deadline:
|
|
733
|
-
self.cache.active = False
|
|
734
|
-
self.frame.dirty_scene = True
|
|
735
|
-
|
|
736
822
|
self.effects.shutdown()
|
|
737
823
|
|
|
738
824
|
def _service_hud_metrics(self, t0: float) -> None:
|
|
@@ -834,6 +920,7 @@ class GPULinePlot:
|
|
|
834
920
|
self.frame.dirty_pick = True
|
|
835
921
|
|
|
836
922
|
def _on_mouse_button(self, window, button, action, mods) -> None:
|
|
923
|
+
self.frame.dirty_ui = True
|
|
837
924
|
self.hud.on_mouse_button(window, button, action, mods)
|
|
838
925
|
if self.hud.wants_mouse():
|
|
839
926
|
return
|
|
@@ -903,14 +990,16 @@ class GPULinePlot:
|
|
|
903
990
|
self.frame.dirty_scene = True
|
|
904
991
|
|
|
905
992
|
def _on_cursor(self, window, x, y) -> None:
|
|
906
|
-
if self.
|
|
907
|
-
# Still update world coords for status panel
|
|
908
|
-
self.mouse_world = self.camera_controller.screen_to_world(x, y, self.width, self.height)
|
|
993
|
+
if (x, y) == self.interaction.last_mouse:
|
|
909
994
|
return
|
|
910
|
-
|
|
995
|
+
|
|
911
996
|
self.mouse_world = self.camera_controller.screen_to_world(x, y, self.width, self.height)
|
|
912
997
|
self.frame.dirty_ui = True
|
|
913
998
|
|
|
999
|
+
if self.hud.wants_mouse():
|
|
1000
|
+
self.interaction.last_mouse = (x, y)
|
|
1001
|
+
return
|
|
1002
|
+
|
|
914
1003
|
if self.interaction.drag_active:
|
|
915
1004
|
px, py = self.interaction.press_mouse
|
|
916
1005
|
dist2 = (x - px) ** 2 + (y - py) ** 2
|
|
@@ -965,6 +1054,8 @@ class GPULinePlot:
|
|
|
965
1054
|
elif self.interaction.right_drag_active:
|
|
966
1055
|
self.interaction.last_mouse = (x, y)
|
|
967
1056
|
self.frame.dirty_ui = True
|
|
1057
|
+
else:
|
|
1058
|
+
self.interaction.last_mouse = (x, y)
|
|
968
1059
|
|
|
969
1060
|
def _run_picking_pass(self) -> None:
|
|
970
1061
|
if not self.interaction.last_mouse:
|
|
@@ -1144,11 +1235,11 @@ class GPULinePlot:
|
|
|
1144
1235
|
transparent=transparent
|
|
1145
1236
|
)
|
|
1146
1237
|
|
|
1147
|
-
def to_matplotlib(self, ax=None, **kwargs):
|
|
1238
|
+
def to_matplotlib(self, ax: Optional[Axes] = None, mpl_kwargs: dict = {}, **kwargs):
|
|
1148
1239
|
"""Level 2 API: Render and embed directly into Matplotlib."""
|
|
1149
1240
|
from .utils.mpl_bridge import snapshot_to_matplotlib
|
|
1150
1241
|
snap = self.capture_snapshot(**kwargs)
|
|
1151
|
-
return snapshot_to_matplotlib(snap, ax=ax)
|
|
1242
|
+
return snapshot_to_matplotlib(snap, ax=ax, **mpl_kwargs)
|
|
1152
1243
|
|
|
1153
1244
|
def set_matplotlib_transfer_target(self, ax=None, callback=None):
|
|
1154
1245
|
"""Level 3 API Setup: Redirect 'M' key transfers."""
|
|
@@ -1173,6 +1264,7 @@ class GPULinePlot:
|
|
|
1173
1264
|
self.frame.dirty_scene = True
|
|
1174
1265
|
|
|
1175
1266
|
def _on_key(self, window, key, sc, action, mods) -> None:
|
|
1267
|
+
self.frame.dirty_ui = True
|
|
1176
1268
|
self.hud.on_key(window, key, sc, action, mods)
|
|
1177
1269
|
|
|
1178
1270
|
if action in (glfw.PRESS, glfw.REPEAT):
|
|
@@ -1190,6 +1282,13 @@ class GPULinePlot:
|
|
|
1190
1282
|
elif key == glfw.KEY_C and action == glfw.PRESS:
|
|
1191
1283
|
self.toggle_line_colormap()
|
|
1192
1284
|
|
|
1285
|
+
elif key == glfw.KEY_F3 and action == glfw.PRESS:
|
|
1286
|
+
self.hud.state.show_profiler = not self.hud.state.show_profiler
|
|
1287
|
+
if self.hud.state.show_profiler:
|
|
1288
|
+
self.options.enable_hud = True
|
|
1289
|
+
self.frame.dirty_scene = True
|
|
1290
|
+
self.frame.dirty_ui = True
|
|
1291
|
+
|
|
1193
1292
|
# --- Visual Parameters (Arrows) ---
|
|
1194
1293
|
if key == glfw.KEY_UP:
|
|
1195
1294
|
if self.display_density:
|
|
@@ -1242,11 +1341,11 @@ class GPULinePlot:
|
|
|
1242
1341
|
self.frame.dirty_scene = True
|
|
1243
1342
|
|
|
1244
1343
|
elif key == glfw.KEY_LEFT_BRACKET and action in (glfw.PRESS, glfw.REPEAT):
|
|
1245
|
-
self.
|
|
1344
|
+
self.decrease_density_gain()
|
|
1246
1345
|
self.frame.dirty_scene = True
|
|
1247
1346
|
|
|
1248
1347
|
elif key == glfw.KEY_RIGHT_BRACKET and action in (glfw.PRESS, glfw.REPEAT):
|
|
1249
|
-
self.
|
|
1348
|
+
self.increase_density_gain()
|
|
1250
1349
|
self.frame.dirty_scene = True
|
|
1251
1350
|
|
|
1252
1351
|
elif key == glfw.KEY_H and action == glfw.PRESS:
|
|
@@ -1268,4 +1367,5 @@ class GPULinePlot:
|
|
|
1268
1367
|
self.interaction.shift_down = False
|
|
1269
1368
|
|
|
1270
1369
|
def _on_char(self, window, char) -> None:
|
|
1370
|
+
self.frame.dirty_ui = True
|
|
1271
1371
|
self.hud.on_char(window, char)
|
|
@@ -74,6 +74,8 @@ class EffectManager:
|
|
|
74
74
|
# ------------------------------------------------------------------
|
|
75
75
|
|
|
76
76
|
def any_post_enabled(self) -> bool:
|
|
77
|
+
if self.options.reactive_rendering:
|
|
78
|
+
return True
|
|
77
79
|
v = self.options.visual
|
|
78
80
|
return v.glow.enabled
|
|
79
81
|
|
|
@@ -168,6 +170,18 @@ class EffectManager:
|
|
|
168
170
|
"""
|
|
169
171
|
Draw the background into whichever framebuffer is currently bound.
|
|
170
172
|
"""
|
|
173
|
+
# Overwrite background color with the colormap's minimum color in density mode
|
|
174
|
+
if getattr(self.plot, "display_density", False):
|
|
175
|
+
scheme_idx = getattr(self.options, "density_scheme_index", 0)
|
|
176
|
+
invert = getattr(self.options, "density_invert", False)
|
|
177
|
+
ltc = getattr(self.options, "density_light_to_color", True)
|
|
178
|
+
|
|
179
|
+
from ..utils.shaders import get_colormap_min_color
|
|
180
|
+
c = get_colormap_min_color(scheme_idx, invert, ltc)
|
|
181
|
+
glClearColor(c[0], c[1], c[2], 1.0)
|
|
182
|
+
glClear(GL_COLOR_BUFFER_BIT)
|
|
183
|
+
return
|
|
184
|
+
|
|
171
185
|
v = self.options.visual.gradient_background
|
|
172
186
|
|
|
173
187
|
if not v.enabled:
|
|
@@ -359,13 +359,43 @@ class HudManager:
|
|
|
359
359
|
self.plot.frame.dirty_scene = True
|
|
360
360
|
imgui.pop_item_width()
|
|
361
361
|
|
|
362
|
+
# Light BG Mode
|
|
363
|
+
light_mode = getattr(self.options, "light_bg_mode", False)
|
|
364
|
+
changed_bg, val_bg = imgui.checkbox("Light BG Mode", light_mode)
|
|
365
|
+
if changed_bg:
|
|
366
|
+
self.options.light_bg_mode = val_bg
|
|
367
|
+
self.options.density_invert = val_bg
|
|
368
|
+
self.plot.frame.dirty_scene = True
|
|
369
|
+
self.plot.frame.dirty_ui = True
|
|
370
|
+
|
|
371
|
+
# Invert Colors
|
|
372
|
+
invert_mode = getattr(self.options, "density_invert", False)
|
|
373
|
+
changed_inv, val_inv = imgui.checkbox("Invert Colors", invert_mode)
|
|
374
|
+
if changed_inv:
|
|
375
|
+
self.options.density_invert = val_inv
|
|
376
|
+
self.plot.frame.dirty_scene = True
|
|
377
|
+
self.plot.frame.dirty_ui = True
|
|
378
|
+
|
|
379
|
+
# Light to Color Only option
|
|
380
|
+
is_inverted = getattr(self.options, "density_invert", False)
|
|
381
|
+
label = "Light to Color Only" if is_inverted else "Dark to Color Only"
|
|
382
|
+
light_to_color = getattr(self.options, "density_light_to_color", True)
|
|
383
|
+
changed_ltc, val_ltc = imgui.checkbox(label, light_to_color)
|
|
384
|
+
if changed_ltc:
|
|
385
|
+
self.options.density_light_to_color = val_ltc
|
|
386
|
+
self.plot.frame.dirty_scene = True
|
|
387
|
+
self.plot.frame.dirty_ui = True
|
|
388
|
+
|
|
362
389
|
# Gain and Scale
|
|
363
390
|
imgui.push_item_width(180)
|
|
364
391
|
changed, gain = imgui.drag_float("Gain (Intensity)", self.options.density_gain, 1.0, 0.1, 10000.0, "%.1f")
|
|
365
392
|
if changed: self.options.density_gain = gain; self.plot.frame.dirty_scene = True
|
|
366
393
|
|
|
367
|
-
|
|
368
|
-
|
|
394
|
+
is_log = getattr(self.options, "density_is_log", True)
|
|
395
|
+
changed_log, val_log = imgui.checkbox("Logarithmic Scale", is_log)
|
|
396
|
+
if changed_log:
|
|
397
|
+
self.options.density_is_log = val_log
|
|
398
|
+
self.plot.frame.dirty_scene = True
|
|
369
399
|
|
|
370
400
|
changed, res = imgui.slider_float("Inner Resolution", self.options.density_resolution_scale, 0.1, 1.0, "%.2f")
|
|
371
401
|
if changed: self.controller.set_density_resolution(res)
|
|
@@ -378,28 +408,25 @@ class HudManager:
|
|
|
378
408
|
pos = imgui.get_cursor_screen_pos()
|
|
379
409
|
w, h = 220, 18
|
|
380
410
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if scheme == 2: # Plasma
|
|
387
|
-
return (0.1+v*1.8, 0.0, 0.5+v*0.5) if v < 0.5 else (1.0, (v-0.5)*2.0, 1.0-v)
|
|
388
|
-
if scheme == 3: # Inferno
|
|
389
|
-
return (v*0.5, 0.0, v*0.2) if v < 0.5 else (v, (v-0.5)*1.5, (v-0.5)*2.0)
|
|
390
|
-
if scheme == 4: # Turbo
|
|
391
|
-
if v < 0.33: return (0.2, v*2.5, 0.8)
|
|
392
|
-
if v < 0.66: return (v*1.5, 0.8, 0.2)
|
|
393
|
-
return (0.9, 0.2, 0.1)
|
|
394
|
-
if scheme == 5: # Ink Fire (White BG)
|
|
395
|
-
return (1.0, 1.0-(v*0.5), 1.0-(v*1.5)) if v < 0.5 else (1.0-(v-0.5)*2.0, 0.25-(v-0.5)*0.5, 0.0)
|
|
396
|
-
if scheme == 6: # Magma
|
|
397
|
-
return (v*0.4, 0.1, 0.5) if v < 0.5 else (v, 0.5+(v-0.5), 0.8+(v-0.5)*0.4)
|
|
398
|
-
return (v, v, v)
|
|
399
|
-
|
|
411
|
+
from ..utils.shaders import eval_colormap
|
|
412
|
+
is_inverted = getattr(self.options, "density_invert", False)
|
|
413
|
+
ltc = getattr(self.options, "density_light_to_color", True)
|
|
414
|
+
scheme_idx = self.options.density_scheme_index
|
|
415
|
+
|
|
400
416
|
for i in range(20):
|
|
401
417
|
v = i / 19.0
|
|
402
|
-
|
|
418
|
+
if is_inverted:
|
|
419
|
+
if ltc:
|
|
420
|
+
norm = 1.0 - 0.75 * v
|
|
421
|
+
else:
|
|
422
|
+
norm = 1.0 - v
|
|
423
|
+
else:
|
|
424
|
+
if ltc:
|
|
425
|
+
norm = 0.75 * v
|
|
426
|
+
else:
|
|
427
|
+
norm = v
|
|
428
|
+
|
|
429
|
+
c = eval_colormap(scheme_idx, norm)
|
|
403
430
|
color = imgui.get_color_u32_rgba(max(0,min(1,c[0])), max(0,min(1,c[1])), max(0,min(1,c[2])), 1.0)
|
|
404
431
|
draw_list.add_rect_filled(pos.x + i*(w/20), pos.y, pos.x + (i+1)*(w/20), pos.y + h, color)
|
|
405
432
|
|
|
@@ -456,23 +483,49 @@ class HudManager:
|
|
|
456
483
|
imgui.end()
|
|
457
484
|
|
|
458
485
|
def _draw_profiler_panel(self):
|
|
459
|
-
imgui.set_next_window_size(
|
|
460
|
-
imgui.begin("Profiler", True)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
# Sparkline
|
|
467
|
-
import numpy as np
|
|
468
|
-
history = np.array(self.state.cpu_frame_times, dtype=np.float32)
|
|
469
|
-
imgui.plot_lines("##FPSGraph", history, overlay_text=f"{1000.0/avg_ms:.1f} FPS",
|
|
470
|
-
scale_min=0, scale_max=0.033, graph_size=(0, 60))
|
|
486
|
+
imgui.set_next_window_size(350, 420, imgui.FIRST_USE_EVER)
|
|
487
|
+
expanded, opened = imgui.begin("Profiler", True)
|
|
488
|
+
if not opened:
|
|
489
|
+
self.state.show_profiler = False
|
|
490
|
+
imgui.end()
|
|
491
|
+
return
|
|
471
492
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
imgui.
|
|
493
|
+
if expanded:
|
|
494
|
+
# 1. Performance Overview
|
|
495
|
+
imgui.text_colored("PERFORMANCE OVERVIEW", 0.3, 0.8, 1.0)
|
|
496
|
+
if self.state.cpu_frame_times:
|
|
497
|
+
avg_ms = sum(self.state.cpu_frame_times) / len(self.state.cpu_frame_times) * 1000.0
|
|
498
|
+
imgui.text(f"Avg CPU Frame: {avg_ms:5.2f} ms ({1000.0/avg_ms:.1f} FPS)")
|
|
499
|
+
|
|
500
|
+
# Sparkline
|
|
501
|
+
import numpy as np
|
|
502
|
+
history = np.array(self.state.cpu_frame_times, dtype=np.float32)
|
|
503
|
+
imgui.plot_lines("##FPSGraph", history, overlay_text="",
|
|
504
|
+
scale_min=0, scale_max=0.033, graph_size=(0, 50))
|
|
505
|
+
|
|
506
|
+
imgui.separator()
|
|
507
|
+
|
|
508
|
+
# 2. Rendering Pipelines Detailed Timings
|
|
509
|
+
imgui.text_colored("SUB-OPERATION TIMINGS", 0.3, 0.8, 1.0)
|
|
510
|
+
for k, v in sorted(self.state.gpu_timings.items()):
|
|
511
|
+
imgui.text(f" - {k}: {v*1000.0:5.2f} ms")
|
|
475
512
|
|
|
513
|
+
imgui.separator()
|
|
514
|
+
|
|
515
|
+
# 3. Engine State & Statistics
|
|
516
|
+
imgui.text_colored("ENGINE STATISTICS", 0.3, 0.8, 1.0)
|
|
517
|
+
mode_str = self.plot.policy.runtime.current_mode.name
|
|
518
|
+
imgui.text(f"Render Mode: {mode_str}")
|
|
519
|
+
|
|
520
|
+
sleep_str = "SLEEPING (Reactive)" if self.plot.options.reactive_rendering and not self.plot.hud.state.show_profiler else "ACTIVE Redrawing"
|
|
521
|
+
imgui.text(f"Gating Loop: {sleep_str}")
|
|
522
|
+
|
|
523
|
+
imgui.text(f"Total Lines: {self.plot.scene.lines.count:,}")
|
|
524
|
+
|
|
525
|
+
win = self.plot.camera_controller.world_window(self.plot.width, self.plot.height)
|
|
526
|
+
imgui.text(f"X range: [{win[0]:.2f}, {win[1]:.2f}]")
|
|
527
|
+
imgui.text(f"Y range: [{win[2]:.2f}, {win[3]:.2f}]")
|
|
528
|
+
|
|
476
529
|
imgui.end()
|
|
477
530
|
|
|
478
531
|
def _draw_selection_panel(self):
|
|
@@ -88,6 +88,7 @@ class EngineOptions:
|
|
|
88
88
|
enable_multisample: bool = False
|
|
89
89
|
enable_antialiasing: bool = True
|
|
90
90
|
always_lod: bool = False
|
|
91
|
+
reactive_rendering: bool = True
|
|
91
92
|
|
|
92
93
|
# Picking policy
|
|
93
94
|
shift_required_for_picking: bool = True
|
|
@@ -99,10 +100,14 @@ class EngineOptions:
|
|
|
99
100
|
# Density rendering
|
|
100
101
|
density_gain: float = 1.0
|
|
101
102
|
density_resolution_scale: float = 1.0 # 1.0 = full-res, 0.5 = faster
|
|
102
|
-
density_scheme_index: int =
|
|
103
|
+
density_scheme_index: int = 9
|
|
103
104
|
density_gain_step: float = 1.25
|
|
104
105
|
density_log_scale: float = 3.0 # Divisor for log normalization
|
|
105
106
|
density_weighted: bool = False # Accumulate alpha instead of 1.0
|
|
107
|
+
density_invert: bool = True
|
|
108
|
+
density_is_log: bool = True # Enable logarithmic colormap scaling by default
|
|
109
|
+
light_bg_mode: bool = True
|
|
110
|
+
density_light_to_color: bool = True # Go from white to color (instead of white to color to black)
|
|
106
111
|
|
|
107
112
|
# Style
|
|
108
113
|
blend_mode: BlendMode = BlendMode.AUTO
|