moleditpy 2.6.0__tar.gz → 2.6.1__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.
Files changed (66) hide show
  1. {moleditpy-2.6.0 → moleditpy-2.6.1}/PKG-INFO +1 -1
  2. {moleditpy-2.6.0 → moleditpy-2.6.1}/pyproject.toml +1 -1
  3. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
  5. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/main.py +1 -1
  6. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/__init__.py +1 -11
  7. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/about_dialog.py +14 -14
  8. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/align_plane_dialog.py +36 -29
  9. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/alignment_dialog.py +44 -41
  10. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/analysis_window.py +30 -24
  11. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/angle_dialog.py +63 -57
  12. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/atom_item.py +47 -39
  13. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/bond_item.py +67 -56
  14. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/bond_length_dialog.py +59 -51
  15. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/calculation_worker.py +104 -101
  16. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/color_settings_dialog.py +13 -6
  17. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/constants.py +3 -3
  18. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/constrained_optimization_dialog.py +91 -84
  19. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/custom_interactor_style.py +73 -79
  20. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/custom_qt_interactor.py +6 -5
  21. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +18 -19
  22. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/dihedral_dialog.py +72 -65
  23. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window.py +5 -12
  24. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_app_state.py +48 -59
  25. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_compute.py +48 -60
  26. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_dialog_manager.py +70 -75
  27. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_3d.py +64 -74
  28. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_actions.py +101 -102
  29. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_export.py +111 -120
  30. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_main_init.py +190 -184
  31. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_molecular_parsers.py +74 -71
  32. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_project_io.py +68 -73
  33. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_string_importers.py +20 -27
  34. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_ui_manager.py +42 -45
  35. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_3d.py +130 -137
  36. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_loaders.py +24 -26
  37. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/mirror_dialog.py +37 -26
  38. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/mol_geometry.py +0 -1
  39. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/molecular_data.py +12 -12
  40. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/molecule_scene.py +134 -147
  41. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/move_group_dialog.py +107 -101
  42. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/periodic_table_dialog.py +3 -1
  43. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/planarize_dialog.py +10 -7
  44. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_interface.py +13 -16
  45. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager.py +54 -55
  46. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager_window.py +42 -33
  47. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/settings_dialog.py +95 -91
  48. moleditpy-2.6.1/src/moleditpy/modules/sip_isdeleted_safe.py +16 -0
  49. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_item.py +18 -27
  50. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_view.py +9 -9
  51. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/translation_dialog.py +42 -35
  52. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/user_template_dialog.py +115 -103
  53. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/modules/zoomable_view.py +7 -12
  54. {moleditpy-2.6.0 → moleditpy-2.6.1}/LICENSE +0 -0
  55. {moleditpy-2.6.0 → moleditpy-2.6.1}/README.md +0 -0
  56. {moleditpy-2.6.0 → moleditpy-2.6.1}/setup.cfg +0 -0
  57. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  58. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  59. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/requires.txt +0 -0
  60. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
  61. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/__init__.py +0 -0
  62. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/__main__.py +0 -0
  63. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/assets/file_icon.ico +0 -0
  64. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/assets/icon.icns +0 -0
  65. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/assets/icon.ico +0 -0
  66. {moleditpy-2.6.0 → moleditpy-2.6.1}/src/moleditpy/assets/icon.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.6.0
3
+ Version: 2.6.1
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
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "2.6.0"
8
+ version = "2.6.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.6.0
3
+ Version: 2.6.1
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
@@ -62,6 +62,7 @@ src/moleditpy/modules/plugin_interface.py
62
62
  src/moleditpy/modules/plugin_manager.py
63
63
  src/moleditpy/modules/plugin_manager_window.py
64
64
  src/moleditpy/modules/settings_dialog.py
65
+ src/moleditpy/modules/sip_isdeleted_safe.py
65
66
  src/moleditpy/modules/template_preview_item.py
66
67
  src/moleditpy/modules/template_preview_view.py
67
68
  src/moleditpy/modules/translation_dialog.py
