MoleditPy 1.15.1__py3-none-any.whl → 1.16.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. moleditpy/__init__.py +4 -0
  2. moleditpy/__main__.py +29 -19748
  3. moleditpy/main.py +37 -0
  4. moleditpy/modules/__init__.py +36 -0
  5. moleditpy/modules/about_dialog.py +92 -0
  6. moleditpy/modules/align_plane_dialog.py +281 -0
  7. moleditpy/modules/alignment_dialog.py +261 -0
  8. moleditpy/modules/analysis_window.py +197 -0
  9. moleditpy/modules/angle_dialog.py +428 -0
  10. moleditpy/modules/assets/icon.icns +0 -0
  11. moleditpy/modules/atom_item.py +336 -0
  12. moleditpy/modules/bond_item.py +303 -0
  13. moleditpy/modules/bond_length_dialog.py +368 -0
  14. moleditpy/modules/calculation_worker.py +754 -0
  15. moleditpy/modules/color_settings_dialog.py +309 -0
  16. moleditpy/modules/constants.py +76 -0
  17. moleditpy/modules/constrained_optimization_dialog.py +667 -0
  18. moleditpy/modules/custom_interactor_style.py +737 -0
  19. moleditpy/modules/custom_qt_interactor.py +49 -0
  20. moleditpy/modules/dialog3_d_picking_mixin.py +96 -0
  21. moleditpy/modules/dihedral_dialog.py +431 -0
  22. moleditpy/modules/main_window.py +830 -0
  23. moleditpy/modules/main_window_app_state.py +747 -0
  24. moleditpy/modules/main_window_compute.py +1203 -0
  25. moleditpy/modules/main_window_dialog_manager.py +454 -0
  26. moleditpy/modules/main_window_edit_3d.py +531 -0
  27. moleditpy/modules/main_window_edit_actions.py +1449 -0
  28. moleditpy/modules/main_window_export.py +744 -0
  29. moleditpy/modules/main_window_main_init.py +1641 -0
  30. moleditpy/modules/main_window_molecular_parsers.py +956 -0
  31. moleditpy/modules/main_window_project_io.py +429 -0
  32. moleditpy/modules/main_window_string_importers.py +270 -0
  33. moleditpy/modules/main_window_ui_manager.py +567 -0
  34. moleditpy/modules/main_window_view_3d.py +1163 -0
  35. moleditpy/modules/main_window_view_loaders.py +350 -0
  36. moleditpy/modules/mirror_dialog.py +110 -0
  37. moleditpy/modules/molecular_data.py +290 -0
  38. moleditpy/modules/molecule_scene.py +1895 -0
  39. moleditpy/modules/move_group_dialog.py +586 -0
  40. moleditpy/modules/periodic_table_dialog.py +72 -0
  41. moleditpy/modules/planarize_dialog.py +209 -0
  42. moleditpy/modules/settings_dialog.py +1034 -0
  43. moleditpy/modules/template_preview_item.py +148 -0
  44. moleditpy/modules/template_preview_view.py +62 -0
  45. moleditpy/modules/translation_dialog.py +353 -0
  46. moleditpy/modules/user_template_dialog.py +621 -0
  47. moleditpy/modules/zoomable_view.py +98 -0
  48. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/METADATA +1 -1
  49. moleditpy-1.16.0.dist-info/RECORD +54 -0
  50. moleditpy-1.15.1.dist-info/RECORD +0 -9
  51. /moleditpy/{assets → modules/assets}/icon.ico +0 -0
  52. /moleditpy/{assets → modules/assets}/icon.png +0 -0
  53. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/WHEEL +0 -0
  54. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/entry_points.txt +0 -0
  55. {moleditpy-1.15.1.dist-info → moleditpy-1.16.0.dist-info}/top_level.txt +0 -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()