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.
- glplot/__init__.py +6 -0
- glplot/backend.py +15 -0
- glplot/controllers.py +69 -0
- glplot/core/__init__.py +0 -0
- glplot/core/context.py +50 -0
- glplot/core/layers.py +163 -0
- glplot/core/legacy.py +98 -0
- glplot/engine.py +1270 -0
- glplot/managers/__init__.py +0 -0
- glplot/managers/axis.py +66 -0
- glplot/managers/effects.py +343 -0
- glplot/managers/hud.py +510 -0
- glplot/managers/hud_state.py +95 -0
- glplot/managers/picking.py +174 -0
- glplot/managers/renderer_manager.py +158 -0
- glplot/options.py +120 -0
- glplot/policy.py +108 -0
- glplot/pyplot.py +735 -0
- glplot/renderers/__init__.py +0 -0
- glplot/renderers/axis.py +126 -0
- glplot/renderers/base.py +40 -0
- glplot/renderers/density.py +120 -0
- glplot/renderers/exact.py +215 -0
- glplot/renderers/interaction.py +77 -0
- glplot/renderers/line_family.py +250 -0
- glplot/renderers/patch.py +149 -0
- glplot/renderers/polyline.py +230 -0
- glplot/renderers/scatter.py +185 -0
- glplot/renderers/text.py +72 -0
- glplot/scratch/__init__.py +0 -0
- glplot/utils/__init__.py +0 -0
- glplot/utils/export.py +112 -0
- glplot/utils/gl_utils.py +32 -0
- glplot/utils/mpl_bridge.py +60 -0
- glplot/utils/shaders.py +889 -0
- glplot-0.1.0.dist-info/METADATA +75 -0
- glplot-0.1.0.dist-info/RECORD +39 -0
- glplot-0.1.0.dist-info/WHEEL +4 -0
- glplot-0.1.0.dist-info/licenses/LICENSE +0 -0
glplot/managers/hud.py
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import typing
|
|
3
|
+
from typing import TYPE_CHECKING, Optional, Tuple, Any
|
|
4
|
+
|
|
5
|
+
from .hud_state import HudState, HudController
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..options import EngineOptions, BlendMode
|
|
9
|
+
from ..core import CameraState, FrameState
|
|
10
|
+
from ..engine import GPULinePlot
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import imgui
|
|
14
|
+
from imgui.integrations.glfw import GlfwRenderer
|
|
15
|
+
IMGUI_AVAILABLE = True
|
|
16
|
+
except (ImportError, Exception):
|
|
17
|
+
IMGUI_AVAILABLE = False
|
|
18
|
+
imgui = None
|
|
19
|
+
GlfwRenderer = None
|
|
20
|
+
|
|
21
|
+
class HudManager:
|
|
22
|
+
def __init__(self, plot: GPULinePlot):
|
|
23
|
+
self.plot = plot
|
|
24
|
+
self.options = plot.options
|
|
25
|
+
self.state = HudState()
|
|
26
|
+
self.controller = HudController(plot)
|
|
27
|
+
self.imgui_ctx = None
|
|
28
|
+
self.imgui_impl = None
|
|
29
|
+
|
|
30
|
+
def initialize(self, window) -> None:
|
|
31
|
+
if not IMGUI_AVAILABLE:
|
|
32
|
+
return
|
|
33
|
+
self.imgui_ctx = imgui.create_context()
|
|
34
|
+
self.imgui_impl = GlfwRenderer(window, attach_callbacks=False)
|
|
35
|
+
# Use a more professional dark theme
|
|
36
|
+
style = imgui.get_style()
|
|
37
|
+
imgui.style_colors_dark(style)
|
|
38
|
+
style.window_rounding = 4.0
|
|
39
|
+
style.frame_rounding = 3.0
|
|
40
|
+
|
|
41
|
+
def process_inputs(self) -> None:
|
|
42
|
+
if self.imgui_impl:
|
|
43
|
+
self.imgui_impl.process_inputs()
|
|
44
|
+
|
|
45
|
+
def on_scroll(self, window, dx, dy) -> None:
|
|
46
|
+
if self.imgui_impl:
|
|
47
|
+
self.imgui_impl.scroll_callback(window, dx, dy)
|
|
48
|
+
|
|
49
|
+
def on_mouse_button(self, window, button, action, mods) -> None:
|
|
50
|
+
if self.imgui_impl:
|
|
51
|
+
self.imgui_impl.mouse_callback(window, button, action, mods)
|
|
52
|
+
|
|
53
|
+
def on_key(self, window, key, sc, action, mods) -> None:
|
|
54
|
+
if self.imgui_impl:
|
|
55
|
+
self.imgui_impl.keyboard_callback(window, key, sc, action, mods)
|
|
56
|
+
|
|
57
|
+
def on_char(self, window, char) -> None:
|
|
58
|
+
if self.imgui_impl:
|
|
59
|
+
self.imgui_impl.char_callback(window, char)
|
|
60
|
+
|
|
61
|
+
def wants_mouse(self) -> bool:
|
|
62
|
+
if not IMGUI_AVAILABLE or not self.imgui_impl:
|
|
63
|
+
return False
|
|
64
|
+
return imgui.get_io().want_capture_mouse
|
|
65
|
+
|
|
66
|
+
def get_draw_list(self):
|
|
67
|
+
"""Returns the ImGui background draw list if available."""
|
|
68
|
+
if not IMGUI_AVAILABLE:
|
|
69
|
+
return None
|
|
70
|
+
return imgui.get_background_draw_list()
|
|
71
|
+
|
|
72
|
+
def wants_keyboard(self) -> bool:
|
|
73
|
+
if not IMGUI_AVAILABLE or not self.imgui_impl:
|
|
74
|
+
return False
|
|
75
|
+
return imgui.get_io().want_capture_keyboard
|
|
76
|
+
|
|
77
|
+
def begin(self) -> None:
|
|
78
|
+
if self.imgui_impl:
|
|
79
|
+
imgui.new_frame()
|
|
80
|
+
|
|
81
|
+
def update(self):
|
|
82
|
+
"""Main draw orchestration for all HUD components."""
|
|
83
|
+
if not self.imgui_impl or not self.options.enable_hud:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
# SYNC: selected_layer_id between engine and HUD
|
|
87
|
+
# We detect which one changed since last frame and propagate it
|
|
88
|
+
engine_sel = self.plot.interaction.selected_layer_id
|
|
89
|
+
hud_sel = self.state.selected_layer_id
|
|
90
|
+
|
|
91
|
+
if engine_sel != self.state._last_engine_selection:
|
|
92
|
+
# Picking changed it (Engine -> HUD)
|
|
93
|
+
self.state.selected_layer_id = engine_sel
|
|
94
|
+
self.state._last_engine_selection = engine_sel
|
|
95
|
+
self.state._last_hud_selection = engine_sel
|
|
96
|
+
elif hud_sel != self.state._last_hud_selection:
|
|
97
|
+
# UI changed it (HUD -> Engine)
|
|
98
|
+
self.plot.interaction.selected_layer_id = hud_sel
|
|
99
|
+
self.state._last_hud_selection = hud_sel
|
|
100
|
+
self.state._last_engine_selection = hud_sel
|
|
101
|
+
|
|
102
|
+
self._draw_main_menu()
|
|
103
|
+
|
|
104
|
+
if self.state.show_status_overlay:
|
|
105
|
+
self._draw_status_overlay()
|
|
106
|
+
|
|
107
|
+
if self.state.show_layers:
|
|
108
|
+
self._draw_layers_panel()
|
|
109
|
+
|
|
110
|
+
if self.state.show_render_controls:
|
|
111
|
+
self._draw_render_panel()
|
|
112
|
+
|
|
113
|
+
if self.state.show_profiler:
|
|
114
|
+
self._draw_profiler_panel()
|
|
115
|
+
|
|
116
|
+
if self.state.show_selection and self.state.selected_object:
|
|
117
|
+
self._draw_selection_panel()
|
|
118
|
+
|
|
119
|
+
if self.state.show_analysis:
|
|
120
|
+
self._draw_analysis_panel()
|
|
121
|
+
|
|
122
|
+
self._draw_layer_inspector()
|
|
123
|
+
|
|
124
|
+
def _draw_main_menu(self):
|
|
125
|
+
if imgui.begin_main_menu_bar():
|
|
126
|
+
if imgui.begin_menu("View"):
|
|
127
|
+
_, self.state.show_status_overlay = imgui.menu_item("Status Overlay", None, self.state.show_status_overlay)
|
|
128
|
+
_, self.state.show_layers = imgui.menu_item("Layers", None, self.state.show_layers)
|
|
129
|
+
_, self.state.show_render_controls = imgui.menu_item("Render Controls", None, self.state.show_render_controls)
|
|
130
|
+
_, self.state.show_profiler = imgui.menu_item("Profiler", None, self.state.show_profiler)
|
|
131
|
+
_, self.state.show_selection = imgui.menu_item("Selection Info", None, self.state.show_selection)
|
|
132
|
+
_, self.state.show_analysis = imgui.menu_item("Analysis", None, self.state.show_analysis)
|
|
133
|
+
imgui.end_menu()
|
|
134
|
+
|
|
135
|
+
if imgui.begin_menu("Actions"):
|
|
136
|
+
if imgui.menu_item("Reset View")[0]: self.controller.reset_view()
|
|
137
|
+
if imgui.menu_item("Autoscale")[0]: self.controller.autoscale()
|
|
138
|
+
imgui.separator()
|
|
139
|
+
if imgui.menu_item("Toggle Density")[0]: self.controller.toggle_density()
|
|
140
|
+
if imgui.menu_item("Export PNG")[0]: self.controller.export()
|
|
141
|
+
imgui.end_menu()
|
|
142
|
+
|
|
143
|
+
imgui.end_main_menu_bar()
|
|
144
|
+
|
|
145
|
+
def _draw_status_overlay(self):
|
|
146
|
+
# Transparent overlay strip at the top (below menu)
|
|
147
|
+
imgui.set_next_window_position(0, 20)
|
|
148
|
+
imgui.set_next_window_size(self.plot.width, 30)
|
|
149
|
+
flags = (imgui.WINDOW_NO_TITLE_BAR | imgui.WINDOW_NO_RESIZE |
|
|
150
|
+
imgui.WINDOW_NO_MOVE | imgui.WINDOW_NO_SCROLLBAR |
|
|
151
|
+
imgui.WINDOW_NO_BACKGROUND)
|
|
152
|
+
|
|
153
|
+
imgui.begin("StatusOverlay", flags=flags)
|
|
154
|
+
|
|
155
|
+
fps = 1.0 / max(0.0001, self.state.cpu_frame_times[-1]) if self.state.cpu_frame_times else 0.0
|
|
156
|
+
n_lines = self.plot.scene.lines.count
|
|
157
|
+
|
|
158
|
+
imgui.text(f"FPS: {fps:4.1f} | Lines: {n_lines:,} | Mode: {'Density' if self.plot.display_density else 'Exact'} | ")
|
|
159
|
+
imgui.same_line()
|
|
160
|
+
if self.plot.mouse_world:
|
|
161
|
+
imgui.text(f"Mouse: ({self.plot.mouse_world[0]:.4f}, {self.plot.mouse_world[1]:.4f}) | ")
|
|
162
|
+
imgui.same_line()
|
|
163
|
+
|
|
164
|
+
imgui.text(f"Alpha: {self.options.default_global_alpha:.3f}")
|
|
165
|
+
|
|
166
|
+
imgui.end()
|
|
167
|
+
|
|
168
|
+
def _draw_layers_panel(self):
|
|
169
|
+
imgui.set_next_window_size(300, 400, imgui.FIRST_USE_EVER)
|
|
170
|
+
imgui.begin("Layers & Stacking", True)
|
|
171
|
+
|
|
172
|
+
imgui.text_disabled("Drag to reorder (List order = Render order)")
|
|
173
|
+
imgui.separator()
|
|
174
|
+
|
|
175
|
+
layers = self.plot.scene.layers
|
|
176
|
+
to_move = None
|
|
177
|
+
|
|
178
|
+
for i, layer in enumerate(layers):
|
|
179
|
+
imgui.push_id(str(layer.layer_id))
|
|
180
|
+
|
|
181
|
+
# Visibility checkbox
|
|
182
|
+
changed, visible = imgui.checkbox("##vis", layer.style.visible)
|
|
183
|
+
if changed:
|
|
184
|
+
layer.style.visible = visible
|
|
185
|
+
layer.dirty.style_dirty = True
|
|
186
|
+
self.plot.frame.dirty_scene = True
|
|
187
|
+
self.plot.cache.refresh_requested = True
|
|
188
|
+
|
|
189
|
+
imgui.same_line()
|
|
190
|
+
|
|
191
|
+
# Layer Selectable (Drag Source/Target)
|
|
192
|
+
is_selected = (self.state.selected_layer_id == layer.layer_id)
|
|
193
|
+
_, selected = imgui.selectable(f"{layer.label}##{i}", is_selected)
|
|
194
|
+
if selected:
|
|
195
|
+
self.state.selected_layer_id = layer.layer_id
|
|
196
|
+
|
|
197
|
+
# Drag and Drop Reordering
|
|
198
|
+
if imgui.is_item_active() and not imgui.is_item_hovered():
|
|
199
|
+
# Potential drag start
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
if imgui.begin_drag_drop_source():
|
|
203
|
+
imgui.set_drag_drop_payload("LAYER_ORDER", str(i).encode())
|
|
204
|
+
imgui.text(f"Moving {layer.label}...")
|
|
205
|
+
imgui.end_drag_drop_source()
|
|
206
|
+
|
|
207
|
+
if imgui.begin_drag_drop_target():
|
|
208
|
+
payload = imgui.accept_drag_drop_payload("LAYER_ORDER")
|
|
209
|
+
if payload:
|
|
210
|
+
src_idx = int(payload.decode())
|
|
211
|
+
to_move = (src_idx, i)
|
|
212
|
+
imgui.end_drag_drop_target()
|
|
213
|
+
|
|
214
|
+
imgui.pop_id()
|
|
215
|
+
|
|
216
|
+
if to_move:
|
|
217
|
+
src, dst = to_move
|
|
218
|
+
layer = layers.pop(src)
|
|
219
|
+
layers.insert(dst, layer)
|
|
220
|
+
self.plot.frame.dirty_scene = True
|
|
221
|
+
self.plot.cache.refresh_requested = True
|
|
222
|
+
|
|
223
|
+
imgui.end()
|
|
224
|
+
|
|
225
|
+
def _draw_layer_inspector(self):
|
|
226
|
+
if self.state.selected_layer_id is None: return
|
|
227
|
+
|
|
228
|
+
layer = next((l for l in self.plot.scene.layers if l.layer_id == self.state.selected_layer_id), None)
|
|
229
|
+
if not layer: return
|
|
230
|
+
|
|
231
|
+
def _mark_dirty():
|
|
232
|
+
layer.dirty.style_dirty = True
|
|
233
|
+
self.plot.frame.dirty_scene = True
|
|
234
|
+
self.plot.cache.refresh_requested = True
|
|
235
|
+
|
|
236
|
+
imgui.set_next_window_size(300, 300, imgui.FIRST_USE_EVER)
|
|
237
|
+
imgui.begin("Layer Inspector", True)
|
|
238
|
+
|
|
239
|
+
imgui.text(f"ID: {layer.layer_id}")
|
|
240
|
+
imgui.text(f"Type: {layer.layer_type.upper()}")
|
|
241
|
+
|
|
242
|
+
changed, label = imgui.input_text("Label", layer.label, 64)
|
|
243
|
+
if changed: layer.label = label
|
|
244
|
+
|
|
245
|
+
imgui.separator()
|
|
246
|
+
|
|
247
|
+
style = layer.style
|
|
248
|
+
if imgui.collapsing_header("Style Properties", imgui.TREE_NODE_DEFAULT_OPEN)[0]:
|
|
249
|
+
# Alpha
|
|
250
|
+
changed, alpha = imgui.slider_float("Alpha", style.alpha, 0.0, 1.0)
|
|
251
|
+
if changed: style.alpha = alpha; _mark_dirty()
|
|
252
|
+
|
|
253
|
+
# Color (if applicable)
|
|
254
|
+
if style.color is not None:
|
|
255
|
+
changed, color = imgui.color_edit4("Primary Color", *style.color)
|
|
256
|
+
if changed: style.color = color; _mark_dirty()
|
|
257
|
+
|
|
258
|
+
# Line Width
|
|
259
|
+
if layer.layer_type in ["polyline"]:
|
|
260
|
+
changed, lw = imgui.slider_float("Line Width", style.line_width, 0.1, 10.0)
|
|
261
|
+
if changed: style.line_width = lw; _mark_dirty()
|
|
262
|
+
|
|
263
|
+
# Point Size
|
|
264
|
+
if layer.layer_type == "scatter":
|
|
265
|
+
changed, ps = imgui.slider_float("Point Size", style.point_size, 1.0, 100.0)
|
|
266
|
+
if changed: style.point_size = ps; _mark_dirty()
|
|
267
|
+
|
|
268
|
+
imgui.separator()
|
|
269
|
+
imgui.text("Outline")
|
|
270
|
+
changed, out_en = imgui.checkbox("Enable Outline", style.point_outline_enabled)
|
|
271
|
+
if changed: style.point_outline_enabled = out_en; _mark_dirty()
|
|
272
|
+
|
|
273
|
+
changed, out_col = imgui.color_edit4("Outline Color", *style.point_outline_color)
|
|
274
|
+
if changed: style.point_outline_color = out_col; _mark_dirty()
|
|
275
|
+
|
|
276
|
+
changed, out_w = imgui.slider_float("Outline Width", style.point_outline_width, 0.1, 5.0)
|
|
277
|
+
if changed: style.point_outline_width = out_w; _mark_dirty()
|
|
278
|
+
|
|
279
|
+
if imgui.collapsing_header("Transformation", imgui.TREE_NODE_DEFAULT_OPEN)[0]:
|
|
280
|
+
imgui.text("World space offset")
|
|
281
|
+
tx, ty = layer.translation
|
|
282
|
+
changed, n_trans = imgui.drag_float2("Translation", tx, ty, 0.05)
|
|
283
|
+
if changed:
|
|
284
|
+
layer.translation = (n_trans[0], n_trans[1])
|
|
285
|
+
self.plot.frame.dirty_scene = True
|
|
286
|
+
self.plot.cache.refresh_requested = True
|
|
287
|
+
|
|
288
|
+
if imgui.button("Reset Position"):
|
|
289
|
+
layer.translation = (0.0, 0.0)
|
|
290
|
+
self.plot.frame.dirty_scene = True
|
|
291
|
+
self.plot.cache.refresh_requested = True
|
|
292
|
+
|
|
293
|
+
imgui.end()
|
|
294
|
+
|
|
295
|
+
def _draw_render_panel(self):
|
|
296
|
+
imgui.set_next_window_size(300, 450, imgui.FIRST_USE_EVER)
|
|
297
|
+
imgui.begin("Render & Style", True)
|
|
298
|
+
|
|
299
|
+
if imgui.collapsing_header("Global Style", imgui.TREE_NODE_DEFAULT_OPEN)[0]:
|
|
300
|
+
ov = self.options.visual.overrides
|
|
301
|
+
vis = self.options.visual
|
|
302
|
+
|
|
303
|
+
_, vis.background_color = imgui.color_edit3("Background Color", *vis.background_color)
|
|
304
|
+
if imgui.is_item_deactivated_after_edit():
|
|
305
|
+
self.plot.frame.dirty_scene = True
|
|
306
|
+
|
|
307
|
+
changed, alpha = imgui.slider_float("Alpha Mult", ov.alpha_multiplier, 0.0, 2.0)
|
|
308
|
+
if changed: ov.alpha_multiplier = alpha; self.plot.frame.dirty_scene = True
|
|
309
|
+
|
|
310
|
+
changed, lw = imgui.slider_float("Line Width Mult", ov.line_width_multiplier, 0.1, 5.0)
|
|
311
|
+
if changed: ov.line_width_multiplier = lw; self.plot.frame.dirty_scene = True
|
|
312
|
+
|
|
313
|
+
changed, ps = imgui.slider_float("Point Size Mult", ov.point_size_multiplier, 0.1, 5.0)
|
|
314
|
+
if changed: ov.point_size_multiplier = ps; self.plot.frame.dirty_scene = True
|
|
315
|
+
|
|
316
|
+
# Blending Mode Dropdown
|
|
317
|
+
from ..options import BlendMode
|
|
318
|
+
items = [m.name for m in BlendMode]
|
|
319
|
+
try:
|
|
320
|
+
current = items.index(self.options.blend_mode.name)
|
|
321
|
+
except:
|
|
322
|
+
current = 0
|
|
323
|
+
|
|
324
|
+
imgui.push_item_width(120)
|
|
325
|
+
changed, clicked = imgui.combo("Blending", current, items)
|
|
326
|
+
if changed:
|
|
327
|
+
self.options.blend_mode = list(BlendMode)[clicked]
|
|
328
|
+
self.plot.frame.dirty_scene = True
|
|
329
|
+
imgui.pop_item_width()
|
|
330
|
+
|
|
331
|
+
if imgui.collapsing_header("Density Engine", imgui.TREE_NODE_DEFAULT_OPEN)[0]:
|
|
332
|
+
from ..utils.shaders import DENSITY_SCHEMES
|
|
333
|
+
|
|
334
|
+
# Mode Toggle
|
|
335
|
+
changed, den_en = imgui.checkbox("Enable Density Mode (Heatmap)", self.plot.display_density)
|
|
336
|
+
if changed: self.plot.set_density_enabled(den_en)
|
|
337
|
+
|
|
338
|
+
# Weighted Density Toggle
|
|
339
|
+
changed, weighted = imgui.checkbox("Weighted Accumulation", self.options.density_weighted)
|
|
340
|
+
if changed:
|
|
341
|
+
self.options.density_weighted = weighted
|
|
342
|
+
self.plot.frame.dirty_scene = True
|
|
343
|
+
|
|
344
|
+
# Scheme Selection
|
|
345
|
+
imgui.push_item_width(150)
|
|
346
|
+
changed, idx = imgui.combo("Scheme", self.options.density_scheme_index, DENSITY_SCHEMES)
|
|
347
|
+
if changed:
|
|
348
|
+
self.options.density_scheme_index = idx
|
|
349
|
+
self.plot.frame.dirty_scene = True
|
|
350
|
+
imgui.pop_item_width()
|
|
351
|
+
|
|
352
|
+
# Gain and Scale
|
|
353
|
+
imgui.push_item_width(180)
|
|
354
|
+
changed, gain = imgui.drag_float("Gain (Intensity)", self.options.density_gain, 1.0, 0.1, 10000.0, "%.1f")
|
|
355
|
+
if changed: self.options.density_gain = gain; self.plot.frame.dirty_scene = True
|
|
356
|
+
|
|
357
|
+
changed, lscale = imgui.drag_float("Log Scale (Contrast)", self.options.density_log_scale, 0.05, 0.1, 20.0, "%.2f")
|
|
358
|
+
if changed: self.options.density_log_scale = lscale; self.plot.frame.dirty_scene = True
|
|
359
|
+
|
|
360
|
+
changed, res = imgui.slider_float("Inner Resolution", self.options.density_resolution_scale, 0.1, 1.0, "%.2f")
|
|
361
|
+
if changed: self.controller.set_density_resolution(res)
|
|
362
|
+
imgui.text_disabled("0.5x is 4x faster, 1.0x is sharpest")
|
|
363
|
+
imgui.pop_item_width()
|
|
364
|
+
|
|
365
|
+
# Color Bar Preview
|
|
366
|
+
imgui.text("Colormap Preview:")
|
|
367
|
+
draw_list = imgui.get_window_draw_list()
|
|
368
|
+
pos = imgui.get_cursor_screen_pos()
|
|
369
|
+
w, h = 220, 18
|
|
370
|
+
|
|
371
|
+
def get_scheme_col(v):
|
|
372
|
+
scheme = self.options.density_scheme_index
|
|
373
|
+
if scheme == 1: # Viridis
|
|
374
|
+
if v < 0.5: return (0.2+v*0.1, 0.1+v*0.8, 0.4+v*0.2)
|
|
375
|
+
return (0.3+(v-0.5)*1.4, 0.5+(v-0.5)*0.8, 0.5-(v-0.5)*0.8)
|
|
376
|
+
if scheme == 2: # Plasma
|
|
377
|
+
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)
|
|
378
|
+
if scheme == 3: # Inferno
|
|
379
|
+
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)
|
|
380
|
+
if scheme == 4: # Turbo
|
|
381
|
+
if v < 0.33: return (0.2, v*2.5, 0.8)
|
|
382
|
+
if v < 0.66: return (v*1.5, 0.8, 0.2)
|
|
383
|
+
return (0.9, 0.2, 0.1)
|
|
384
|
+
if scheme == 5: # Ink Fire (White BG)
|
|
385
|
+
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)
|
|
386
|
+
if scheme == 6: # Magma
|
|
387
|
+
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)
|
|
388
|
+
return (v, v, v)
|
|
389
|
+
|
|
390
|
+
for i in range(20):
|
|
391
|
+
v = i / 19.0
|
|
392
|
+
c = get_scheme_col(v)
|
|
393
|
+
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)
|
|
394
|
+
draw_list.add_rect_filled(pos.x + i*(w/20), pos.y, pos.x + (i+1)*(w/20), pos.y + h, color)
|
|
395
|
+
|
|
396
|
+
imgui.dummy(w, h + 5)
|
|
397
|
+
|
|
398
|
+
if imgui.collapsing_header("Plot Framework", imgui.TREE_NODE_DEFAULT_OPEN)[0]:
|
|
399
|
+
_, self.options.axis_show_grid = imgui.checkbox("Show Grid", self.options.axis_show_grid)
|
|
400
|
+
if self.options.axis_show_grid:
|
|
401
|
+
imgui.same_line()
|
|
402
|
+
imgui.push_item_width(100)
|
|
403
|
+
_, self.options.axis_grid_alpha = imgui.slider_float("Alpha##Grid", self.options.axis_grid_alpha, 0.0, 1.0)
|
|
404
|
+
imgui.pop_item_width()
|
|
405
|
+
|
|
406
|
+
imgui.indent(10)
|
|
407
|
+
_, self.options.axis_grid_color = imgui.color_edit3("Grid Color", *self.options.axis_grid_color)
|
|
408
|
+
imgui.unindent(10)
|
|
409
|
+
|
|
410
|
+
_, self.options.axis_show_labels = imgui.checkbox("Show Scale (Labels)", self.options.axis_show_labels)
|
|
411
|
+
_, self.options.axis_show_frame = imgui.checkbox("Show Framework", self.options.axis_show_frame)
|
|
412
|
+
|
|
413
|
+
if imgui.collapsing_header("LOD Configuration", imgui.TREE_NODE_DEFAULT_OPEN)[0]:
|
|
414
|
+
changed, lod_en = imgui.checkbox("LOD Enabled (Sub-sampling)", self.options.lod_enabled)
|
|
415
|
+
if changed: self.controller.set_lod_enabled(lod_en)
|
|
416
|
+
|
|
417
|
+
if self.options.lod_enabled:
|
|
418
|
+
imgui.push_item_width(180)
|
|
419
|
+
# Range 0.1 to 500.0 covers from very sparse to extreme fidelity (overkill)
|
|
420
|
+
changed, budget = imgui.slider_float("Complexity Budget", self.options.lod_target_coverage, 0.1, 500.0, "%.2f")
|
|
421
|
+
if changed: self.controller.set_lod_budget(budget)
|
|
422
|
+
imgui.pop_item_width()
|
|
423
|
+
imgui.text_disabled("Higher = More detail, lower FPS")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
if imgui.collapsing_header("Visual Effects", imgui.TREE_NODE_DEFAULT_OPEN)[0]:
|
|
427
|
+
v = self.options.visual
|
|
428
|
+
|
|
429
|
+
# --- Background ---
|
|
430
|
+
if imgui.tree_node("Gradient Background", imgui.TREE_NODE_DEFAULT_OPEN):
|
|
431
|
+
_, v.gradient_background.enabled = imgui.checkbox("Enabled##BG", v.gradient_background.enabled)
|
|
432
|
+
_, v.gradient_background.top_color = imgui.color_edit3("Top Color", *v.gradient_background.top_color)
|
|
433
|
+
_, v.gradient_background.bottom_color = imgui.color_edit3("Bottom Color", *v.gradient_background.bottom_color)
|
|
434
|
+
imgui.tree_pop()
|
|
435
|
+
|
|
436
|
+
# --- Bloom ---
|
|
437
|
+
if imgui.tree_node("Bloom / Glow", imgui.TREE_NODE_DEFAULT_OPEN):
|
|
438
|
+
_, v.glow.enabled = imgui.checkbox("Enabled##Bloom", v.glow.enabled)
|
|
439
|
+
imgui.push_item_width(120)
|
|
440
|
+
_, v.glow.intensity = imgui.slider_float("Intensity", v.glow.intensity, 0.0, 5.0)
|
|
441
|
+
_, v.glow.threshold = imgui.slider_float("Threshold", v.glow.threshold, 0.0, 1.0)
|
|
442
|
+
_, v.glow.radius_px = imgui.slider_float("Radius", v.glow.radius_px, 1.0, 20.0)
|
|
443
|
+
imgui.pop_item_width()
|
|
444
|
+
imgui.tree_pop()
|
|
445
|
+
|
|
446
|
+
imgui.end()
|
|
447
|
+
|
|
448
|
+
def _draw_profiler_panel(self):
|
|
449
|
+
imgui.set_next_window_size(300, 200, imgui.FIRST_USE_EVER)
|
|
450
|
+
imgui.begin("Profiler", True)
|
|
451
|
+
|
|
452
|
+
if self.state.cpu_frame_times:
|
|
453
|
+
avg_ms = sum(self.state.cpu_frame_times) / len(self.state.cpu_frame_times) * 1000.0
|
|
454
|
+
imgui.text(f"Avg CPU Frame: {avg_ms:5.2f} ms")
|
|
455
|
+
|
|
456
|
+
# Sparkline
|
|
457
|
+
import numpy as np
|
|
458
|
+
history = np.array(self.state.cpu_frame_times, dtype=np.float32)
|
|
459
|
+
imgui.plot_lines("##FPSGraph", history, overlay_text=f"{1000.0/avg_ms:.1f} FPS",
|
|
460
|
+
scale_min=0, scale_max=0.033, graph_size=(0, 60))
|
|
461
|
+
|
|
462
|
+
imgui.separator()
|
|
463
|
+
for k, v in self.state.gpu_timings.items():
|
|
464
|
+
imgui.text(f"{k}: {v*1000.0:5.2f} ms")
|
|
465
|
+
|
|
466
|
+
imgui.end()
|
|
467
|
+
|
|
468
|
+
def _draw_selection_panel(self):
|
|
469
|
+
imgui.set_next_window_size(250, 180, imgui.FIRST_USE_EVER)
|
|
470
|
+
imgui.begin("Selection Info", True)
|
|
471
|
+
|
|
472
|
+
info = self.state.selected_object
|
|
473
|
+
imgui.text_colored(f"Type: {info.get('type', 'N/A').upper()}", 1.0, 0.8, 0.4)
|
|
474
|
+
imgui.text(f"Dataset: {info.get('dataset_idx', 0)}")
|
|
475
|
+
imgui.text(f"Index: {info.get('element_idx', 0)}")
|
|
476
|
+
imgui.separator()
|
|
477
|
+
imgui.text(f"X: {info.get('x', 0.0):.6f}")
|
|
478
|
+
imgui.text(f"Y: {info.get('y', 0.0):.6f}")
|
|
479
|
+
|
|
480
|
+
if imgui.button("Isolate"):
|
|
481
|
+
# Logic later
|
|
482
|
+
pass
|
|
483
|
+
imgui.same_line()
|
|
484
|
+
if imgui.button("Reset"): self.state.selected_object = None
|
|
485
|
+
|
|
486
|
+
imgui.end()
|
|
487
|
+
|
|
488
|
+
def _draw_analysis_panel(self):
|
|
489
|
+
imgui.set_next_window_size(400, 300, imgui.FIRST_USE_EVER)
|
|
490
|
+
imgui.begin("Analysis (Sampled)", True)
|
|
491
|
+
|
|
492
|
+
if self.state.sampled_histogram_a is not None:
|
|
493
|
+
imgui.text("Slope (a) Distribution")
|
|
494
|
+
imgui.plot_histogram("##HistA", self.state.sampled_histogram_a, graph_size=(0, 80))
|
|
495
|
+
|
|
496
|
+
if self.state.sampled_histogram_b is not None:
|
|
497
|
+
imgui.text("Intercept (b) Distribution")
|
|
498
|
+
imgui.plot_histogram("##HistB", self.state.sampled_histogram_b, graph_size=(0, 80))
|
|
499
|
+
|
|
500
|
+
imgui.end()
|
|
501
|
+
|
|
502
|
+
def end(self) -> None:
|
|
503
|
+
if self.imgui_impl:
|
|
504
|
+
imgui.render()
|
|
505
|
+
self.imgui_impl.render(imgui.get_draw_data())
|
|
506
|
+
|
|
507
|
+
def get_draw_list(self) -> Any:
|
|
508
|
+
if not IMGUI_AVAILABLE:
|
|
509
|
+
return None
|
|
510
|
+
return imgui.get_background_draw_list()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import time
|
|
3
|
+
import collections
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import TYPE_CHECKING, Optional, List, Dict, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..engine import GPULinePlot
|
|
9
|
+
from ..options import EngineOptions
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class HudState:
|
|
13
|
+
# Visibility
|
|
14
|
+
show_hud: bool = False
|
|
15
|
+
show_status_overlay: bool = True
|
|
16
|
+
show_layers: bool = False
|
|
17
|
+
show_render_controls: bool = False
|
|
18
|
+
show_profiler: bool = False
|
|
19
|
+
show_selection: bool = True
|
|
20
|
+
show_analysis: bool = False
|
|
21
|
+
|
|
22
|
+
# Selection info
|
|
23
|
+
selected_object: Optional[dict] = None
|
|
24
|
+
selected_layer_id: Optional[int] = None
|
|
25
|
+
|
|
26
|
+
# Internal state for bidirectional sync
|
|
27
|
+
_last_engine_selection: Optional[int] = None
|
|
28
|
+
_last_hud_selection: Optional[int] = None
|
|
29
|
+
|
|
30
|
+
# Profiler metrics (Rolling buffers)
|
|
31
|
+
fps_history: collections.deque = field(default_factory=lambda: collections.deque(maxlen=120))
|
|
32
|
+
cpu_frame_times: collections.deque = field(default_factory=lambda: collections.deque(maxlen=120))
|
|
33
|
+
gpu_timings: Dict[str, float] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
# Analysis Cache
|
|
36
|
+
cached_stats: Dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
sampled_histogram_a: Optional[Any] = None
|
|
38
|
+
sampled_histogram_b: Optional[Any] = None
|
|
39
|
+
|
|
40
|
+
# Timestamps
|
|
41
|
+
last_medium_update: float = 0.0
|
|
42
|
+
last_slow_update: float = 0.0
|
|
43
|
+
|
|
44
|
+
class HudController:
|
|
45
|
+
def __init__(self, plot: GPULinePlot):
|
|
46
|
+
self.plot = plot
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def options(self) -> EngineOptions:
|
|
50
|
+
return self.plot.options
|
|
51
|
+
|
|
52
|
+
def toggle_hud(self):
|
|
53
|
+
self.plot.set_hud_enabled(not self.options.enable_hud)
|
|
54
|
+
|
|
55
|
+
def reset_view(self):
|
|
56
|
+
self.plot.reset_view()
|
|
57
|
+
|
|
58
|
+
def autoscale(self):
|
|
59
|
+
self.plot.autoscale()
|
|
60
|
+
|
|
61
|
+
def toggle_density(self):
|
|
62
|
+
self.plot.toggle_density()
|
|
63
|
+
|
|
64
|
+
def cycle_scheme(self, direction: int = 1):
|
|
65
|
+
if direction > 0:
|
|
66
|
+
self.plot.next_density_scheme()
|
|
67
|
+
else:
|
|
68
|
+
self.plot.previous_density_scheme()
|
|
69
|
+
|
|
70
|
+
def cycle_blending(self):
|
|
71
|
+
self.plot.cycle_blending_mode()
|
|
72
|
+
|
|
73
|
+
def export(self):
|
|
74
|
+
# Trigger default export
|
|
75
|
+
import time
|
|
76
|
+
self.plot.savefig(f"plot_{int(time.time())}.png")
|
|
77
|
+
|
|
78
|
+
def toggle_layer(self, layer_id: str, visible: bool):
|
|
79
|
+
# Placeholder for real layer management
|
|
80
|
+
# For now, we only have one main set of lines
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def set_lod_enabled(self, val: bool):
|
|
84
|
+
self.options.lod_enabled = val
|
|
85
|
+
self.plot.frame.dirty_scene = True
|
|
86
|
+
self.plot.cache.refresh_requested = True
|
|
87
|
+
|
|
88
|
+
def set_lod_budget(self, val: float):
|
|
89
|
+
self.options.lod_target_coverage = val
|
|
90
|
+
self.plot.frame.dirty_scene = True
|
|
91
|
+
self.plot.cache.refresh_requested = True
|
|
92
|
+
|
|
93
|
+
def set_density_resolution(self, val: float):
|
|
94
|
+
self.options.density_resolution_scale = val
|
|
95
|
+
self.plot.rebuild_density_renderer()
|