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.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. 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))