@@ -11,8 +11,8 @@ DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
13
 
14
- import sys
15
14
  import ctypes
15
+ import sys
16
16
 
17
17
  from PyQt6.QtWidgets import QApplication
18
18
 
@@ -10,24 +10,14 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
-
14
- # Open Babel Python binding (optional; required for fallback)
15
- # Do not import `pybel` at module import time. Only expose the presence
16
- # of Open Babel via `OBABEL_AVAILABLE`. Modules should import `pybel`
17
- # lazily if and when they need it.
18
13
  try: # pragma: no cover
19
14
  import importlib.util
20
15
  OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
21
16
  except Exception:
22
17
  OBABEL_AVAILABLE = False
23
18
 
24
- # Optional SIP helper: on some PyQt6 builds sip.isdeleted is available and
25
- # allows safely detecting C++ wrapper objects that have been deleted. Import
26
- # it once at module import time and expose a small, robust wrapper so callers
27
- # can avoid re-importing sip repeatedly and so we centralize exception
28
- # handling (this reduces crash risk during teardown and deletion operations).
29
19
  try: # pragma: no cover
30
- import sip as _sip # type: ignore
20
+ from PyQt6 import sip as _sip # type: ignore
31
21
  _sip_isdeleted = getattr(_sip, 'isdeleted', None)
32
22
  except Exception:
33
23
  _sip = None
@@ -10,12 +10,12 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- from PyQt6.QtWidgets import (
14
- QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout
15
- )
16
- from PyQt6.QtGui import QPixmap, QPainter, QPen, QCursor
17
- from PyQt6.QtCore import Qt
18
13
  import os
14
+
15
+ from PyQt6.QtCore import Qt
16
+ from PyQt6.QtGui import QCursor, QPainter, QPen, QPixmap
17
+ from PyQt6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
18
+
19
19
  try:
20
20
  from .constants import VERSION
21
21
  except Exception:
@@ -28,14 +28,14 @@ class AboutDialog(QDialog): # pragma: no cover
28
28
  self.setWindowTitle("About MoleditPy")
29
29
  self.setFixedSize(250, 300)
30
30
  self.init_ui()
31
-
31
+
32
32
  def init_ui(self):
33
33
  layout = QVBoxLayout(self)
34
-
34
+
35
35
  # Create a clickable image label
36
36
  self.image_label = QLabel()
37
37
  self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
38
-
38
+
39
39
  # Load the original icon image
40
40
  icon_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'icon.png')
41
41
  if os.path.exists(icon_path):
@@ -50,7 +50,7 @@ class AboutDialog(QDialog): # pragma: no cover
50
50
  painter.setPen(QPen(Qt.GlobalColor.black, 2))
51
51
  painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "MoleditPy")
52
52
  painter.end()
53
-
53
+
54
54
  self.image_label.setPixmap(pixmap)
55
55
  try:
56
56
  self.image_label.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
@@ -58,27 +58,27 @@ class AboutDialog(QDialog): # pragma: no cover
58
58
  pass
59
59
 
60
60
  self.image_label.mousePressEvent = self.image_mouse_press_event
61
-
61
+
62
62
  layout.addWidget(self.image_label)
63
-
63
+
64
64
  # Add text information
65
65
  info_text = f"MoleditPy Ver. {VERSION}\nAuthor: Hiromichi Yokoyama\nLicense: GPL-3.0 license"
66
66
  info_label = QLabel(info_text)
67
67
  info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
68
68
  layout.addWidget(info_label)
69
-
69
+
70
70
  # Add OK button
71
71
  ok_button = QPushButton("OK")
72
72
  ok_button.setFixedSize(80, 30) # 小さいサイズに固定
73
73
  ok_button.clicked.connect(self.accept)
74
-
74
+
75
75
  # Center the button
76
76
  button_layout = QHBoxLayout()
77
77
  button_layout.addStretch()
78
78
  button_layout.addWidget(ok_button)
79
79
  button_layout.addStretch()
80
80
  layout.addLayout(button_layout)
81
-
81
+
82
82
  def image_clicked(self, event):
