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