MoleditPy-linux 3.5.2__tar.gz → 3.6.1__tar.gz

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 (81) hide show
  1. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/PKG-INFO +3 -3
  2. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/README.md +2 -2
  3. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/pyproject.toml +1 -1
  4. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/MoleditPy_linux.egg-info/PKG-INFO +3 -3
  5. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/MoleditPy_linux.egg-info/SOURCES.txt +1 -0
  6. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/__init__.py +3 -4
  7. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/main.py +10 -0
  8. moleditpy_linux-3.6.1/src/moleditpy_linux/ui/atom_picking.py +310 -0
  9. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -3
  10. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/custom_interactor_style.py +64 -16
  11. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/dialog_logic.py +61 -13
  12. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/edit_3d_logic.py +16 -20
  13. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/edit_actions_logic.py +3 -3
  14. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/main_window_init.py +8 -0
  15. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/molecular_scene_handler.py +129 -36
  16. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/molecule_scene.py +38 -26
  17. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/move_group_dialog.py +27 -17
  18. moleditpy_linux-3.6.1/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +640 -0
  19. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +27 -0
  20. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +3 -1
  21. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +18 -5
  22. moleditpy_linux-3.6.1/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +107 -0
  23. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/ui_manager.py +1 -0
  24. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/utils/default_settings.py +2 -1
  25. moleditpy_linux-3.5.2/src/moleditpy_linux/ui/atom_picking.py +0 -163
  26. moleditpy_linux-3.5.2/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -70
  27. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/LICENSE +0 -0
  28. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/setup.cfg +0 -0
  29. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  30. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  31. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  32. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  33. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/__main__.py +0 -0
  34. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  35. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/assets/icon.icns +0 -0
  36. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/assets/icon.ico +0 -0
  37. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/assets/icon.png +0 -0
  38. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/core/__init__.py +0 -0
  39. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/core/mol_geometry.py +0 -0
  40. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/core/molecular_data.py +0 -0
  41. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/plugins/__init__.py +0 -0
  42. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
  43. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
  44. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
  45. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/__init__.py +0 -0
  46. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/about_dialog.py +0 -0
  47. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
  48. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
  49. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/analysis_window.py +0 -0
  50. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
  51. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/app_state.py +0 -0
  52. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/atom_item.py +0 -0
  53. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
  54. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/bond_item.py +0 -0
  55. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
  56. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
  57. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
  58. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/compute_logic.py +0 -0
  59. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
  60. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
  61. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
  62. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/export_logic.py +0 -0
  63. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
  64. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/io_logic.py +0 -0
  65. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/main_window.py +0 -0
  66. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
  67. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
  68. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
  69. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
  70. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  71. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/string_importers.py +0 -0
  72. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
  73. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
  74. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
  75. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
  76. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
  77. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
  78. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/utils/__init__.py +0 -0
  79. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/utils/constants.py +0 -0
  80. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
  81. {moleditpy_linux-3.5.2 → moleditpy_linux-3.6.1}/src/moleditpy_linux/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 3.5.2