83
83
  """Easter egg: Clear all and load bipyrimidine from SMILES"""
84
84
  # Clear the current scene
@@ -10,11 +10,16 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ import numpy as np
14
+ from PyQt6.QtCore import Qt
13
15
  from PyQt6.QtWidgets import (
14
- QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QMessageBox
16
+ QDialog,
17
+ QHBoxLayout,
18
+ QLabel,
19
+ QMessageBox,
20
+ QPushButton,
21
+ QVBoxLayout,
15
22
  )
16
- from PyQt6.QtCore import Qt
17
- import numpy as np
18
23
 
19
24
  try:
20
25
  from .dialog3_d_picking_mixin import Dialog3DPickingMixin
@@ -29,47 +34,47 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
29
34
  self.main_window = main_window
30
35
  self.plane = plane
31
36
  self.selected_atoms = set()
32
-
37
+
33
38
  # 事前選択された原子を追加
34
39
  if preselected_atoms:
35
40
  self.selected_atoms.update(preselected_atoms)
36
-
41
+
37
42
  self.init_ui()
38
-
43
+
39
44
  # 事前選択された原子にラベルを追加
40
45
  if self.selected_atoms:
41
46
  self.show_atom_labels()
42
47
  self.update_display()
43
-
48
+
44
49
  def init_ui(self):
45
50
  plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
46
51
  self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
47
52
  self.setModal(False)
48
53
  layout = QVBoxLayout(self)
49
-
54
+
50
55
  # Instructions
51
56
  instruction_label = QLabel(f"Click atoms in the 3D view to select them for align to the {plane_names[self.plane]} plane. At least 3 atoms are required.")
52
57
  instruction_label.setWordWrap(True)
53
58
  layout.addWidget(instruction_label)
54
-
59
+
55
60
  # Selected atoms display
56
61
  self.selection_label = QLabel("No atoms selected")
57
62
  layout.addWidget(self.selection_label)
58
-
63
+
59
64
  # Buttons
60
65
  button_layout = QHBoxLayout()
61
66
  self.clear_button = QPushButton("Clear Selection")
62
67
  self.clear_button.clicked.connect(self.clear_selection)
63
68
  button_layout.addWidget(self.clear_button)
64
-
69
+
65
70
  # Select all atoms button
66
71
  self.select_all_button = QPushButton("Select All Atoms")
67
72
  self.select_all_button.setToolTip("Select all atoms in the molecule for alignment")
68
73
  self.select_all_button.clicked.connect(self.select_all_atoms)
69
74
  button_layout.addWidget(self.select_all_button)
70
-
75
+
71
76
  button_layout.addStretch()
72
-
77
+
73
78
  self.apply_button = QPushButton("Apply align")
74
79
  self.apply_button.clicked.connect(self.apply_PlaneAlign)
75
80
  self.apply_button.setEnabled(False)
@@ -78,35 +83,35 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
78
83
  close_button = QPushButton("Close")
79
84
  close_button.clicked.connect(self.reject)
80
85
  button_layout.addWidget(close_button)
81
-
86
+
82
87
  layout.addLayout(button_layout)
83
-
88
+
84
89
  # Connect to main window's picker
85
90
  self.picker_connection = None
86
91
  self.enable_picking()
87
-
92
+
88
93
  def enable_picking(self):
89
94
  """3Dビューでの原子選択を有効にする"""
90
95
  self.main_window.plotter.interactor.installEventFilter(self)
91
96
  self.picking_enabled = True
92
-
97
+
93
98
  def disable_picking(self):
94
99
  """3Dビューでの原子選択を無効にする"""
95
100
  if hasattr(self, 'picking_enabled') and self.picking_enabled:
96
101
  self.main_window.plotter.interactor.removeEventFilter(self)
97
102
  self.picking_enabled = False
98
-
103
+
99
104
  def on_atom_picked(self, atom_idx):
100
105
  """原子がピックされたときの処理"""
101
106
  if atom_idx in self.selected_atoms:
102
107
  self.selected_atoms.remove(atom_idx)
