MoleditPy-linux 2.2.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.
Files changed (58) hide show
  1. moleditpy_linux/__init__.py +17 -0
  2. moleditpy_linux/__main__.py +29 -0
  3. moleditpy_linux/main.py +37 -0
  4. moleditpy_linux/modules/__init__.py +41 -0
  5. moleditpy_linux/modules/about_dialog.py +104 -0
  6. moleditpy_linux/modules/align_plane_dialog.py +293 -0
  7. moleditpy_linux/modules/alignment_dialog.py +273 -0
  8. moleditpy_linux/modules/analysis_window.py +209 -0
  9. moleditpy_linux/modules/angle_dialog.py +440 -0
  10. moleditpy_linux/modules/assets/icon.icns +0 -0
  11. moleditpy_linux/modules/assets/icon.ico +0 -0
  12. moleditpy_linux/modules/assets/icon.png +0 -0
  13. moleditpy_linux/modules/atom_item.py +348 -0
  14. moleditpy_linux/modules/bond_item.py +406 -0
  15. moleditpy_linux/modules/bond_length_dialog.py +380 -0
  16. moleditpy_linux/modules/calculation_worker.py +766 -0
  17. moleditpy_linux/modules/color_settings_dialog.py +321 -0
  18. moleditpy_linux/modules/constants.py +88 -0
  19. moleditpy_linux/modules/constrained_optimization_dialog.py +679 -0
  20. moleditpy_linux/modules/custom_interactor_style.py +749 -0
  21. moleditpy_linux/modules/custom_qt_interactor.py +59 -0
  22. moleditpy_linux/modules/dialog3_d_picking_mixin.py +108 -0
  23. moleditpy_linux/modules/dihedral_dialog.py +443 -0
  24. moleditpy_linux/modules/main_window.py +842 -0
  25. moleditpy_linux/modules/main_window_app_state.py +780 -0
  26. moleditpy_linux/modules/main_window_compute.py +1242 -0
  27. moleditpy_linux/modules/main_window_dialog_manager.py +460 -0
  28. moleditpy_linux/modules/main_window_edit_3d.py +536 -0
  29. moleditpy_linux/modules/main_window_edit_actions.py +1455 -0
  30. moleditpy_linux/modules/main_window_export.py +806 -0
  31. moleditpy_linux/modules/main_window_main_init.py +2006 -0
  32. moleditpy_linux/modules/main_window_molecular_parsers.py +1044 -0
  33. moleditpy_linux/modules/main_window_project_io.py +434 -0
  34. moleditpy_linux/modules/main_window_string_importers.py +275 -0
  35. moleditpy_linux/modules/main_window_ui_manager.py +606 -0
  36. moleditpy_linux/modules/main_window_view_3d.py +1531 -0
  37. moleditpy_linux/modules/main_window_view_loaders.py +355 -0
  38. moleditpy_linux/modules/mirror_dialog.py +122 -0
  39. moleditpy_linux/modules/molecular_data.py +302 -0
  40. moleditpy_linux/modules/molecule_scene.py +2000 -0
  41. moleditpy_linux/modules/move_group_dialog.py +598 -0
  42. moleditpy_linux/modules/periodic_table_dialog.py +84 -0
  43. moleditpy_linux/modules/planarize_dialog.py +221 -0
  44. moleditpy_linux/modules/plugin_interface.py +195 -0
  45. moleditpy_linux/modules/plugin_manager.py +309 -0
  46. moleditpy_linux/modules/plugin_manager_window.py +221 -0
  47. moleditpy_linux/modules/settings_dialog.py +1149 -0
  48. moleditpy_linux/modules/template_preview_item.py +157 -0
  49. moleditpy_linux/modules/template_preview_view.py +74 -0
  50. moleditpy_linux/modules/translation_dialog.py +365 -0
  51. moleditpy_linux/modules/user_template_dialog.py +692 -0
  52. moleditpy_linux/modules/zoomable_view.py +129 -0
  53. moleditpy_linux-2.2.4.dist-info/METADATA +936 -0
  54. moleditpy_linux-2.2.4.dist-info/RECORD +58 -0
  55. moleditpy_linux-2.2.4.dist-info/WHEEL +5 -0
  56. moleditpy_linux-2.2.4.dist-info/entry_points.txt +2 -0
  57. moleditpy_linux-2.2.4.dist-info/licenses/LICENSE +674 -0
  58. moleditpy_linux-2.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,598 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from PyQt6.QtWidgets import (
14
+ QDialog, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout, QPushButton, QLineEdit, QMessageBox
15
+ )
16
+ from PyQt6.QtCore import Qt, QEvent
17
+ import numpy as np
18
+ import pyvista as pv
19
+ from rdkit import Chem
20
+ try:
21
+ from .constants import VDW_RADII
22
+ except Exception:
23
+ from modules.constants import VDW_RADII
24
+
25
+ try:
26
+ from .dialog3_d_picking_mixin import Dialog3DPickingMixin
27
+ except Exception:
28
+ from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
29
+
30
+ class MoveGroupDialog(Dialog3DPickingMixin, QDialog):
31
+ """結合している分子グループを選択して並行移動・回転するダイアログ"""
32
+
33
+ def __init__(self, mol, main_window, parent=None):
34
+ QDialog.__init__(self, parent)
35
+ Dialog3DPickingMixin.__init__(self)
36
+ self.mol = mol
37
+ self.main_window = main_window
38
+ self.selected_atoms = set()
39
+ self.group_atoms = set() # 選択原子に結合している全原子
40
+ self.init_ui()
41
+
42
+ def init_ui(self):
43
+ self.setWindowTitle("Move Group")
44
+ self.setModal(False)
45
+ self.setWindowFlags(Qt.WindowType.Window | Qt.WindowType.WindowStaysOnTopHint)
46
+ self.resize(300,400) # ウィンドウサイズを設定
47
+ layout = QVBoxLayout(self)
48
+
49
+ # ドラッグ状態管理
50
+ self.is_dragging_group = False
51
+ self.drag_start_pos = None
52
+ self.mouse_moved_during_drag = False # ドラッグ中にマウスが動いたかを追跡
53
+
54
+ # Instructions
55
+ instruction_label = QLabel("Click an atom in the 3D view to select its connected molecule group.\n"
56
+ "Left-drag: Move the group\n"
57
+ "Right-drag: Rotate the group around its center")
58
+ instruction_label.setWordWrap(True)
59
+ layout.addWidget(instruction_label)
60
+
61
+ # Selected group display
62
+ self.selection_label = QLabel("No group selected")
63
+ layout.addWidget(self.selection_label)
64
+
65
+ # Translation controls
66
+ trans_group = QLabel("Translation (Å):")
67
+ trans_group.setStyleSheet("font-weight: bold;")
68
+ layout.addWidget(trans_group)
69
+
70
+ trans_layout = QGridLayout()
71
+ self.x_trans_input = QLineEdit("0.0")
72
+ self.y_trans_input = QLineEdit("0.0")
73
+ self.z_trans_input = QLineEdit("0.0")
74
+
75
+ # Enterキーでapply_translationを実行
76
+ self.x_trans_input.returnPressed.connect(self.apply_translation)
77
+ self.y_trans_input.returnPressed.connect(self.apply_translation)
78
+ self.z_trans_input.returnPressed.connect(self.apply_translation)
79
+
80
+ trans_layout.addWidget(QLabel("X:"), 0, 0)
81
+ trans_layout.addWidget(self.x_trans_input, 0, 1)
82
+ trans_layout.addWidget(QLabel("Y:"), 1, 0)
83
+ trans_layout.addWidget(self.y_trans_input, 1, 1)
84
+ trans_layout.addWidget(QLabel("Z:"), 2, 0)
85
+ trans_layout.addWidget(self.z_trans_input, 2, 1)
86
+
87
+ trans_button_layout = QHBoxLayout()
88
+ reset_trans_button = QPushButton("Reset")
89
+ reset_trans_button.clicked.connect(self.reset_translation_inputs)
90
+ trans_button_layout.addWidget(reset_trans_button)
91
+
92
+ apply_trans_button = QPushButton("Apply Translation")
93
+ apply_trans_button.clicked.connect(self.apply_translation)
94
+ trans_button_layout.addWidget(apply_trans_button)
95
+
96
+ trans_layout.addLayout(trans_button_layout, 3, 0, 1, 2)
97
+
98
+ layout.addLayout(trans_layout)
99
+
100
+ layout.addSpacing(10)
101
+
102
+ # Rotation controls
103
+ rot_group = QLabel("Rotation (degrees):")
104
+ rot_group.setStyleSheet("font-weight: bold;")
105
+ layout.addWidget(rot_group)
106
+
107
+ rot_layout = QGridLayout()
108
+ self.x_rot_input = QLineEdit("0.0")
109
+ self.y_rot_input = QLineEdit("0.0")
110
+ self.z_rot_input = QLineEdit("0.0")
111
+
112
+ # Enterキーでapply_rotationを実行
113
+ self.x_rot_input.returnPressed.connect(self.apply_rotation)
114
+ self.y_rot_input.returnPressed.connect(self.apply_rotation)
115
+ self.z_rot_input.returnPressed.connect(self.apply_rotation)
116
+
117
+ rot_layout.addWidget(QLabel("Around X:"), 0, 0)
118
+ rot_layout.addWidget(self.x_rot_input, 0, 1)
119
+ rot_layout.addWidget(QLabel("Around Y:"), 1, 0)
120
+ rot_layout.addWidget(self.y_rot_input, 1, 1)
121
+ rot_layout.addWidget(QLabel("Around Z:"), 2, 0)
122
+ rot_layout.addWidget(self.z_rot_input, 2, 1)
123
+
124
+ rot_button_layout = QHBoxLayout()
125
+ reset_rot_button = QPushButton("Reset")
126
+ reset_rot_button.clicked.connect(self.reset_rotation_inputs)
127
+ rot_button_layout.addWidget(reset_rot_button)
128
+
129
+ apply_rot_button = QPushButton("Apply Rotation")
130
+ apply_rot_button.clicked.connect(self.apply_rotation)
131
+ rot_button_layout.addWidget(apply_rot_button)
132
+
133
+ rot_layout.addLayout(rot_button_layout, 3, 0, 1, 2)
134
+
135
+ layout.addLayout(rot_layout)
136
+
137
+ # Buttons
138
+ button_layout = QHBoxLayout()
139
+ self.clear_button = QPushButton("Clear Selection")
140
+ self.clear_button.clicked.connect(self.clear_selection)
141
+ button_layout.addWidget(self.clear_button)
142
+
143
+ button_layout.addStretch()
144
+
145
+ close_button = QPushButton("Close")
146
+ close_button.clicked.connect(self.reject)
147
+ button_layout.addWidget(close_button)
148
+
149
+ layout.addLayout(button_layout)
150
+
151
+ # Enable picking to handle atom selection
152
+ self.enable_picking()
153
+
154
+ def eventFilter(self, obj, event):
155
+ """3Dビューでのマウスイベント処理 - グループが選択されている場合はCustomInteractorStyleに任せる"""
156
+ if obj == self.main_window.plotter.interactor:
157
+ # ダブルクリック/トリプルクリックで状態が混乱するのを防ぐ
158
+ if event.type() == QEvent.Type.MouseButtonDblClick:
159
+ # ダブルクリックは無視し、状態をリセット
160
+ self.is_dragging_group = False
161
+ self.drag_start_pos = None
162
+ self.mouse_moved_during_drag = False
163
+ self.potential_drag = False
164
+ if hasattr(self, 'clicked_atom_for_toggle'):
165
+ delattr(self, 'clicked_atom_for_toggle')
166
+ return False
167
+
168
+ if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
169
+ # 前回の状態をクリーンアップ(トリプルクリック対策)
170
+ self.is_dragging_group = False
171
+ self.potential_drag = False
172
+ if hasattr(self, 'clicked_atom_for_toggle'):
173
+ delattr(self, 'clicked_atom_for_toggle')
174
+ # グループが既に選択されている場合は、CustomInteractorStyleに処理を任せる
175
+ if self.group_atoms:
176
+ return False
177
+
178
+ # マウスプレス時の処理
179
+ # マウスプレス時の処理
180
+ try:
181
+ interactor = self.main_window.plotter.interactor
182
+ click_pos = interactor.GetEventPosition()
183
+
184
+ # まずピッキングしてどの原子がクリックされたか確認
185
+ picker = self.main_window.plotter.picker
186
+ picker.Pick(click_pos[0], click_pos[1], 0, self.main_window.plotter.renderer)
187
+
188
+ clicked_atom_idx = None
189
+ if picker.GetActor() is self.main_window.atom_actor:
190
+ picked_position = np.array(picker.GetPickPosition())
191
+ distances = np.linalg.norm(self.main_window.atom_positions_3d - picked_position, axis=1)
192
+ closest_atom_idx = np.argmin(distances)
193
+
194
+ # 閾値チェック
195
+ if 0 <= closest_atom_idx < self.mol.GetNumAtoms():
196
+ atom = self.mol.GetAtomWithIdx(int(closest_atom_idx))
197
+ if atom:
198
+ atomic_num = atom.GetAtomicNum()
199
+ pt = Chem.GetPeriodicTable()
200
+ vdw_radius = pt.GetRvdw(atomic_num)
201
+ click_threshold = vdw_radius * 1.5
202
+
203
+ if distances[closest_atom_idx] < click_threshold:
204
+ clicked_atom_idx = int(closest_atom_idx)
205
+
206
+
207
+ # クリックされた原子の処理
208
+ if clicked_atom_idx is not None:
209
+ if self.group_atoms and clicked_atom_idx in self.group_atoms:
210
+ # 既存のグループ内の原子 - ドラッグ準備(まだドラッグとは確定しない)
211
+ self.is_dragging_group = False # まだドラッグ中ではない
212
+ self.drag_start_pos = click_pos
213
+ self.drag_atom_idx = clicked_atom_idx
214
+ self.mouse_moved_during_drag = False
215
+ self.potential_drag = True # ドラッグの可能性がある
216
+ self.clicked_atom_for_toggle = clicked_atom_idx # トグル用に保存
217
+ # イベントを消費せず、カメラ操作を許可(閾値超えたらドラッグ開始)
218
+ return False
219
+ else:
220
+ # グループ外の原子 - 新しいグループを選択
221
+ # 親クラス(Mixin)のon_atom_pickedを手動で呼ぶ
222
+ self.on_atom_picked(clicked_atom_idx)
223
+ return True
224
+ else:
225
+ # 原子以外をクリック
226
+ # グループがあっても通常のカメラ操作を許可
227
+ return False
228
+
229
+ except Exception as e:
230
+ print(f"Error in mouse press: {e}")
231
+ return False
232
+
233
+ elif event.type() == QEvent.Type.MouseMove:
234
+ # マウス移動時の処理
235
+ if getattr(self, 'potential_drag', False) and self.drag_start_pos and not self.is_dragging_group:
236
+ # potential_drag状態:閾値チェック
237
+ try:
238
+ interactor = self.main_window.plotter.interactor
239
+ current_pos = interactor.GetEventPosition()
240
+ dx = current_pos[0] - self.drag_start_pos[0]
241
+ dy = current_pos[1] - self.drag_start_pos[1]
242
+
243
+ # 閾値を超えたらドラッグ開始
244
+ drag_threshold = 5 # ピクセル
245
+ if abs(dx) > drag_threshold or abs(dy) > drag_threshold:
246
+ # ドラッグ開始を確定
247
+ self.is_dragging_group = True
248
+ self.potential_drag = False
249
+ try:
250
+ self.main_window.plotter.setCursor(Qt.CursorShape.ClosedHandCursor)
251
+ except Exception:
252
+ pass
253
+ except Exception:
254
+ pass
255
+
256
+ # 閾値以下の場合はカメラ操作を許可
257
+ if not self.is_dragging_group:
258
+ return False
259
+
260
+ if self.is_dragging_group and self.drag_start_pos:
261
+ # ドラッグモード中 - 移動距離を記録するのみ(リアルタイム更新なし)
262
+ try:
263
+ interactor = self.main_window.plotter.interactor
264
+ current_pos = interactor.GetEventPosition()
265
+
266
+ dx = current_pos[0] - self.drag_start_pos[0]
267
+ dy = current_pos[1] - self.drag_start_pos[1]
268
+
269
+ if abs(dx) > 2 or abs(dy) > 2:
270
+ self.mouse_moved_during_drag = True
271
+ except Exception:
272
+ pass
273
+
274
+ # ドラッグ中はイベントを消費してカメラ回転を防ぐ
275
+ return True
276
+
277
+ # ホバー処理(ドラッグ中でない場合)
278
+ if self.group_atoms:
279
+ try:
280
+ interactor = self.main_window.plotter.interactor
281
+ current_pos = interactor.GetEventPosition()
282
+ picker = self.main_window.plotter.picker
283
+ picker.Pick(current_pos[0], current_pos[1], 0, self.main_window.plotter.renderer)
284
+
285
+ if picker.GetActor() is self.main_window.atom_actor:
286
+ picked_position = np.array(picker.GetPickPosition())
287
+ distances = np.linalg.norm(self.main_window.atom_positions_3d - picked_position, axis=1)
288
+ closest_atom_idx = np.argmin(distances)
289
+
290
+ if closest_atom_idx in self.group_atoms:
291
+ self.main_window.plotter.setCursor(Qt.CursorShape.OpenHandCursor)
292
+ else:
293
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
294
+ else:
295
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
296
+ except Exception:
297
+ pass
298
+
299
+ # ドラッグ中でない場合はカメラ回転を許可
300
+ return False
301
+
302
+ elif event.type() == QEvent.Type.MouseButtonRelease and event.button() == Qt.MouseButton.LeftButton:
303
+ # マウスリリース時の処理
304
+ if getattr(self, 'potential_drag', False) or (self.is_dragging_group and self.drag_start_pos):
305
+ try:
306
+ if self.is_dragging_group and self.mouse_moved_during_drag:
307
+ # ドラッグが実行された - CustomInteractorStyleに任せる(何もしない)
308
+ pass
309
+ else:
310
+ # マウスが閾値以下の移動 = 単なるクリック
311
+ # グループ内の原子をクリックした場合は選択/解除をトグル
312
+ if hasattr(self, 'clicked_atom_for_toggle'):
313
+ clicked_atom = self.clicked_atom_for_toggle
314
+ delattr(self, 'clicked_atom_for_toggle')
315
+ # ドラッグ状態をリセットしてからトグル処理
316
+ self.is_dragging_group = False
317
+ self.drag_start_pos = None
318
+ self.mouse_moved_during_drag = False
319
+ self.potential_drag = False
320
+ if hasattr(self, 'last_drag_positions'):
321
+ delattr(self, 'last_drag_positions')
322
+ # トグル処理を実行
323
+ self.on_atom_picked(clicked_atom)
324
+ try:
325
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
326
+ except Exception:
327
+ pass
328
+ return True
329
+
330
+ except Exception:
331
+ pass
332
+ finally:
333
+ # ドラッグ状態をリセット
334
+ self.is_dragging_group = False
335
+ self.drag_start_pos = None
336
+ self.mouse_moved_during_drag = False
337
+ self.potential_drag = False
338
+ # 保存していた位置情報をクリア
339
+ if hasattr(self, 'last_drag_positions'):
340
+ delattr(self, 'last_drag_positions')
341
+ try:
342
+ self.main_window.plotter.setCursor(Qt.CursorShape.ArrowCursor)
343
+ except Exception:
344
+ pass
345
+
346
+ return True # イベントを消費
347
+
348
+ # ドラッグ中でない場合は通常のリリース処理
349
+ return False
350
+
351
+ # その他のイベントは親クラスに渡す
352
+ return False
353
+
354
+ def on_atom_picked(self, atom_idx):
355
+ """原子がピックされたときに、その原子が属する連結成分全体を選択(複数グループ対応)"""
356
+ # ドラッグ中は選択を変更しない(ただしリリース時のトグルは許可)
357
+ if getattr(self, 'is_dragging_group', False):
358
+ return
359
+
360
+ # BFS/DFSで連結成分を探索
361
+ visited = set()
362
+ queue = [atom_idx]
363
+ visited.add(atom_idx)
364
+
365
+ while queue:
366
+ current_idx = queue.pop(0)
367
+ for bond_idx in range(self.mol.GetNumBonds()):
368
+ bond = self.mol.GetBondWithIdx(bond_idx)
369
+ begin_idx = bond.GetBeginAtomIdx()
370
+ end_idx = bond.GetEndAtomIdx()
371
+
372
+ if begin_idx == current_idx and end_idx not in visited:
373
+ visited.add(end_idx)
374
+ queue.append(end_idx)
375
+ elif end_idx == current_idx and begin_idx not in visited:
376
+ visited.add(begin_idx)
377
+ queue.append(begin_idx)
378
+
379
+ # 新しいグループとして追加または解除
380
+ if visited.issubset(self.group_atoms):
381
+ # すでに選択されている - 解除
382
+ self.group_atoms -= visited
383
+ else:
384
+ # 新しいグループを追加
385
+ self.group_atoms |= visited
386
+
387
+ self.selected_atoms.add(atom_idx)
388
+ self.show_atom_labels()
389
+ self.update_display()
390
+
391
+ def update_display(self):
392
+ if not self.group_atoms:
393
+ self.selection_label.setText("No group selected")
394
+ else:
395
+ atom_info = []
396
+ for atom_idx in sorted(self.group_atoms):
397
+ symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
398
+ atom_info.append(f"{symbol}({atom_idx})")
399
+
400
+ self.selection_label.setText(f"Selected group: {len(self.group_atoms)} atoms - {', '.join(atom_info[:5])}{' ...' if len(atom_info) > 5 else ''}")
401
+
402
+ def show_atom_labels(self):
403
+ """選択されたグループの原子をハイライト表示(Ctrlクリックと同じスタイル)"""
404
+ self.clear_atom_labels()
405
+
406
+ if not self.group_atoms:
407
+ return
408
+
409
+ # 選択された原子のインデックスリストを作成
410
+ selected_indices = list(self.group_atoms)
411
+
412
+ # 選択された原子の位置を取得
413
+ selected_positions = self.main_window.atom_positions_3d[selected_indices]
414
+
415
+ # 原子の半径を少し大きくしてハイライト表示
416
+ selected_radii = np.array([VDW_RADII.get(
417
+ self.mol.GetAtomWithIdx(i).GetSymbol(), 0.4) * 1.3
418
+ for i in selected_indices])
419
+
420
+ # ハイライト用のデータセットを作成
421
+ highlight_source = pv.PolyData(selected_positions)
422
+ highlight_source['radii'] = selected_radii
423
+
424
+ # 黄色の半透明球でハイライト
425
+ highlight_glyphs = highlight_source.glyph(
426
+ scale='radii',
427
+ geom=pv.Sphere(radius=1.0, theta_resolution=16, phi_resolution=16),
428
+ orient=False
429
+ )
430
+
431
+ # ハイライトアクターを追加して保存(ピッキング不可に設定)
432
+ self.highlight_actor = self.main_window.plotter.add_mesh(
433
+ highlight_glyphs,
434
+ color='yellow',
435
+ opacity=0.3,
436
+ name='move_group_highlight',
437
+ pickable=False # ピッキングを無効化
438
+ )
439
+
440
+ self.main_window.plotter.render()
441
+
442
+ def clear_atom_labels(self):
443
+ """原子ハイライトをクリア"""
444
+ try:
445
+ self.main_window.plotter.remove_actor('move_group_highlight')
446
+ except Exception:
447
+ pass
448
+
449
+ if hasattr(self, 'highlight_actor'):
450
+ try:
451
+ self.main_window.plotter.remove_actor(self.highlight_actor)
452
+ except Exception:
453
+ pass
454
+ self.highlight_actor = None
455
+
456
+ try:
457
+ self.main_window.plotter.render()
458
+ except Exception:
459
+ pass
460
+
461
+ def reset_translation_inputs(self):
462
+ """Translation入力フィールドをリセット"""
463
+ self.x_trans_input.setText("0.0")
464
+ self.y_trans_input.setText("0.0")
465
+ self.z_trans_input.setText("0.0")
466
+
467
+ def apply_translation(self):
468
+ """選択したグループを並行移動"""
469
+ if not self.group_atoms:
470
+ QMessageBox.warning(self, "Warning", "Please select a group first.")
471
+ return
472
+
473
+ try:
474
+ dx = float(self.x_trans_input.text())
475
+ dy = float(self.y_trans_input.text())
476
+ dz = float(self.z_trans_input.text())
477
+ except ValueError:
478
+ QMessageBox.warning(self, "Warning", "Please enter valid translation values.")
479
+ return
480
+
481
+ translation_vector = np.array([dx, dy, dz])
482
+
483
+ conf = self.mol.GetConformer()
484
+ for atom_idx in self.group_atoms:
485
+ atom_pos = np.array(conf.GetAtomPosition(atom_idx))
486
+ new_pos = atom_pos + translation_vector
487
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
488
+ self.main_window.atom_positions_3d[atom_idx] = new_pos
489
+
490
+ self.main_window.draw_molecule_3d(self.mol)
491
+ self.main_window.update_chiral_labels()
492
+ self.show_atom_labels() # ラベルを再描画
493
+ self.main_window.push_undo_state()
494
+
495
+ def reset_rotation_inputs(self):
496
+ """Rotation入力フィールドをリセット"""
497
+ self.x_rot_input.setText("0.0")
498
+ self.y_rot_input.setText("0.0")
499
+ self.z_rot_input.setText("0.0")
500
+
501
+ def apply_rotation(self):
502
+ """選択したグループを回転"""
503
+ if not self.group_atoms:
504
+ QMessageBox.warning(self, "Warning", "Please select a group first.")
505
+ return
506
+
507
+ try:
508
+ rx = float(self.x_rot_input.text())
509
+ ry = float(self.y_rot_input.text())
510
+ rz = float(self.z_rot_input.text())
511
+ except ValueError:
512
+ QMessageBox.warning(self, "Warning", "Please enter valid rotation values.")
513
+ return
514
+
515
+ # 度をラジアンに変換
516
+ rx_rad = np.radians(rx)
517
+ ry_rad = np.radians(ry)
518
+ rz_rad = np.radians(rz)
519
+
520
+ # グループの重心を計算
521
+ conf = self.mol.GetConformer()
522
+ positions = []
523
+ for atom_idx in self.group_atoms:
524
+ pos = conf.GetAtomPosition(atom_idx)
525
+ positions.append([pos.x, pos.y, pos.z])
526
+ centroid = np.mean(positions, axis=0)
527
+
528
+ # 回転行列を作成
529
+ # X軸周り
530
+ Rx = np.array([
531
+ [1, 0, 0],
532
+ [0, np.cos(rx_rad), -np.sin(rx_rad)],
533
+ [0, np.sin(rx_rad), np.cos(rx_rad)]
534
+ ])
535
+ # Y軸周り
536
+ Ry = np.array([
537
+ [np.cos(ry_rad), 0, np.sin(ry_rad)],
538
+ [0, 1, 0],
539
+ [-np.sin(ry_rad), 0, np.cos(ry_rad)]
540
+ ])
541
+ # Z軸周り
542
+ Rz = np.array([
543
+ [np.cos(rz_rad), -np.sin(rz_rad), 0],
544
+ [np.sin(rz_rad), np.cos(rz_rad), 0],
545
+ [0, 0, 1]
546
+ ])
547
+
548
+ # 合成回転行列 (Z * Y * X)
549
+ R = Rz @ Ry @ Rx
550
+
551
+ # 各原子を回転
552
+ for atom_idx in self.group_atoms:
553
+ atom_pos = np.array(conf.GetAtomPosition(atom_idx))
554
+ # 重心を原点に移動
555
+ centered_pos = atom_pos - centroid
556
+ # 回転
557
+ rotated_pos = R @ centered_pos
558
+ # 重心を元に戻す
559
+ new_pos = rotated_pos + centroid
560
+ conf.SetAtomPosition(atom_idx, new_pos.tolist())
561
+ self.main_window.atom_positions_3d[atom_idx] = new_pos
562
+
563
+ self.main_window.draw_molecule_3d(self.mol)
564
+ self.main_window.update_chiral_labels()
565
+ self.show_atom_labels() # ラベルを再描画
566
+ self.main_window.push_undo_state()
567
+
568
+ def clear_selection(self):
569
+ """選択をクリア"""
570
+ self.selected_atoms.clear()
571
+ self.group_atoms.clear()
572
+ self.clear_atom_labels()
573
+ self.update_display()
574
+ # ドラッグ関連のフラグもリセット
575
+ self.is_dragging_group = False
576
+ self.drag_start_pos = None
577
+ if hasattr(self, 'last_drag_positions'):
578
+ delattr(self, 'last_drag_positions')
579
+
580
+ def closeEvent(self, event):
581
+ """ダイアログが閉じられる時の処理"""
582
+ self.clear_atom_labels()
583
+ self.disable_picking()
584
+ try:
585
+ self.main_window.draw_molecule_3d(self.mol)
586
+ except Exception:
587
+ pass
588
+ super().closeEvent(event)
589
+
590
+ def reject(self):
591
+ """キャンセル時の処理"""
592
+ self.clear_atom_labels()
593
+ self.disable_picking()
594
+ try:
595
+ self.main_window.draw_molecule_3d(self.mol)
596
+ except Exception:
597
+ pass
598
+ super().reject()
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ MoleditPy — A Python-based molecular editing software
6
+
7
+ Author: Hiromichi Yokoyama
8
+ License: GPL-3.0 license
9
+ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
+ DOI: 10.5281/zenodo.17268532
11
+ """
12
+
13
+ from PyQt6.QtWidgets import QDialog, QGridLayout, QPushButton
14
+ from PyQt6.QtGui import QColor
15
+ try:
16
+ from .constants import CPK_COLORS
17
+ except Exception:
18
+ from modules.constants import CPK_COLORS
19
+
20
+ from PyQt6.QtCore import pyqtSignal
21
+
22
+ class PeriodicTableDialog(QDialog):
23
+ element_selected = pyqtSignal(str)
24
+ def __init__(self, parent=None):
25
+ super().__init__(parent)
26
+ self.setWindowTitle("Select an Element")
27
+ layout = QGridLayout(self)
28
+ self.setLayout(layout)
29
+
30
+ elements = [
31
+ ('H',1,1), ('He',1,18),
32
+ ('Li',2,1), ('Be',2,2), ('B',2,13), ('C',2,14), ('N',2,15), ('O',2,16), ('F',2,17), ('Ne',2,18),
33
+ ('Na',3,1), ('Mg',3,2), ('Al',3,13), ('Si',3,14), ('P',3,15), ('S',3,16), ('Cl',3,17), ('Ar',3,18),
34
+ ('K',4,1), ('Ca',4,2), ('Sc',4,3), ('Ti',4,4), ('V',4,5), ('Cr',4,6), ('Mn',4,7), ('Fe',4,8),
35
+ ('Co',4,9), ('Ni',4,10), ('Cu',4,11), ('Zn',4,12), ('Ga',4,13), ('Ge',4,14), ('As',4,15), ('Se',4,16),
36
+ ('Br',4,17), ('Kr',4,18),
37
+ ('Rb',5,1), ('Sr',5,2), ('Y',5,3), ('Zr',5,4), ('Nb',5,5), ('Mo',5,6), ('Tc',5,7), ('Ru',5,8),
38
+ ('Rh',5,9), ('Pd',5,10), ('Ag',5,11), ('Cd',5,12), ('In',5,13), ('Sn',5,14), ('Sb',5,15), ('Te',5,16),
39
+ ('I',5,17), ('Xe',5,18),
40
+ ('Cs',6,1), ('Ba',6,2), ('Hf',6,4), ('Ta',6,5), ('W',6,6), ('Re',6,7), ('Os',6,8),
41
+ ('Ir',6,9), ('Pt',6,10), ('Au',6,11), ('Hg',6,12), ('Tl',6,13), ('Pb',6,14), ('Bi',6,15), ('Po',6,16),
42
+ ('At',6,17), ('Rn',6,18),
43
+ ('Fr',7,1), ('Ra',7,2), ('Rf',7,4), ('Db',7,5), ('Sg',7,6), ('Bh',7,7), ('Hs',7,8),
44
+ ('Mt',7,9), ('Ds',7,10), ('Rg',7,11), ('Cn',7,12), ('Nh',7,13), ('Fl',7,14), ('Mc',7,15), ('Lv',7,16),
45
+ ('Ts',7,17), ('Og',7,18),
46
+ # Lanthanides (placed on a separate row)
47
+ ('La',8,3), ('Ce',8,4), ('Pr',8,5), ('Nd',8,6), ('Pm',8,7), ('Sm',8,8), ('Eu',8,9), ('Gd',8,10), ('Tb',8,11),
48
+ ('Dy',8,12), ('Ho',8,13), ('Er',8,14), ('Tm',8,15), ('Yb',8,16), ('Lu',8,17),
49
+ # Actinides (separate row)
50
+ ('Ac',9,3), ('Th',9,4), ('Pa',9,5), ('U',9,6), ('Np',9,7), ('Pu',9,8), ('Am',9,9), ('Cm',9,10), ('Bk',9,11),
51
+ ('Cf',9,12), ('Es',9,13), ('Fm',9,14), ('Md',9,15), ('No',9,16), ('Lr',9,17),
52
+ ]
53
+ for symbol, row, col in elements:
54
+ b = QPushButton(symbol)
55
+ b.setFixedSize(40,40)
56
+
57
+ # Prefer saved user override (from parent.settings), otherwise use CPK_COLORS
58
+ try:
59
+ overrides = parent.settings.get('cpk_colors', {}) if parent and hasattr(parent, 'settings') else {}
60
+ override = overrides.get(symbol)
61
+ except Exception:
62
+ override = None
63
+ q_color = QColor(override) if override else CPK_COLORS.get(symbol, CPK_COLORS['DEFAULT'])
64
+
65
+ # 背景色の輝度を計算して、文字色を黒か白に決定
66
+ # 輝度 = (R*299 + G*587 + B*114) / 1000
67
+ brightness = (q_color.red() * 299 + q_color.green() * 587 + q_color.blue() * 114) / 1000
68
+ text_color = "white" if brightness < 128 else "black"
69
+
70
+ # ボタンのスタイルシートを設定
71
+ b.setStyleSheet(
72
+ f"background-color: {q_color.name()};"
73
+ f"color: {text_color};"
74
+ "border: 1px solid #555;"
75
+ "font-weight: bold;"
76
+ )
77
+
78
+ b.clicked.connect(self.on_button_clicked)
79
+ layout.addWidget(b, row, col)
80
+
81
+ def on_button_clicked(self):
82
+ b=self.sender()
83
+ self.element_selected.emit(b.text())
84
+ self.accept()