meltygui 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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
"""Graph view functions and supporting definitions."""
|
|
2
|
+
from meltygui.core.graphics.gl_state import GLState
|
|
3
|
+
from meltygui.core.graphics.shader_func import shader_func
|
|
4
|
+
from meltygui.hdr_color import pack_color
|
|
5
|
+
from meltygui.core.melty import Melty
|
|
6
|
+
from meltygui.model.lut_model import Lut, LutPalette
|
|
7
|
+
from meltygui.model.tensor_model import TensorDim
|
|
8
|
+
from meltygui.model.tensor_model import TensorDims
|
|
9
|
+
from meltygui.core.rendering.modes import Modes
|
|
10
|
+
from meltygui.core.core_render import render_func
|
|
11
|
+
from meltygui.core.rendering.shaped import Shaped
|
|
12
|
+
from meltygui.state.graph_state import GraphViewState
|
|
13
|
+
from meltygui.core.runtime.toggles import SwooshMode
|
|
14
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
15
|
+
from meltygui.view.header_view import draw_header
|
|
16
|
+
from meltygui.view.tensor_view import _tick_values
|
|
17
|
+
from meltygui.state.file_state import ROOT
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
import OpenGL.GL as gl
|
|
20
|
+
import colorsys
|
|
21
|
+
import math
|
|
22
|
+
import meltygui_imgui as imgui
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@render_func(
|
|
27
|
+
# Shape-routed: 1-D and 2-D tensors come here, 3-D+ go to draw_voxels
|
|
28
|
+
# (via `Shaped("Tensor", (None, None, None, None))`). The plain "Tensor"
|
|
29
|
+
# name entry on draw_voxels stays as fallback for anything unshaped.
|
|
30
|
+
is_default_for=(Shaped("Tensor", (None,)), Shaped("Tensor", (None, None)),
|
|
31
|
+
Shaped("ndarray", (None,)), Shaped("ndarray", (None, None))),
|
|
32
|
+
show_bg=True, selectable=True, auto_resize=False, min_width=269,
|
|
33
|
+
with_header=draw_header, bg_offset=0, min_height=293,
|
|
34
|
+
disable_scroll=True, use_cache=True)
|
|
35
|
+
def draw_line_graph(input_value=None, gl_state: GLState = None, selectable=False,
|
|
36
|
+
draw_state=None,
|
|
37
|
+
# ── camera: zoom/pan in the shader. cam_* and zoom_* names
|
|
38
|
+
# dodge the legacy DrawState zoom field (name-colling
|
|
39
|
+
# args are excluded from auto-state). ──
|
|
40
|
+
zoom_x=1.0, zoom_y=1.0, pan_x=0.0, pan_y=0.0, fit_margin=0.92,
|
|
41
|
+
# auto_scale=False (default): x and y share ONE pixel scale
|
|
42
|
+
# and ONE zoom - the plot keeps its aspect however the
|
|
43
|
+
# window is shaped or the wheel is rolled, like the voxel
|
|
44
|
+
# box. True: each axis fits the image and zooms on its own
|
|
45
|
+
# (shift/ctrl-wheel for x/y only).
|
|
46
|
+
auto_scale=False,
|
|
47
|
+
# ── line styling ──
|
|
48
|
+
line_width=1.5, line_opacity=1.0, lut=Lut("jet"),
|
|
49
|
+
single_color=(0.35, 0.75, 1.0), max_lines=1024,
|
|
50
|
+
show_axes=True,
|
|
51
|
+
# ── data mapping: dims by index or NAME; -1 = derive
|
|
52
|
+
# (x = second-to-last, lines = last). ──
|
|
53
|
+
dim_names=("layer", "batch", "token", "feature"),
|
|
54
|
+
x_dim=TensorDim(-1), line_dim=TensorDim(-1),
|
|
55
|
+
slices=(), mean_dims=TensorDims(()), normalize=False,
|
|
56
|
+
# ── events (hover-routed wrapper kwargs) ──
|
|
57
|
+
middle_mouse_drag=None, scroll_y_changed=None,
|
|
58
|
+
left_mouse_double_clicked=None, slash_pressed=None,
|
|
59
|
+
kp_divide_pressed=None, kp_decimal_pressed=None,
|
|
60
|
+
luts: LutPalette = None, keyboard_available=True, **kwargs):
|
|
61
|
+
"""The line-graph renderer — draw_voxels' sibling (see the module doc).
|
|
62
|
+
CUDA tensors are sampled in place; CPU tensors/ndarrays are uploaded.
|
|
63
|
+
Input can also be an already-packed series GLTexture (rendered as-is; needs n_samples/tex_w/y_range stamped
|
|
64
|
+
on it)."""
|
|
65
|
+
from meltygui.core.graphics.gl_state import GLTexture
|
|
66
|
+
from meltygui.core.graphics.gl_state import gl_limits
|
|
67
|
+
from meltygui.core.graphics.gl_state import texture3d_fit
|
|
68
|
+
from meltygui.model.texture_model import _cached_volume_texture
|
|
69
|
+
from meltygui.model.tensor_model import _clean_dim_name
|
|
70
|
+
from meltygui.view.tensor_view import _describe_tensor
|
|
71
|
+
from meltygui.view.tensor_view import _draw_image_notice
|
|
72
|
+
from meltygui.view.tensor_view import _draw_voxel_error
|
|
73
|
+
from meltygui.view.tensor_view import _draw_slice_sliders
|
|
74
|
+
from meltygui.view.tensor_view import _view_size
|
|
75
|
+
from meltygui.core.graphics.tensor_core import source_identity
|
|
76
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
77
|
+
from meltygui.model.graph_model import _finite_range
|
|
78
|
+
from meltygui.model.graph_model import pack_series
|
|
79
|
+
from meltygui.model.graph_model import slice_lines
|
|
80
|
+
from meltygui.core.rendering.render_dispatch import draw_any
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
import torch
|
|
84
|
+
except ImportError:
|
|
85
|
+
_draw_voxel_error(draw_state, "Line graphs of arrays and tensors require torch:\n"
|
|
86
|
+
"pip install meltygui[tensor]", who="draw_line_graph")
|
|
87
|
+
return False, input_value
|
|
88
|
+
|
|
89
|
+
src = input_value
|
|
90
|
+
cuda_lines = None
|
|
91
|
+
img_origin = imgui.get_cursor_screen_pos() # the image draws here below
|
|
92
|
+
dim_names = tuple(_clean_dim_name(x, i) for i, x in enumerate(dim_names or ()))
|
|
93
|
+
slices = tuple(int(v) for v in (slices or ()))
|
|
94
|
+
mean_dims = tuple(int(v) for v in (mean_dims or ()))
|
|
95
|
+
margin = max(0.1, min(1.0, float(fit_margin)))
|
|
96
|
+
auto_scale = bool(auto_scale)
|
|
97
|
+
|
|
98
|
+
# ── src → (lines, samples) → packed 3-D texture, parameter-driven and
|
|
99
|
+
# stateless: slice_lines is a pure function of these params, the upload
|
|
100
|
+
# re-runs exactly when its deps change; metadata rides the texture. ──
|
|
101
|
+
if isinstance(src, GLTexture):
|
|
102
|
+
tex, mapping = src, None
|
|
103
|
+
source_shape = tuple(getattr(src, "source_shape", src.shape))
|
|
104
|
+
n_samples = int(getattr(src, "n_samples", src.shape[2]))
|
|
105
|
+
tex_w = int(getattr(src, "tex_w", src.shape[2]))
|
|
106
|
+
y_range = tuple(getattr(src, "y_range", (0.0, 1.0)))
|
|
107
|
+
n_lines = int(src.shape[0])
|
|
108
|
+
clamp_note = getattr(src, "clamp_note", None)
|
|
109
|
+
else:
|
|
110
|
+
try:
|
|
111
|
+
t = src if isinstance(src, torch.Tensor) else torch.from_numpy(np.asarray(src))
|
|
112
|
+
except (TypeError, ValueError, RuntimeError) as e:
|
|
113
|
+
_draw_voxel_error(draw_state, f"{type(src).__name__} is not tensor-shaped:\n{e}",
|
|
114
|
+
who="draw_line_graph")
|
|
115
|
+
gl_state.drop("series"); gl_state.drop("series_cuda")
|
|
116
|
+
return False, None
|
|
117
|
+
# Cache gate BEFORE any tensor work (same fix as draw_voxels): the
|
|
118
|
+
# slice / finite-range / pack are whole-tensor passes; on a hit the
|
|
119
|
+
# metadata rides the cached texture exactly like a GLTexture input.
|
|
120
|
+
vol_key = (source_identity(src), dim_names,
|
|
121
|
+
str(x_dim), str(line_dim), slices, mean_dims,
|
|
122
|
+
bool(normalize), int(max_lines))
|
|
123
|
+
if t.is_cuda:
|
|
124
|
+
from types import SimpleNamespace
|
|
125
|
+
from meltygui.view import graph_cuda_view
|
|
126
|
+
from meltygui.core.graphics.cuda_kernel_core import available
|
|
127
|
+
if not available():
|
|
128
|
+
_draw_voxel_error(draw_state, "CUDA line rendering requires meltygui[tensor]; "
|
|
129
|
+
"the tensor was not copied to the CPU.", who="draw_line_graph")
|
|
130
|
+
return False, None
|
|
131
|
+
try:
|
|
132
|
+
cuda_lines, mapping, source_shape = slice_lines(
|
|
133
|
+
t, dim_names, x_dim, line_dim, slices, mean_dims, False, materialize=False)
|
|
134
|
+
cuda_lines = cuda_lines[:max(1, int(max_lines))]
|
|
135
|
+
n_lines, n_samples = map(int, cuda_lines.shape)
|
|
136
|
+
stats = gl_state.get('line_ranges', lambda: graph_cuda_view.ranges(cuda_lines), deps=vol_key)
|
|
137
|
+
if normalize:
|
|
138
|
+
y_range = (0., 1.)
|
|
139
|
+
else:
|
|
140
|
+
lo, hi = float(stats[:, 0].min()), float(stats[:, 1].max())
|
|
141
|
+
if not math.isfinite(lo) or not math.isfinite(hi):
|
|
142
|
+
lo, hi = 0., 1.
|
|
143
|
+
if hi - lo < 1e-12:
|
|
144
|
+
pad = abs(lo) * .5 or .5
|
|
145
|
+
lo, hi = lo - pad, hi + pad
|
|
146
|
+
y_range = (lo, hi)
|
|
147
|
+
tex_w, clamp_note = 0, None
|
|
148
|
+
tex = SimpleNamespace(clamp_note=None)
|
|
149
|
+
gl_state.drop('series'); gl_state.drop('series_cuda')
|
|
150
|
+
except (ValueError, TypeError, RuntimeError) as error:
|
|
151
|
+
_draw_voxel_error(draw_state, str(error), who="draw_line_graph")
|
|
152
|
+
return False, None
|
|
153
|
+
else:
|
|
154
|
+
tex = _cached_volume_texture(gl_state, vol_key, keys=("series_cuda", "series"))
|
|
155
|
+
if tex is not None:
|
|
156
|
+
mapping, source_shape = tex.mapping, tex.source_shape
|
|
157
|
+
n_samples, tex_w, y_range = tex.n_samples, tex.tex_w, tex.y_range
|
|
158
|
+
n_lines, clamp_note = int(tex.shape[0]), tex.clamp_note
|
|
159
|
+
if not isinstance(src, GLTexture) and tex is None:
|
|
160
|
+
try:
|
|
161
|
+
lines, mapping, source_shape = slice_lines(
|
|
162
|
+
t, dim_names, x_dim, line_dim, slices, mean_dims, normalize)
|
|
163
|
+
except (ValueError, TypeError, RuntimeError, IndexError) as e:
|
|
164
|
+
_draw_voxel_error(draw_state, f"can't build lines from "
|
|
165
|
+
f"{_describe_tensor(t)}:\n{e}", who="draw_line_graph")
|
|
166
|
+
gl_state.drop("series"); gl_state.drop("series_cuda")
|
|
167
|
+
return False, None
|
|
168
|
+
n_lines, n_samples = (int(s) for s in lines.shape)
|
|
169
|
+
max_3d = int(gl_limits()["max_3d"])
|
|
170
|
+
notes = []
|
|
171
|
+
cap = max(1, min(int(max_lines), max_3d))
|
|
172
|
+
if n_lines > cap:
|
|
173
|
+
notes.append(f"{n_lines} lines — showing the first {cap} "
|
|
174
|
+
f"(max_lines={int(max_lines)}, GL depth limit {max_3d})")
|
|
175
|
+
lines = lines[:cap]
|
|
176
|
+
n_lines = cap
|
|
177
|
+
y_range = _finite_range(lines)
|
|
178
|
+
vol, tex_w = pack_series(lines, max_3d)
|
|
179
|
+
clamped_shape, problems = texture3d_fit(vol.shape, vol.element_size(),
|
|
180
|
+
max_bytes=float("inf"))
|
|
181
|
+
if problems:
|
|
182
|
+
# Rows past the GL limit: the sample axis longer than max_3d² -
|
|
183
|
+
# show the displayable prefix of samples.
|
|
184
|
+
if any("GL_MAX_3D_TEXTURE_SIZE" not in p for p in problems):
|
|
185
|
+
_draw_voxel_error(draw_state, f"{_describe_tensor(t)} → series "
|
|
186
|
+
f"{tuple(int(s) for s in vol.shape)}:\n"
|
|
187
|
+
+ "\n".join(problems), who="draw_line_graph")
|
|
188
|
+
gl_state.drop("series"); gl_state.drop("series_cuda")
|
|
189
|
+
return False, None
|
|
190
|
+
d3, h3, w3 = clamped_shape
|
|
191
|
+
vol = vol[:d3, :h3, :w3].contiguous()
|
|
192
|
+
n_samples = min(n_samples, h3 * w3)
|
|
193
|
+
notes.append(f"{n_samples}+ samples exceed the GL texture budget; "
|
|
194
|
+
f"showing the first {h3 * w3}")
|
|
195
|
+
clamp_note = "; ".join(notes) if notes else None
|
|
196
|
+
version = (source_identity(src), mapping, slices,
|
|
197
|
+
mean_dims, bool(normalize), n_lines, tuple(vol.shape))
|
|
198
|
+
tex = None
|
|
199
|
+
try:
|
|
200
|
+
if vol.is_cuda:
|
|
201
|
+
from meltygui.model.cuda_texture_model import tensor_to_texture
|
|
202
|
+
tex = tensor_to_texture(gl_state, "series_cuda", vol,
|
|
203
|
+
version=version)
|
|
204
|
+
if tex is None:
|
|
205
|
+
tex = gl_state.texture3d("series", vol.cpu().numpy(), version=version)
|
|
206
|
+
gl_state.drop("series_cuda")
|
|
207
|
+
else:
|
|
208
|
+
gl_state.drop("series")
|
|
209
|
+
except Exception as e:
|
|
210
|
+
_draw_voxel_error(draw_state, f"GPU upload failed for series "
|
|
211
|
+
f"{tuple(int(s) for s in vol.shape)} ({vol.dtype}):\n{e}",
|
|
212
|
+
who="draw_line_graph")
|
|
213
|
+
gl_state.drop("series"); gl_state.drop("series_cuda")
|
|
214
|
+
return False, None
|
|
215
|
+
tex.source_shape = source_shape
|
|
216
|
+
tex.source_ndim = len(source_shape)
|
|
217
|
+
tex.n_samples, tex.tex_w, tex.y_range = n_samples, tex_w, y_range
|
|
218
|
+
tex.clamp_note = clamp_note
|
|
219
|
+
tex.mapping = mapping
|
|
220
|
+
tex._vol_key = vol_key
|
|
221
|
+
|
|
222
|
+
# ── labels: the x dim's name + the series caption ──
|
|
223
|
+
x_label, caption = "", ""
|
|
224
|
+
if mapping is not None:
|
|
225
|
+
xd, ld = mapping
|
|
226
|
+
x_label = dim_names[xd] if xd < len(dim_names) else f"dim{xd}"
|
|
227
|
+
if ld is not None:
|
|
228
|
+
ln = dim_names[ld] if ld < len(dim_names) else f"dim{ld}"
|
|
229
|
+
caption = f"{ln} × {n_lines} lines"
|
|
230
|
+
else:
|
|
231
|
+
caption = f"{n_lines} lines" if n_lines > 1 else ""
|
|
232
|
+
|
|
233
|
+
# ── slice sliders: one per UNMAPPED dim with extent > 1 (not plotted,
|
|
234
|
+
# not averaged) - exactly draw_voxels' scrubbers. ──
|
|
235
|
+
slider_dims = []
|
|
236
|
+
if mapping is not None and len(source_shape) > 2:
|
|
237
|
+
used = {d for d in mapping if d is not None}
|
|
238
|
+
slider_dims = [d for d in range(len(source_shape))
|
|
239
|
+
if d not in used and d not in mean_dims
|
|
240
|
+
and source_shape[d] > 1]
|
|
241
|
+
|
|
242
|
+
# ── size from the OWNING WINDOW (see _view_size) ──
|
|
243
|
+
win = draw_state if draw_state.closable else (draw_state.parent_window or draw_state)
|
|
244
|
+
width, height = _view_size(draw_state)
|
|
245
|
+
if slider_dims:
|
|
246
|
+
height = max(100, height - int(imgui.get_frame_height_with_spacing())
|
|
247
|
+
* len(slider_dims))
|
|
248
|
+
|
|
249
|
+
# ── in-flight state values (deferred slow-source writes): re-read any
|
|
250
|
+
# camera param with a timer set so drags accumulate off those values.
|
|
251
|
+
_pending = getattr(draw_state, "_sa_pending", None) or {}
|
|
252
|
+
_precise = getattr(draw_state, "_sa_precise", None) or {}
|
|
253
|
+
_in_flight = _pending.keys() | _precise.keys()
|
|
254
|
+
if _in_flight:
|
|
255
|
+
def _fly(n, cur):
|
|
256
|
+
if n not in _in_flight:
|
|
257
|
+
return cur
|
|
258
|
+
v = getattr(draw_state, "locate_" + n)
|
|
259
|
+
return cur if v is None else v
|
|
260
|
+
zoom_x, zoom_y = _fly("zoom_x", zoom_x), _fly("zoom_y", zoom_y)
|
|
261
|
+
pan_x, pan_y = _fly("pan_x", pan_x), _fly("pan_y", pan_y)
|
|
262
|
+
|
|
263
|
+
# ── gestures → draw_state params (auto-state: the write diverges the
|
|
264
|
+
# param so it persists; events are hover-routed wrapper kwargs) ──
|
|
265
|
+
unit = _unit_px(width, height, auto_scale)
|
|
266
|
+
if not auto_scale and zoom_y != zoom_x:
|
|
267
|
+
# Locked aspect: one zoom. A stray divergence (auto_scale is on,
|
|
268
|
+
# or a panel edit to one of them) collapses onto zoom_x.
|
|
269
|
+
zoom_y = zoom_x
|
|
270
|
+
draw_state.locate_zoom_y = zoom_y
|
|
271
|
+
if middle_mouse_drag is not None:
|
|
272
|
+
# Pan tracks the cursor 1:1 at any zoom: a pixel is 1/z graph
|
|
273
|
+
# units, and pan lives in pre-zoom graph units.
|
|
274
|
+
pan_x -= middle_mouse_drag.dx / (unit[0] * max(zoom_x, 1e-6))
|
|
275
|
+
pan_y += middle_mouse_drag.dy / (unit[1] * max(zoom_y, 1e-6))
|
|
276
|
+
draw_state.locate_pan_x = pan_x
|
|
277
|
+
draw_state.locate_pan_y = pan_y
|
|
278
|
+
if scroll_y_changed is not None:
|
|
279
|
+
# Zoom about the cursor: the data point under the mouse stays put -
|
|
280
|
+
# (v·2-1)·m = c/z + pan = c/z' + pan' → pan' = pan + c/z - c/z'
|
|
281
|
+
# with c the cursor in graph units. Locked aspect: both axes get one
|
|
282
|
+
# zoom; auto_scale: shift = x only, ctrl = y only, plain = both.
|
|
283
|
+
factor = math.exp(0.23 * scroll_y_changed.value)
|
|
284
|
+
mx, my = imgui.get_mouse_pos()
|
|
285
|
+
cx = (mx - img_origin[0] - width * 0.5) / unit[0]
|
|
286
|
+
cy = (img_origin[1] + height * 0.5 - my) / unit[1]
|
|
287
|
+
lim_x, lim_y = width * 0.5 / unit[0], height * 0.5 / unit[1]
|
|
288
|
+
cx, cy = max(-lim_x, min(lim_x, cx)), max(-lim_y, min(lim_y, cy))
|
|
289
|
+
do_x = (not auto_scale) or not scroll_y_changed.ctrl
|
|
290
|
+
do_y = (not auto_scale) or not scroll_y_changed.shift
|
|
291
|
+
if do_x:
|
|
292
|
+
nz = min(1e6, max(1e-3, zoom_x * factor))
|
|
293
|
+
pan_x += cx / zoom_x - cx / nz
|
|
294
|
+
zoom_x = nz
|
|
295
|
+
draw_state.locate_zoom_x = zoom_x
|
|
296
|
+
draw_state.locate_pan_x = pan_x
|
|
297
|
+
if do_y:
|
|
298
|
+
nz = min(1e6, max(1e-3, zoom_y * factor))
|
|
299
|
+
pan_y += cy / zoom_y - cy / nz
|
|
300
|
+
zoom_y = nz
|
|
301
|
+
draw_state.locate_zoom_y = zoom_y
|
|
302
|
+
draw_state.locate_pan_y = pan_y
|
|
303
|
+
if keyboard_available and (slash_pressed is not None
|
|
304
|
+
or kp_divide_pressed is not None
|
|
305
|
+
or kp_decimal_pressed is not None):
|
|
306
|
+
zoom_x = zoom_y = 1.0
|
|
307
|
+
pan_x = pan_y = 0.0
|
|
308
|
+
draw_state.locate_zoom_x = 1.0
|
|
309
|
+
draw_state.locate_zoom_y = 1.0
|
|
310
|
+
draw_state.locate_pan_x = 0.0
|
|
311
|
+
draw_state.locate_pan_y = 0.0
|
|
312
|
+
|
|
313
|
+
lut_tex = luts.texture(lut)
|
|
314
|
+
|
|
315
|
+
# ── GL pass: every resource keyed + lifecycle-managed by gl_state ──
|
|
316
|
+
fb = gl_state.fbo("target", width, height)
|
|
317
|
+
depth_was_on = gl.glIsEnabled(gl.GL_DEPTH_TEST)
|
|
318
|
+
with fb:
|
|
319
|
+
gl.glDisable(gl.GL_DEPTH_TEST)
|
|
320
|
+
gl.glClearColor(0.0, 0.0, 0.0, 0.0)
|
|
321
|
+
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
|
322
|
+
_blend_was = bool(gl.glIsEnabled(gl.GL_BLEND))
|
|
323
|
+
gl.glEnable(gl.GL_BLEND)
|
|
324
|
+
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
|
325
|
+
gl.glBlendFuncSeparate(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA,
|
|
326
|
+
gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
|
327
|
+
if cuda_lines is not None:
|
|
328
|
+
from meltygui.model.texture_model import _upload_cuda_image
|
|
329
|
+
from meltygui.view.texture_view import image_blit_pass
|
|
330
|
+
from meltygui.view import graph_cuda_view
|
|
331
|
+
out = gl_state.get('cuda_line_image',
|
|
332
|
+
lambda: torch.empty((height, width, 4), device=cuda_lines.device, dtype=torch.float16),
|
|
333
|
+
deps=(height, width, str(cuda_lines.device)))
|
|
334
|
+
cuda_lut = lut_tex.cuda(cuda_lines.device)
|
|
335
|
+
graph_cuda_view.render(cuda_lines, stats, out, cuda_lut, normalize=normalize,
|
|
336
|
+
zoom_x=zoom_x, zoom_y=zoom_y, pan_x=pan_x, pan_y=pan_y,
|
|
337
|
+
y_range=y_range, margin=margin, unit=unit, line_width=max(.5, float(line_width)),
|
|
338
|
+
line_opacity=max(0., min(1., float(line_opacity))), single_color=single_color)
|
|
339
|
+
image_blit_pass(gl_state, image=_upload_cuda_image(gl_state, out))
|
|
340
|
+
else:
|
|
341
|
+
line_pass(gl_state, series=tex, lut=lut_tex,
|
|
342
|
+
n_samples=int(n_samples), tex_w=int(tex_w), n_lines=int(n_lines),
|
|
343
|
+
zoom_x=float(zoom_x), zoom_y=float(zoom_y),
|
|
344
|
+
pan_x=float(pan_x), pan_y=float(pan_y),
|
|
345
|
+
y_min=float(y_range[0]), y_max=float(y_range[1]),
|
|
346
|
+
margin=float(margin), viewport=(float(width), float(height)),
|
|
347
|
+
unit_px=(float(unit[0]), float(unit[1])),
|
|
348
|
+
line_width=max(0.5, float(line_width)),
|
|
349
|
+
line_opacity=max(0.0, min(1.0, float(line_opacity))),
|
|
350
|
+
single_color=tuple(float(c) for c in single_color)[:3])
|
|
351
|
+
if not _blend_was:
|
|
352
|
+
gl.glDisable(gl.GL_BLEND)
|
|
353
|
+
if depth_was_on:
|
|
354
|
+
gl.glEnable(gl.GL_DEPTH_TEST)
|
|
355
|
+
|
|
356
|
+
img_pos = imgui.get_cursor_screen_pos()
|
|
357
|
+
imgui.image(fb.texture_id, width, height, uv0=(0, 1), uv1=(1, 0))
|
|
358
|
+
if show_axes:
|
|
359
|
+
_draw_axes_overlay(imgui.get_window_draw_list(), img_pos, width, height,
|
|
360
|
+
int(n_samples), y_range, zoom_x, zoom_y, pan_x, pan_y,
|
|
361
|
+
margin, unit, x_label, caption, imgui.get_font_size())
|
|
362
|
+
clamp_note = getattr(tex, "clamp_note", None)
|
|
363
|
+
if clamp_note:
|
|
364
|
+
_draw_image_notice(img_pos, width, clamp_note)
|
|
365
|
+
|
|
366
|
+
_draw_slice_sliders(draw_state, slider_dims, dim_names, slices, source_shape, width)
|
|
367
|
+
|
|
368
|
+
# ── params panel: the function's OWN params, satellite to and right
|
|
369
|
+
# of the window, double-click to show/hide (draw_voxels' panel verbatim
|
|
370
|
+
# - see the comments there for the mode/cursor/closed= contracts). ──
|
|
371
|
+
init = "params_panel" not in draw_state.misc
|
|
372
|
+
toggled = False
|
|
373
|
+
if left_mouse_double_clicked is not None:
|
|
374
|
+
draw_state.misc["params_panel"] = not draw_state.misc.get("params_panel", False)
|
|
375
|
+
toggled = True
|
|
376
|
+
draw_state.invalidate()
|
|
377
|
+
request_render()
|
|
378
|
+
panel_open = bool(draw_state.misc.get("params_panel", False))
|
|
379
|
+
panel_kwargs = {"closed": not panel_open} if (init or toggled) else {}
|
|
380
|
+
if not middle_mouse_drag and scroll_y_changed is None:
|
|
381
|
+
_flow_cursor = imgui.get_cursor_screen_pos()
|
|
382
|
+
_anchor_y = win.abs_top if draw_state.closable else draw_state.abs_top
|
|
383
|
+
imgui.set_cursor_screen_pos((win.abs_left + (win.width or width) + 12, _anchor_y))
|
|
384
|
+
changed, _, panel_ds = draw_any(draw_state.locate_params,
|
|
385
|
+
name=f"controls##{draw_state.name}",
|
|
386
|
+
is_tree=False,
|
|
387
|
+
use_cache=False,
|
|
388
|
+
show_name=False,
|
|
389
|
+
layer_offset=7,
|
|
390
|
+
tint=draw_state._kwargs.get("tint", None),
|
|
391
|
+
swoosh_mode=SwooshMode.LINE,
|
|
392
|
+
mode=Modes.WINDOW_PARAMS, show_tint=False,
|
|
393
|
+
parent_window=win, auto_resize=True,
|
|
394
|
+
shadow=False, return_extras=True,
|
|
395
|
+
initial={"expanded": True},
|
|
396
|
+
**panel_kwargs)
|
|
397
|
+
imgui.set_cursor_screen_pos(_flow_cursor)
|
|
398
|
+
if panel_ds is not None:
|
|
399
|
+
draw_state.misc["params_panel"] = not panel_ds.closed
|
|
400
|
+
if not panel_ds.closed:
|
|
401
|
+
if (not imgui.is_mouse_down(2) and not imgui.is_mouse_down(1) and not
|
|
402
|
+
imgui.is_mouse_down(0) and scroll_y_changed is None) and changed:
|
|
403
|
+
panel_ds.invalidate_up()
|
|
404
|
+
|
|
405
|
+
if line_pass.last_error:
|
|
406
|
+
imgui.text_colored(line_pass.last_error.splitlines()[0], 1.0, 0.45, 0.40, 1.0)
|
|
407
|
+
|
|
408
|
+
if changed:
|
|
409
|
+
draw_state.invalidate()
|
|
410
|
+
request_render()
|
|
411
|
+
return changed, input_value
|
|
412
|
+
|
|
413
|
+
return False, None
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@render_func(tint=(0.42, 0.36, 0.54), auto_resize=False, selectable=False)
|
|
417
|
+
def render_import_graph(input_value=None, draw_state=None,
|
|
418
|
+
graph_view_state: GraphViewState = None, root=ROOT,
|
|
419
|
+
left_mouse_down=False, left_mouse_double_clicked=False,
|
|
420
|
+
middle_mouse_drag=None, scroll_y_changed=None,
|
|
421
|
+
escape_key_pressed=False, slash_key_pressed=False,
|
|
422
|
+
**kwargs):
|
|
423
|
+
# ── knobs ────────────────────────────────────────────────────────────────
|
|
424
|
+
# Box geometry (px at ui_scale 1): padding around the name, corner
|
|
425
|
+
# radius, the gap between rows and between columns; the name's font
|
|
426
|
+
# scale grows with usage (the file's importer count on the graph's log
|
|
427
|
+
# scale, 0..1), so a hub's box is bigger.
|
|
428
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
429
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
430
|
+
from meltygui.view.header_view import flat_button
|
|
431
|
+
from meltygui.core.layout.header_runtime import _brightness_clamp_fn
|
|
432
|
+
from meltygui.model.import_graph_model import start_build
|
|
433
|
+
from meltygui.core.files.file_tree_core import _meta
|
|
434
|
+
from meltygui.core.files.file_tree_core import _tint_of
|
|
435
|
+
from meltygui.core.files.file_tree_core import open_file
|
|
436
|
+
import meltygui.model.import_graph_model as file_graph
|
|
437
|
+
|
|
438
|
+
box_pad_x = 8.0
|
|
439
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
440
|
+
box_pad_y = 4.0
|
|
441
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
442
|
+
box_radius = 6.0
|
|
443
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
444
|
+
row_gap = 6.0
|
|
445
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
446
|
+
column_gap = 60.0
|
|
447
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
448
|
+
usage_font_scale_max = 1.5
|
|
449
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
450
|
+
edge_alpha = 0.10
|
|
451
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
452
|
+
faded_alpha = 0.18 # everything unrelated while a node is selected
|
|
453
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
454
|
+
highlight_fallback_tint = (0.55, 0.72, 0.95)
|
|
455
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
456
|
+
importer_value_boost = 0.35
|
|
457
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
458
|
+
importer_saturation = 0.45
|
|
459
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
460
|
+
bg_theme_factor = 0.1
|
|
461
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
462
|
+
toolbar_height = 26.0
|
|
463
|
+
# [tint=(0.65, 0.55, 0.95)]
|
|
464
|
+
unpainted_node = (0.55, 0.57, 0.62)
|
|
465
|
+
|
|
466
|
+
state = graph_view_state
|
|
467
|
+
px = Melty.px
|
|
468
|
+
dl = imgui.get_window_draw_list()
|
|
469
|
+
cw = draw_state.content_width or (draw_state.width or 400)
|
|
470
|
+
|
|
471
|
+
# ── toolbar: build button + status (same as the tree's) ─────────────────
|
|
472
|
+
build = state._build
|
|
473
|
+
if build is not None and not build.running:
|
|
474
|
+
if build.result is not None:
|
|
475
|
+
state._graph = build.result
|
|
476
|
+
state._build = None
|
|
477
|
+
draw_state.invalidate()
|
|
478
|
+
if state._graph is None and file_graph.current() is not None:
|
|
479
|
+
state._graph = file_graph.current()
|
|
480
|
+
draw_state.invalidate()
|
|
481
|
+
if flat_button("Build import graph##import_graph", draw_state,
|
|
482
|
+
view_id="import_graph_build", height=px(toolbar_height) - px(4),
|
|
483
|
+
event="left_mouse_down"):
|
|
484
|
+
if state._build is None:
|
|
485
|
+
state._build = start_build(Path(root))
|
|
486
|
+
request_render()
|
|
487
|
+
status_x, status_y = imgui.get_item_rect_max().x + px(8), imgui.get_item_rect_min().y + px(4)
|
|
488
|
+
graph = state._graph
|
|
489
|
+
if build is not None and build.running:
|
|
490
|
+
status = "building…"
|
|
491
|
+
elif build is not None and build.error:
|
|
492
|
+
status = build.error
|
|
493
|
+
elif graph is not None:
|
|
494
|
+
status = f"{graph.file_count} files, {graph.edge_count} imports"
|
|
495
|
+
if state.selected:
|
|
496
|
+
status += f" · {Path(state.selected).name}"
|
|
497
|
+
else:
|
|
498
|
+
status = "no graph yet — build one"
|
|
499
|
+
dl.add_text(status_x, status_y, pack_color(0.75, 0.78, 0.82, 1.0), status)
|
|
500
|
+
imgui.set_cursor_pos_y(imgui.get_cursor_pos_y() + px(4))
|
|
501
|
+
x0, y0 = imgui.get_cursor_screen_pos()
|
|
502
|
+
height = max(1.0, (draw_state.height or 400) - (y0 - draw_state.abs_top) - px(4))
|
|
503
|
+
imgui.dummy(cw, height)
|
|
504
|
+
if graph is None or not getattr(graph, "layers", None):
|
|
505
|
+
return False, None
|
|
506
|
+
|
|
507
|
+
# ── pixel layout from the graph in column/row order ───────────────────────
|
|
508
|
+
# Box sizes memoized per (name, scale) on the state (underscored, not
|
|
509
|
+
# saved); a column is its widest box, rows share one pitch = the
|
|
510
|
+
# tallest box + row gap, so boxes never overlap.
|
|
511
|
+
size_memo = state.__dict__.setdefault("_size_memo", {})
|
|
512
|
+
font_size = imgui.get_font_size()
|
|
513
|
+
|
|
514
|
+
def font_scale_of(p):
|
|
515
|
+
return 1.0 + (usage_font_scale_max - 1.0) * graph.usage(p)
|
|
516
|
+
|
|
517
|
+
def box_size(p):
|
|
518
|
+
scale = font_scale_of(p)
|
|
519
|
+
key = (p.name, round(scale, 3))
|
|
520
|
+
size = size_memo.get(key)
|
|
521
|
+
if size is None:
|
|
522
|
+
text = imgui.calc_text_size(p.name)
|
|
523
|
+
size = size_memo[key] = (text.x * scale + 2 * px(box_pad_x),
|
|
524
|
+
font_size * scale + 2 * px(box_pad_y))
|
|
525
|
+
return size
|
|
526
|
+
|
|
527
|
+
sizes = {p: box_size(p) for layer in graph.layers for p in layer}
|
|
528
|
+
pitch = max(h for _w, h in sizes.values()) + px(row_gap)
|
|
529
|
+
boxes = {} # p → (x, y, w, h) in layout px (unzoomed)
|
|
530
|
+
column_x = 0.0
|
|
531
|
+
for layer in graph.layers:
|
|
532
|
+
widest = max(sizes[p][0] for p in layer)
|
|
533
|
+
total_h = len(layer) * pitch - px(row_gap)
|
|
534
|
+
y = -total_h * 0.5
|
|
535
|
+
for p in layer:
|
|
536
|
+
w, h = sizes[p]
|
|
537
|
+
boxes[p] = (column_x + (widest - w) * 0.5, y + (pitch - px(row_gap) - h) * 0.5, w, h)
|
|
538
|
+
y += pitch
|
|
539
|
+
column_x += widest + px(column_gap)
|
|
540
|
+
total_w = column_x - px(column_gap)
|
|
541
|
+
tallest = max(len(layer) for layer in graph.layers) * pitch
|
|
542
|
+
fit = min(cw / max(total_w, 1.0), height / max(tallest, 1.0), 1.0)
|
|
543
|
+
|
|
544
|
+
# ── camera ───────────────────────────────────────────────────────────────
|
|
545
|
+
if slash_key_pressed:
|
|
546
|
+
state.zoom, state.pan_x, state.pan_y = 1.0, 0.0, 0.0
|
|
547
|
+
if middle_mouse_drag is not None:
|
|
548
|
+
state.pan_x += middle_mouse_drag.dx
|
|
549
|
+
state.pan_y += middle_mouse_drag.dy
|
|
550
|
+
origin_x, origin_y = x0 + cw * 0.5, y0 + height * 0.5
|
|
551
|
+
if scroll_y_changed is not None:
|
|
552
|
+
# Zoom about the cursor: the box under the mouse stays put.
|
|
553
|
+
factor = math.exp(0.23 * scroll_y_changed.value)
|
|
554
|
+
new_zoom = min(50.0, max(0.1, state.zoom * factor))
|
|
555
|
+
mx, my = imgui.get_mouse_pos()
|
|
556
|
+
ratio = new_zoom / state.zoom
|
|
557
|
+
state.pan_x = (mx - origin_x) - ((mx - origin_x) - state.pan_x) * ratio
|
|
558
|
+
state.pan_y = (my - origin_y) - ((my - origin_y) - state.pan_y) * ratio
|
|
559
|
+
state.zoom = new_zoom
|
|
560
|
+
scale = fit * state.zoom
|
|
561
|
+
centre_x = total_w * 0.5
|
|
562
|
+
|
|
563
|
+
def to_screen(box):
|
|
564
|
+
x, y, w, h = box
|
|
565
|
+
return (origin_x + (x - centre_x) * scale + state.pan_x,
|
|
566
|
+
origin_y + y * scale + state.pan_y, w * scale, h * scale)
|
|
567
|
+
|
|
568
|
+
screen = {p: to_screen(box) for p, box in boxes.items()}
|
|
569
|
+
meta = _meta()
|
|
570
|
+
|
|
571
|
+
# ── selection / hover ────────────────────────────────────────────────────
|
|
572
|
+
if escape_key_pressed and state.selected is not None:
|
|
573
|
+
state.select(None)
|
|
574
|
+
request_render()
|
|
575
|
+
mx, my = imgui.get_mouse_pos()
|
|
576
|
+
hover_ok = draw_state._bounding_hovered
|
|
577
|
+
|
|
578
|
+
def node_at(pt):
|
|
579
|
+
if pt is None:
|
|
580
|
+
return None
|
|
581
|
+
for p, (sx, sy, sw, sh) in screen.items():
|
|
582
|
+
if sx <= pt[0] <= sx + sw and sy <= pt[1] <= sy + sh:
|
|
583
|
+
return p
|
|
584
|
+
return None
|
|
585
|
+
|
|
586
|
+
click = ((left_mouse_down.x, left_mouse_down.y)
|
|
587
|
+
if (left_mouse_down and hasattr(left_mouse_down, "x")) else None)
|
|
588
|
+
if click is not None:
|
|
589
|
+
state.select(node_at(click))
|
|
590
|
+
request_render()
|
|
591
|
+
dclick = ((left_mouse_double_clicked.x, left_mouse_double_clicked.y)
|
|
592
|
+
if (left_mouse_double_clicked and hasattr(left_mouse_double_clicked, "x")) else None)
|
|
593
|
+
if dclick is not None:
|
|
594
|
+
hit = node_at(dclick)
|
|
595
|
+
if hit is not None:
|
|
596
|
+
open_file(hit)
|
|
597
|
+
hovered = node_at((mx, my)) if hover_ok else None
|
|
598
|
+
|
|
599
|
+
selected = state.selected_path
|
|
600
|
+
if selected is not None and selected not in screen:
|
|
601
|
+
selected = None
|
|
602
|
+
imports = graph.imports_of(selected) if selected is not None else set()
|
|
603
|
+
importers = graph.importers_of(selected) if selected is not None else set()
|
|
604
|
+
related = imports | importers | ({selected} if selected is not None else set())
|
|
605
|
+
|
|
606
|
+
# ── colours ──────────────────────────────────────────────────────────────
|
|
607
|
+
style_manager = Melty.style_manager
|
|
608
|
+
brightness_clamp = _brightness_clamp_fn()
|
|
609
|
+
fill_memo = {}
|
|
610
|
+
|
|
611
|
+
def node_fill(tint, alpha):
|
|
612
|
+
"""The tab bar's mix of the file tint (same knobs as the tree rows)."""
|
|
613
|
+
key = (tint, alpha)
|
|
614
|
+
col = fill_memo.get(key)
|
|
615
|
+
if col is None:
|
|
616
|
+
mixed = style_manager.make_color_rgb(
|
|
617
|
+
tint[0], tint[1], tint[2],
|
|
618
|
+
value=Toggles.CodeEditor.tab_active_bg_brightness,
|
|
619
|
+
factor=bg_theme_factor,
|
|
620
|
+
saturation_scale=Toggles.CodeEditor.tab_active_bg_saturation,
|
|
621
|
+
alpha=1.0)
|
|
622
|
+
mixed = brightness_clamp(mixed[0], mixed[1], mixed[2], 0.0,
|
|
623
|
+
Toggles.CodeEditor.tab_active_bg_max_brightness)
|
|
624
|
+
col = fill_memo[key] = pack_color(mixed[0], mixed[1], mixed[2], alpha)
|
|
625
|
+
return col
|
|
626
|
+
|
|
627
|
+
selected_tint = (_tint_of(meta, selected) if selected is not None else None) or highlight_fallback_tint
|
|
628
|
+
hue, saturation, value = colorsys.rgb_to_hsv(*selected_tint)
|
|
629
|
+
importers_tint = colorsys.hsv_to_rgb(hue, saturation * importer_saturation,
|
|
630
|
+
min(1.0, value + importer_value_boost))
|
|
631
|
+
imports_col = pack_color(*selected_tint, 0.9)
|
|
632
|
+
importers_col = pack_color(*importers_tint, 0.9)
|
|
633
|
+
edge_col = pack_color(0.8, 0.85, 0.95, edge_alpha)
|
|
634
|
+
faded_edge_col = pack_color(0.8, 0.85, 0.95, edge_alpha * faded_alpha)
|
|
635
|
+
text_col = pack_color(0.92, 0.92, 0.92, 1.0)
|
|
636
|
+
faded_text_col = pack_color(0.92, 0.92, 0.92, faded_alpha)
|
|
637
|
+
outline_col = pack_color(1.0, 1.0, 1.0, 0.85)
|
|
638
|
+
|
|
639
|
+
# ── edges (under the boxes): importer → imported, right edge → left edge
|
|
640
|
+
# when the line runs left → right (the layered case), centre → centre
|
|
641
|
+
# for a cycle's back edge ────────────────────────────────────────────────
|
|
642
|
+
clip = getattr(draw_state, "abs_clip_rect", None)
|
|
643
|
+
|
|
644
|
+
def visible(box):
|
|
645
|
+
sx, sy, sw, sh = box
|
|
646
|
+
return clip is None or (sx + sw >= clip[0] and sx <= clip[2] and sy + sh >= clip[1] and sy <= clip[3])
|
|
647
|
+
|
|
648
|
+
def anchors(box_a, box_b):
|
|
649
|
+
ax, ay, aw, ah = box_a
|
|
650
|
+
bx, by, bw, bh = box_b
|
|
651
|
+
if ax + aw <= bx:
|
|
652
|
+
return (ax + aw, ay + ah * 0.5), (bx, by + bh * 0.5)
|
|
653
|
+
return (ax + aw * 0.5, ay + ah * 0.5), (bx + bw * 0.5, by + bh * 0.5)
|
|
654
|
+
|
|
655
|
+
for importer, targets in graph.imports.items():
|
|
656
|
+
box_a = screen.get(importer)
|
|
657
|
+
if box_a is None:
|
|
658
|
+
continue
|
|
659
|
+
for imported in targets:
|
|
660
|
+
box_b = screen.get(imported)
|
|
661
|
+
if box_b is None or not (visible(box_a) or visible(box_b)):
|
|
662
|
+
continue
|
|
663
|
+
a, b = anchors(box_a, box_b)
|
|
664
|
+
if selected is None:
|
|
665
|
+
col, thickness = edge_col, 1.0
|
|
666
|
+
elif importer == selected:
|
|
667
|
+
col, thickness = imports_col, 1.5 # what the selection imports
|
|
668
|
+
elif imported == selected:
|
|
669
|
+
col, thickness = importers_col, 1.5 # who imports the selection
|
|
670
|
+
else:
|
|
671
|
+
col, thickness = faded_edge_col, 1.0
|
|
672
|
+
dl.add_line(a[0], a[1], b[0], b[1], col, thickness)
|
|
673
|
+
|
|
674
|
+
# ── boxes: rounded rect in the file's tint, the name inside ──────────────
|
|
675
|
+
radius = px(box_radius) * scale
|
|
676
|
+
for p, (sx, sy, sw, sh) in screen.items():
|
|
677
|
+
if not visible((sx, sy, sw, sh)):
|
|
678
|
+
continue
|
|
679
|
+
tint = _tint_of(meta, p) or unpainted_node
|
|
680
|
+
faded = selected is not None and p not in related
|
|
681
|
+
alpha = faded_alpha if faded else 1.0
|
|
682
|
+
dl.add_rect_filled(sx, sy, sx + sw, sy + sh, node_fill(tint, alpha), rounding=radius)
|
|
683
|
+
if p == selected:
|
|
684
|
+
dl.add_rect(sx - px(1), sy - px(1), sx + sw + px(1), sy + sh + px(1), outline_col, rounding=radius, thickness=2.0)
|
|
685
|
+
elif p in importers:
|
|
686
|
+
dl.add_rect(sx, sy, sx + sw, sy + sh, importers_col, rounding=radius, thickness=1.5)
|
|
687
|
+
elif p in imports:
|
|
688
|
+
dl.add_rect(sx, sy, sx + sw, sy + sh, imports_col, rounding=radius, thickness=1.5)
|
|
689
|
+
elif p == hovered:
|
|
690
|
+
dl.add_rect(sx, sy, sx + sw, sy + sh, outline_col, rounding=radius, thickness=1.0)
|
|
691
|
+
# The name at the box's own font scale × the camera scale (window
|
|
692
|
+
# font scale retargets the entire list's font size; reset right after).
|
|
693
|
+
text_scale = font_scale_of(p) * scale
|
|
694
|
+
if text_scale * font_size >= 4.0: # unreadable below this: skip
|
|
695
|
+
imgui.set_window_font_scale(text_scale)
|
|
696
|
+
dl.add_text(sx + px(box_pad_x) * scale, sy + px(box_pad_y) * scale,
|
|
697
|
+
faded_text_col if faded else text_col, p.name)
|
|
698
|
+
imgui.set_window_font_scale(1.0)
|
|
699
|
+
|
|
700
|
+
return False, None
|
|
701
|
+
|
|
702
|
+
def _unit_px(width, height, auto_scale):
|
|
703
|
+
"""Pixels per graph unit per axis — the one place the scaling policy
|
|
704
|
+
lives (the shader's unit_px). auto_scale stretches the fitted graph to
|
|
705
|
+
the image; otherwise both axes share min(w, h)/2 so the plot keeps its
|
|
706
|
+
aspect whatever the window's shape (draw_voxels never distorts its box
|
|
707
|
+
either)."""
|
|
708
|
+
if auto_scale:
|
|
709
|
+
return width * 0.5, height * 0.5
|
|
710
|
+
u = min(width, height) * 0.5
|
|
711
|
+
return u, u
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _graph_to_norm(g, zoom, pan, margin):
|
|
715
|
+
"""graph units (the shader's g) → normalized data coordinate [0, 1]."""
|
|
716
|
+
return ((g / zoom + pan) / margin + 1.0) * 0.5
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _norm_to_px(v, zoom, pan, margin, px, unit, flip=False):
|
|
720
|
+
"""normalized data coordinate → pixel offset inside the image (one axis)."""
|
|
721
|
+
g = ((v * 2.0 - 1.0) * margin - pan) * zoom
|
|
722
|
+
return px * 0.5 - g * unit if flip else px * 0.5 + g * unit
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _nice_step(span, target_ticks):
|
|
726
|
+
"""1-2-5·10ᵏ step giving about `target_ticks` over `span`."""
|
|
727
|
+
if span <= 0 or target_ticks <= 0:
|
|
728
|
+
return 1.0
|
|
729
|
+
raw = span / target_ticks
|
|
730
|
+
k = 10.0 ** math.floor(math.log10(raw))
|
|
731
|
+
for s in (1.0, 2.0, 5.0, 10.0):
|
|
732
|
+
if s * k >= raw:
|
|
733
|
+
return s * k
|
|
734
|
+
return 10.0 * k
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _fmt_value(v, step):
|
|
738
|
+
if step >= 1.0:
|
|
739
|
+
return f"{v:.0f}"
|
|
740
|
+
decimals = min(9, max(0, int(math.ceil(-math.log10(step)))))
|
|
741
|
+
return f"{v:.{decimals}f}"
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _draw_axes_overlay(draw_list, img_pos, width, height, n_samples, y_range,
|
|
745
|
+
zoom_x, zoom_y, pan_x, pan_y, margin, unit, x_label,
|
|
746
|
+
caption, font_px):
|
|
747
|
+
"""2-D axis furniture over the image: faint grid, sample-index ticks
|
|
748
|
+
along the bottom, value ticks along the left, the x dim's name, and a
|
|
749
|
+
caption (series dim × line count) top-right. Pure imgui draw-list
|
|
750
|
+
text, recomputed per frame from the camera params."""
|
|
751
|
+
x0, y0 = img_pos
|
|
752
|
+
grid_col = pack_color(1.0, 1.0, 1.0, 0.07)
|
|
753
|
+
tick_col = pack_color(0.85, 0.85, 0.85, 0.8)
|
|
754
|
+
dim_col = pack_color(0.85, 0.85, 0.85, 0.55)
|
|
755
|
+
# x: visible index span from the inverse camera at the image edges.
|
|
756
|
+
ux, uy = unit
|
|
757
|
+
if n_samples > 1:
|
|
758
|
+
# visible index span: the image edges are g = ±(half-extent / unit)
|
|
759
|
+
gx = width * 0.5 / ux
|
|
760
|
+
lo = _graph_to_norm(-gx, zoom_x, pan_x, margin) * (n_samples - 1)
|
|
761
|
+
hi = _graph_to_norm(gx, zoom_x, pan_x, margin) * (n_samples - 1)
|
|
762
|
+
lo, hi = max(0.0, lo), min(float(n_samples - 1), hi)
|
|
763
|
+
px_per_idx = 2.0 * ux * zoom_x * margin / (n_samples - 1)
|
|
764
|
+
for i in _tick_values(lo, hi, px_per_idx, font_px):
|
|
765
|
+
px = x0 + _norm_to_px(i / (n_samples - 1), zoom_x, pan_x, margin, width, ux)
|
|
766
|
+
draw_list.add_line(px, y0, px, y0 + height, grid_col)
|
|
767
|
+
label = str(i)
|
|
768
|
+
tw, th = imgui.calc_text_size(label)
|
|
769
|
+
draw_list.add_text(px - tw * 0.5, y0 + height - th - 2, tick_col, label)
|
|
770
|
+
# y: value ticks at a nice step over the visible value span.
|
|
771
|
+
ymin, ymax = y_range
|
|
772
|
+
gy = height * 0.5 / uy
|
|
773
|
+
v_lo = ymin + _graph_to_norm(-gy, zoom_y, pan_y, margin) * (ymax - ymin)
|
|
774
|
+
v_hi = ymin + _graph_to_norm(gy, zoom_y, pan_y, margin) * (ymax - ymin)
|
|
775
|
+
step = _nice_step(v_hi - v_lo, max(2, height / 70.0))
|
|
776
|
+
v = math.ceil(v_lo / step) * step
|
|
777
|
+
guard = 0
|
|
778
|
+
while v <= v_hi and guard < 200:
|
|
779
|
+
guard += 1
|
|
780
|
+
vn = (v - ymin) / (ymax - ymin)
|
|
781
|
+
py = y0 + _norm_to_px(vn, zoom_y, pan_y, margin, height, uy, flip=True)
|
|
782
|
+
draw_list.add_line(x0, py, x0 + width, py, grid_col)
|
|
783
|
+
label = _fmt_value(v, step)
|
|
784
|
+
tw, th = imgui.calc_text_size(label)
|
|
785
|
+
draw_list.add_text(x0 + 4, py - th * 0.5, tick_col, label)
|
|
786
|
+
v += step
|
|
787
|
+
if x_label:
|
|
788
|
+
tw, th = imgui.calc_text_size(x_label)
|
|
789
|
+
draw_list.add_text(x0 + width - tw - 6, y0 + height - th - 2, dim_col, x_label)
|
|
790
|
+
if caption:
|
|
791
|
+
tw, th = imgui.calc_text_size(caption)
|
|
792
|
+
draw_list.add_text(x0 + width - tw - 6, y0 + 4, dim_col, caption)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
LINE_VERT = """
|
|
796
|
+
#version 330 core
|
|
797
|
+
uniform sampler3D series; // texel (i % tex_w, i / tex_w, line) = sample i of line
|
|
798
|
+
out float v_edge; // signed pixel distance from the line's centerline
|
|
799
|
+
flat out int v_line;
|
|
800
|
+
|
|
801
|
+
// data (sample index, value) → pixel. Zoom/pan ARE the camera: a fitted
|
|
802
|
+
// graph spans [-margin, margin] graph units at zoom 1, pan shifts in that
|
|
803
|
+
// space, and unit_px (pixels per graph unit, per axis) places it in the
|
|
804
|
+
// image — equal components keep the plot's aspect (the default), the
|
|
805
|
+
// image's half-extents stretch it to fill (auto_scale).
|
|
806
|
+
vec2 toPix(int i, float y) {
|
|
807
|
+
float xn = n_samples > 1 ? float(i) / float(n_samples - 1) : 0.5;
|
|
808
|
+
float yn = (y - y_min) / max(y_max - y_min, 1e-30);
|
|
809
|
+
vec2 g = vec2(((xn * 2.0 - 1.0) * margin - pan_x) * zoom_x,
|
|
810
|
+
((yn * 2.0 - 1.0) * margin - pan_y) * zoom_y);
|
|
811
|
+
return viewport * 0.5 + g * unit_px;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
float sampleAt(int i, int line) {
|
|
815
|
+
return texelFetch(series, ivec3(i % tex_w, i / tex_w, line), 0).r;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
void main() {
|
|
819
|
+
int seg = gl_VertexID / 6;
|
|
820
|
+
int corner = gl_VertexID % 6;
|
|
821
|
+
int line = gl_InstanceID;
|
|
822
|
+
v_line = line;
|
|
823
|
+
float y0 = sampleAt(seg, line);
|
|
824
|
+
float y1 = sampleAt(seg + 1, line);
|
|
825
|
+
if (isnan(y0) || isnan(y1) || isinf(y0) || isinf(y1)) {
|
|
826
|
+
// A gap in the data: park the vertex outside the clip volume.
|
|
827
|
+
v_edge = 0.0;
|
|
828
|
+
gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
vec2 s0 = toPix(seg, y0);
|
|
832
|
+
vec2 s1 = toPix(seg + 1, y1);
|
|
833
|
+
vec2 dir = s1 - s0;
|
|
834
|
+
float len = length(dir);
|
|
835
|
+
vec2 ext = len > 1e-6 ? dir / len : vec2(1.0, 0.0);
|
|
836
|
+
vec2 nrm = vec2(-ext.y, ext.x);
|
|
837
|
+
float hw = line_width * 0.5 + 1.0; // +1px skirt for the AA ramp
|
|
838
|
+
// two triangles: (s0,-)(s1,-)(s1,+) and (s0,-)(s1,+)(s0,+); the ends
|
|
839
|
+
// extend by hw along the segment so consecutive segments overlap at
|
|
840
|
+
// joins instead of leaving wedge gaps on sharp turns.
|
|
841
|
+
bool at1 = (corner == 1 || corner == 2 || corner == 4);
|
|
842
|
+
float side = (corner == 2 || corner == 4 || corner == 5) ? 1.0 : -1.0;
|
|
843
|
+
vec2 p = (at1 ? s1 + ext * hw : s0 - ext * hw) + nrm * side * hw;
|
|
844
|
+
v_edge = side * hw;
|
|
845
|
+
gl_Position = vec4(p / viewport * 2.0 - 1.0, 0.0, 1.0);
|
|
846
|
+
}
|
|
847
|
+
"""
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
LINE_FRAG = """
|
|
851
|
+
#version 330 core
|
|
852
|
+
in float v_edge;
|
|
853
|
+
flat in int v_line;
|
|
854
|
+
out vec4 FragColor;
|
|
855
|
+
uniform sampler1D lut;
|
|
856
|
+
|
|
857
|
+
void main() {
|
|
858
|
+
float d = abs(v_edge);
|
|
859
|
+
float a = 1.0 - smoothstep(line_width * 0.5 - 0.5, line_width * 0.5 + 0.5, d);
|
|
860
|
+
a *= line_opacity;
|
|
861
|
+
vec3 c = n_lines > 1
|
|
862
|
+
? texture(lut, (float(v_line) + 0.5) / float(n_lines)).rgb
|
|
863
|
+
: single_color;
|
|
864
|
+
c = pow(max(c, 0.0), vec3(2.2)); // LUT / tint are display-referred sRGB; the FBO is linear (hdr_color.py)
|
|
865
|
+
FragColor = vec4(c * a, a); // premultiplied — the FBO composites ONE / 1-a
|
|
866
|
+
}
|
|
867
|
+
"""
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
@shader_func(fragment=LINE_FRAG, vertex=LINE_VERT)
|
|
871
|
+
def line_pass(gl_state: GLState = None, series=None, lut=None, n_samples=2,
|
|
872
|
+
tex_w=1, n_lines=1, zoom_x=1.0, zoom_y=1.0, pan_x=0.0, pan_y=0.0,
|
|
873
|
+
y_min=0.0, y_max=1.0, margin=0.92, viewport=(1.0, 1.0),
|
|
874
|
+
unit_px=(0.5, 0.5), line_width=1.5, line_opacity=1.0, single_color=(0.35, 0.75, 1.0),
|
|
875
|
+
**kwargs):
|
|
876
|
+
# Program bound, uniforms set. Attribute-less instanced draw: the vertex
|
|
877
|
+
# shader computes every segment quad from gl_VertexID / gl_InstanceID and
|
|
878
|
+
# the series texture - 6 vertices per segment, one instance per line.
|
|
879
|
+
if n_samples < 2 or n_lines < 1:
|
|
880
|
+
return
|
|
881
|
+
gl.glBindVertexArray(gl_state.vao("fs_triangle"))
|
|
882
|
+
gl.glDrawArraysInstanced(gl.GL_TRIANGLES, 0, 6 * (int(n_samples) - 1), int(n_lines))
|