MoleditPy 1.16.3__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/__init__.py +4 -0
- moleditpy/__main__.py +29 -0
- moleditpy/main.py +37 -0
- moleditpy/modules/__init__.py +36 -0
- moleditpy/modules/about_dialog.py +92 -0
- moleditpy/modules/align_plane_dialog.py +281 -0
- moleditpy/modules/alignment_dialog.py +261 -0
- moleditpy/modules/analysis_window.py +197 -0
- moleditpy/modules/angle_dialog.py +428 -0
- moleditpy/modules/assets/icon.icns +0 -0
- moleditpy/modules/assets/icon.ico +0 -0
- moleditpy/modules/assets/icon.png +0 -0
- moleditpy/modules/atom_item.py +336 -0
- moleditpy/modules/bond_item.py +303 -0
- moleditpy/modules/bond_length_dialog.py +368 -0
- moleditpy/modules/calculation_worker.py +754 -0
- moleditpy/modules/color_settings_dialog.py +309 -0
- moleditpy/modules/constants.py +76 -0
- moleditpy/modules/constrained_optimization_dialog.py +667 -0
- moleditpy/modules/custom_interactor_style.py +737 -0
- moleditpy/modules/custom_qt_interactor.py +49 -0
- moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
- moleditpy/modules/dihedral_dialog.py +431 -0
- moleditpy/modules/main_window.py +830 -0
- moleditpy/modules/main_window_app_state.py +747 -0
- moleditpy/modules/main_window_compute.py +1203 -0
- moleditpy/modules/main_window_dialog_manager.py +454 -0
- moleditpy/modules/main_window_edit_3d.py +531 -0
- moleditpy/modules/main_window_edit_actions.py +1449 -0
- moleditpy/modules/main_window_export.py +744 -0
- moleditpy/modules/main_window_main_init.py +1668 -0
- moleditpy/modules/main_window_molecular_parsers.py +1037 -0
- moleditpy/modules/main_window_project_io.py +429 -0
- moleditpy/modules/main_window_string_importers.py +270 -0
- moleditpy/modules/main_window_ui_manager.py +567 -0
- moleditpy/modules/main_window_view_3d.py +1211 -0
- moleditpy/modules/main_window_view_loaders.py +350 -0
- moleditpy/modules/mirror_dialog.py +110 -0
- moleditpy/modules/molecular_data.py +290 -0
- moleditpy/modules/molecule_scene.py +1964 -0
- moleditpy/modules/move_group_dialog.py +586 -0
- moleditpy/modules/periodic_table_dialog.py +72 -0
- moleditpy/modules/planarize_dialog.py +209 -0
- moleditpy/modules/settings_dialog.py +1071 -0
- moleditpy/modules/template_preview_item.py +148 -0
- moleditpy/modules/template_preview_view.py +62 -0
- moleditpy/modules/translation_dialog.py +353 -0
- moleditpy/modules/user_template_dialog.py +621 -0
- moleditpy/modules/zoomable_view.py +98 -0
- moleditpy-1.16.3.dist-info/METADATA +274 -0
- moleditpy-1.16.3.dist-info/RECORD +54 -0
- moleditpy-1.16.3.dist-info/WHEEL +5 -0
- moleditpy-1.16.3.dist-info/entry_points.txt +2 -0
- moleditpy-1.16.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
from PyQt6.QtWidgets import (
|
|
2
|
+
QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QMessageBox
|
|
3
|
+
)
|
|
4
|
+
from PyQt6.QtCore import Qt
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from .dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
9
|
+
except Exception:
|
|
10
|
+
from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
|
|
11
|
+
|
|
12
|
+
class PlanarizeDialog(Dialog3DPickingMixin, QDialog):
|
|
13
|
+
|
|
14
|
+
"""選択原子群を最適フィット平面へ投影して planarize するダイアログ
|
|
15
|
+
AlignPlane を参考にした選択UIを持ち、Apply ボタンで選択原子を平面へ直交射影する。
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, mol, main_window, preselected_atoms=None, parent=None):
|
|
18
|
+
QDialog.__init__(self, parent)
|
|
19
|
+
Dialog3DPickingMixin.__init__(self)
|
|
20
|
+
self.mol = mol
|
|
21
|
+
self.main_window = main_window
|
|
22
|
+
self.selected_atoms = set()
|
|
23
|
+
|
|
24
|
+
if preselected_atoms:
|
|
25
|
+
# 事前選択された原子を追加
|
|
26
|
+
self.selected_atoms.update(preselected_atoms)
|
|
27
|
+
|
|
28
|
+
self.init_ui()
|
|
29
|
+
|
|
30
|
+
if self.selected_atoms:
|
|
31
|
+
self.show_atom_labels()
|
|
32
|
+
self.update_display()
|
|
33
|
+
|
|
34
|
+
def init_ui(self):
|
|
35
|
+
self.setWindowTitle("Planarize")
|
|
36
|
+
self.setModal(False)
|
|
37
|
+
self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
|
|
38
|
+
layout = QVBoxLayout(self)
|
|
39
|
+
|
|
40
|
+
instruction_label = QLabel("Click atoms in the 3D view to select them for planarization (minimum 3 required).")
|
|
41
|
+
instruction_label.setWordWrap(True)
|
|
42
|
+
layout.addWidget(instruction_label)
|
|
43
|
+
|
|
44
|
+
self.selection_label = QLabel("No atoms selected")
|
|
45
|
+
layout.addWidget(self.selection_label)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
button_layout = QHBoxLayout()
|
|
49
|
+
self.clear_button = QPushButton("Clear Selection")
|
|
50
|
+
self.clear_button.clicked.connect(self.clear_selection)
|
|
51
|
+
button_layout.addWidget(self.clear_button)
|
|
52
|
+
|
|
53
|
+
# Select All Atoms ボタンを追加
|
|
54
|
+
self.select_all_button = QPushButton("Select All Atoms")
|
|
55
|
+
self.select_all_button.setToolTip("Select all atoms in the molecule for planarization")
|
|
56
|
+
self.select_all_button.clicked.connect(self.select_all_atoms)
|
|
57
|
+
button_layout.addWidget(self.select_all_button)
|
|
58
|
+
|
|
59
|
+
self.apply_button = QPushButton("Apply planarize")
|
|
60
|
+
self.apply_button.clicked.connect(self.apply_planarize)
|
|
61
|
+
self.apply_button.setEnabled(False)
|
|
62
|
+
button_layout.addWidget(self.apply_button)
|
|
63
|
+
|
|
64
|
+
close_button = QPushButton("Close")
|
|
65
|
+
close_button.clicked.connect(self.reject)
|
|
66
|
+
button_layout.addWidget(close_button)
|
|
67
|
+
|
|
68
|
+
button_layout.addStretch()
|
|
69
|
+
|
|
70
|
+
layout.addLayout(button_layout)
|
|
71
|
+
|
|
72
|
+
# enable picking
|
|
73
|
+
self.picker_connection = None
|
|
74
|
+
self.enable_picking()
|
|
75
|
+
|
|
76
|
+
def on_atom_picked(self, atom_idx):
|
|
77
|
+
if atom_idx in self.selected_atoms:
|
|
78
|
+
self.selected_atoms.remove(atom_idx)
|
|
79
|
+
else:
|
|
80
|
+
self.selected_atoms.add(atom_idx)
|
|
81
|
+
self.show_atom_labels()
|
|
82
|
+
self.update_display()
|
|
83
|
+
|
|
84
|
+
def clear_selection(self):
|
|
85
|
+
self.selected_atoms.clear()
|
|
86
|
+
self.clear_atom_labels()
|
|
87
|
+
self.update_display()
|
|
88
|
+
|
|
89
|
+
def update_display(self):
|
|
90
|
+
count = len(self.selected_atoms)
|
|
91
|
+
if count == 0:
|
|
92
|
+
self.selection_label.setText("Click atoms to select for planarize (minimum 3 required)")
|
|
93
|
+
self.apply_button.setEnabled(False)
|
|
94
|
+
else:
|
|
95
|
+
atom_list = sorted(self.selected_atoms)
|
|
96
|
+
atom_display = []
|
|
97
|
+
for i, atom_idx in enumerate(atom_list):
|
|
98
|
+
symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
|
|
99
|
+
atom_display.append(f"#{i+1}: {symbol}({atom_idx})")
|
|
100
|
+
self.selection_label.setText(f"Selected {count} atoms: {', '.join(atom_display)}")
|
|
101
|
+
self.apply_button.setEnabled(count >= 3)
|
|
102
|
+
|
|
103
|
+
def select_all_atoms(self):
|
|
104
|
+
"""Select all atoms in the current molecule (or fallback) and update labels/UI."""
|
|
105
|
+
try:
|
|
106
|
+
# Prefer RDKit molecule if available
|
|
107
|
+
if hasattr(self, 'mol') and self.mol is not None:
|
|
108
|
+
try:
|
|
109
|
+
n = self.mol.GetNumAtoms()
|
|
110
|
+
# create a set of indices [0..n-1]
|
|
111
|
+
self.selected_atoms = set(range(n))
|
|
112
|
+
except Exception:
|
|
113
|
+
# fallback to main_window data map
|
|
114
|
+
self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
|
|
115
|
+
else:
|
|
116
|
+
# fallback to main_window data map
|
|
117
|
+
self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
|
|
118
|
+
|
|
119
|
+
# Update labels and display
|
|
120
|
+
self.show_atom_labels()
|
|
121
|
+
self.update_display()
|
|
122
|
+
|
|
123
|
+
except Exception as e:
|
|
124
|
+
QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
|
|
125
|
+
|
|
126
|
+
def show_atom_labels(self):
|
|
127
|
+
self.clear_atom_labels()
|
|
128
|
+
if not hasattr(self, 'selection_labels'):
|
|
129
|
+
self.selection_labels = []
|
|
130
|
+
if self.selected_atoms:
|
|
131
|
+
for i, atom_idx in enumerate(sorted(self.selected_atoms)):
|
|
132
|
+
pos = self.main_window.atom_positions_3d[atom_idx]
|
|
133
|
+
label_text = f"#{i+1}"
|
|
134
|
+
label_actor = self.main_window.plotter.add_point_labels(
|
|
135
|
+
[pos], [label_text],
|
|
136
|
+
point_size=20,
|
|
137
|
+
font_size=12,
|
|
138
|
+
text_color='cyan',
|
|
139
|
+
always_visible=True
|
|
140
|
+
)
|
|
141
|
+
self.selection_labels.append(label_actor)
|
|
142
|
+
|
|
143
|
+
def clear_atom_labels(self):
|
|
144
|
+
if hasattr(self, 'selection_labels'):
|
|
145
|
+
for label_actor in self.selection_labels:
|
|
146
|
+
try:
|
|
147
|
+
self.main_window.plotter.remove_actor(label_actor)
|
|
148
|
+
except:
|
|
149
|
+
pass
|
|
150
|
+
self.selection_labels = []
|
|
151
|
+
|
|
152
|
+
def apply_planarize(self):
|
|
153
|
+
if not self.selected_atoms or len(self.selected_atoms) < 3:
|
|
154
|
+
QMessageBox.warning(self, "Warning", "Please select at least 3 atoms for planarize.")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
selected_indices = list(sorted(self.selected_atoms))
|
|
159
|
+
selected_positions = self.main_window.atom_positions_3d[selected_indices].copy()
|
|
160
|
+
|
|
161
|
+
centroid = np.mean(selected_positions, axis=0)
|
|
162
|
+
centered_positions = selected_positions - centroid
|
|
163
|
+
|
|
164
|
+
# SVDによる最小二乗平面の法線取得
|
|
165
|
+
u, s, vh = np.linalg.svd(centered_positions, full_matrices=False)
|
|
166
|
+
normal = vh[-1]
|
|
167
|
+
norm = np.linalg.norm(normal)
|
|
168
|
+
if norm == 0:
|
|
169
|
+
QMessageBox.warning(self, "Warning", "Cannot determine fit plane (degenerate positions).")
|
|
170
|
+
return
|
|
171
|
+
normal = normal / norm
|
|
172
|
+
|
|
173
|
+
# 各点を重心を通る平面へ直交射影
|
|
174
|
+
projections = centered_positions - np.outer(np.dot(centered_positions, normal), normal)
|
|
175
|
+
new_positions = projections + centroid
|
|
176
|
+
|
|
177
|
+
# 分子座標を更新
|
|
178
|
+
conf = self.mol.GetConformer()
|
|
179
|
+
for i, new_pos in zip(selected_indices, new_positions):
|
|
180
|
+
conf.SetAtomPosition(int(i), new_pos.tolist())
|
|
181
|
+
self.main_window.atom_positions_3d[int(i)] = new_pos
|
|
182
|
+
|
|
183
|
+
# 3Dビュー更新
|
|
184
|
+
self.main_window.draw_molecule_3d(self.mol)
|
|
185
|
+
self.main_window.update_chiral_labels()
|
|
186
|
+
self.main_window.push_undo_state()
|
|
187
|
+
|
|
188
|
+
QMessageBox.information(self, "Success", f"Planarized {len(selected_indices)} atoms to best-fit plane.")
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
QMessageBox.critical(self, "Error", f"Failed to planarize: {e}")
|
|
192
|
+
|
|
193
|
+
def closeEvent(self, event):
|
|
194
|
+
"""ダイアログが閉じられる時の処理"""
|
|
195
|
+
self.clear_atom_labels()
|
|
196
|
+
self.disable_picking()
|
|
197
|
+
super().closeEvent(event)
|
|
198
|
+
|
|
199
|
+
def reject(self):
|
|
200
|
+
"""キャンセル時の処理"""
|
|
201
|
+
self.clear_atom_labels()
|
|
202
|
+
self.disable_picking()
|
|
203
|
+
super().reject()
|
|
204
|
+
|
|
205
|
+
def accept(self):
|
|
206
|
+
"""OK時の処理"""
|
|
207
|
+
self.clear_atom_labels()
|
|
208
|
+
self.disable_picking()
|
|
209
|
+
super().accept()
|