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,338 @@
1
+ """
2
+ 3D mouse (3Dconnexion SpaceMouse) input for the InputHandler.
3
+
4
+ The device is read through spacenavd's Unix socket, not the evdev node:
5
+ spacenavd grabs /dev/input/eventN exclusively (so the puck can't double as
6
+ a pointer), needs no group membership on our side, and applies the user's
7
+ /etc/spnavrc sensitivity / dead zone / axis inversion before we see anything.
8
+ The wire format is spacenavd's protocol v0 — the one every client gets
9
+ without a handshake: a stream of 32-byte frames, eight native int32s each:
10
+
11
+ [0, x, y, z, rx, ry, rz, period_ms] motion (raw units, ±FULL_DEFLECTION)
12
+ [1, button, 0, ...] button press
13
+ [2, button, 0, ...] button release
14
+
15
+ Three pieces, deliberately separate:
16
+
17
+ - `_Reader`: one daemon thread per PROCESS (parked on sys._lsd_space_mouse
18
+ so a studio restart or a hotswap never spawns a second one) that keeps the
19
+ socket open, reconnects while spacenavd is away, and holds the LEVEL
20
+ state — the latest deflection of the six axes, stamped with its arrival
21
+ time — plus a queue of button edges. It never touches the handler.
22
+ - `pump(handler)`: called once per frame beside the GLFW backend's pump. It
23
+ samples the level state like the cursor position is sampled (level
24
+ state, per frame: nothing is lost by not queueing every report) and
25
+ feeds ONE axes event holding the deflection integrated over the frame —
26
+ full-deflection-seconds, so a view's sensitivity is "per second at full
27
+ push" and frame rate never changes how fast the camera moves.
28
+ - The mapping helpers (`normalize`) — dead zone, axis swap / inversion,
29
+ unit scale — pure functions over the raw tuple, tested offline.
30
+
31
+ Every knob is in Toggles.SpaceMouse (device side); what a view DOES with the
32
+ axes is that view's business (draw_voxels: the navigation knobs in the same Toggles.SpaceMouse).
33
+ """
34
+
35
+ from __future__ import annotations
36
+ import socket
37
+ import struct
38
+ import sys
39
+ import threading
40
+ import time
41
+ import weakref
42
+ from collections import deque
43
+
44
+ from meltygui.core.runtime.toggles import Toggles
45
+
46
+ # The input_id a view subscribes to: `space_mouse_changed(event` on a
47
+ # render_func signature. Buttons are `space_mouse_button_0_down` etc.
48
+ INPUT_ID = "space_mouse"
49
+ BUTTON_ID = "space_mouse_button_{}"
50
+ # Axis order on the wire and in event.axes.
51
+ AXES = ("tx", "ty", "tz", "rx", "ry", "rz")
52
+ # Blender's hand, measured against it on 09-04 (Lukas, spacenavd's stream on
53
+ # a SpaceMouse Wireless): translation passes straight through - push right,
54
+ # the volume shifts right; push forward, it comes closer - and every ROTATION
55
+ # is negated relative to the raw stream. Applied in normalize() so the axes
56
+ # reach views in OBJECT terms (voxel_camera.apply_space_mouse).
57
+ BLENDER_SIGNS = (1.0, 1.0, 1.0, -1.0, -1.0, -1.0)
58
+ _FRAME = struct.Struct("@8i")
59
+ FRAME_SIZE = _FRAME.size
60
+ _MOTION, _PRESS, _RELEASE = 0, 1, 2
61
+
62
+
63
+ def parse_frame(data: bytes):
64
+ """One protocol-v0 frame → ("motion", (x, y, z, rx, ry, rz)) or
65
+ ("button", index, pressed) or None for a frame type we don't know
66
+ (skipped, the stream stays aligned since every frame is FRAME_SIZE)."""
67
+ kind, a, b, c, d, e, f, _period = _FRAME.unpack(data)
68
+ if kind == _MOTION:
69
+ return "motion", (a, b, c, d, e, f)
70
+ if kind == _PRESS or kind == _RELEASE:
71
+ return "button", a, kind == _PRESS
72
+ return None
73
+
74
+
75
+ def axis_sensitivities():
76
+ """Toggles.SpaceMouse's six per-axis floats, in AXES order."""
77
+ return (float(Toggles.SpaceMouse.tx_sensitivity), float(Toggles.SpaceMouse.ty_sensitivity),
78
+ float(Toggles.SpaceMouse.tz_sensitivity), float(Toggles.SpaceMouse.rx_sensitivity),
79
+ float(Toggles.SpaceMouse.ry_sensitivity), float(Toggles.SpaceMouse.rz_sensitivity))
80
+
81
+
82
+ def normalize(raw, *, full_deflection=None, dead_zone=None, swap_yz=None,
83
+ invert_axes=None, sensitivities=None):
84
+ """Raw device units → (tx, ty, tz, rx, ry, rz) in full-deflection units
85
+ (±1 at full push before the per-axis sensitivity), with
86
+ Toggles.SpaceMouse's dead zone, y/z swap, Blender's hand (BLENDER_SIGNS),
87
+ per-axis inversion and per-axis sensitivity applied (keyword overrides
88
+ exist for the tests). The result is in OBJECT terms — the motion the
89
+ view's content should make — so no consumer needs to know the hand."""
90
+ full = float(Toggles.SpaceMouse.full_deflection if full_deflection is None else full_deflection)
91
+ dead = float(Toggles.SpaceMouse.dead_zone if dead_zone is None else dead_zone)
92
+ swap = Toggles.SpaceMouse.swap_yz if swap_yz is None else swap_yz
93
+ invert = Toggles.SpaceMouse.invert_axes if invert_axes is None else invert_axes
94
+ sens = axis_sensitivities() if sensitivities is None else sensitivities
95
+ v = [float(r) / full for r in raw]
96
+ if swap:
97
+ v[1], v[2], v[4], v[5] = v[2], v[1], v[5], v[4]
98
+ out = []
99
+ for name, x, hand, gain in zip(AXES, v, BLENDER_SIGNS, sens):
100
+ # Dead zone re-scaled so the live range still starts at 0 (no jump
101
+ # across the threshold) and still reaches 1.0 at full push.
102
+ mag = abs(x)
103
+ if mag <= dead:
104
+ x = 0.0
105
+ elif dead > 0.0:
106
+ x = (mag - dead) / (1.0 - dead) * (1.0 if x > 0 else -1.0)
107
+ x = max(-1.0, min(1.0, x * hand)) * gain
108
+ if name in invert:
109
+ x = -x
110
+ out.append(x)
111
+ return tuple(out)
112
+
113
+
114
+ class _Reader:
115
+ """The socket thread + level state. One per process."""
116
+
117
+ def __init__(self):
118
+ self.lock = threading.Lock()
119
+ self.raw = (0, 0, 0, 0, 0, 0) # latest motion frame, raw units
120
+ self.stamp = 0.0 # perf_counter of latest frame
121
+ self.buttons = [] # button (index, pressed) edges
122
+ self.connected = False
123
+ self.error = None
124
+ self.frames = 0
125
+ self._sock = None
126
+ self._thread = threading.Thread(target=self._run, name="space-mouse", daemon=True)
127
+ self._thread.start()
128
+
129
+ # ── the thread ──────────────────────────────────────────────────────────
130
+ def _run(self):
131
+ while True:
132
+ if not Toggles.SpaceMouse.enabled:
133
+ self._drop()
134
+ time.sleep(0.5)
135
+ continue
136
+ if self._sock is None and not self._connect():
137
+ time.sleep(float(Toggles.SpaceMouse.retry_s))
138
+ continue
139
+ try:
140
+ data = self._sock.recv(FRAME_SIZE * 64)
141
+ except socket.timeout:
142
+ continue
143
+ except OSError as e:
144
+ self.error = str(e)
145
+ self._drop()
146
+ continue
147
+ if not data:
148
+ self.error = "spacenavd closed the socket"
149
+ self._drop()
150
+ continue
151
+ self._feed(data)
152
+
153
+ def _connect(self):
154
+ if not hasattr(socket, "AF_UNIX"): # Windows: spacenavd is unix-socket only
155
+ self.error = "spacenavd needs unix sockets, which this platform lacks"
156
+ return False
157
+ try:
158
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
159
+ s.settimeout(1.0)
160
+ s.connect(str(Toggles.SpaceMouse.socket_path))
161
+ except OSError as e:
162
+ self.error = str(e)
163
+ return False
164
+ self._sock = s
165
+ self._buffer = b""
166
+ self.connected = True
167
+ self.error = None
168
+ return True
169
+
170
+ def _drop(self):
171
+ if self._sock is not None:
172
+ try:
173
+ self._sock.close()
174
+ except OSError:
175
+ pass
176
+ self._sock = None
177
+ self.connected = False
178
+ with self.lock:
179
+ self.raw = (0, 0, 0, 0, 0, 0)
180
+
181
+ def _feed(self, data: bytes):
182
+ buf = self._buffer + data
183
+ pos = 0
184
+ now = time.perf_counter()
185
+ moved = False
186
+ while len(buf) - pos >= FRAME_SIZE:
187
+ frame = parse_frame(buf[pos:pos + FRAME_SIZE])
188
+ pos += FRAME_SIZE
189
+ self.frames += 1
190
+ if frame is None:
191
+ continue
192
+ if frame[0] == "motion":
193
+ with self.lock:
194
+ self.raw = frame[1]
195
+ self.stamp = now
196
+ moved = True
197
+ else:
198
+ with self.lock:
199
+ self.buttons.append((frame[1], frame[2]))
200
+ moved = True
201
+ self._buffer = buf[pos:]
202
+ if moved:
203
+ # Wake the render loop (glfw.wait_events otherwise sleeps until
204
+ # a pointer/keyboard event): the next frame's pump samples us.
205
+ _wake()
206
+
207
+
208
+ def _wake():
209
+ try:
210
+ from meltygui.core.windowing.glfw_utils import request_render
211
+ request_render()
212
+ except Exception:
213
+ pass
214
+
215
+
216
+ def reader() -> _Reader:
217
+ """The process-lifetime reader, created on first use."""
218
+ r = getattr(sys, "_lsd_space_mouse", None)
219
+ if r is None:
220
+ r = _Reader()
221
+ sys._lsd_space_mouse = r
222
+ return r
223
+
224
+
225
+ def start():
226
+ """Start reading (idempotent). Called from Melty.init_input_backend."""
227
+ reader()
228
+
229
+
230
+ # Per-frame sampling state: the last pump time and the axes last fed, so a
231
+ # release still delivers one final zero event (the view sees a gesture end).
232
+ _PUMP = globals().get("_PUMP") or {"last": 0.0, "active": False, "buttons_down": set(),
233
+ "status": None, "frame_ms": 0.0}
234
+ # (pump time, reader.frames) samples for the report-rate estimate in stats().
235
+ # Counted on the PUMP side, not in the reader thread: the reader is a
236
+ # process-lifetime object bound to whatever class it was born with, so a
237
+ # reload swap of this file never changes the code its thread runs: a stamp field
238
+ # filled by _feed caused 0 Hz after a swap (Lukas 09-04). The frames counter is
239
+ # the one thing every reader version has always kept.
240
+ _RATE = globals().get("_RATE") or deque(maxlen=128)
241
+ # Views that show the reading location (draw_space_mouse) without being the
242
+ # hovered event target: their draw_states, invalidated in the pump on every
243
+ # active frame, once more on release, and when the connection status flips.
244
+ _WATCHERS: "weakref.WeakSet" = globals().get("_WATCHERS") or weakref.WeakSet()
245
+
246
+
247
+ def watch(draw_state):
248
+ """Register a draw_state to be invalidated whenever the reading moves."""
249
+ _WATCHERS.add(draw_state)
250
+
251
+
252
+ def _invalidate_watchers():
253
+ for draw_state in list(_WATCHERS):
254
+ try:
255
+ draw_state.invalidate()
256
+ except Exception:
257
+ pass
258
+
259
+
260
+ def pump(handler, now: float = None):
261
+ """Per frame: integrate the level state over the frame and feed the
262
+ handler. `now` is injectable for the tests."""
263
+ r = getattr(sys, "_lsd_space_mouse", None)
264
+ if r is None:
265
+ return
266
+ now = time.perf_counter() if now is None else now
267
+ dt = now - _PUMP["last"]
268
+ _PUMP["last"] = now
269
+ _PUMP["frame_ms"] = dt * 1000.0
270
+ _RATE.append((now, int(getattr(r, "frames", 0))))
271
+ dt = max(0.0, min(dt, float(Toggles.SpaceMouse.max_frame_dt)))
272
+ with r.lock:
273
+ raw, stamp = r.raw, r.stamp
274
+ buttons, r.buttons = r.buttons, []
275
+ # A stuck level (spacedavd died mid-push, a dropped release frame) should
276
+ # never keep the camera moving: past stale_s the reading is zero.
277
+ if now - stamp > float(Toggles.SpaceMouse.stale_s):
278
+ raw = (0, 0, 0, 0, 0, 0)
279
+ axes = normalize(raw)
280
+ active = any(a != 0.0 for a in axes)
281
+ if active or _PUMP["active"]:
282
+ handler.feed_axes(INPUT_ID, tuple(a * dt for a in axes), now)
283
+ _invalidate_watchers()
284
+ _PUMP["active"] = active
285
+ current_status = (r.connected, r.error)
286
+ if current_status != _PUMP["status"]:
287
+ _PUMP["status"] = current_status
288
+ _invalidate_watchers()
289
+ if active:
290
+ _wake() # keep frames coming while the puck is pressed
291
+ for index, pressed in buttons:
292
+ input_id = BUTTON_ID.format(index)
293
+ if pressed:
294
+ _PUMP["buttons_down"].add(input_id)
295
+ handler.feed_down(input_id, t=now)
296
+ elif input_id in _PUMP["buttons_down"]:
297
+ _PUMP["buttons_down"].discard(input_id)
298
+ handler.feed_up(input_id, t=now)
299
+
300
+
301
+ def active() -> bool:
302
+ """True while the cap is deflected (as of the last pump) — the studio's
303
+ gesture flag: Melty folds it into on_drag so the churn suppression a
304
+ mouse drag gets (hover invalidation, occluder diffs, content-height
305
+ commits, RenderHost draws) holds for a 3D-mouse flight too."""
306
+ return bool(_PUMP["active"])
307
+
308
+
309
+ def stats(now: float = None) -> dict:
310
+ """Latency diagnostics: `rate_hz` = the device's motion-report rate over
311
+ the last reports, `age_ms` = how old the newest report is at this call,
312
+ `frame_ms` = the interval between the last two pumps (the studio's frame
313
+ time while the cap is held). Latency = age + the frame + presentation."""
314
+ r = getattr(sys, "_lsd_space_mouse", None)
315
+ now = time.perf_counter() if now is None else now
316
+ if r is None:
317
+ return {"rate_hz": 0.0, "age_ms": 0.0, "frame_ms": _PUMP["frame_ms"]}
318
+ with r.lock:
319
+ stamp = r.stamp
320
+ # Reports per second in the pump samples of the last second: the
321
+ # counter delta over the time delta, so reports between the frames all
322
+ # count (a per-frame stamp compare would fold them into one).
323
+ rate = 0.0
324
+ recent = [(t, n) for t, n in _RATE if now - t <= 1.0]
325
+ if len(recent) >= 2 and recent[-1][0] > recent[0][0]:
326
+ rate = (recent[-1][1] - recent[0][1]) / (recent[-1][0] - recent[0][0])
327
+ age = (now - stamp) * 1000.0 if stamp else 0.0
328
+ return {"rate_hz": rate, "age_ms": age, "frame_ms": _PUMP["frame_ms"]}
329
+
330
+
331
+ def status() -> str:
332
+ """One line for a status row / the console."""
333
+ r = getattr(sys, "_lsd_space_mouse", None)
334
+ if r is None:
335
+ return "not started"
336
+ if r.connected:
337
+ return f"connected ({r.frames} frames)"
338
+ return f"disconnected: {r.error or 'connecting'}"