103
108
  else:
104
109
  self.selected_atoms.add(atom_idx)
105
-
110
+
106
111
  # 原子ラベルを表示
107
112
  self.show_atom_labels()
108
113
  self.update_display()
109
-
114
+
110
115
  def keyPressEvent(self, event):
111
116
  """キーボードイベントを処理"""
112
117
  if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
@@ -115,13 +120,13 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
115
120
  event.accept()
116
121
  else:
117
122
  super().keyPressEvent(event)
118
-
123
+
119
124
  def clear_selection(self):
120
125
  """選択をクリア"""
121
126
  self.selected_atoms.clear()
122
127
  self.clear_atom_labels()
123
128
  self.update_display()
124
-
129
+
125
130
  def select_all_atoms(self):
126
131
  """Select all atoms in the current molecule and update labels/UI."""
127
132
  try:
@@ -144,7 +149,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
144
149
 
145
150
  except Exception as e:
146
151
  QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
147
-
152
+
148
153
  def update_display(self):
149
154
  """表示を更新"""
150
155
  count = len(self.selected_atoms)
@@ -157,10 +162,10 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
157
162
  for i, atom_idx in enumerate(atom_list):
158
163
  symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
159
164
  atom_display.append(f"#{i+1}: {symbol}({atom_idx})")
160
-
165
+
161
166
  self.selection_label.setText(f"Selected {count} atoms: {', '.join(atom_display)}")
162
167
  self.apply_button.setEnabled(count >= 3)
163
-
168
+
164
169
  def show_atom_labels(self):
165
170
  """選択された原子にラベルを表示"""
166
171
  if self.selected_atoms:
@@ -169,7 +174,7 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
169
174
  self.show_atom_labels_for(pairs, color='blue')
170
175
  else:
171
176
  self.clear_atom_labels()
172
-
177
+
173
178
  def apply_PlaneAlign(self):
174
179
  """alignを適用(回転ベース)"""
175
180
  if len(self.selected_atoms) < 3:
@@ -202,6 +207,8 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
202
207
  target_normal = np.array([0, 1, 0]) # Y軸方向
203
208
  elif self.plane == 'yz':
204
209
  target_normal = np.array([1, 0, 0]) # X軸方向
210
+ else:
211
+ target_normal = np.array([0, 0, 1]) # Default to Z-axis (XY plane)
205
212
 
206
213
  # 法線ベクトルの向きを調整(内積が正になるように)
207
214
  if np.dot(normal_vector, target_normal) < 0:
@@ -245,19 +252,19 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
245
252
 
246
253
  except Exception as e:
247
254
  QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
248
-
255
+
249
256
  def closeEvent(self, event):
250
257
  """ダイアログが閉じられる時の処理"""
251
258
  self.clear_atom_labels()
252
259
  self.disable_picking()
253
260
  super().closeEvent(event)
254
-
261
+
255
262
  def reject(self):
256
263
  """キャンセル時の処理"""
257
264
  self.clear_atom_labels()
258
265
  self.disable_picking()
259
266
  super().reject()
260
-
267
+
261
268
  def accept(self):
262
269
  """OK時の処理"""
263
270
  self.clear_atom_labels()
@@ -10,13 +10,16 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ import numpy as np
13
14
  from PyQt6.QtWidgets import (
14
- QDialog, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
15
+ QDialog,
16
+ QHBoxLayout,
17
+ QLabel,
18
+ QMessageBox,
19
+ QPushButton,
20
+ QVBoxLayout,
15
21
  )
16
22
 
17
- from PyQt6.QtWidgets import QMessageBox
18
- import numpy as np
19
-
20
23
  try:
21
24
  from .dialog3_d_picking_mixin import Dialog3DPickingMixin
22
25
  except Exception:
@@ -30,72 +33,72 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
30
33
  self.main_window = main_window
31
34
  self.axis = axis
32
35
  self.selected_atoms = set()
33
-
36
+
34
37
  # 事前選択された原子を追加(最大2個まで)
35
38
  if preselected_atoms:
