MoleditPy-linux 3.6.2__py3-none-any.whl → 3.6.4__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.
- moleditpy_linux/ui/align_plane_dialog.py +95 -42
- moleditpy_linux/ui/alignment_dialog.py +98 -66
- moleditpy_linux/ui/base_picking_dialog.py +20 -0
- moleditpy_linux/ui/dialog_logic.py +18 -5
- moleditpy_linux/ui/translation_dialog.py +95 -24
- {moleditpy_linux-3.6.2.dist-info → moleditpy_linux-3.6.4.dist-info}/METADATA +1 -1
- {moleditpy_linux-3.6.2.dist-info → moleditpy_linux-3.6.4.dist-info}/RECORD +11 -11
- {moleditpy_linux-3.6.2.dist-info → moleditpy_linux-3.6.4.dist-info}/WHEEL +0 -0
- {moleditpy_linux-3.6.2.dist-info → moleditpy_linux-3.6.4.dist-info}/entry_points.txt +0 -0
- {moleditpy_linux-3.6.2.dist-info → moleditpy_linux-3.6.4.dist-info}/licenses/LICENSE +0 -0
- {moleditpy_linux-3.6.2.dist-info → moleditpy_linux-3.6.4.dist-info}/top_level.txt +0 -0
|
@@ -10,10 +10,13 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import logging
|
|
14
14
|
from typing import TYPE_CHECKING, Literal, Optional, Sequence
|
|
15
15
|
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
16
18
|
from PyQt6.QtWidgets import (
|
|
19
|
+
QCheckBox,
|
|
17
20
|
QHBoxLayout,
|
|
18
21
|
QLabel,
|
|
19
22
|
QMessageBox,
|
|
@@ -24,15 +27,22 @@ from PyQt6.QtWidgets import (
|
|
|
24
27
|
from rdkit import Chem
|
|
25
28
|
|
|
26
29
|
try:
|
|
27
|
-
from .base_picking_dialog import BasePickingDialog
|
|
30
|
+
from .base_picking_dialog import BasePickingDialog, SelectionList
|
|
28
31
|
except ImportError:
|
|
29
|
-
from moleditpy_linux.ui.base_picking_dialog import BasePickingDialog
|
|
32
|
+
from moleditpy_linux.ui.base_picking_dialog import BasePickingDialog, SelectionList
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from ..core.mol_geometry import rodrigues_rotate
|
|
36
|
+
except ImportError:
|
|
37
|
+
from moleditpy_linux.core.mol_geometry import rodrigues_rotate
|
|
30
38
|
|
|
31
39
|
if TYPE_CHECKING:
|
|
32
40
|
from .main_window import MainWindow
|
|
33
41
|
|
|
34
42
|
|
|
35
43
|
class AlignPlaneDialog(BasePickingDialog):
|
|
44
|
+
"""Dialog for aligning selected atoms to a principal plane (XY, XZ, or YZ)."""
|
|
45
|
+
|
|
36
46
|
def __init__(
|
|
37
47
|
self,
|
|
38
48
|
mol: Chem.Mol,
|
|
@@ -43,20 +53,31 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
43
53
|
) -> None:
|
|
44
54
|
super().__init__(mol, main_window, parent)
|
|
45
55
|
self.plane = plane
|
|
46
|
-
self.
|
|
56
|
+
self._selected_atoms = SelectionList()
|
|
47
57
|
|
|
48
58
|
# Add preselected atoms
|
|
49
59
|
if preselected_atoms:
|
|
50
|
-
self.
|
|
60
|
+
self._selected_atoms.update(preselected_atoms)
|
|
51
61
|
|
|
52
62
|
self.init_ui()
|
|
53
63
|
|
|
54
64
|
# Add labels to preselected atoms
|
|
55
|
-
if self.
|
|
65
|
+
if self._selected_atoms:
|
|
56
66
|
self.show_atom_labels()
|
|
57
67
|
self.update_display()
|
|
58
68
|
|
|
69
|
+
@property
|
|
70
|
+
def selected_atoms(self) -> SelectionList:
|
|
71
|
+
"""Return the ordered list of selected atom indices."""
|
|
72
|
+
return self._selected_atoms
|
|
73
|
+
|
|
74
|
+
@selected_atoms.setter
|
|
75
|
+
def selected_atoms(self, val: object) -> None:
|
|
76
|
+
"""Replace the selection with a new SelectionList built from val."""
|
|
77
|
+
self._selected_atoms = SelectionList(val) # type: ignore[arg-type]
|
|
78
|
+
|
|
59
79
|
def init_ui(self) -> None:
|
|
80
|
+
"""Build and lay out all widgets for the plane-alignment dialog."""
|
|
60
81
|
plane_names = {"xy": "XY", "xz": "XZ", "yz": "YZ"}
|
|
61
82
|
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
62
83
|
self.setModal(False)
|
|
@@ -64,11 +85,18 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
64
85
|
|
|
65
86
|
# Instructions
|
|
66
87
|
instruction_label = QLabel(
|
|
67
|
-
f"Click atoms in the 3D view to select them for align to
|
|
88
|
+
f"Click atoms in the 3D view to select them for align to "
|
|
89
|
+
f"the {plane_names[self.plane]} plane. At least 3 atoms "
|
|
90
|
+
f"are required."
|
|
68
91
|
)
|
|
69
92
|
instruction_label.setWordWrap(True)
|
|
70
93
|
layout.addWidget(instruction_label)
|
|
71
94
|
|
|
95
|
+
# Move to zero plane option (default False)
|
|
96
|
+
self.move_to_zero_plane_checkbox = QCheckBox("Move the plane to the zero plane")
|
|
97
|
+
self.move_to_zero_plane_checkbox.setChecked(False)
|
|
98
|
+
layout.addWidget(self.move_to_zero_plane_checkbox)
|
|
99
|
+
|
|
72
100
|
# Selected atoms display
|
|
73
101
|
self.selection_label = QLabel("No atoms selected")
|
|
74
102
|
layout.addWidget(self.selection_label)
|
|
@@ -106,11 +134,12 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
106
134
|
|
|
107
135
|
def on_atom_picked(self, atom_idx: int) -> None:
|
|
108
136
|
"""Handle the event when an atom is picked in the 3D view."""
|
|
109
|
-
if atom_idx in self.
|
|
110
|
-
self.
|
|
137
|
+
if atom_idx in self._selected_atoms:
|
|
138
|
+
self._selected_atoms.remove(atom_idx)
|
|
111
139
|
else:
|
|
112
|
-
self.
|
|
140
|
+
self._selected_atoms.append(atom_idx)
|
|
113
141
|
|
|
142
|
+
self.show_atom_labels()
|
|
114
143
|
self.update_display()
|
|
115
144
|
|
|
116
145
|
def clear_selection(self) -> None:
|
|
@@ -139,6 +168,7 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
139
168
|
self.update_display()
|
|
140
169
|
|
|
141
170
|
except (AttributeError, RuntimeError, TypeError, KeyError) as e:
|
|
171
|
+
logging.exception("Failed to select all atoms")
|
|
142
172
|
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
143
173
|
|
|
144
174
|
def update_display(self) -> None:
|
|
@@ -150,8 +180,15 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
150
180
|
)
|
|
151
181
|
self.apply_button.setEnabled(False)
|
|
152
182
|
else:
|
|
153
|
-
|
|
154
|
-
|
|
183
|
+
atom_list = sorted(self.selected_atoms)
|
|
184
|
+
atom_display = []
|
|
185
|
+
for i, atom_idx in enumerate(atom_list):
|
|
186
|
+
symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
187
|
+
atom_display.append(f"#{i + 1}: {symbol}({atom_idx})")
|
|
188
|
+
|
|
189
|
+
self.selection_label.setText(
|
|
190
|
+
f"Selected {count} atoms: {', '.join(atom_display)}"
|
|
191
|
+
)
|
|
155
192
|
self.apply_button.setEnabled(count >= 3)
|
|
156
193
|
|
|
157
194
|
def show_atom_labels(self) -> None:
|
|
@@ -159,7 +196,7 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
159
196
|
if self.selected_atoms:
|
|
160
197
|
sorted_atoms = sorted(self.selected_atoms)
|
|
161
198
|
pairs = [(idx, f"#{i + 1}") for i, idx in enumerate(sorted_atoms)]
|
|
162
|
-
self.show_atom_labels_for(pairs, color="
|
|
199
|
+
self.show_atom_labels_for(pairs, color="yellow")
|
|
163
200
|
else:
|
|
164
201
|
self.clear_atom_labels()
|
|
165
202
|
|
|
@@ -167,7 +204,9 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
167
204
|
"""Apply plane alignment (rotation-based)."""
|
|
168
205
|
if len(self.selected_atoms) < 3:
|
|
169
206
|
QMessageBox.warning(
|
|
170
|
-
self,
|
|
207
|
+
self,
|
|
208
|
+
"Warning",
|
|
209
|
+
"Please select at least 3 atoms for align.",
|
|
171
210
|
)
|
|
172
211
|
return
|
|
173
212
|
try:
|
|
@@ -197,7 +236,8 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
197
236
|
elif self.plane == "yz":
|
|
198
237
|
target_normal = np.array([1, 0, 0]) # X-axis direction
|
|
199
238
|
else:
|
|
200
|
-
|
|
239
|
+
# Default to Z-axis (XY plane)
|
|
240
|
+
target_normal = np.array([0, 0, 1])
|
|
201
241
|
|
|
202
242
|
# Adjust normal vector direction
|
|
203
243
|
if np.dot(normal_vector, target_normal) < 0:
|
|
@@ -207,36 +247,49 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
207
247
|
rotation_axis = np.cross(normal_vector, target_normal)
|
|
208
248
|
rotation_axis_norm = np.linalg.norm(rotation_axis)
|
|
209
249
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
# Rotate all atoms
|
|
229
|
-
conf = self.mol.GetConformer()
|
|
230
|
-
for i in range(self.mol.GetNumAtoms()):
|
|
231
|
-
current_pos = np.array(conf.GetAtomPosition(i))
|
|
232
|
-
centered_pos = current_pos - centroid
|
|
233
|
-
rotated_pos = rodrigues_rotation(
|
|
234
|
-
centered_pos, rotation_axis, rotation_angle
|
|
250
|
+
# Calculate new positions (rotated, centered back by default)
|
|
251
|
+
conf = self.mol.GetConformer()
|
|
252
|
+
new_positions = np.empty_like(positions)
|
|
253
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
254
|
+
current_pos = np.array(conf.GetAtomPosition(i))
|
|
255
|
+
centered_pos = current_pos - centroid
|
|
256
|
+
if rotation_axis_norm > 1e-10:
|
|
257
|
+
rot_norm = rotation_axis_norm
|
|
258
|
+
rotation_axis_normalized = rotation_axis / rot_norm
|
|
259
|
+
cos_angle = np.dot(normal_vector, target_normal)
|
|
260
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
261
|
+
rotation_angle = np.arccos(cos_angle)
|
|
262
|
+
rotated_pos = rodrigues_rotate(
|
|
263
|
+
centered_pos,
|
|
264
|
+
rotation_axis_normalized,
|
|
265
|
+
rotation_angle,
|
|
235
266
|
)
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
else:
|
|
268
|
+
rotated_pos = centered_pos
|
|
269
|
+
new_pos = rotated_pos + centroid
|
|
270
|
+
new_positions[i] = new_pos
|
|
271
|
+
|
|
272
|
+
# If move_to_zero_plane is True, translate so the plane
|
|
273
|
+
# of selected atoms is at zero
|
|
274
|
+
if self.move_to_zero_plane_checkbox.isChecked():
|
|
275
|
+
selected_new_positions = new_positions[selected_indices]
|
|
276
|
+
new_centroid = np.mean(selected_new_positions, axis=0)
|
|
277
|
+
translation_offset = np.zeros(3)
|
|
278
|
+
if self.plane == "xy":
|
|
279
|
+
translation_offset[2] = new_centroid[2]
|
|
280
|
+
elif self.plane == "xz":
|
|
281
|
+
translation_offset[1] = new_centroid[1]
|
|
282
|
+
elif self.plane == "yz":
|
|
283
|
+
translation_offset[0] = new_centroid[0]
|
|
284
|
+
new_positions = new_positions - translation_offset
|
|
285
|
+
|
|
286
|
+
# Update the conformer positions array in place
|
|
287
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
288
|
+
positions[i] = new_positions[i]
|
|
238
289
|
|
|
239
290
|
self._update_molecule_geometry(positions)
|
|
291
|
+
self.show_atom_labels()
|
|
240
292
|
|
|
241
293
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
294
|
+
logging.exception("Failed to apply align")
|
|
242
295
|
QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
|
|
@@ -10,11 +10,14 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import logging
|
|
14
14
|
from typing import TYPE_CHECKING, Literal, Optional, Sequence
|
|
15
15
|
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
16
18
|
from PyQt6.QtGui import QCloseEvent
|
|
17
19
|
from PyQt6.QtWidgets import (
|
|
20
|
+
QCheckBox,
|
|
18
21
|
QDialog,
|
|
19
22
|
QHBoxLayout,
|
|
20
23
|
QLabel,
|
|
@@ -30,11 +33,23 @@ try:
|
|
|
30
33
|
except ImportError:
|
|
31
34
|
from moleditpy_linux.ui.dialog_3d_picking_mixin import Dialog3DPickingMixin
|
|
32
35
|
|
|
36
|
+
try:
|
|
37
|
+
from .base_picking_dialog import SelectionList
|
|
38
|
+
except ImportError:
|
|
39
|
+
from moleditpy_linux.ui.base_picking_dialog import SelectionList
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from ..core.mol_geometry import rodrigues_rotate
|
|
43
|
+
except ImportError:
|
|
44
|
+
from moleditpy_linux.core.mol_geometry import rodrigues_rotate
|
|
45
|
+
|
|
33
46
|
if TYPE_CHECKING:
|
|
34
47
|
from .main_window import MainWindow
|
|
35
48
|
|
|
36
49
|
|
|
37
50
|
class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
51
|
+
"""Dialog for aligning two selected atoms along a principal axis (X, Y, or Z)."""
|
|
52
|
+
|
|
38
53
|
def __init__(
|
|
39
54
|
self,
|
|
40
55
|
mol: Chem.Mol,
|
|
@@ -48,21 +63,32 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
48
63
|
self.mol = mol
|
|
49
64
|
self.main_window = main_window
|
|
50
65
|
self.axis = axis
|
|
51
|
-
self.
|
|
66
|
+
self._selected_atoms = SelectionList()
|
|
52
67
|
|
|
53
68
|
# Add preselected atoms (maximum 2)
|
|
54
69
|
if preselected_atoms:
|
|
55
|
-
self.
|
|
70
|
+
self._selected_atoms.update(preselected_atoms[:2])
|
|
56
71
|
|
|
57
72
|
self.init_ui()
|
|
58
73
|
|
|
59
74
|
# Add labels to preselected atoms
|
|
60
|
-
if self.
|
|
61
|
-
for i, atom_idx in enumerate(
|
|
62
|
-
self.add_selection_label(atom_idx, f"
|
|
75
|
+
if self._selected_atoms:
|
|
76
|
+
for i, atom_idx in enumerate(self._selected_atoms, 1):
|
|
77
|
+
self.add_selection_label(atom_idx, f"#{i}", color="yellow")
|
|
63
78
|
self.update_display()
|
|
64
79
|
|
|
80
|
+
@property
|
|
81
|
+
def selected_atoms(self) -> SelectionList:
|
|
82
|
+
"""Return the ordered list of selected atom indices."""
|
|
83
|
+
return self._selected_atoms
|
|
84
|
+
|
|
85
|
+
@selected_atoms.setter
|
|
86
|
+
def selected_atoms(self, val: object) -> None:
|
|
87
|
+
"""Replace the selection with a new SelectionList built from val."""
|
|
88
|
+
self._selected_atoms = SelectionList(val) # type: ignore[arg-type]
|
|
89
|
+
|
|
65
90
|
def init_ui(self) -> None:
|
|
91
|
+
"""Build and lay out all widgets for the alignment dialog."""
|
|
66
92
|
axis_names = {"x": "X-axis", "y": "Y-axis", "z": "Z-axis"}
|
|
67
93
|
self.setWindowTitle(f"Align to {axis_names[self.axis]}")
|
|
68
94
|
self.setModal(False)
|
|
@@ -70,11 +96,17 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
70
96
|
|
|
71
97
|
# Instructions
|
|
72
98
|
instruction_label = QLabel(
|
|
73
|
-
f"Click atoms in the 3D view to select them for alignment to the
|
|
99
|
+
f"Click atoms in the 3D view to select them for alignment to the "
|
|
100
|
+
f"{axis_names[self.axis]}. Exactly 2 atoms are required."
|
|
74
101
|
)
|
|
75
102
|
instruction_label.setWordWrap(True)
|
|
76
103
|
layout.addWidget(instruction_label)
|
|
77
104
|
|
|
105
|
+
# Move to origin option (default False)
|
|
106
|
+
self.move_to_origin_checkbox = QCheckBox("Move the first atom to the origin")
|
|
107
|
+
self.move_to_origin_checkbox.setChecked(False)
|
|
108
|
+
layout.addWidget(self.move_to_origin_checkbox)
|
|
109
|
+
|
|
78
110
|
# Selected atoms display
|
|
79
111
|
self.selection_label = QLabel("No atoms selected")
|
|
80
112
|
layout.addWidget(self.selection_label)
|
|
@@ -114,10 +146,10 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
114
146
|
else:
|
|
115
147
|
# Maximum of 2 atoms can be selected
|
|
116
148
|
if len(self.selected_atoms) < 2:
|
|
117
|
-
self.selected_atoms.
|
|
149
|
+
self.selected_atoms.append(atom_idx)
|
|
118
150
|
# Show label indicating selection order
|
|
119
|
-
label_text = f"
|
|
120
|
-
self.add_selection_label(atom_idx, label_text)
|
|
151
|
+
label_text = f"#{len(self.selected_atoms)}"
|
|
152
|
+
self.add_selection_label(atom_idx, label_text, color="yellow")
|
|
121
153
|
|
|
122
154
|
self.update_display()
|
|
123
155
|
|
|
@@ -129,14 +161,12 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
129
161
|
)
|
|
130
162
|
self.apply_button.setEnabled(False)
|
|
131
163
|
elif len(self.selected_atoms) == 1:
|
|
132
|
-
|
|
133
|
-
atom = self.mol.GetAtomWithIdx(selected_list[0])
|
|
164
|
+
atom = self.mol.GetAtomWithIdx(self.selected_atoms[0])
|
|
134
165
|
self.selection_label.setText(f"Selected 1 atom: {atom.GetSymbol()}")
|
|
135
166
|
self.apply_button.setEnabled(False)
|
|
136
167
|
elif len(self.selected_atoms) == 2:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
atom2 = self.mol.GetAtomWithIdx(selected_list[1])
|
|
168
|
+
atom1 = self.mol.GetAtomWithIdx(self.selected_atoms[0])
|
|
169
|
+
atom2 = self.mol.GetAtomWithIdx(self.selected_atoms[1])
|
|
140
170
|
self.selection_label.setText(
|
|
141
171
|
f"Selected 2 atoms: {atom1.GetSymbol()}, {atom2.GetSymbol()}"
|
|
142
172
|
)
|
|
@@ -148,13 +178,11 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
148
178
|
self.selected_atoms.clear()
|
|
149
179
|
self.update_display()
|
|
150
180
|
|
|
151
|
-
def remove_atom_label(self,
|
|
152
|
-
"""Remove a label for a specific atom."""
|
|
153
|
-
# Re-draw all labels for simplicity
|
|
181
|
+
def remove_atom_label(self, _atom_idx: int) -> None:
|
|
182
|
+
"""Remove a label for a specific atom (redraws all labels)."""
|
|
154
183
|
self.clear_selection_labels()
|
|
155
|
-
for i, idx in enumerate(
|
|
156
|
-
|
|
157
|
-
self.add_selection_label(idx, f"Atom {i}")
|
|
184
|
+
for i, idx in enumerate(self.selected_atoms, 1):
|
|
185
|
+
self.add_selection_label(idx, f"#{i}", color="yellow")
|
|
158
186
|
|
|
159
187
|
def apply_alignment(self) -> None:
|
|
160
188
|
"""Apply the specific axial alignment to the molecule."""
|
|
@@ -164,26 +192,21 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
164
192
|
)
|
|
165
193
|
return
|
|
166
194
|
try:
|
|
167
|
-
|
|
168
|
-
|
|
195
|
+
atom1_idx = self.selected_atoms[0]
|
|
196
|
+
atom2_idx = self.selected_atoms[1]
|
|
169
197
|
|
|
170
198
|
conf = self.mol.GetConformer()
|
|
171
199
|
|
|
172
|
-
# Get
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
translation = -pos1
|
|
178
|
-
for i in range(self.mol.GetNumAtoms()):
|
|
179
|
-
current_pos = np.array(conf.GetAtomPosition(i))
|
|
180
|
-
new_pos = current_pos + translation
|
|
181
|
-
conf.SetAtomPosition(i, new_pos.tolist())
|
|
200
|
+
# Get original atom positions
|
|
201
|
+
positions = np.array(
|
|
202
|
+
[list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())]
|
|
203
|
+
)
|
|
204
|
+
centroid = np.mean(positions, axis=0)
|
|
182
205
|
|
|
183
|
-
|
|
184
|
-
|
|
206
|
+
pos1 = positions[atom1_idx]
|
|
207
|
+
pos2 = positions[atom2_idx]
|
|
185
208
|
|
|
186
|
-
# Calculate rotation to align atom2 relative to the chosen axis
|
|
209
|
+
# Calculate rotation to align atom1 -> atom2 relative to the chosen axis
|
|
187
210
|
axis_vectors = {
|
|
188
211
|
"x": np.array([1.0, 0.0, 0.0]),
|
|
189
212
|
"y": np.array([0.0, 1.0, 0.0]),
|
|
@@ -191,10 +214,13 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
191
214
|
}
|
|
192
215
|
target_axis = axis_vectors[self.axis]
|
|
193
216
|
|
|
194
|
-
# Direction vector from
|
|
195
|
-
current_vector =
|
|
217
|
+
# Direction vector from atom1 to atom2
|
|
218
|
+
current_vector = pos2 - pos1
|
|
196
219
|
current_length = np.linalg.norm(current_vector)
|
|
197
220
|
|
|
221
|
+
# Keep track of rotated positions (initially original positions)
|
|
222
|
+
new_positions = np.copy(positions)
|
|
223
|
+
|
|
198
224
|
if current_length > 1e-10: # If not a zero vector
|
|
199
225
|
current_vector_normalized = current_vector / current_length
|
|
200
226
|
|
|
@@ -208,41 +234,44 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
208
234
|
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
209
235
|
rotation_angle = np.arccos(cos_angle)
|
|
210
236
|
|
|
211
|
-
#
|
|
212
|
-
def rodrigues_rotation(
|
|
213
|
-
v: np.ndarray, k: np.ndarray, theta: float
|
|
214
|
-
) -> np.ndarray:
|
|
215
|
-
cos_theta = np.cos(theta)
|
|
216
|
-
sin_theta = np.sin(theta)
|
|
217
|
-
return ( # type: ignore[no-any-return]
|
|
218
|
-
v * cos_theta
|
|
219
|
-
+ np.cross(k, v) * sin_theta
|
|
220
|
-
+ k * np.dot(k, v) * (1 - cos_theta)
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
# Apply rotation to all atoms
|
|
237
|
+
# Apply rotation to all atoms about the molecule's centroid
|
|
224
238
|
for i in range(self.mol.GetNumAtoms()):
|
|
225
|
-
|
|
226
|
-
rotated_pos =
|
|
227
|
-
|
|
228
|
-
)
|
|
229
|
-
conf.SetAtomPosition(
|
|
230
|
-
i,
|
|
231
|
-
Geometry.Point3D(
|
|
232
|
-
float(rotated_pos[0]),
|
|
233
|
-
float(rotated_pos[1]),
|
|
234
|
-
float(rotated_pos[2]),
|
|
235
|
-
),
|
|
239
|
+
rel_pos = positions[i] - centroid
|
|
240
|
+
rotated_pos = rodrigues_rotate(
|
|
241
|
+
rel_pos, rotation_axis, rotation_angle
|
|
236
242
|
)
|
|
243
|
+
new_positions[i] = rotated_pos + centroid
|
|
244
|
+
|
|
245
|
+
# If move_to_origin is True, translate entire molecule so atom1 ends up at origin
|
|
246
|
+
if (
|
|
247
|
+
hasattr(self, "move_to_origin_checkbox")
|
|
248
|
+
and self.move_to_origin_checkbox.isChecked()
|
|
249
|
+
):
|
|
250
|
+
new_pos1 = new_positions[atom1_idx]
|
|
251
|
+
new_positions = new_positions - new_pos1
|
|
252
|
+
|
|
253
|
+
# Update conformer positions
|
|
254
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
255
|
+
conf.SetAtomPosition(
|
|
256
|
+
i,
|
|
257
|
+
Geometry.Point3D(
|
|
258
|
+
float(new_positions[i][0]),
|
|
259
|
+
float(new_positions[i][1]),
|
|
260
|
+
float(new_positions[i][2]),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
237
263
|
|
|
238
264
|
# Update 3D positions
|
|
239
|
-
self.main_window.view_3d_manager.atom_positions_3d =
|
|
240
|
-
[list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())]
|
|
241
|
-
)
|
|
265
|
+
self.main_window.view_3d_manager.atom_positions_3d = new_positions
|
|
242
266
|
|
|
243
267
|
# Update 3D visualization
|
|
244
268
|
self.main_window.view_3d_manager.draw_molecule_3d(self.mol)
|
|
245
269
|
|
|
270
|
+
# Restore selection labels
|
|
271
|
+
self.clear_selection_labels()
|
|
272
|
+
for i, idx in enumerate(self.selected_atoms, 1):
|
|
273
|
+
self.add_selection_label(idx, f"#{i}", color="yellow")
|
|
274
|
+
|
|
246
275
|
# Update chirality labels
|
|
247
276
|
self.main_window.view_3d_manager.update_chiral_labels()
|
|
248
277
|
|
|
@@ -250,10 +279,13 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
250
279
|
self.main_window.edit_actions_manager.push_undo_state()
|
|
251
280
|
|
|
252
281
|
QMessageBox.information(
|
|
253
|
-
self,
|
|
282
|
+
self,
|
|
283
|
+
"Success",
|
|
284
|
+
f"Alignment to {self.axis.upper()}-axis completed.",
|
|
254
285
|
)
|
|
255
286
|
|
|
256
287
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
288
|
+
logging.exception("Failed to apply alignment")
|
|
257
289
|
QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
|
|
258
290
|
|
|
259
291
|
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
|
@@ -28,6 +28,26 @@ if TYPE_CHECKING:
|
|
|
28
28
|
from .main_window import MainWindow
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
class SelectionList(list):
|
|
32
|
+
"""Order-preserving list that compares equal to sets/lists/tuples of same elements."""
|
|
33
|
+
|
|
34
|
+
def __eq__(self, other: object) -> bool:
|
|
35
|
+
"""Compare by membership, ignoring order."""
|
|
36
|
+
if isinstance(other, (set, list, tuple)):
|
|
37
|
+
return set(self) == set(other)
|
|
38
|
+
return super().__eq__(other)
|
|
39
|
+
|
|
40
|
+
def add(self, item: int) -> None:
|
|
41
|
+
"""Append item only if not already present."""
|
|
42
|
+
if item not in self:
|
|
43
|
+
self.append(item)
|
|
44
|
+
|
|
45
|
+
def update(self, items: object) -> None:
|
|
46
|
+
"""Append each item that is not already present."""
|
|
47
|
+
for item in items: # type: ignore[union-attr]
|
|
48
|
+
self.add(item)
|
|
49
|
+
|
|
50
|
+
|
|
31
51
|
class BasePickingDialog(Dialog3DPickingMixin, QDialog):
|
|
32
52
|
"""
|
|
33
53
|
Base class for any dialog requiring 3D atom picking.
|
|
@@ -16,6 +16,7 @@ import logging
|
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
18
|
import os
|
|
19
|
+
import sys
|
|
19
20
|
from typing import Any, List, Literal, Optional, cast
|
|
20
21
|
|
|
21
22
|
from PyQt6.QtWidgets import QInputDialog, QMessageBox, QDialog
|
|
@@ -23,7 +24,7 @@ from PyQt6.QtCore import Qt
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
try:
|
|
26
|
-
# package relative imports (preferred when running as
|
|
27
|
+
# package relative imports (preferred when running as python -m moleditpy)
|
|
27
28
|
from .about_dialog import AboutDialog
|
|
28
29
|
from .align_plane_dialog import AlignPlaneDialog
|
|
29
30
|
from .alignment_dialog import AlignmentDialog
|
|
@@ -71,13 +72,19 @@ except ImportError:
|
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
class DialogManager:
|
|
74
|
-
"""Independent manager for UI dialogs
|
|
75
|
+
"""Independent manager for UI dialogs.
|
|
76
|
+
|
|
77
|
+
Ported from MainWindowDialogManager mixin.
|
|
78
|
+
"""
|
|
75
79
|
|
|
76
80
|
def __init__(self, host: Any) -> None:
|
|
77
81
|
self.host = host
|
|
78
82
|
|
|
79
83
|
def _get_preselected_atoms_3d(self) -> List[int]:
|
|
80
|
-
"""Helper to collect preselected atoms from measurement mode
|
|
84
|
+
"""Helper to collect preselected atoms from measurement mode.
|
|
85
|
+
|
|
86
|
+
Specifically for 3D Select.
|
|
87
|
+
"""
|
|
81
88
|
preselected_atoms = []
|
|
82
89
|
if hasattr(self.host, "edit_3d_manager"):
|
|
83
90
|
if self.host.edit_3d_manager.selected_atoms_for_measurement:
|
|
@@ -96,6 +103,7 @@ class DialogManager:
|
|
|
96
103
|
dialog.exec()
|
|
97
104
|
|
|
98
105
|
def open_periodic_table_dialog(self) -> None:
|
|
106
|
+
"""Open the periodic table dialog and wire up element-selection callback."""
|
|
99
107
|
dialog = PeriodicTableDialog(self.host)
|
|
100
108
|
dialog.element_selected.connect(
|
|
101
109
|
self.host.ui_manager.set_atom_from_periodic_table
|
|
@@ -108,6 +116,7 @@ class DialogManager:
|
|
|
108
116
|
dialog.exec()
|
|
109
117
|
|
|
110
118
|
def open_analysis_window(self) -> None:
|
|
119
|
+
"""Open the analysis window for the current 3D molecule, if available."""
|
|
111
120
|
if self.host.view_3d_manager.current_mol:
|
|
112
121
|
dialog = AnalysisWindow(
|
|
113
122
|
self.host.view_3d_manager.current_mol,
|
|
@@ -126,7 +135,10 @@ class DialogManager:
|
|
|
126
135
|
dialog.exec()
|
|
127
136
|
|
|
128
137
|
def open_template_dialog_and_activate(self) -> None:
|
|
129
|
-
"""Open the template dialog and activate
|
|
138
|
+
"""Open the template dialog and activate it.
|
|
139
|
+
|
|
140
|
+
Used in the main window.
|
|
141
|
+
"""
|
|
130
142
|
# Check for existing dialog
|
|
131
143
|
_template_dialog = getattr(self.host, "_template_dialog", None)
|
|
132
144
|
if _template_dialog and not _template_dialog.isHidden():
|
|
@@ -218,7 +230,8 @@ class DialogManager:
|
|
|
218
230
|
|
|
219
231
|
def _show_modeless_dialog(self, dialog: QDialog) -> None:
|
|
220
232
|
"""Show a modeless dialog on top, especially important for macOS."""
|
|
221
|
-
|
|
233
|
+
if sys.platform == "darwin":
|
|
234
|
+
dialog.setWindowFlag(Qt.WindowType.WindowStaysOnTopHint, True)
|
|
222
235
|
dialog.show()
|
|
223
236
|
dialog.raise_()
|
|
224
237
|
dialog.activateWindow()
|
|
@@ -10,6 +10,7 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import logging
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
15
16
|
import numpy as np
|
|
@@ -35,6 +36,8 @@ _TAB_DELTA = 1
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
class TranslationDialog(BasePickingDialog):
|
|
39
|
+
"""Dialog for translating selected atoms either absolutely or relatively."""
|
|
40
|
+
|
|
38
41
|
def __init__(
|
|
39
42
|
self,
|
|
40
43
|
mol: Any,
|
|
@@ -45,6 +48,19 @@ class TranslationDialog(BasePickingDialog):
|
|
|
45
48
|
super().__init__(mol, main_window, parent)
|
|
46
49
|
self.selected_atoms = set()
|
|
47
50
|
|
|
51
|
+
# Predefine widgets to satisfy pylint W0201
|
|
52
|
+
self.abs_selection_label = None
|
|
53
|
+
self.abs_x_input = None
|
|
54
|
+
self.abs_y_input = None
|
|
55
|
+
self.abs_z_input = None
|
|
56
|
+
self.move_mol_checkbox = None
|
|
57
|
+
self.abs_apply_btn = None
|
|
58
|
+
self.delta_selection_label = None
|
|
59
|
+
self.dx_input = None
|
|
60
|
+
self.dy_input = None
|
|
61
|
+
self.dz_input = None
|
|
62
|
+
self.apply_button = None
|
|
63
|
+
|
|
48
64
|
if preselected_atoms:
|
|
49
65
|
self.selected_atoms.update(preselected_atoms)
|
|
50
66
|
|
|
@@ -56,7 +72,7 @@ class TranslationDialog(BasePickingDialog):
|
|
|
56
72
|
self.tabs.blockSignals(True)
|
|
57
73
|
if len(self.selected_atoms) == 1:
|
|
58
74
|
self.tabs.setCurrentIndex(_TAB_ABSOLUTE)
|
|
59
|
-
self.
|
|
75
|
+
self._populate_abs_inputs_from_centroid()
|
|
60
76
|
else:
|
|
61
77
|
self.tabs.setCurrentIndex(_TAB_DELTA)
|
|
62
78
|
self.tabs.blockSignals(False)
|
|
@@ -68,6 +84,7 @@ class TranslationDialog(BasePickingDialog):
|
|
|
68
84
|
# ------------------------------------------------------------------
|
|
69
85
|
|
|
70
86
|
def init_ui(self) -> None:
|
|
87
|
+
"""Initialize and lay out all dialog widgets."""
|
|
71
88
|
self.setWindowTitle("Translate Atoms")
|
|
72
89
|
self.setModal(False)
|
|
73
90
|
layout = QVBoxLayout(self)
|
|
@@ -89,16 +106,18 @@ class TranslationDialog(BasePickingDialog):
|
|
|
89
106
|
self.enable_picking()
|
|
90
107
|
|
|
91
108
|
def _build_absolute_tab(self) -> QWidget:
|
|
109
|
+
"""Build the Absolute tab widget."""
|
|
92
110
|
widget = QWidget()
|
|
93
111
|
layout = QVBoxLayout(widget)
|
|
94
112
|
|
|
95
113
|
instr = QLabel(
|
|
96
|
-
"Click
|
|
114
|
+
"Click atoms to select them. The centroid of the selection "
|
|
115
|
+
"will be translated to the target absolute coordinates (Å)."
|
|
97
116
|
)
|
|
98
117
|
instr.setWordWrap(True)
|
|
99
118
|
layout.addWidget(instr)
|
|
100
119
|
|
|
101
|
-
self.abs_selection_label = QLabel("No
|
|
120
|
+
self.abs_selection_label = QLabel("No atoms selected")
|
|
102
121
|
layout.addWidget(self.abs_selection_label)
|
|
103
122
|
|
|
104
123
|
coord_row = QHBoxLayout()
|
|
@@ -122,10 +141,17 @@ class TranslationDialog(BasePickingDialog):
|
|
|
122
141
|
abs_clear_btn = QPushButton("Clear Selection")
|
|
123
142
|
abs_clear_btn.clicked.connect(self._abs_clear_selection)
|
|
124
143
|
btn_row.addWidget(abs_clear_btn)
|
|
144
|
+
|
|
145
|
+
abs_all_btn = QPushButton("Select All Atoms")
|
|
146
|
+
abs_all_btn.setToolTip("Select all atoms in the molecule")
|
|
147
|
+
abs_all_btn.clicked.connect(self._abs_select_all)
|
|
148
|
+
btn_row.addWidget(abs_all_btn)
|
|
149
|
+
|
|
125
150
|
origin_btn = QPushButton("Set to Origin")
|
|
126
151
|
origin_btn.setToolTip("Set target coordinates to the origin")
|
|
127
152
|
origin_btn.clicked.connect(self._set_origin)
|
|
128
153
|
btn_row.addWidget(origin_btn)
|
|
154
|
+
|
|
129
155
|
btn_row.addStretch()
|
|
130
156
|
self.abs_apply_btn = QPushButton("Move Molecule")
|
|
131
157
|
self.abs_apply_btn.clicked.connect(self.apply_absolute)
|
|
@@ -185,7 +211,8 @@ class TranslationDialog(BasePickingDialog):
|
|
|
185
211
|
# Tab switching
|
|
186
212
|
# ------------------------------------------------------------------
|
|
187
213
|
|
|
188
|
-
def _on_tab_changed(self,
|
|
214
|
+
def _on_tab_changed(self, _index: int) -> None:
|
|
215
|
+
"""Handle tab switching event to reset active selections."""
|
|
189
216
|
if hasattr(self, "_is_initializing") and self._is_initializing:
|
|
190
217
|
return
|
|
191
218
|
self.selected_atoms.clear()
|
|
@@ -203,9 +230,12 @@ class TranslationDialog(BasePickingDialog):
|
|
|
203
230
|
self._delta_on_atom_picked(atom_idx)
|
|
204
231
|
|
|
205
232
|
def _abs_on_atom_picked(self, atom_idx: int) -> None:
|
|
206
|
-
|
|
207
|
-
self.selected_atoms
|
|
208
|
-
|
|
233
|
+
"""Toggle atom in/out of the absolute-tab selection, then refresh centroid."""
|
|
234
|
+
if atom_idx in self.selected_atoms:
|
|
235
|
+
self.selected_atoms.discard(atom_idx)
|
|
236
|
+
else:
|
|
237
|
+
self.selected_atoms.add(atom_idx)
|
|
238
|
+
self._populate_abs_inputs_from_centroid()
|
|
209
239
|
self.update_display()
|
|
210
240
|
self.show_atom_labels()
|
|
211
241
|
|
|
@@ -221,19 +251,24 @@ class TranslationDialog(BasePickingDialog):
|
|
|
221
251
|
# Absolute tab helpers
|
|
222
252
|
# ------------------------------------------------------------------
|
|
223
253
|
|
|
224
|
-
def
|
|
254
|
+
def _populate_abs_inputs_from_centroid(self) -> None:
|
|
255
|
+
"""Populate X/Y/Z inputs with the centroid of all currently selected atoms."""
|
|
256
|
+
if not self.selected_atoms:
|
|
257
|
+
return
|
|
225
258
|
mol = self.main_window.view_3d_manager.current_mol
|
|
226
259
|
if mol is None:
|
|
227
260
|
return
|
|
228
261
|
conf = mol.GetConformer()
|
|
229
262
|
if conf is None:
|
|
230
263
|
return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
self.
|
|
234
|
-
self.
|
|
264
|
+
positions = conf.GetPositions()
|
|
265
|
+
centroid = np.mean([positions[i] for i in self.selected_atoms], axis=0)
|
|
266
|
+
self.abs_x_input.setText(f"{centroid[0]:.4f}")
|
|
267
|
+
self.abs_y_input.setText(f"{centroid[1]:.4f}")
|
|
268
|
+
self.abs_z_input.setText(f"{centroid[2]:.4f}")
|
|
235
269
|
|
|
236
270
|
def _abs_clear_selection(self) -> None:
|
|
271
|
+
"""Clear the absolute-tab selection and reset coordinate inputs."""
|
|
237
272
|
self.selected_atoms.clear()
|
|
238
273
|
self.clear_atom_labels()
|
|
239
274
|
self.abs_x_input.setText("0.000")
|
|
@@ -241,19 +276,35 @@ class TranslationDialog(BasePickingDialog):
|
|
|
241
276
|
self.abs_z_input.setText("0.000")
|
|
242
277
|
self.update_display()
|
|
243
278
|
|
|
279
|
+
def _abs_select_all(self) -> None:
|
|
280
|
+
"""Select all atoms in the molecule for the absolute tab."""
|
|
281
|
+
try:
|
|
282
|
+
mol = self.main_window.view_3d_manager.current_mol
|
|
283
|
+
if mol is not None:
|
|
284
|
+
self.selected_atoms = set(range(mol.GetNumAtoms()))
|
|
285
|
+
self._populate_abs_inputs_from_centroid()
|
|
286
|
+
self.show_atom_labels()
|
|
287
|
+
self.update_display()
|
|
288
|
+
except (AttributeError, RuntimeError, TypeError) as exc:
|
|
289
|
+
logging.exception("Failed to select all atoms: %s", exc)
|
|
290
|
+
|
|
244
291
|
def _set_origin(self) -> None:
|
|
245
292
|
self.abs_x_input.setText("0.0000")
|
|
246
293
|
self.abs_y_input.setText("0.0000")
|
|
247
294
|
self.abs_z_input.setText("0.0000")
|
|
248
295
|
|
|
249
|
-
def _on_move_mol_toggled(self,
|
|
250
|
-
|
|
296
|
+
def _on_move_mol_toggled(self, _state: int) -> None:
|
|
297
|
+
"""Update Apply button label to reflect move-molecule vs move-selected mode."""
|
|
298
|
+
label = (
|
|
299
|
+
"Move Molecule" if self.move_mol_checkbox.isChecked() else "Move Selected"
|
|
300
|
+
)
|
|
251
301
|
self.abs_apply_btn.setText(label)
|
|
252
302
|
|
|
253
303
|
def apply_absolute(self) -> None:
|
|
304
|
+
"""Translate selected atoms so their centroid reaches the target coordinates."""
|
|
254
305
|
self.mol = self.main_window.view_3d_manager.current_mol
|
|
255
|
-
if
|
|
256
|
-
QMessageBox.warning(self, "Warning", "Please select
|
|
306
|
+
if not self.selected_atoms:
|
|
307
|
+
QMessageBox.warning(self, "Warning", "Please select at least one atom.")
|
|
257
308
|
return
|
|
258
309
|
|
|
259
310
|
try:
|
|
@@ -266,10 +317,9 @@ class TranslationDialog(BasePickingDialog):
|
|
|
266
317
|
)
|
|
267
318
|
return
|
|
268
319
|
|
|
269
|
-
atom_idx = next(iter(self.selected_atoms))
|
|
270
320
|
positions = self.mol.GetConformer().GetPositions()
|
|
271
|
-
|
|
272
|
-
delta = np.array([tx, ty, tz]) -
|
|
321
|
+
centroid = np.mean([positions[i] for i in self.selected_atoms], axis=0)
|
|
322
|
+
delta = np.array([tx, ty, tz]) - centroid
|
|
273
323
|
|
|
274
324
|
if np.allclose(delta, 0):
|
|
275
325
|
return
|
|
@@ -277,10 +327,12 @@ class TranslationDialog(BasePickingDialog):
|
|
|
277
327
|
if self.move_mol_checkbox.isChecked():
|
|
278
328
|
positions += delta
|
|
279
329
|
else:
|
|
280
|
-
|
|
330
|
+
for atom_idx in self.selected_atoms:
|
|
331
|
+
positions[atom_idx] += delta
|
|
281
332
|
|
|
282
333
|
self._update_molecule_geometry(positions)
|
|
283
334
|
self._push_undo()
|
|
335
|
+
self._populate_abs_inputs_from_centroid()
|
|
284
336
|
self.show_atom_labels()
|
|
285
337
|
|
|
286
338
|
# ------------------------------------------------------------------
|
|
@@ -288,11 +340,13 @@ class TranslationDialog(BasePickingDialog):
|
|
|
288
340
|
# ------------------------------------------------------------------
|
|
289
341
|
|
|
290
342
|
def clear_selection(self) -> None:
|
|
343
|
+
"""Clear active atom selection for the relative tab."""
|
|
291
344
|
self.selected_atoms.clear()
|
|
292
345
|
self.clear_atom_labels()
|
|
293
346
|
self.update_display()
|
|
294
347
|
|
|
295
348
|
def select_all_atoms(self) -> None:
|
|
349
|
+
"""Select all atoms in the molecule for relative translation."""
|
|
296
350
|
try:
|
|
297
351
|
if hasattr(self, "mol") and self.mol is not None:
|
|
298
352
|
self.selected_atoms = set(range(self.mol.GetNumAtoms()))
|
|
@@ -308,6 +362,7 @@ class TranslationDialog(BasePickingDialog):
|
|
|
308
362
|
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
309
363
|
|
|
310
364
|
def apply_translation(self) -> None:
|
|
365
|
+
"""Apply relative translation vector to selected atoms."""
|
|
311
366
|
self.mol = self.main_window.view_3d_manager.current_mol
|
|
312
367
|
if not self.selected_atoms:
|
|
313
368
|
QMessageBox.warning(self, "Warning", "Please select at least one atom.")
|
|
@@ -340,17 +395,32 @@ class TranslationDialog(BasePickingDialog):
|
|
|
340
395
|
# ------------------------------------------------------------------
|
|
341
396
|
|
|
342
397
|
def update_display(self) -> None:
|
|
398
|
+
"""Update label descriptions and button enablement states."""
|
|
343
399
|
tab = self.tabs.currentIndex()
|
|
344
400
|
count = len(self.selected_atoms)
|
|
345
401
|
|
|
346
402
|
if tab == _TAB_ABSOLUTE:
|
|
347
403
|
if count == 0:
|
|
348
|
-
self.abs_selection_label.setText("Click
|
|
404
|
+
self.abs_selection_label.setText("Click atoms to select them")
|
|
349
405
|
self.abs_apply_btn.setEnabled(False)
|
|
350
406
|
else:
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
407
|
+
mol = self.main_window.view_3d_manager.current_mol
|
|
408
|
+
if mol is not None and self.selected_atoms:
|
|
409
|
+
positions = mol.GetConformer().GetPositions()
|
|
410
|
+
centroid = np.mean(
|
|
411
|
+
[positions[i] for i in self.selected_atoms], axis=0
|
|
412
|
+
)
|
|
413
|
+
centroid_str = (
|
|
414
|
+
f"({centroid[0]:.3f}, {centroid[1]:.3f}, {centroid[2]:.3f})"
|
|
415
|
+
)
|
|
416
|
+
self.abs_selection_label.setText(
|
|
417
|
+
f"{count} atom{'s' if count != 1 else ''} selected "
|
|
418
|
+
f"— centroid: {centroid_str}"
|
|
419
|
+
)
|
|
420
|
+
else:
|
|
421
|
+
self.abs_selection_label.setText(
|
|
422
|
+
f"{count} atom{'s' if count != 1 else ''} selected"
|
|
423
|
+
)
|
|
354
424
|
self.abs_apply_btn.setEnabled(True)
|
|
355
425
|
else:
|
|
356
426
|
if count == 0:
|
|
@@ -365,6 +435,7 @@ class TranslationDialog(BasePickingDialog):
|
|
|
365
435
|
self.apply_button.setEnabled(True)
|
|
366
436
|
|
|
367
437
|
def show_atom_labels(self) -> None:
|
|
438
|
+
"""Redraw selection numeric tags in the active 3D viewport."""
|
|
368
439
|
if self.selected_atoms:
|
|
369
440
|
sorted_atoms = sorted(self.selected_atoms)
|
|
370
441
|
pairs = [(idx, str(i + 1)) for i, idx in enumerate(sorted_atoms)]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 3.6.
|
|
3
|
+
Version: 3.6.4
|
|
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
|
|
@@ -14,14 +14,14 @@ moleditpy_linux/plugins/plugin_manager.py,sha256=WQkmxpj0X6jcA-snr4dL0tWiZJYBYFr
|
|
|
14
14
|
moleditpy_linux/plugins/plugin_manager_window.py,sha256=T_AyfycPvEfgfe_Dy-DONTrydvIqLS60V3J3pGRcVEY,12681
|
|
15
15
|
moleditpy_linux/ui/__init__.py,sha256=_BslFbhSM4Ba4GDzH1XpvlqlGgvi4OFkcuU3pPeIz3s,751
|
|
16
16
|
moleditpy_linux/ui/about_dialog.py,sha256=hWt-SXixbk1UUgMPHTVd0ecaTIoy2LWuGxkps6nqPiA,4447
|
|
17
|
-
moleditpy_linux/ui/align_plane_dialog.py,sha256=
|
|
18
|
-
moleditpy_linux/ui/alignment_dialog.py,sha256=
|
|
17
|
+
moleditpy_linux/ui/align_plane_dialog.py,sha256=doxaj-5HdN-DnrMfyh9VCf7SXCBNoJwal6odUE0SMM0,11359
|
|
18
|
+
moleditpy_linux/ui/alignment_dialog.py,sha256=2yZyXu0Ge-iKE4Dfd6rystA6UebdJPCxlZgPXiopsa8,11554
|
|
19
19
|
moleditpy_linux/ui/analysis_window.py,sha256=iVYzxLpL78XS46lzRC0DpT3hwD6FS60su_Kit04__Oo,9452
|
|
20
20
|
moleditpy_linux/ui/angle_dialog.py,sha256=jEtq0BcMmaIq7iZA26j5w7n6DBwPZswcHuGhsj2nRUA,17803
|
|
21
21
|
moleditpy_linux/ui/app_state.py,sha256=aNU2dy2lbW17pkZOurORFYmIimu715DioSH33UZn2QQ,40162
|
|
22
22
|
moleditpy_linux/ui/atom_item.py,sha256=tN7paZGVVOLR2ppuI_qcYajiQ7YLH4mOhjzBYHeEN3w,21144
|
|
23
23
|
moleditpy_linux/ui/atom_picking.py,sha256=HhJ8kH1zVF3DIzMG1Qymk44d8MwUqbg9xr4pQ6H0lHA,10391
|
|
24
|
-
moleditpy_linux/ui/base_picking_dialog.py,sha256=
|
|
24
|
+
moleditpy_linux/ui/base_picking_dialog.py,sha256=uKzos-t41gzlbpTwj_wjoa5D51F0X1NDCHba_H3uuTE,6392
|
|
25
25
|
moleditpy_linux/ui/bond_item.py,sha256=sn3Vk1XblmjfDP1zzPS_mn_5byFA4xPR_-qoYWjKTe8,26588
|
|
26
26
|
moleditpy_linux/ui/bond_length_dialog.py,sha256=U7YQ6r75TueSA7lV_B9dUEnaEjOLEgkT1aW8KUItbtI,14962
|
|
27
27
|
moleditpy_linux/ui/calculation_worker.py,sha256=HJz5NkXfNS66OODxKkR-IfGUtv7hRG-mp4Wfapw6D7M,41125
|
|
@@ -31,7 +31,7 @@ moleditpy_linux/ui/constrained_optimization_dialog.py,sha256=PNQN2d0nufI9s-N-60z
|
|
|
31
31
|
moleditpy_linux/ui/custom_interactor_style.py,sha256=poOgprQeffKtdL7cvoxWVPgRefO0OAOkAja_NOag4Ms,42564
|
|
32
32
|
moleditpy_linux/ui/custom_qt_interactor.py,sha256=oAfCHIDVOJmLTdowrBiGsaCoQMJ-ZnQhpSkDLIHMe9U,3735
|
|
33
33
|
moleditpy_linux/ui/dialog_3d_picking_mixin.py,sha256=iJg9bZDj2wvOG178mwOus5Y-6HmSmIMCX0KyvOJ-l0U,9901
|
|
34
|
-
moleditpy_linux/ui/dialog_logic.py,sha256=
|
|
34
|
+
moleditpy_linux/ui/dialog_logic.py,sha256=TLK4tB0P4ZzJS5YOV6flFoemFGSNRwg_3CEO3IGtQDw,21628
|
|
35
35
|
moleditpy_linux/ui/dihedral_dialog.py,sha256=ZDihYt32_3V_uJXsJ4u3qGxn3pASWPcl8BIY3nTBQ0Q,18103
|
|
36
36
|
moleditpy_linux/ui/edit_3d_logic.py,sha256=y-6GabTZzlLA0u3j8ugy7yHV3jA4HvWTVCAmgFEKwh4,19740
|
|
37
37
|
moleditpy_linux/ui/edit_actions_logic.py,sha256=KZZJOnAcN_wHwXX48z5j5zZPTO5v5f0hgikJXogbVvs,67671
|
|
@@ -51,7 +51,7 @@ moleditpy_linux/ui/settings_dialog.py,sha256=WTexmB8HOmdA-CjHK5M6TcS7ZrJtywyq7qM
|
|
|
51
51
|
moleditpy_linux/ui/string_importers.py,sha256=oJVLjLflvuX59Isi2OjF2P0gOOiqKwt5i9_IhPccKxE,8375
|
|
52
52
|
moleditpy_linux/ui/template_preview_item.py,sha256=X8_3dLHalDRhNmxokOgi85pMjSbFzPFVaqSkficg2PU,7306
|
|
53
53
|
moleditpy_linux/ui/template_preview_view.py,sha256=LrbZqpB3Ff0MkRw7m_YWlSpSr177An1QUvreGk-km4g,3791
|
|
54
|
-
moleditpy_linux/ui/translation_dialog.py,sha256=
|
|
54
|
+
moleditpy_linux/ui/translation_dialog.py,sha256=zEJzcx-ufuLZSozOeJGLkBU9Ei2t-K5m-bZlRBd9ySU,16700
|
|
55
55
|
moleditpy_linux/ui/ui_manager.py,sha256=P3B9pVqluw_cTivq0NgvttB_HWORk2Hx8URm9kbBUH8,27339
|
|
56
56
|
moleditpy_linux/ui/user_template_dialog.py,sha256=lH3gf5XtmOMxdMHWFWNhxh-c9vQcb4T7nHvU5uH-7C4,29829
|
|
57
57
|
moleditpy_linux/ui/view_3d_logic.py,sha256=rO6svIUu7XurU6Vj3C7avAV5EvIAdrogevbOHXgBxIE,94668
|
|
@@ -66,9 +66,9 @@ moleditpy_linux/utils/constants.py,sha256=HsOqb9sAzRNujvBQwAeTWXS7fHfho46shmnSa3
|
|
|
66
66
|
moleditpy_linux/utils/default_settings.py,sha256=OxgIrdk-LCXBdhUPF_qzsbdsiM18kDWgP3seELyUgJU,2731
|
|
67
67
|
moleditpy_linux/utils/sip_isdeleted_safe.py,sha256=My6IJqDewbYY6SoRYNk6XwFUQ9_yihaR3Ym7EOETuAw,1189
|
|
68
68
|
moleditpy_linux/utils/system_utils.py,sha256=K5c9cJRgMFqtUtLefSu3w2hy2drgxNqfT200bSIFy2k,2325
|
|
69
|
-
moleditpy_linux-3.6.
|
|
70
|
-
moleditpy_linux-3.6.
|
|
71
|
-
moleditpy_linux-3.6.
|
|
72
|
-
moleditpy_linux-3.6.
|
|
73
|
-
moleditpy_linux-3.6.
|
|
74
|
-
moleditpy_linux-3.6.
|
|
69
|
+
moleditpy_linux-3.6.4.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
70
|
+
moleditpy_linux-3.6.4.dist-info/METADATA,sha256=h5ZQVhsJ6S959BqXow8nxyGAcHfDvCKwXyRuTFeFsP8,62978
|
|
71
|
+
moleditpy_linux-3.6.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
72
|
+
moleditpy_linux-3.6.4.dist-info/entry_points.txt,sha256=-OzipSi__yVwlimNtu3eiRP5t5UMg55Cs0udyhXYiyw,60
|
|
73
|
+
moleditpy_linux-3.6.4.dist-info/top_level.txt,sha256=qyqe-hDYL6CXyin9E5Me5rVl3PG84VqiOjf9bQvfJLs,16
|
|
74
|
+
moleditpy_linux-3.6.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|