MoleditPy-linux 3.6.2__tar.gz → 3.6.3__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.
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/PKG-INFO +1 -1
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/pyproject.toml +1 -1
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/align_plane_dialog.py +100 -40
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/alignment_dialog.py +63 -24
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/base_picking_dialog.py +20 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/dialog_logic.py +18 -5
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/LICENSE +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/README.md +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/setup.cfg +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/core/molecular_data.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/about_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/app_state.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/atom_item.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/bond_item.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/compute_logic.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/edit_actions_logic.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/io_logic.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/main_window.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/main_window_init.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/ui_manager.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/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.6.
|
|
3
|
+
Version: 3.6.3
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 3.6.
|
|
3
|
+
Version: 3.6.3
|
|
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
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/align_plane_dialog.py
RENAMED
|
@@ -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,17 @@ 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
|
|
30
33
|
|
|
31
34
|
if TYPE_CHECKING:
|
|
32
35
|
from .main_window import MainWindow
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
class AlignPlaneDialog(BasePickingDialog):
|
|
39
|
+
"""Dialog for aligning selected atoms to a principal plane (XY, XZ, or YZ)."""
|
|
40
|
+
|
|
36
41
|
def __init__(
|
|
37
42
|
self,
|
|
38
43
|
mol: Chem.Mol,
|
|
@@ -43,20 +48,31 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
43
48
|
) -> None:
|
|
44
49
|
super().__init__(mol, main_window, parent)
|
|
45
50
|
self.plane = plane
|
|
46
|
-
self.
|
|
51
|
+
self._selected_atoms = SelectionList()
|
|
47
52
|
|
|
48
53
|
# Add preselected atoms
|
|
49
54
|
if preselected_atoms:
|
|
50
|
-
self.
|
|
55
|
+
self._selected_atoms.update(preselected_atoms)
|
|
51
56
|
|
|
52
57
|
self.init_ui()
|
|
53
58
|
|
|
54
59
|
# Add labels to preselected atoms
|
|
55
|
-
if self.
|
|
60
|
+
if self._selected_atoms:
|
|
56
61
|
self.show_atom_labels()
|
|
57
62
|
self.update_display()
|
|
58
63
|
|
|
64
|
+
@property
|
|
65
|
+
def selected_atoms(self) -> SelectionList:
|
|
66
|
+
"""Return the ordered list of selected atom indices."""
|
|
67
|
+
return self._selected_atoms
|
|
68
|
+
|
|
69
|
+
@selected_atoms.setter
|
|
70
|
+
def selected_atoms(self, val: object) -> None:
|
|
71
|
+
"""Replace the selection with a new SelectionList built from val."""
|
|
72
|
+
self._selected_atoms = SelectionList(val) # type: ignore[arg-type]
|
|
73
|
+
|
|
59
74
|
def init_ui(self) -> None:
|
|
75
|
+
"""Build and lay out all widgets for the plane-alignment dialog."""
|
|
60
76
|
plane_names = {"xy": "XY", "xz": "XZ", "yz": "YZ"}
|
|
61
77
|
self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
|
|
62
78
|
self.setModal(False)
|
|
@@ -64,11 +80,18 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
64
80
|
|
|
65
81
|
# Instructions
|
|
66
82
|
instruction_label = QLabel(
|
|
67
|
-
f"Click atoms in the 3D view to select them for align to
|
|
83
|
+
f"Click atoms in the 3D view to select them for align to "
|
|
84
|
+
f"the {plane_names[self.plane]} plane. At least 3 atoms "
|
|
85
|
+
f"are required."
|
|
68
86
|
)
|
|
69
87
|
instruction_label.setWordWrap(True)
|
|
70
88
|
layout.addWidget(instruction_label)
|
|
71
89
|
|
|
90
|
+
# Move to zero plane option (default False)
|
|
91
|
+
self.move_to_zero_plane_checkbox = QCheckBox("Move the plane to the zero plane")
|
|
92
|
+
self.move_to_zero_plane_checkbox.setChecked(False)
|
|
93
|
+
layout.addWidget(self.move_to_zero_plane_checkbox)
|
|
94
|
+
|
|
72
95
|
# Selected atoms display
|
|
73
96
|
self.selection_label = QLabel("No atoms selected")
|
|
74
97
|
layout.addWidget(self.selection_label)
|
|
@@ -106,11 +129,12 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
106
129
|
|
|
107
130
|
def on_atom_picked(self, atom_idx: int) -> None:
|
|
108
131
|
"""Handle the event when an atom is picked in the 3D view."""
|
|
109
|
-
if atom_idx in self.
|
|
110
|
-
self.
|
|
132
|
+
if atom_idx in self._selected_atoms:
|
|
133
|
+
self._selected_atoms.remove(atom_idx)
|
|
111
134
|
else:
|
|
112
|
-
self.
|
|
135
|
+
self._selected_atoms.append(atom_idx)
|
|
113
136
|
|
|
137
|
+
self.show_atom_labels()
|
|
114
138
|
self.update_display()
|
|
115
139
|
|
|
116
140
|
def clear_selection(self) -> None:
|
|
@@ -139,6 +163,7 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
139
163
|
self.update_display()
|
|
140
164
|
|
|
141
165
|
except (AttributeError, RuntimeError, TypeError, KeyError) as e:
|
|
166
|
+
logging.exception("Failed to select all atoms")
|
|
142
167
|
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
143
168
|
|
|
144
169
|
def update_display(self) -> None:
|
|
@@ -150,8 +175,15 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
150
175
|
)
|
|
151
176
|
self.apply_button.setEnabled(False)
|
|
152
177
|
else:
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
atom_list = sorted(self.selected_atoms)
|
|
179
|
+
atom_display = []
|
|
180
|
+
for i, atom_idx in enumerate(atom_list):
|
|
181
|
+
symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
182
|
+
atom_display.append(f"#{i + 1}: {symbol}({atom_idx})")
|
|
183
|
+
|
|
184
|
+
self.selection_label.setText(
|
|
185
|
+
f"Selected {count} atoms: {', '.join(atom_display)}"
|
|
186
|
+
)
|
|
155
187
|
self.apply_button.setEnabled(count >= 3)
|
|
156
188
|
|
|
157
189
|
def show_atom_labels(self) -> None:
|
|
@@ -159,7 +191,7 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
159
191
|
if self.selected_atoms:
|
|
160
192
|
sorted_atoms = sorted(self.selected_atoms)
|
|
161
193
|
pairs = [(idx, f"#{i + 1}") for i, idx in enumerate(sorted_atoms)]
|
|
162
|
-
self.show_atom_labels_for(pairs, color="
|
|
194
|
+
self.show_atom_labels_for(pairs, color="yellow")
|
|
163
195
|
else:
|
|
164
196
|
self.clear_atom_labels()
|
|
165
197
|
|
|
@@ -167,7 +199,9 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
167
199
|
"""Apply plane alignment (rotation-based)."""
|
|
168
200
|
if len(self.selected_atoms) < 3:
|
|
169
201
|
QMessageBox.warning(
|
|
170
|
-
self,
|
|
202
|
+
self,
|
|
203
|
+
"Warning",
|
|
204
|
+
"Please select at least 3 atoms for align.",
|
|
171
205
|
)
|
|
172
206
|
return
|
|
173
207
|
try:
|
|
@@ -197,7 +231,8 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
197
231
|
elif self.plane == "yz":
|
|
198
232
|
target_normal = np.array([1, 0, 0]) # X-axis direction
|
|
199
233
|
else:
|
|
200
|
-
|
|
234
|
+
# Default to Z-axis (XY plane)
|
|
235
|
+
target_normal = np.array([0, 0, 1])
|
|
201
236
|
|
|
202
237
|
# Adjust normal vector direction
|
|
203
238
|
if np.dot(normal_vector, target_normal) < 0:
|
|
@@ -207,36 +242,61 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
207
242
|
rotation_axis = np.cross(normal_vector, target_normal)
|
|
208
243
|
rotation_axis_norm = np.linalg.norm(rotation_axis)
|
|
209
244
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
#
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
sin_a = np.sin(angle)
|
|
222
|
-
return ( # type: ignore[no-any-return]
|
|
223
|
-
v * cos_a
|
|
224
|
-
+ np.cross(axis, v) * sin_a
|
|
225
|
-
+ axis * np.dot(axis, v) * (1 - cos_a)
|
|
226
|
-
)
|
|
245
|
+
# Rodrigues' rotation formula
|
|
246
|
+
def rodrigues_rotation(
|
|
247
|
+
v: np.ndarray, axis: np.ndarray, angle: float
|
|
248
|
+
) -> np.ndarray:
|
|
249
|
+
cos_a = np.cos(angle)
|
|
250
|
+
sin_a = np.sin(angle)
|
|
251
|
+
return ( # type: ignore[no-any-return]
|
|
252
|
+
v * cos_a
|
|
253
|
+
+ np.cross(axis, v) * sin_a
|
|
254
|
+
+ axis * np.dot(axis, v) * (1 - cos_a)
|
|
255
|
+
)
|
|
227
256
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
257
|
+
# Calculate new positions (rotated, centered back by default)
|
|
258
|
+
conf = self.mol.GetConformer()
|
|
259
|
+
new_positions = np.empty_like(positions)
|
|
260
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
261
|
+
current_pos = np.array(conf.GetAtomPosition(i))
|
|
262
|
+
centered_pos = current_pos - centroid
|
|
263
|
+
if rotation_axis_norm > 1e-10:
|
|
264
|
+
rot_norm = rotation_axis_norm
|
|
265
|
+
rotation_axis_normalized = rotation_axis / rot_norm
|
|
266
|
+
cos_angle = np.dot(normal_vector, target_normal)
|
|
267
|
+
cos_angle = np.clip(cos_angle, -1.0, 1.0)
|
|
268
|
+
rotation_angle = np.arccos(cos_angle)
|
|
233
269
|
rotated_pos = rodrigues_rotation(
|
|
234
|
-
centered_pos,
|
|
270
|
+
centered_pos,
|
|
271
|
+
rotation_axis_normalized,
|
|
272
|
+
rotation_angle,
|
|
235
273
|
)
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
else:
|
|
275
|
+
rotated_pos = centered_pos
|
|
276
|
+
new_pos = rotated_pos + centroid
|
|
277
|
+
new_positions[i] = new_pos
|
|
278
|
+
|
|
279
|
+
# If move_to_zero_plane is True, translate so the plane
|
|
280
|
+
# of selected atoms is at zero
|
|
281
|
+
if self.move_to_zero_plane_checkbox.isChecked():
|
|
282
|
+
selected_new_positions = new_positions[selected_indices]
|
|
283
|
+
new_centroid = np.mean(selected_new_positions, axis=0)
|
|
284
|
+
translation_offset = np.zeros(3)
|
|
285
|
+
if self.plane == "xy":
|
|
286
|
+
translation_offset[2] = new_centroid[2]
|
|
287
|
+
elif self.plane == "xz":
|
|
288
|
+
translation_offset[1] = new_centroid[1]
|
|
289
|
+
elif self.plane == "yz":
|
|
290
|
+
translation_offset[0] = new_centroid[0]
|
|
291
|
+
new_positions = new_positions - translation_offset
|
|
292
|
+
|
|
293
|
+
# Update the conformer positions array in place
|
|
294
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
295
|
+
positions[i] = new_positions[i]
|
|
238
296
|
|
|
239
297
|
self._update_molecule_geometry(positions)
|
|
298
|
+
self.show_atom_labels()
|
|
240
299
|
|
|
241
300
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
301
|
+
logging.exception("Failed to apply align")
|
|
242
302
|
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,18 @@ 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
|
+
|
|
33
41
|
if TYPE_CHECKING:
|
|
34
42
|
from .main_window import MainWindow
|
|
35
43
|
|
|
36
44
|
|
|
37
45
|
class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
46
|
+
"""Dialog for aligning two selected atoms along a principal axis (X, Y, or Z)."""
|
|
47
|
+
|
|
38
48
|
def __init__(
|
|
39
49
|
self,
|
|
40
50
|
mol: Chem.Mol,
|
|
@@ -48,21 +58,32 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
48
58
|
self.mol = mol
|
|
49
59
|
self.main_window = main_window
|
|
50
60
|
self.axis = axis
|
|
51
|
-
self.
|
|
61
|
+
self._selected_atoms = SelectionList()
|
|
52
62
|
|
|
53
63
|
# Add preselected atoms (maximum 2)
|
|
54
64
|
if preselected_atoms:
|
|
55
|
-
self.
|
|
65
|
+
self._selected_atoms.update(preselected_atoms[:2])
|
|
56
66
|
|
|
57
67
|
self.init_ui()
|
|
58
68
|
|
|
59
69
|
# Add labels to preselected atoms
|
|
60
|
-
if self.
|
|
61
|
-
for i, atom_idx in enumerate(
|
|
62
|
-
self.add_selection_label(atom_idx, f"
|
|
70
|
+
if self._selected_atoms:
|
|
71
|
+
for i, atom_idx in enumerate(self._selected_atoms, 1):
|
|
72
|
+
self.add_selection_label(atom_idx, f"#{i}", color="yellow")
|
|
63
73
|
self.update_display()
|
|
64
74
|
|
|
75
|
+
@property
|
|
76
|
+
def selected_atoms(self) -> SelectionList:
|
|
77
|
+
"""Return the ordered list of selected atom indices."""
|
|
78
|
+
return self._selected_atoms
|
|
79
|
+
|
|
80
|
+
@selected_atoms.setter
|
|
81
|
+
def selected_atoms(self, val: object) -> None:
|
|
82
|
+
"""Replace the selection with a new SelectionList built from val."""
|
|
83
|
+
self._selected_atoms = SelectionList(val) # type: ignore[arg-type]
|
|
84
|
+
|
|
65
85
|
def init_ui(self) -> None:
|
|
86
|
+
"""Build and lay out all widgets for the alignment dialog."""
|
|
66
87
|
axis_names = {"x": "X-axis", "y": "Y-axis", "z": "Z-axis"}
|
|
67
88
|
self.setWindowTitle(f"Align to {axis_names[self.axis]}")
|
|
68
89
|
self.setModal(False)
|
|
@@ -70,11 +91,17 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
70
91
|
|
|
71
92
|
# Instructions
|
|
72
93
|
instruction_label = QLabel(
|
|
73
|
-
f"Click atoms in the 3D view to select them for alignment to the
|
|
94
|
+
f"Click atoms in the 3D view to select them for alignment to the "
|
|
95
|
+
f"{axis_names[self.axis]}. Exactly 2 atoms are required."
|
|
74
96
|
)
|
|
75
97
|
instruction_label.setWordWrap(True)
|
|
76
98
|
layout.addWidget(instruction_label)
|
|
77
99
|
|
|
100
|
+
# Move to origin option (default False)
|
|
101
|
+
self.move_to_origin_checkbox = QCheckBox("Move the first atom to the origin")
|
|
102
|
+
self.move_to_origin_checkbox.setChecked(False)
|
|
103
|
+
layout.addWidget(self.move_to_origin_checkbox)
|
|
104
|
+
|
|
78
105
|
# Selected atoms display
|
|
79
106
|
self.selection_label = QLabel("No atoms selected")
|
|
80
107
|
layout.addWidget(self.selection_label)
|
|
@@ -114,10 +141,10 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
114
141
|
else:
|
|
115
142
|
# Maximum of 2 atoms can be selected
|
|
116
143
|
if len(self.selected_atoms) < 2:
|
|
117
|
-
self.selected_atoms.
|
|
144
|
+
self.selected_atoms.append(atom_idx)
|
|
118
145
|
# Show label indicating selection order
|
|
119
|
-
label_text = f"
|
|
120
|
-
self.add_selection_label(atom_idx, label_text)
|
|
146
|
+
label_text = f"#{len(self.selected_atoms)}"
|
|
147
|
+
self.add_selection_label(atom_idx, label_text, color="yellow")
|
|
121
148
|
|
|
122
149
|
self.update_display()
|
|
123
150
|
|
|
@@ -129,14 +156,12 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
129
156
|
)
|
|
130
157
|
self.apply_button.setEnabled(False)
|
|
131
158
|
elif len(self.selected_atoms) == 1:
|
|
132
|
-
|
|
133
|
-
atom = self.mol.GetAtomWithIdx(selected_list[0])
|
|
159
|
+
atom = self.mol.GetAtomWithIdx(self.selected_atoms[0])
|
|
134
160
|
self.selection_label.setText(f"Selected 1 atom: {atom.GetSymbol()}")
|
|
135
161
|
self.apply_button.setEnabled(False)
|
|
136
162
|
elif len(self.selected_atoms) == 2:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
atom2 = self.mol.GetAtomWithIdx(selected_list[1])
|
|
163
|
+
atom1 = self.mol.GetAtomWithIdx(self.selected_atoms[0])
|
|
164
|
+
atom2 = self.mol.GetAtomWithIdx(self.selected_atoms[1])
|
|
140
165
|
self.selection_label.setText(
|
|
141
166
|
f"Selected 2 atoms: {atom1.GetSymbol()}, {atom2.GetSymbol()}"
|
|
142
167
|
)
|
|
@@ -148,13 +173,11 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
148
173
|
self.selected_atoms.clear()
|
|
149
174
|
self.update_display()
|
|
150
175
|
|
|
151
|
-
def remove_atom_label(self,
|
|
152
|
-
"""Remove a label for a specific atom."""
|
|
153
|
-
# Re-draw all labels for simplicity
|
|
176
|
+
def remove_atom_label(self, _atom_idx: int) -> None:
|
|
177
|
+
"""Remove a label for a specific atom (redraws all labels)."""
|
|
154
178
|
self.clear_selection_labels()
|
|
155
|
-
for i, idx in enumerate(
|
|
156
|
-
|
|
157
|
-
self.add_selection_label(idx, f"Atom {i}")
|
|
179
|
+
for i, idx in enumerate(self.selected_atoms, 1):
|
|
180
|
+
self.add_selection_label(idx, f"#{i}", color="yellow")
|
|
158
181
|
|
|
159
182
|
def apply_alignment(self) -> None:
|
|
160
183
|
"""Apply the specific axial alignment to the molecule."""
|
|
@@ -164,8 +187,8 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
164
187
|
)
|
|
165
188
|
return
|
|
166
189
|
try:
|
|
167
|
-
|
|
168
|
-
|
|
190
|
+
atom1_idx = self.selected_atoms[0]
|
|
191
|
+
atom2_idx = self.selected_atoms[1]
|
|
169
192
|
|
|
170
193
|
conf = self.mol.GetConformer()
|
|
171
194
|
|
|
@@ -235,6 +258,14 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
235
258
|
),
|
|
236
259
|
)
|
|
237
260
|
|
|
261
|
+
# If move_to_origin is False, translate back so atom1 is
|
|
262
|
+
# at its original position
|
|
263
|
+
if not self.move_to_origin_checkbox.isChecked():
|
|
264
|
+
for i in range(self.mol.GetNumAtoms()):
|
|
265
|
+
current_pos = np.array(conf.GetAtomPosition(i))
|
|
266
|
+
restored_pos = current_pos - translation
|
|
267
|
+
conf.SetAtomPosition(i, restored_pos.tolist())
|
|
268
|
+
|
|
238
269
|
# Update 3D positions
|
|
239
270
|
self.main_window.view_3d_manager.atom_positions_3d = np.array(
|
|
240
271
|
[list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())]
|
|
@@ -243,6 +274,11 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
243
274
|
# Update 3D visualization
|
|
244
275
|
self.main_window.view_3d_manager.draw_molecule_3d(self.mol)
|
|
245
276
|
|
|
277
|
+
# Restore selection labels
|
|
278
|
+
self.clear_selection_labels()
|
|
279
|
+
for i, idx in enumerate(self.selected_atoms, 1):
|
|
280
|
+
self.add_selection_label(idx, f"#{i}", color="yellow")
|
|
281
|
+
|
|
246
282
|
# Update chirality labels
|
|
247
283
|
self.main_window.view_3d_manager.update_chiral_labels()
|
|
248
284
|
|
|
@@ -250,10 +286,13 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
250
286
|
self.main_window.edit_actions_manager.push_undo_state()
|
|
251
287
|
|
|
252
288
|
QMessageBox.information(
|
|
253
|
-
self,
|
|
289
|
+
self,
|
|
290
|
+
"Success",
|
|
291
|
+
f"Alignment to {self.axis.upper()}-axis completed.",
|
|
254
292
|
)
|
|
255
293
|
|
|
256
294
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
295
|
+
logging.exception("Failed to apply alignment")
|
|
257
296
|
QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
|
|
258
297
|
|
|
259
298
|
def closeEvent(self, event: Optional[QCloseEvent]) -> None:
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/base_picking_dialog.py
RENAMED
|
@@ -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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/MoleditPy_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/plugins/plugin_manager_window.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/bond_length_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/calculation_worker.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/color_settings_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/custom_interactor_style.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/custom_qt_interactor.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/geometry_base_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/periodic_table_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/settings_tabs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/template_preview_item.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/template_preview_view.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/translation_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/ui/user_template_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/utils/default_settings.py
RENAMED
|
File without changes
|
{moleditpy_linux-3.6.2 → moleditpy_linux-3.6.3}/src/moleditpy_linux/utils/sip_isdeleted_safe.py
RENAMED
|
File without changes
|
|
File without changes
|