36
39
  self.selected_atoms.update(preselected_atoms[:2])
37
-
40
+
38
41
  self.init_ui()
39
-
42
+
40
43
  # 事前選択された原子にラベルを追加
41
44
  if self.selected_atoms:
42
45
  for i, atom_idx in enumerate(sorted(self.selected_atoms), 1):
43
46
  self.add_selection_label(atom_idx, f"Atom {i}")
44
47
  self.update_display()
45
-
48
+
46
49
  def init_ui(self):
47
50
  axis_names = {'x': 'X-axis', 'y': 'Y-axis', 'z': 'Z-axis'}
48
51
  self.setWindowTitle(f"Align to {axis_names[self.axis]}")
49
52
  self.setModal(False)
50
53
  layout = QVBoxLayout(self)
51
-
54
+
52
55
  # Instructions
53
56
  instruction_label = QLabel(f"Click atoms in the 3D view to select them for alignment to the {axis_names[self.axis]}. Exactly 2 atoms are required. The first atom will be moved to the origin, and the second atom will be positioned on the {axis_names[self.axis]}.")
54
57
  instruction_label.setWordWrap(True)
55
58
  layout.addWidget(instruction_label)
56
-
59
+
57
60
  # Selected atoms display
58
61
  self.selection_label = QLabel("No atoms selected")
59
62
  layout.addWidget(self.selection_label)
60
-
63
+
61
64
  # Buttons
62
65
  button_layout = QHBoxLayout()
63
66
  self.clear_button = QPushButton("Clear Selection")
64
67
  self.clear_button.clicked.connect(self.clear_selection)
65
68
  button_layout.addWidget(self.clear_button)
66
-
69
+
67
70
  button_layout.addStretch()
68
-
71
+
69
72
  self.apply_button = QPushButton("Apply Alignment")
70
73
  self.apply_button.clicked.connect(self.apply_alignment)
71
74
  self.apply_button.setEnabled(False)
72
75
  button_layout.addWidget(self.apply_button)
73
-
76
+
74
77
  close_button = QPushButton("Close")
75
78
  close_button.clicked.connect(self.reject)
76
79
  button_layout.addWidget(close_button)
77
-
80
+
78
81
  layout.addLayout(button_layout)
79
-
82
+
80
83
  # Connect to main window's picker
81
84
  self.picker_connection = None
82
85
  self.enable_picking()
83
-
86
+
84
87
  def enable_picking(self):
85
88
  """3Dビューでの原子選択を有効にする"""
86
89
  # Dialog3DPickingMixinの機能を使用
87
90
  super().enable_picking()
88
-
91
+
89
92
  def disable_picking(self):
90
93
  """3Dビューでの原子選択を無効にする"""
91
94
  # Dialog3DPickingMixinの機能を使用
92
95
  super().disable_picking()
93
-
96
+
94
97
  def on_atom_picked(self, atom_idx):
95
98
  """原子がクリックされた時の処理"""
96
99
  if self.main_window.current_mol is None:
97
100
  return
98
-
101
+
99
102
  if atom_idx in self.selected_atoms:
100
103
  # 既に選択されている場合は選択解除
101
104
  self.selected_atoms.remove(atom_idx)
@@ -107,9 +110,9 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
107
110
  # ラベルの順番を示す
108
111
  label_text = f"Atom {len(self.selected_atoms)}"
109
112
  self.add_selection_label(atom_idx, label_text)
110
-
113
+
111
114
  self.update_display()
112
-
115
+
113
116
  def update_display(self):
114
117
  """選択状態の表示を更新"""
115
118
  if len(self.selected_atoms) == 0:
@@ -126,13 +129,13 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
126
129
  atom2 = self.mol.GetAtomWithIdx(selected_list[1])
127
130
  self.selection_label.setText(f"Selected 2 atoms: {atom1.GetSymbol()}{selected_list[0]+1}, {atom2.GetSymbol()}{selected_list[1]+1}")
128
131
  self.apply_button.setEnabled(True)
129
-
132
+
130
133
  def clear_selection(self):