3
+ Version: 3.6.1
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -804,7 +804,7 @@ For detailed instructions, please refer to the project [Wiki](https://github.com
804
804
  After installation, run this command to create the shortcut in your application menu (e.g., Start Menu or Applications folder).
805
805
 
806
806
  ```bash
807
- moleditpy-installer
807
+ python -m moleditpy_installer
808
808
  ```
809
809
 
810
810
  #### Running the Application
@@ -937,7 +937,7 @@ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Ze
937
937
  インストール後、このコマンドを実行すると、アプリケーションメニュー(スタートメニューやアプリケーションフォルダなど)にショートカットが作成されます。
938
938
 
939
939
  ```bash
940
- moleditpy-installer
940
+ python -m moleditpy_installer
941
941
  ```
942
942
 
943
943
  #### アプリケーションの起動
@@ -101,7 +101,7 @@ For detailed instructions, please refer to the project [Wiki](https://github.com
101
101
  After installation, run this command to create the shortcut in your application menu (e.g., Start Menu or Applications folder).
102
102
 
103
103
  ```bash
104
- moleditpy-installer
104
+ python -m moleditpy_installer
105
105
  ```
106
106
 
107
107
  #### Running the Application
@@ -234,7 +234,7 @@ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Ze
234
234
  インストール後、このコマンドを実行すると、アプリケーションメニュー(スタートメニューやアプリケーションフォルダなど)にショートカットが作成されます。
235
235
 
236
236
  ```bash
237
- moleditpy-installer
237
+ python -m moleditpy_installer
238
238
  ```
239
239
 
240
240
  #### アプリケーションの起動
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "3.5.2"
8
+ version = "3.6.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 3.5.2
3
+ Version: 3.6.1
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -804,7 +804,7 @@ For detailed instructions, please refer to the project [Wiki](https://github.com
804
804
  After installation, run this command to create the shortcut in your application menu (e.g., Start Menu or Applications folder).
805
805
 
806
806
  ```bash
807
- moleditpy-installer
807
+ python -m moleditpy_installer
808
808
  ```
809
809
 
810
810
  #### Running the Application
@@ -937,7 +937,7 @@ Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Ze
937
937
  インストール後、このコマンドを実行すると、アプリケーションメニュー(スタートメニューやアプリケーションフォルダなど)にショートカットが作成されます。
938
938
 
939
939
  ```bash
940
- moleditpy-installer
940
+ python -m moleditpy_installer
941
941
  ```
942
942
 
943
943
  #### アプリケーションの起動
@@ -53,6 +53,7 @@ src/moleditpy_linux/ui/mirror_dialog.py
53
53
  src/moleditpy_linux/ui/molecular_scene_handler.py
54
54
  src/moleditpy_linux/ui/molecule_scene.py
55
55
  src/moleditpy_linux/ui/move_group_dialog.py
56
+ src/moleditpy_linux/ui/move_selected_atoms_dialog.py
56
57
  src/moleditpy_linux/ui/periodic_table_dialog.py
57
58
  src/moleditpy_linux/ui/planarize_dialog.py
58
59
  src/moleditpy_linux/ui/settings_dialog.py
@@ -8,15 +8,14 @@ Author: Hiromichi Yokoyama
8
8
  License: GPL-3.0 license
9
9
  Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
- """
12
11
 
13
- """Top-level package for moleditpy_linux."""
12
+ Top-level package for moleditpy_linux.
13
+ """
14
14
 
15
15
  import importlib.util
16
+ from .utils.constants import VERSION as __version__ # noqa: F401
16
17
 
17
18
  try:
18
19
  OBABEL_AVAILABLE = False
19
20
  except ImportError:
20
21
  OBABEL_AVAILABLE = False
21
-
22
- from .utils.constants import VERSION as __version__ # noqa: F401
@@ -148,4 +148,14 @@ def main() -> None:
148
148
  app = QApplication([sys.argv[0]] + remaining)
149
149
  window = MainWindow(initial_file=args.file, safe_mode=args.safe)
150
150
  window.show()
151
+
152
+ # Force Windows to refresh taskbar/titlebar icon after event loop starts
153
+ if sys.platform == "win32":
154
+ try:
155
+ from PyQt6.QtCore import QTimer
156
+
157
+ QTimer.singleShot(100, lambda: window.setWindowIcon(window.windowIcon()))
158
+ except Exception:
159
+ pass
160
+
151
161
  sys.exit(app.exec())
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Optional
16
+ import logging
17
+
18
+ import numpy as np
19
+
20
+ try:
21
+ from ..utils.constants import VDW_RADII, pt
22
+ except ImportError:
23
+ from moleditpy_linux.utils.constants import VDW_RADII, pt
24
+
25
+
26
+ def _world_to_display(renderer: Any, pos: Any) -> Optional[tuple[float, float, float]]:
27
+ try:
28
+ renderer.SetWorldPoint(float(pos[0]), float(pos[1]), float(pos[2]), 1.0)
29
+ renderer.WorldToDisplay()
30
+ display = renderer.GetDisplayPoint()
31
+ return (float(display[0]), float(display[1]), float(display[2]))
32
+ except (AttributeError, RuntimeError, TypeError, ValueError, IndexError):
33
+ return None
34
+
35
+
36
+ def _atom_world_radius(view_3d_manager: Any, mol: Any, atom_idx: int) -> float:
37
+ try:
38
+ atom = mol.GetAtomWithIdx(int(atom_idx))
39
+ symbol = atom.GetSymbol()
40
+ except (AttributeError, RuntimeError, TypeError, ValueError):
41
+ symbol = "C"
42
+
43
+ settings = {}
44
+ try:
45
+ settings = view_3d_manager.host.init_manager.settings
46
+ except (AttributeError, RuntimeError, TypeError):
47
+ pass
48
+
49
+ style = str(getattr(view_3d_manager, "current_3d_style", "ball_and_stick"))
50
+ style = style.lower().replace(" ", "_")
51
+
52
+ if style == "cpk":
53
+ scale = settings.get("cpk_atom_scale", 1.0)
54
+ try:
55
+ radius = pt.GetRvdw(pt.GetAtomicNumber(symbol))
56
+ return float(radius if radius > 0.1 else 1.5) * float(scale)
57
+ except (AttributeError, RuntimeError, TypeError, ValueError):
58
+ return 1.5 * float(scale)
59
+
60
+ if style == "stick":
61
+ return float(settings.get("stick_bond_radius", 0.15))
62
+
63
+ if style == "wireframe":
64
+ return 0.01
65
+
66
+ scale = settings.get("ball_stick_atom_scale", 1.0)
67
+ return float(VDW_RADII.get(symbol, 0.4)) * float(scale)
68
+
69
+
70
+ def _projected_radius_px(
71
+ renderer: Any, center: Any, world_radius: float
72
+ ) -> Optional[float]:
73
+ center_display = _world_to_display(renderer, center)
74
+ if center_display is None:
75
+ return None
76
+
77
+ offsets = (
78
+ (world_radius, 0.0, 0.0),
79
+ (0.0, world_radius, 0.0),
80
+ (0.0, 0.0, world_radius),
81
+ )
82
+ radius_px = 0.0
83
+ for offset in offsets:
84
+ edge = (
85
+ float(center[0]) + offset[0],
86
+ float(center[1]) + offset[1],
87
+ float(center[2]) + offset[2],
88
+ )
89
+ edge_display = _world_to_display(renderer, edge)
90
+ if edge_display is None:
91
+ continue
92
+ radius_px = max(
93
+ radius_px,
94
+ float(
95
+ np.hypot(
96
+ edge_display[0] - center_display[0],
97
+ edge_display[1] - center_display[1],
98
+ )
99
+ ),
100
+ )
101
+
102
+ return radius_px
103
+
104
+
105
+ def pick_atom_index_from_screen_sequential(
106
+ view_3d_manager: Any,
107
+ click_pos: tuple[int, int],
108
+ mol: Optional[Any] = None,
109
+ padding_px: float = 8.0,
110
+ min_radius_px: float = 14.0,
111
+ max_radius_px: float = 96.0,
112
+ ) -> Optional[int]:
113
+ """Return the atom nearest a screen click without invoking VTK cell picking."""
114
+ try:
115
+ plotter = view_3d_manager.plotter
116
+ renderer = plotter.renderer
117
+ positions = view_3d_manager.atom_positions_3d
118
+ except (AttributeError, RuntimeError, TypeError):
119
+ return None
120
+
121
+ if positions is None:
122
+ return None
123
+
124
+ try:
125
+ positions_array = np.asarray(positions, dtype=float)
126
+ except (TypeError, ValueError):
127
+ return None
128
+
129
+ if positions_array.ndim != 2 or positions_array.shape[1] < 3:
130
+ return None
131
+
132
+ if mol is None:
133
+ mol = getattr(view_3d_manager, "current_mol", None)
134
+
135
+ try:
136
+ atom_count = int(mol.GetNumAtoms()) if mol is not None else len(positions_array)
137
+ except (AttributeError, RuntimeError, TypeError, ValueError):
138
+ atom_count = len(positions_array)
139
+
140
+ best_idx: Optional[int] = None
141
+ best_score: Optional[tuple[float, float]] = None
142
+
143
+ for atom_idx in range(min(atom_count, len(positions_array))):
144
+ center = positions_array[atom_idx]
145
+ display = _world_to_display(renderer, center)
146
+ if display is None or not np.all(np.isfinite(display[:2])):
147
+ continue
148
+
149
+ world_radius = _atom_world_radius(view_3d_manager, mol, atom_idx)
150
+ projected_radius = _projected_radius_px(renderer, center, world_radius)
151
+ hit_radius = max(
152
+ float(min_radius_px),
153
+ min(float(max_radius_px), float(projected_radius or 0.0) + padding_px),
154
+ )
155
+ distance = float(np.hypot(display[0] - click_pos[0], display[1] - click_pos[1]))
156
+ if distance > hit_radius:
157
+ continue
158
+
159
+ score = (distance / hit_radius, distance)
160
+ if best_score is None or score < best_score:
161
+ best_idx = atom_idx
162
+ best_score = score
163
+
164
+ return best_idx
165
+
166
+
167
+ def pick_atom_index_from_screen_vectorized(
168
+ view_3d_manager: Any,
169
+ click_pos: tuple[int, int],
170
+ mol: Optional[Any] = None,
171
+ padding_px: float = 8.0,
172
+ min_radius_px: float = 14.0,
173
+ max_radius_px: float = 96.0,
174
+ ) -> Optional[int]:
175
+ """
176
+ Vectorized atom picking using the camera's projection matrix.
177
+ Eliminates the O(N) Python loop and VTK C++ boundary calls.
178
+ """
179
+ try:
180
+ plotter = view_3d_manager.plotter
181
+ renderer = plotter.renderer
182
+ positions = view_3d_manager.atom_positions_3d
183
+ except (AttributeError, RuntimeError, TypeError):
184
+ return None
185
+
186
+ if positions is None or len(positions) == 0:
187
+ return None
188
+
189
+ try:
190
+ positions_array = np.asarray(positions, dtype=float) # Shape: (N, 3)
191
+ except (TypeError, ValueError):
192
+ return None
193
+
194
+ if positions_array.ndim != 2 or positions_array.shape[1] < 3:
195
+ return None
196
+
197
+ if mol is None:
198
+ mol = getattr(view_3d_manager, "current_mol", None)
199
+
200
+ try:
201
+ atom_count = int(mol.GetNumAtoms()) if mol is not None else len(positions_array)
202
+ except (AttributeError, RuntimeError, TypeError, ValueError):
203
+ atom_count = len(positions_array)
204
+
205
+ active_atoms = min(atom_count, len(positions_array))
206
+ if active_atoms == 0:
207
+ return None
208
+
209
+ positions_array = positions_array[:active_atoms]
210
+
211
+ # 1. Retrieve the View-Projection (Composite) Matrix from the active camera
212
+ try:
213
+ camera = renderer.GetActiveCamera()
214
+ aspect_ratio = renderer.GetTiledAspectRatio()
215
+ # Get the 4x4 composite projection matrix (vtkMatrix4x4)
216
+ vtk_matrix = camera.GetCompositeProjectionTransformMatrix(aspect_ratio, -1, 1)
217
+
218
+ # Convert vtkMatrix4x4 to a NumPy 4x4 array
219
+ matrix = np.zeros((4, 4))
220
+ for i in range(4):
221
+ for j in range(4):
222
+ matrix[i, j] = vtk_matrix.GetElement(i, j)
223
+ except Exception:
224
+ return None
225
+
226
+ # 2. Convert all N world coordinates to homogeneous coordinates (N, 4)
227
+ homogeneous_coords = np.hstack([positions_array, np.ones((active_atoms, 1))])
228
+
229
+ # 3. Apply Matrix Multiplication: (N, 4) x (4, 4).T -> Clip Space Coordinates
230
+ clip_coords = homogeneous_coords @ matrix.T
231
+
232
+ # 4. Perform Perspective Divide -> Normalized Device Coordinates (NDC)
233
+ w = clip_coords[:, 3:4]
234
+ w_copy = np.copy(w)
235
+ w_copy[np.abs(w_copy) < 1e-5] = 1.0
236
+ ndc_coords = clip_coords[:, :3] / w_copy
237
+
238
+ # 5. Transform NDC to Screen/Display Coordinates
239
+ try:
240
+ size = renderer.GetSize() # (width, height)
241
+ except Exception:
242
+ return None
243
+
244
+ # VTK display space coordinates: X: [0, W], Y: [0, H]
245
+ display_coords = np.zeros((active_atoms, 2))
246
+ display_coords[:, 0] = (ndc_coords[:, 0] + 1.0) * 0.5 * size[0]
247
+ display_coords[:, 1] = (ndc_coords[:, 1] + 1.0) * 0.5 * size[1]
248
+
249
+ # 6. Vectorized Distance and Hit Radius Calculation
250
+ dx = display_coords[:, 0] - click_pos[0]
251
+ dy = display_coords[:, 1] - click_pos[1]
252
+ distances = np.hypot(dx, dy)
253
+
254
+ try:
255
+ if camera.GetParallelProjection():
256
+ pixel_scale = size[1] / (2.0 * camera.GetParallelScale())
257
+ else:
258
+ view_angle_rad = np.radians(camera.GetViewAngle())
259
+ pixel_scale = size[1] / (
260
+ 2.0 * np.abs(w.flatten()) * np.tan(view_angle_rad / 2.0)
261
+ )
262
+ except Exception:
263
+ pixel_scale = 20.0 # Safe fallback scale
264
+
265
+ # Pre-calculate world radii for all atoms
266
+ world_radii = np.array(
267
+ [_atom_world_radius(view_3d_manager, mol, idx) for idx in range(active_atoms)]
268
+ )
269
+
270
+ projected_radii = world_radii * pixel_scale
271
+ hit_radii = np.maximum(
272
+ min_radius_px, np.minimum(max_radius_px, projected_radii + padding_px)
273
+ )
274
+
275
+ # 7. Mask out atoms that are further than their hit radius
276
+ valid_mask = distances <= hit_radii
277
+ if not np.any(valid_mask):
278
+ return None
279
+
280
+ # 8. Score calculation and find the best index
281
+ # Tie-breaking logic: (ratio) + (distances * 1e-8)
282
+ scores = (distances / hit_radii) + (distances * 1e-8)
283
+
284
+ scores[~valid_mask] = np.inf
285
+ best_idx = int(np.argmin(scores))
286
+
287
+ return best_idx if scores[best_idx] != np.inf else None
288
+
289
+
290
+ def pick_atom_index_from_screen(
291
+ view_3d_manager: Any,
292
+ click_pos: tuple[int, int],
293
+ mol: Optional[Any] = None,
294
+ padding_px: float = 8.0,
295
+ min_radius_px: float = 14.0,
296
+ max_radius_px: float = 96.0,
297
+ ) -> Optional[int]:
298
+ """Return the atom nearest a screen click, trying vectorized first, falling back to sequential."""
299
+ try:
300
+ best_idx = pick_atom_index_from_screen_vectorized(
301
+ view_3d_manager, click_pos, mol, padding_px, min_radius_px, max_radius_px
302
+ )
303
+ if best_idx is not None:
304
+ return best_idx
305
+ except Exception as e:
306
+ logging.debug("Vectorized picking failed, falling back to sequential: %s", e)
307
+
308
+ return pick_atom_index_from_screen_sequential(
309
+ view_3d_manager, click_pos, mol, padding_px, min_radius_px, max_radius_px
310
+ )
@@ -877,6 +877,3 @@ class ConstrainedOptimizationDialog(Dialog3DPickingMixin, QDialog):
877
877
 
878
878
  # Default processing for other keys
879
879
  QDialog.keyPressEvent(self, event)
880
-
881
-
882
- from typing import Any
@@ -22,10 +22,8 @@ from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTrackballCamera #
22
22
 
23
23
 
24
24
  try:
25
- from .move_group_dialog import MoveGroupDialog
26
25
  from .atom_picking import pick_atom_index_from_screen
27
26
  except ImportError:
28
- from moleditpy_linux.ui.move_group_dialog import MoveGroupDialog
29
27
  from moleditpy_linux.ui.atom_picking import pick_atom_index_from_screen
30
28
 
31
29
  from rdkit import Geometry
@@ -107,10 +105,15 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
107
105
  self._mouse_press_pos = None
108
106
 
109
107
  # Check Move Group dialog
108
+ # Check Move Group or Move Selected Atoms dialog
110
109
  move_group_dialog = None
111
110
  for widget in QApplication.topLevelWidgets():
112
111
  try:
113
- if isinstance(widget, MoveGroupDialog) and widget.isVisible():
112
+ if (
113
+ type(widget).__name__
114
+ in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
115
+ and widget.isVisible()
116
+ ):
114
117
  move_group_dialog = widget
115
118
  break
116
119
  except (AttributeError, RuntimeError, TypeError):
@@ -147,6 +150,20 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
147
150
  self._suppress_next_left_button_up = True
148
151
  return # Disable camera rotation
149
152
  else:
153
+ if type(move_group_dialog).__name__ == "MoveSelectedAtomsDialog":
154
+ # For MoveSelectedAtomsDialog, we toggle ONLY the clicked atom, no BFS!
155
+ def _deferred_toggle(
156
+ idx=clicked_atom_idx, dlg=move_group_dialog
157
+ ):
158
+ try:
159
+ dlg.on_atom_picked(idx)
160
+ except (AttributeError, RuntimeError):
161
+ pass
162
+
163
+ QTimer.singleShot(0, _deferred_toggle)
164
+ self._suppress_next_left_button_up = True
165
+ return
166
+
150
167
  # Clicked outside group - Search connected component
151
168
  visited = set()
152
169
  queue = [clicked_atom_idx]
@@ -172,8 +189,14 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
172
189
 
173
190
  # Multi-selection with Ctrl
174
191
  is_ctrl_pressed = bool(
175
- QApplication.keyboardModifiers()
176
- & Qt.KeyboardModifier.ControlModifier
192
+ (
193
+ QApplication.keyboardModifiers()
194
+ & Qt.KeyboardModifier.ControlModifier
195
+ )
196
+ or (
197
+ self.GetInteractor()
198
+ and self.GetInteractor().GetControlKey()
199
+ )
177
200
  )
178
201
 
179
202
  if is_ctrl_pressed:
@@ -208,8 +231,10 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
208
231
  super(CustomInteractorStyle, self).OnLeftButtonDown()
209
232
  return
210
233
 
234
+ interactor = self.GetInteractor()
211
235
  is_temp_mode = bool(
212
- QApplication.keyboardModifiers() & Qt.KeyboardModifier.AltModifier
236
+ (QApplication.keyboardModifiers() & Qt.KeyboardModifier.AltModifier)
237
+ or (interactor and interactor.GetAltKey())
213
238
  )
214
239
  is_edit_active = mw.edit_3d_manager.is_3d_edit_mode or is_temp_mode
215
240
 
@@ -293,11 +318,15 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
293
318
  """
294
319
  mw = self.main_window
295
320
 
296
- # Check if Move Group dialog is open
321
+ # Check if Move Group dialog or Move Selected Atoms dialog is open
297
322
  move_group_dialog = None
298
323
  try:
299
324
  for widget in QApplication.topLevelWidgets():
300
- if isinstance(widget, MoveGroupDialog) and widget.isVisible():
325
+ if (
326
+ type(widget).__name__
327
+ in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
328
+ and widget.isVisible()
329
+ ):
301
330
  move_group_dialog = widget
302
331
  break
303
332
  except (AttributeError, RuntimeError, TypeError) as e:
@@ -350,11 +379,15 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
350
379
  """
351
380
  mw = self.main_window
352
381
 
353
- # Move Group drag handling
382
+ # Move Group / Selected Atoms drag handling
354
383
  move_group_dialog = None
355
384
  try:
356
385
  for widget in QApplication.topLevelWidgets():
357
- if isinstance(widget, MoveGroupDialog) and widget.isVisible():
386
+ if (
387
+ type(widget).__name__
388
+ in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
389
+ and widget.isVisible()
390
+ ):
358
391
  move_group_dialog = widget
359
392
  break
360
393
  except (AttributeError, RuntimeError, TypeError):
@@ -372,7 +405,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
372
405
  dx = current_pos[0] - move_group_dialog._drag_start_pos[0]
373
406
  dy = current_pos[1] - move_group_dialog._drag_start_pos[1]
374
407
 
375
- if abs(dx) > 2 or abs(dy) > 2:
408
+ if abs(dx) > 5 or abs(dy) > 5:
376
409
  move_group_dialog._mouse_moved = True
377
410
 
378
411
  return # Disable camera rotation
@@ -390,7 +423,7 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
390
423
  dx = current_pos[0] - move_group_dialog._rotation_start_pos[0]
391
424
  dy = current_pos[1] - move_group_dialog._rotation_start_pos[1]
392
425
 
393
- if abs(dx) > 2 or abs(dy) > 2:
426
+ if abs(dx) > 5 or abs(dy) > 5:
394
427
  move_group_dialog._rotation_mouse_moved = True
395
428
 
396
429
  return # Disable camera rotation
@@ -442,11 +475,15 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
442
475
  """
443
476
  mw = self.main_window
444
477
 
445
- # Finalize Move Group drag
478
+ # Finalize Move Group / Selected Atoms drag
446
479
  move_group_dialog = None
447
480
  try:
448
481
  for widget in QApplication.topLevelWidgets():
449
- if isinstance(widget, MoveGroupDialog) and widget.isVisible():
482
+ if (
483
+ type(widget).__name__
484
+ in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
485
+ and widget.isVisible()
486
+ ):
450
487
  move_group_dialog = widget
451
488
  break
452
489
  except (AttributeError, RuntimeError, TypeError):
@@ -456,6 +493,13 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
456
493
  if getattr(
457
494
  move_group_dialog, "_is_dragging_group_vtk", False
458
495
  ) and not getattr(move_group_dialog, "_mouse_moved", False):
496
+ # No drag = click only -> toggle
497
+ clicked_atom = getattr(move_group_dialog, "_drag_atom_idx", None)
498
+ if clicked_atom is not None:
499
+ try:
500
+ move_group_dialog.on_atom_picked(clicked_atom)
501
+ except (AttributeError, RuntimeError, TypeError, ValueError) as e:
502
+ logging.error(f"Error in toggle: {e}")
459
503
  # Reset if multi-clicked without drag
460
504
  move_group_dialog._is_dragging_group_vtk = False
461
505
  move_group_dialog._drag_start_pos = None
@@ -749,11 +793,15 @@ class CustomInteractorStyle(vtkInteractorStyleTrackballCamera):
749
793
  """
750
794
  mw = self.main_window
751
795
 
752
- # Finalize Move Group rotation
796
+ # Finalize Move Group / Selected Atoms rotation
753
797
  move_group_dialog = None
754
798
  try:
755
799
  for widget in QApplication.topLevelWidgets():
756
- if isinstance(widget, MoveGroupDialog) and widget.isVisible():
800
+ if (
801
+ type(widget).__name__
802
+ in ("MoveGroupDialog", "MoveSelectedAtomsDialog")
803
+ and widget.isVisible()
804
+ ):
757
805
  move_group_dialog = widget
758
806
  break
759
807
  except (AttributeError, RuntimeError, TypeError):