meltygui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""The desktop's window-gesture toggle, mirrored in meltygui's own title bar.
|
|
2
|
+
|
|
3
|
+
Lukas's patched Hyprland (~/.local/opt/hyprland-hdr, `general:left_drag_move`)
|
|
4
|
+
moves a floating window by a plain left drag on its empty space, and its
|
|
5
|
+
hyprbars title bars carry a stateful button (`state = "left_drag_move"`)
|
|
6
|
+
that shows whether that applies to the window's app and toggles it — the
|
|
7
|
+
app's class goes in / out of `general:left_drag_move_exclude`, live and
|
|
8
|
+
persisted through the desktop's Settings overrides, by
|
|
9
|
+
`desktop/left-drag-toggle`. meltygui's frameless windows draw their own
|
|
10
|
+
controls (titlebar.py), so on that desktop they get the same button:
|
|
11
|
+
|
|
12
|
+
available() the compositor has the option (a stock Hyprland answers
|
|
13
|
+
"no such option"; GNOME / KDE have no socket at all)
|
|
14
|
+
enabled() left-drag move applies to THIS window's class right now
|
|
15
|
+
toggle() runs the desktop's own script for the class — one source
|
|
16
|
+
of truth, one notification, the hyprbars refresh included
|
|
17
|
+
|
|
18
|
+
The button owns BOTH compositor gestures for this app's class (09-13):
|
|
19
|
+
plain left-drag MOVE (general:left_drag_move) and right-drag RESIZE
|
|
20
|
+
(general:right_drag_resize). Lit = the compositor drives them; faded = the
|
|
21
|
+
class is excluded from both and the meltygui app does them itself — its
|
|
22
|
+
drag-anywhere xdg_toplevel.move (titlebar.drag_anywhere_enabled) and its
|
|
23
|
+
right-drag resize through the edge physics (os_frame), which is also what
|
|
24
|
+
lets nested meltygui windows keep their own right-drag (the compositor's
|
|
25
|
+
resize swallows the press otherwise). The studio is excluded statically in
|
|
26
|
+
hyprland.lua; apps flip through this button. `toggle()` runs the script
|
|
27
|
+
with `--gestures`, which moves the class through both exclude lists; a
|
|
28
|
+
probe that finds the right list out of step with the left one (a hyprbars
|
|
29
|
+
button or the Settings app edited only the left) runs `--gestures --sync`
|
|
30
|
+
once to align it (`resize_synced`).
|
|
31
|
+
|
|
32
|
+
Both reads are `getoption` requests on Hyprland's socket
|
|
33
|
+
(geometry_feed.hypr_request, ~0.05 ms) made on a daemon thread, the same
|
|
34
|
+
shape as titlebar_buttons: `state()` answers "unknown" until the first
|
|
35
|
+
probe lands, a change requests a frame, `refresh_if_stale` re-probes at
|
|
36
|
+
Toggles.Melty.titlebar_button_refresh_s (the desktop's Settings app can
|
|
37
|
+
flip the option too), `refresh()` after a focus gain. The toggle script
|
|
38
|
+
is spawned through posix_spawn (absolute path, close_fds=False, no cwd —
|
|
39
|
+
the studio never forks, CLAUDE.md) and a re-probe follows it.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import json
|
|
44
|
+
import os
|
|
45
|
+
import re
|
|
46
|
+
import shutil
|
|
47
|
+
import subprocess
|
|
48
|
+
import threading
|
|
49
|
+
import time
|
|
50
|
+
|
|
51
|
+
OPTION = "general:left_drag_move"
|
|
52
|
+
EXCLUDE_OPTION = "general:left_drag_move_exclude"
|
|
53
|
+
RESIZE_OPTION = "general:right_drag_resize"
|
|
54
|
+
RESIZE_EXCLUDE_OPTION = "general:right_drag_resize_exclude"
|
|
55
|
+
|
|
56
|
+
# The desktop's toggle script: on PATH if the session exports it, else at
|
|
57
|
+
# the desktop's install root (config/hyprland.lua's `root`).
|
|
58
|
+
_DESKTOP_ROOT = os.path.expanduser("~/.local/opt/hyprland-hdr")
|
|
59
|
+
TOGGLE_SCRIPT = shutil.which("left-drag-toggle") \
|
|
60
|
+
or os.path.join(_DESKTOP_ROOT, "desktop", "left-drag-toggle")
|
|
61
|
+
|
|
62
|
+
# Survives hotswap re-exec (module globals are reused).
|
|
63
|
+
_state = globals().get("_state") or {
|
|
64
|
+
"available": None, # None = not probed yet
|
|
65
|
+
"enabled": False,
|
|
66
|
+
"wm_class": "",
|
|
67
|
+
"probed_at": 0.0,
|
|
68
|
+
"thread": None,
|
|
69
|
+
"toggling": False,
|
|
70
|
+
"resize_synced": True, # right_drag_resize_exclude list matches the move list
|
|
71
|
+
"synced_for": None, # (wm_class, enabled) the last --sync ran for: no loop on a failing script
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Reads
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def window_class():
|
|
80
|
+
"""The class Hyprland files this process's window under: what the feed
|
|
81
|
+
saw (`j/clients` → wm_class), else the BOOTED meltygui app's id
|
|
82
|
+
(app.boot's app_id — the GLFW app_id / X11 class hint; known before
|
|
83
|
+
any Surface exists, which is when the first probe runs), else the
|
|
84
|
+
app's declared Surface.app_id, else the studio's own
|
|
85
|
+
(geometry_feed.WM_CLASS). The studio fallback is LAST and only for a
|
|
86
|
+
process that never booted as an app: a probe that read an app as
|
|
87
|
+
"lsd-studio" toggled and synced the wrong class (09-13)."""
|
|
88
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
89
|
+
frame = geometry_feed._STATE.get("frame") if isinstance(geometry_feed._STATE, dict) else None
|
|
90
|
+
if frame and frame.get("wm_class"):
|
|
91
|
+
return frame["wm_class"]
|
|
92
|
+
try:
|
|
93
|
+
import meltygui.core.runtime.app as app
|
|
94
|
+
if app._state.get("booted") and app._state.get("app_id"):
|
|
95
|
+
return app._state["app_id"]
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
try:
|
|
99
|
+
from meltygui.core.windowing.surface import Surface
|
|
100
|
+
if Surface.all:
|
|
101
|
+
return Surface.app_id
|
|
102
|
+
except Exception:
|
|
103
|
+
pass
|
|
104
|
+
return geometry_feed.WM_CLASS
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def class_is_certain():
|
|
108
|
+
"""True when window_class() comes from something that names THIS
|
|
109
|
+
process's window: the feed's frame or the booted app's id. The
|
|
110
|
+
Surface.app_id default and the studio fallback are guesses — good
|
|
111
|
+
enough to paint the button, never to WRITE the compositor's lists
|
|
112
|
+
(an app probed before its Surface existed read itself as the studio
|
|
113
|
+
and synced the studio's entry away, 09-13)."""
|
|
114
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
115
|
+
frame = geometry_feed._STATE.get("frame") if isinstance(geometry_feed._STATE, dict) else None
|
|
116
|
+
if frame and frame.get("wm_class"):
|
|
117
|
+
return True
|
|
118
|
+
try:
|
|
119
|
+
import meltygui.core.runtime.app as app
|
|
120
|
+
return bool(app._state.get("booted") and app._state.get("app_id"))
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _getoption(name, request=None):
|
|
126
|
+
"""(kind, value) of a Hyprland option through the socket, or
|
|
127
|
+
(None, None) when the option does not exist / no socket."""
|
|
128
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
129
|
+
request = request or geometry_feed.hypr_request
|
|
130
|
+
try:
|
|
131
|
+
reply = request(f"j/getoption {name}")
|
|
132
|
+
except Exception:
|
|
133
|
+
return None, None
|
|
134
|
+
reply = reply.strip()
|
|
135
|
+
if not reply.startswith("{"):
|
|
136
|
+
return None, None
|
|
137
|
+
try:
|
|
138
|
+
data = json.loads(reply)
|
|
139
|
+
except ValueError:
|
|
140
|
+
# hyprctl-style replies don't escape quotes inside str values.
|
|
141
|
+
match = re.search(r'"str":\s*"(.*)",\s*"set"', reply)
|
|
142
|
+
return ("str", match.group(1)) if match else (None, None)
|
|
143
|
+
for kind in ("bool", "int", "float", "str"):
|
|
144
|
+
if kind in data:
|
|
145
|
+
return kind, data[kind]
|
|
146
|
+
return None, None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def class_excluded(exclude_text, wm_class):
|
|
150
|
+
"""True when `wm_class` fully matches one of the comma-separated
|
|
151
|
+
regexes in the exclude option — the compositor's own rule, and
|
|
152
|
+
desktop/left-drag-toggle's."""
|
|
153
|
+
for entry in (exclude_text or "").split(","):
|
|
154
|
+
entry = entry.strip()
|
|
155
|
+
if not entry:
|
|
156
|
+
continue
|
|
157
|
+
try:
|
|
158
|
+
if re.fullmatch(entry, wm_class):
|
|
159
|
+
return True
|
|
160
|
+
except re.error:
|
|
161
|
+
continue
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def detect(request=None):
|
|
166
|
+
"""(available, enabled, wm_class) read from the compositor now."""
|
|
167
|
+
return detect_gestures(request)[:3]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def detect_gestures(request=None):
|
|
171
|
+
"""(available, enabled, wm_class, resize_synced): the left-drag read plus
|
|
172
|
+
whether the class's membership in right_drag_resize_exclude matches
|
|
173
|
+
its left-list membership (True when the compositor has no
|
|
174
|
+
right_drag_resize, or the feature is off — nothing to align)."""
|
|
175
|
+
import meltygui.core.windowing.geometry_feed as geometry_feed
|
|
176
|
+
if geometry_feed.backend() != "hyprland":
|
|
177
|
+
return False, False, "", True
|
|
178
|
+
kind, value = _getoption(OPTION, request)
|
|
179
|
+
if kind is None:
|
|
180
|
+
return False, False, "", True
|
|
181
|
+
wm_class = window_class()
|
|
182
|
+
if not value:
|
|
183
|
+
return True, False, wm_class, True # the feature itself is off
|
|
184
|
+
_kind, exclude = _getoption(EXCLUDE_OPTION, request)
|
|
185
|
+
enabled = not class_excluded(str(exclude or ""), wm_class)
|
|
186
|
+
resize_kind, resize_on = _getoption(RESIZE_OPTION, request)
|
|
187
|
+
if resize_kind is None or not resize_on:
|
|
188
|
+
return True, enabled, wm_class, True
|
|
189
|
+
_kind, resize_exclude = _getoption(RESIZE_EXCLUDE_OPTION, request)
|
|
190
|
+
resize_excluded = class_excluded(str(resize_exclude or ""), wm_class)
|
|
191
|
+
return True, enabled, wm_class, resize_excluded == (not enabled)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Probe thread
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
def _probe(run=None):
|
|
199
|
+
try:
|
|
200
|
+
available, enabled, wm_class, resize_synced = detect_gestures()
|
|
201
|
+
except Exception:
|
|
202
|
+
available, enabled, wm_class, resize_synced = False, False, "", True
|
|
203
|
+
changed = (available, enabled) != (_state["available"], _state["enabled"])
|
|
204
|
+
_state.update(available=available, enabled=enabled, wm_class=wm_class,
|
|
205
|
+
resize_synced=resize_synced, probed_at=time.monotonic())
|
|
206
|
+
if changed:
|
|
207
|
+
try:
|
|
208
|
+
from meltygui.core.windowing.glfw_utils import request_render
|
|
209
|
+
request_render()
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
if available and not resize_synced and class_is_certain():
|
|
213
|
+
_sync_resize(wm_class, enabled, run)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _sync_resize(wm_class, enabled, run=None):
|
|
217
|
+
"""Align right_drag_resize_exclude with the left list for this class
|
|
218
|
+
(the script's `--gestures --sync`), once per (class, state) — a script
|
|
219
|
+
that fails must not be re-run every probe. Re-probes after."""
|
|
220
|
+
if _state["synced_for"] == (wm_class, enabled) or not os.path.isfile(TOGGLE_SCRIPT):
|
|
221
|
+
return
|
|
222
|
+
_state["synced_for"] = (wm_class, enabled)
|
|
223
|
+
run = run or subprocess.run
|
|
224
|
+
try:
|
|
225
|
+
run([TOGGLE_SCRIPT, "--gestures", "--sync", wm_class], close_fds=False,
|
|
226
|
+
capture_output=True, timeout=10.0)
|
|
227
|
+
except Exception as ex:
|
|
228
|
+
print(f"[hypr_left_drag] {TOGGLE_SCRIPT} --gestures --sync {wm_class!r}: {ex}", flush=True)
|
|
229
|
+
return
|
|
230
|
+
try:
|
|
231
|
+
_, _, _, resize_synced = detect_gestures()
|
|
232
|
+
except Exception:
|
|
233
|
+
return
|
|
234
|
+
_state["resize_synced"] = resize_synced
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def start_probe(force=False):
|
|
238
|
+
"""Read on a daemon thread (idempotent while one runs; a known answer
|
|
239
|
+
is kept unless ``force``). Returns the thread, or None."""
|
|
240
|
+
thread = _state["thread"]
|
|
241
|
+
if thread is not None and thread.is_alive():
|
|
242
|
+
return thread
|
|
243
|
+
if _state["available"] is not None and not force:
|
|
244
|
+
return None
|
|
245
|
+
thread = threading.Thread(target=_probe, name="hypr-left-drag", daemon=True)
|
|
246
|
+
thread.start()
|
|
247
|
+
_state["thread"] = thread
|
|
248
|
+
return thread
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def available():
|
|
252
|
+
"""True on the patched desktop; False until the first probe lands
|
|
253
|
+
(kicked here if nobody started it)."""
|
|
254
|
+
if _state["available"] is None:
|
|
255
|
+
start_probe()
|
|
256
|
+
return False
|
|
257
|
+
return bool(_state["available"])
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def enabled():
|
|
261
|
+
"""The compositor's gestures (left-drag move, right-drag resize) apply
|
|
262
|
+
to this window's class (as of the last probe). False = the meltygui app
|
|
263
|
+
moves and resizes itself."""
|
|
264
|
+
return bool(_state["enabled"])
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def state():
|
|
268
|
+
""""unknown" before the first probe, "off" without the feature,
|
|
269
|
+
"on" / "excluded" with it — for the info tab / tests."""
|
|
270
|
+
if _state["available"] is None:
|
|
271
|
+
return "unknown"
|
|
272
|
+
if not _state["available"]:
|
|
273
|
+
return "off"
|
|
274
|
+
return "on" if _state["enabled"] else "excluded"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def refresh(min_interval=2.0):
|
|
278
|
+
"""Re-read (a focus gain), at most once per ``min_interval`` seconds."""
|
|
279
|
+
if time.monotonic() - _state["probed_at"] >= min_interval:
|
|
280
|
+
start_probe(force=True)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def refresh_if_stale(interval):
|
|
284
|
+
"""Per-frame: re-read once the last probe is ``interval`` seconds old."""
|
|
285
|
+
if interval and interval > 0:
|
|
286
|
+
refresh(min_interval=float(interval))
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# The toggle
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
def _run_toggle(wm_class, run=None):
|
|
294
|
+
run = run or subprocess.run
|
|
295
|
+
try:
|
|
296
|
+
# posix_spawn: absolute executable, close_fds=False, no env / preexec_fn.
|
|
297
|
+
run([TOGGLE_SCRIPT, "--gestures", wm_class], close_fds=False, capture_output=True, timeout=10.0)
|
|
298
|
+
except Exception as ex:
|
|
299
|
+
print(f"[hypr_left_drag] {TOGGLE_SCRIPT} --gestures {wm_class!r}: {ex}", flush=True)
|
|
300
|
+
finally:
|
|
301
|
+
_state["toggling"] = False
|
|
302
|
+
_probe(run)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def toggle(run=None):
|
|
306
|
+
"""Flip the compositor's window gestures (left-drag move AND right-drag
|
|
307
|
+
resize) for this window's class through the desktop's own script
|
|
308
|
+
(`--gestures`: it edits both exclude lists, persists them, refreshes
|
|
309
|
+
hyprbars and notifies). The button flips at once (the probe after the script
|
|
310
|
+
confirms); a second click while one runs is ignored. Returns the
|
|
311
|
+
worker thread, or None when nothing was started."""
|
|
312
|
+
if not available() or _state["toggling"]:
|
|
313
|
+
return None
|
|
314
|
+
if not os.path.isfile(TOGGLE_SCRIPT):
|
|
315
|
+
print(f"[hypr_left_drag] toggle script missing: {TOGGLE_SCRIPT}", flush=True)
|
|
316
|
+
return None
|
|
317
|
+
wm_class = _state["wm_class"] or window_class()
|
|
318
|
+
_state["toggling"] = True
|
|
319
|
+
_state["enabled"] = not _state["enabled"]
|
|
320
|
+
thread = threading.Thread(target=_run_toggle, args=(wm_class, run),
|
|
321
|
+
name="hypr-left-drag-toggle", daemon=True)
|
|
322
|
+
thread.start()
|
|
323
|
+
return thread
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
draw_space_mouse — a 3-D rendering of the SpaceMouse with its live reading.
|
|
3
|
+
|
|
4
|
+
The puck on screen does what the real one does: the cap slides with the
|
|
5
|
+
three translation axes and leans / twists with the three rotation axes,
|
|
6
|
+
straight off the reader's level state (events/space_mouse.py), so the
|
|
7
|
+
window is the place to check axis signs, dead zone and per-axis
|
|
8
|
+
sensitivity (Toggles.SpaceMouse) by eye. Under the image: connection
|
|
9
|
+
status and one row per axis — raw device units, the normalized reading
|
|
10
|
+
the views receive, and a centered bar.
|
|
11
|
+
|
|
12
|
+
Same shape as draw_voxels / draw_line_graph: a pass into a gl_state FBO
|
|
13
|
+
that imgui.image shows, every knob a parameter on the signature (auto
|
|
14
|
+
draw_state params — the camera orbit and the exaggeration of the cap's
|
|
15
|
+
travel persist per view). The moving piece is drawn with gl_gui/pbr.py —
|
|
16
|
+
the immediate-mode PBR renderer: begin_scene, draw_mesh calls over the
|
|
17
|
+
cap's lathe profiles (`_cap_meshes`: the waisted rubber body, the glossy
|
|
18
|
+
inset disc, the blue ring) with materials and transforms as kwargs,
|
|
19
|
+
end_scene (render_puck). Orbit with the middle mouse, wheel to zoom.
|
|
20
|
+
|
|
21
|
+
Not hover-routed: a space_mouse_changed event only reaches the hovered
|
|
22
|
+
view, and this window should animate wherever the pointer is. It reads the
|
|
23
|
+
reader's level state directly and registers as a WATCHER
|
|
24
|
+
(space_mouse.watch): the pump invalidates it on every active frame and
|
|
25
|
+
once more on release, so the cached tile follows the cap.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
import math
|
|
30
|
+
|
|
31
|
+
import meltygui_imgui as imgui
|
|
32
|
+
from meltygui.hdr_color import pack_color
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
import meltygui.core.input.space_mouse as space_mouse
|
|
36
|
+
from meltygui.core.graphics.gl_state import GLState
|
|
37
|
+
from meltygui.core.melty import Melty
|
|
38
|
+
from meltygui.pbr import begin_scene
|
|
39
|
+
from meltygui.pbr import end_scene
|
|
40
|
+
from meltygui.pbr import draw_cylinder
|
|
41
|
+
from meltygui.pbr import draw_mesh
|
|
42
|
+
from meltygui.pbr import draw_plane
|
|
43
|
+
from meltygui.pbr import draw_prism
|
|
44
|
+
from meltygui.pbr import environment
|
|
45
|
+
from meltygui.pbr import load_model
|
|
46
|
+
from meltygui.pbr import orbit_camera
|
|
47
|
+
from meltygui.pbr import Light
|
|
48
|
+
from meltygui.pbr import model_matrix
|
|
49
|
+
from meltygui.pbr import rotation_matrix
|
|
50
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
51
|
+
from meltygui.code.new_converters import code_hosts_for
|
|
52
|
+
from meltygui.core.layout.column_core import ColumnLayout
|
|
53
|
+
from meltygui.core.core_render import render_func
|
|
54
|
+
from meltygui.core.rendering.window_decoration import window
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _lathe_profile(fn, y0, y1, steps, r_end=None):
|
|
58
|
+
"""Profile rings for pbr.lathe_mesh from a radius function r(y) over
|
|
59
|
+
[y0, y1]: normals from the numeric tangent ((dy, -dr) in the (r, y)
|
|
60
|
+
plane, pointing outward)."""
|
|
61
|
+
ys = np.linspace(y0, y1, steps)
|
|
62
|
+
rs = np.array([fn(y) for y in ys], np.float64)
|
|
63
|
+
dr = np.gradient(rs, ys)
|
|
64
|
+
profile = []
|
|
65
|
+
for r, y, d in zip(rs, ys, dr):
|
|
66
|
+
n = np.array([1.0, -d]); n /= np.linalg.norm(n)
|
|
67
|
+
profile.append((float(r), float(y), float(n[0]), float(n[1])))
|
|
68
|
+
return profile
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _cap_meshes():
|
|
72
|
+
"""The SpaceMouse Wireless as lathes. MOVING: the black rubber cap — a
|
|
73
|
+
waisted body that is narrowest near the TOP under an overhanging rim
|
|
74
|
+
and flares out to its widest at the FOOT (the photo), rolling over the
|
|
75
|
+
rim into a lip with the piano-black disc set into it, and a marker
|
|
76
|
+
dimple on the lip's front. STATIC (the base): the black collar the cap
|
|
77
|
+
sits in, the blue ring on the collar's inner edge, and the brushed
|
|
78
|
+
steel skirt widening to the desk. Unit: the cap's foot radius 1; cap
|
|
79
|
+
height CAP_HEIGHT. Built once (module cache), uploaded per GLState by
|
|
80
|
+
draw_mesh."""
|
|
81
|
+
from meltygui.pbr import lathe_mesh
|
|
82
|
+
h = CAP_HEIGHT
|
|
83
|
+
# [tint=(0.9, 0.6, 0.2)]
|
|
84
|
+
r_rim, r_waist, y_waist = 0.86, 0.78, 0.66 * h # the rim and the narrowest ring
|
|
85
|
+
# [tint=(0.9, 0.6, 0.2)]
|
|
86
|
+
lip_radius, inset_depth, inset_r = 0.06, 0.03, 0.74
|
|
87
|
+
|
|
88
|
+
def body_radius(y):
|
|
89
|
+
# a concave arc either side of the waist: gentle near the waist,
|
|
90
|
+
# steep into the overhanging rim above and the wide foot below
|
|
91
|
+
if y < y_waist:
|
|
92
|
+
t = (y_waist - y) / y_waist
|
|
93
|
+
return r_waist + (1.0 - r_waist) * t ** 1.7
|
|
94
|
+
t = (y - y_waist) / (h - lip_radius - y_waist)
|
|
95
|
+
t = max(0.0, min(1.0, t))
|
|
96
|
+
return r_waist + (r_rim - r_waist) * t ** 1.7
|
|
97
|
+
|
|
98
|
+
# [tint=(0.9, 0.6, 0.2)]
|
|
99
|
+
foot_bevel = 0.05
|
|
100
|
+
side = _lathe_profile(body_radius, foot_bevel, h - lip_radius, 40)
|
|
101
|
+
# the foot: flat underside, then a quarter round of `foot_bevel` up into
|
|
102
|
+
# the side (the cap's radius at the bevel's top is ~1.0)
|
|
103
|
+
foot = [(0.0, 0.0, 0.0, -1.0), (1.0 - foot_bevel, 0.0, 0.0, -1.0)]
|
|
104
|
+
for i in range(1, 7):
|
|
105
|
+
a = -math.pi / 2 + (math.pi / 2) * i / 6
|
|
106
|
+
foot.append((1.0 - foot_bevel + foot_bevel * math.cos(a), foot_bevel + foot_bevel * math.sin(a),
|
|
107
|
+
math.cos(a), math.sin(a)))
|
|
108
|
+
lip = []
|
|
109
|
+
for i in range(1, 9):
|
|
110
|
+
a = (math.pi / 2) * i / 8
|
|
111
|
+
lip.append((r_rim - lip_radius + lip_radius * math.cos(a), h - lip_radius + lip_radius * math.sin(a),
|
|
112
|
+
math.cos(a), math.sin(a)))
|
|
113
|
+
lip += [(inset_r + 0.015, h, 0.0, 1.0), (inset_r, h - inset_depth * 0.5, -1.0, 0.0),
|
|
114
|
+
(inset_r - 0.01, h - inset_depth, 0.0, 1.0), (0.0, h - inset_depth, 0.0, 1.0)]
|
|
115
|
+
body = lathe_mesh(foot + side + lip, segments=96)
|
|
116
|
+
top = lathe_mesh([(0.0, h - inset_depth + 0.006, 0.0, 1.0),
|
|
117
|
+
(inset_r - 0.05, h - inset_depth + 0.006, 0.0, 1.0)], segments=96)
|
|
118
|
+
|
|
119
|
+
# ── the base: ONE piece - a collar under the cap's foot rolling over
|
|
120
|
+
# and flaring out to the desk in a single convex curve ──
|
|
121
|
+
def base_radius(y):
|
|
122
|
+
t = max(0.0, min(1.0, (-0.05 - y) / BASE_HEIGHT))
|
|
123
|
+
return 1.04 + 0.56 * math.sin(math.pi / 2 * t) ** 0.7
|
|
124
|
+
y0 = -0.05 - BASE_HEIGHT
|
|
125
|
+
base = ([(0.0, y0 - 0.05, 0.0, -1.0), (1.52, y0 - 0.05, 0.0, -1.0), (1.60, y0 - 0.02, 1.0, -0.4)]
|
|
126
|
+
+ _lathe_profile(base_radius, y0, -0.05, 28)
|
|
127
|
+
+ [(1.0, -0.05, 0.0, 1.0), (0.0, -0.05, 0.0, 1.0)])
|
|
128
|
+
base = lathe_mesh(base, segments=96)
|
|
129
|
+
# the blue ring: a shelf on the base, under the cap's foot
|
|
130
|
+
ring = lathe_mesh([(1.07, -0.06, 1.0, 0.0), (1.07, -0.03, 1.0, 0.0), (1.07, -0.03, 0.0, 1.0),
|
|
131
|
+
(0.97, -0.03, 0.0, 1.0)], segments=96)
|
|
132
|
+
# the silver bezel around the glossy disc: a flat ring sitting in the inset
|
|
133
|
+
# (outer wall UP, center INWARD, inner wall DOWN - the lathe faces outward
|
|
134
|
+
# along that order; the first version ran the other way and culled)
|
|
135
|
+
# flat ring, a hairline chamfer on its outer edge
|
|
136
|
+
bezel = lathe_mesh([(inset_r + 0.012, h - inset_depth + 0.004, 1.0, 0.0),
|
|
137
|
+
(inset_r + 0.012, h - inset_depth + 0.011, 1.0, 0.0),
|
|
138
|
+
(inset_r + 0.009, h - inset_depth + 0.013, 0.7, 0.7),
|
|
139
|
+
(inset_r - 0.05, h - inset_depth + 0.013, 0.0, 1.0),
|
|
140
|
+
(inset_r - 0.05, h - inset_depth + 0.004, -1.0, 0.0)], segments=96)
|
|
141
|
+
return {"body": body, "top": top, "ring": ring, "base": base, "bezel": bezel}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# [tint=(0.9, 0.6, 0.2)]
|
|
145
|
+
CAP_HEIGHT = 0.85
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def inset_depth_of_cap():
|
|
149
|
+
"""How far the glossy disc sits below the cap's rim (see _cap_meshes)."""
|
|
150
|
+
return 0.03
|
|
151
|
+
# [tint=(0.9, 0.6, 0.2)]
|
|
152
|
+
BASE_HEIGHT = 0.5
|
|
153
|
+
# The cap hovers this far above the base's shelf and pivots about its FOOT
|
|
154
|
+
# centre, so a full tilt (twist radians) swings the foot's far edge down by
|
|
155
|
+
# sin(twist) × 1 — below HOVER_GAP it would cut into the base.
|
|
156
|
+
# [tint=(0.9, 0.6, 0.2)]
|
|
157
|
+
HOVER_GAP = 0.32
|
|
158
|
+
_CAP_MESHES = globals().get("_CAP_MESHES") or {}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
from meltygui.view.input_view import render_puck
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# [tint=(0.181, 0.119, 0.294)]
|
|
165
|
+
from meltygui.view.input_view import draw_space_mouse
|
|
166
|
+
draw_space_mouse = window(name='space_mouse', tint=(0.2, 0.21, 0.22))(draw_space_mouse)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _device_model():
|
|
170
|
+
"""The model Toggles.SpaceMouse.model_path names, or None (missing,
|
|
171
|
+
unreadable — reported once — or unset)."""
|
|
172
|
+
path = str(Toggles.SpaceMouse.model_path or "").strip()
|
|
173
|
+
if not path:
|
|
174
|
+
return None
|
|
175
|
+
import os
|
|
176
|
+
if not os.path.isabs(path):
|
|
177
|
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "..", "..", path)
|
|
178
|
+
try:
|
|
179
|
+
return load_model(path)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
if _MODEL_WARNED.get(path) != str(e):
|
|
182
|
+
_MODEL_WARNED[path] = str(e)
|
|
183
|
+
print(f"space mouse model {path!r} not used: {e}")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
_MODEL_WARNED: dict = globals().get("_MODEL_WARNED") or {}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _environment_tint(view_tint):
|
|
191
|
+
"""The view's tint as a light colour: normalized to unit brightness (a
|
|
192
|
+
window tint is dark by design) and blended toward white by
|
|
193
|
+
Toggles.SpaceMouse.environment_tint_strength, so the scene sits in
|
|
194
|
+
light of the window's hue without going dim."""
|
|
195
|
+
if not view_tint:
|
|
196
|
+
return (1.0, 1.0, 1.0)
|
|
197
|
+
rgb = [max(0.0, float(c)) for c in tuple(view_tint)[:3]]
|
|
198
|
+
peak = max(rgb) or 1.0
|
|
199
|
+
k = max(0.0, min(1.0, float(Toggles.SpaceMouse.environment_tint_strength)))
|
|
200
|
+
return tuple(1.0 - k + k * (c / peak) for c in rgb)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _draw_puck(gl_state, axes, width, height, tilt, spin, cam_zoom, travel, twist,
|
|
204
|
+
view_tint=None):
|
|
205
|
+
"""render_puck into the view's FBO, shown as an image of width × height."""
|
|
206
|
+
from meltygui.pbr import pbr_pass
|
|
207
|
+
fb = render_puck(gl_state, "target", width, height, axes, tilt, spin, cam_zoom,
|
|
208
|
+
travel, twist, Toggles.SpaceMouse.environment, view_tint=view_tint)
|
|
209
|
+
imgui.image(fb.texture_id, width, height, uv0=(0, 1), uv1=(1, 0))
|
|
210
|
+
if pbr_pass.last_error:
|
|
211
|
+
imgui.text_colored(pbr_pass.last_error.splitlines()[0], 1.0, 0.45, 0.40, 1.0)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _draw_readout(draw_state, raw, axes, status, bar_width, row_height,
|
|
215
|
+
axis_tint, axis_tint_negative, draw_hz=False):
|
|
216
|
+
"""Status line + one row per axis: name, raw units, normalized reading,
|
|
217
|
+
a centred bar. Draw-list rows so the wrapper keeps the layout."""
|
|
218
|
+
draw_list = imgui.get_window_draw_list()
|
|
219
|
+
x, y = imgui.get_cursor_screen_pos()
|
|
220
|
+
left = x + 4
|
|
221
|
+
y += 4
|
|
222
|
+
st = space_mouse.stats()
|
|
223
|
+
text = (f"{status} {st['rate_hz']:.0f} Hz reports, newest {st['age_ms']:.0f} ms old, "
|
|
224
|
+
f"frame {st['frame_ms']:.0f} ms ({Toggles.SpaceMouse.socket_path})")
|
|
225
|
+
tint = (0.55, 0.9, 0.55, 1.0) if status.startswith("connected") else (1.0, 0.6, 0.4, 1.0)
|
|
226
|
+
if draw_hz:
|
|
227
|
+
draw_list.add_text(left, y, pack_color(*tint), text)
|
|
228
|
+
y += row_height
|
|
229
|
+
name_x, raw_x, norm_x = left, left + 30, left + 90
|
|
230
|
+
bar_x = left + 160
|
|
231
|
+
mid = bar_x + bar_width * 0.5
|
|
232
|
+
white = pack_color(0.85, 0.85, 0.85, 1.0)
|
|
233
|
+
dim = pack_color(1.0, 1.0, 1.0, 0.12)
|
|
234
|
+
for name, r, a in zip(space_mouse.AXES, raw, axes):
|
|
235
|
+
draw_list.add_text(name_x, y, white, name)
|
|
236
|
+
draw_list.add_text(raw_x, y, white, f"{int(r):5d}")
|
|
237
|
+
draw_list.add_text(norm_x, y, white, f"{a:+.3f}")
|
|
238
|
+
draw_list.add_rect_filled(bar_x, y + 3, bar_x + bar_width, y + row_height - 3, dim, 2.0)
|
|
239
|
+
draw_list.add_line(mid, y + 2, mid, y + row_height - 2, white, 1.0)
|
|
240
|
+
fill = max(-1.0, min(1.0, a)) * bar_width * 0.5
|
|
241
|
+
color = axis_tint if fill >= 0 else axis_tint_negative
|
|
242
|
+
draw_list.add_rect_filled(min(mid, mid + fill), y + 4, max(mid, mid + fill), y + row_height - 4,
|
|
243
|
+
pack_color(*color, 0.9), 2.0)
|
|
244
|
+
y += row_height
|
|
245
|
+
imgui.dummy(bar_x + bar_width - x, y - imgui.get_cursor_screen_pos()[1] + 4)
|