131
134
  """選択をクリア"""
132
135
  self.clear_selection_labels()
133
136
  self.selected_atoms.clear()
134
137
  self.update_display()
135
-
138
+
136
139
  def remove_atom_label(self, atom_idx):
137
140
  """特定の原子のラベルを削除"""
138
141
  # 簡単化のため、全ラベルをクリアして再描画
@@ -140,7 +143,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
140
143
  for i, idx in enumerate(sorted(self.selected_atoms), 1):
141
144
  if idx != atom_idx:
142
145
  self.add_selection_label(idx, f"Atom {i}")
143
-
146
+
144
147
  def apply_alignment(self):
145
148
  """アライメントを適用"""
146
149
  if len(self.selected_atoms) != 2:
@@ -174,69 +177,69 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
174
177
  'z': np.array([0.0, 0.0, 1.0])
175
178
  }
176
179
  target_axis = axis_vectors[self.axis]
177
-
180
+
178
181
  # atom2から原点への方向ベクトル
179
182
  current_vector = pos2_translated
180
183
  current_length = np.linalg.norm(current_vector)
181
-
184
+
182
185
  if current_length > 1e-10: # ゼロベクトルでない場合
183
186
  current_vector_normalized = current_vector / current_length
184
-
187
+
185
188
  # 回転軸と角度を計算
186
189
  rotation_axis = np.cross(current_vector_normalized, target_axis)
187
190
  rotation_axis_length = np.linalg.norm(rotation_axis)
188
-
191
+
189
192
  if rotation_axis_length > 1e-10: # 回転が必要
190
193
  rotation_axis = rotation_axis / rotation_axis_length
191
194
  cos_angle = np.dot(current_vector_normalized, target_axis)
192
195
  cos_angle = np.clip(cos_angle, -1.0, 1.0)
193
196
  rotation_angle = np.arccos(cos_angle)
194
-
197
+
195
198
  # ロドリゲスの回転公式を使用
196
199
  def rodrigues_rotation(v, k, theta):
197
200
  cos_theta = np.cos(theta)
198
201
  sin_theta = np.sin(theta)
199
- return (v * cos_theta +
200
- np.cross(k, v) * sin_theta +
202
+ return (v * cos_theta +
203
+ np.cross(k, v) * sin_theta +
201
204
  k * np.dot(k, v) * (1 - cos_theta))
202
-
205
+
203
206
  # 全ての原子に回転を適用
204
207
  for i in range(self.mol.GetNumAtoms()):
205
208
  current_pos = np.array(conf.GetAtomPosition(i))
206
209
  rotated_pos = rodrigues_rotation(current_pos, rotation_axis, rotation_angle)
207
210
  conf.SetAtomPosition(i, rotated_pos.tolist())
208
-
211
+
209
212
  # 3D座標を更新
210
213
  self.main_window.atom_positions_3d = np.array([
211
214
  list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())
212
215
  ])
213
-
216
+
214
217
  # 3Dビューを更新
215
218
  self.main_window.draw_molecule_3d(self.mol)
216
-
219
+
217
220
  # キラルラベルを更新
218
221
  self.main_window.update_chiral_labels()
219
222
 
220
223
  # Undo状態を保存
221
224
  self.main_window.push_undo_state()
222
-
225
+
223
226
  QMessageBox.information(self, "Success", f"Alignment to {self.axis.upper()}-axis completed.")
224
-
227
+
225
228
  except Exception as e:
226
229
  QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
227
-
230
+
228
231
  def closeEvent(self, event):
229
232
  """ダイアログが閉じられる時の処理"""
230
233
  self.clear_selection_labels()
231
234
  self.disable_picking()
232
235
  super().closeEvent(event)
233
-
236
+
234
237
  def reject(self):
235
238
  """キャンセル時の処理"""
236
239
  self.clear_selection_labels()
237
240
  self.disable_picking()
238
241
  super().reject()
239
-
242
+
240
243
  def accept(self):
241
244
  """OK時の処理"""
242
245
  self.clear_selection_labels()