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/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()