moleditpy 2.6.0__tar.gz → 2.6.2__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 (70) hide show
  1. {moleditpy-2.6.0 → moleditpy-2.6.2}/PKG-INFO +1 -1
  2. {moleditpy-2.6.0 → moleditpy-2.6.2}/pyproject.toml +1 -1
  3. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
  5. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/__init__.py +0 -2
  6. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/__main__.py +1 -3
  7. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/main.py +4 -4
  8. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/__init__.py +8 -16
  9. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/about_dialog.py +29 -21
  10. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/align_plane_dialog.py +83 -50
  11. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/alignment_dialog.py +77 -58
  12. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/analysis_window.py +50 -34
  13. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/angle_dialog.py +126 -82
  14. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/atom_item.py +163 -104
  15. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/bond_item.py +217 -129
  16. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/bond_length_dialog.py +97 -68
  17. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/calculation_worker.py +367 -213
  18. moleditpy-2.6.2/src/moleditpy/modules/color_settings_dialog.py +524 -0
  19. moleditpy-2.6.2/src/moleditpy/modules/constants.py +170 -0
  20. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/constrained_optimization_dialog.py +231 -133
  21. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/custom_interactor_style.py +295 -183
  22. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/custom_qt_interactor.py +11 -10
  23. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/dialog3_d_picking_mixin.py +64 -49
  24. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/dihedral_dialog.py +161 -95
  25. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window.py +50 -35
  26. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_app_state.py +303 -199
  27. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_compute.py +549 -389
  28. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_dialog_manager.py +215 -142
  29. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_edit_3d.py +126 -118
  30. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_edit_actions.py +405 -299
  31. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_export.py +373 -264
  32. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_main_init.py +947 -687
  33. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_molecular_parsers.py +405 -252
  34. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_project_io.py +168 -140
  35. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_string_importers.py +71 -55
  36. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_ui_manager.py +162 -139
  37. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_view_3d.py +715 -419
  38. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/main_window_view_loaders.py +96 -71
  39. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/mirror_dialog.py +47 -30
  40. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/mol_geometry.py +10 -9
  41. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/molecular_data.py +85 -53
  42. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/molecule_scene.py +790 -523
  43. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/move_group_dialog.py +260 -183
  44. moleditpy-2.6.2/src/moleditpy/modules/periodic_table_dialog.py +196 -0
  45. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/planarize_dialog.py +57 -23
  46. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/plugin_interface.py +64 -33
  47. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/plugin_manager.py +239 -194
  48. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/plugin_manager_window.py +131 -84
  49. moleditpy-2.6.2/src/moleditpy/modules/settings_dialog.py +1861 -0
  50. moleditpy-2.6.2/src/moleditpy/modules/sip_isdeleted_safe.py +17 -0
  51. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/template_preview_item.py +50 -37
  52. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/template_preview_view.py +29 -14
  53. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/translation_dialog.py +97 -61
  54. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/user_template_dialog.py +312 -227
  55. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/modules/zoomable_view.py +24 -26
  56. moleditpy-2.6.0/src/moleditpy/modules/color_settings_dialog.py +0 -318
  57. moleditpy-2.6.0/src/moleditpy/modules/constants.py +0 -88
  58. moleditpy-2.6.0/src/moleditpy/modules/periodic_table_dialog.py +0 -84
  59. moleditpy-2.6.0/src/moleditpy/modules/settings_dialog.py +0 -1402
  60. {moleditpy-2.6.0 → moleditpy-2.6.2}/LICENSE +0 -0
  61. {moleditpy-2.6.0 → moleditpy-2.6.2}/README.md +0 -0
  62. {moleditpy-2.6.0 → moleditpy-2.6.2}/setup.cfg +0 -0
  63. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  64. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  65. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/requires.txt +0 -0
  66. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/MoleditPy.egg-info/top_level.txt +0 -0
  67. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/assets/file_icon.ico +0 -0
  68. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/assets/icon.icns +0 -0
  69. {moleditpy-2.6.0 → moleditpy-2.6.2}/src/moleditpy/assets/icon.ico +0 -0
  70. {moleditpy-2.6.0 → moleditpy-2.6.2}/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.2
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.2"
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.2
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
@@ -10,8 +10,6 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
-
14
13
  """Top-level package for moleditpy.
15
14
 
16
15
  """
17
-
@@ -10,7 +10,6 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
-
14
13
  print("-----------------------------------------------------")
15
14
  print("MoleditPy — A Python-based molecular editing software")
16
15
  print("-----------------------------------------------------\n")
@@ -21,6 +20,5 @@ except Exception:
21
20
  from main import main
22
21
 
23
22
  # --- Application Execution ---
24
- if __name__ == '__main__':
23
+ if __name__ == "__main__":
25
24
  main()
26
-
@@ -10,9 +10,8 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
-
14
- import sys
15
13
  import ctypes
14
+ import sys
16
15
 
17
16
  from PyQt6.QtWidgets import QApplication
18
17
 
@@ -21,10 +20,11 @@ try:
21
20
  except Exception:
22
21
  from modules.main_window import MainWindow
23
22
 
23
+
24
24
  def main():
25
25
  # --- Windows タスクバーアイコンのための追加処理 ---
26
- if sys.platform == 'win32':
27
- myappid = 'hyoko.moleditpy.1.0' # アプリケーション固有のID(任意)
26
+ if sys.platform == "win32":
27
+ myappid = "hyoko.moleditpy.1.0" # アプリケーション固有のID(任意)
28
28
  ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
29
29
 
30
30
  app = QApplication(sys.argv)
@@ -10,29 +10,22 @@ 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
- try: # pragma: no cover
13
+ try:
19
14
  import importlib.util
15
+
20
16
  OBABEL_AVAILABLE = importlib.util.find_spec("openbabel") is not None
21
17
  except Exception:
22
18
  OBABEL_AVAILABLE = False
23
19
 
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
- try: # pragma: no cover
30
- import sip as _sip # type: ignore
31
- _sip_isdeleted = getattr(_sip, 'isdeleted', None)
20
+ try:
21
+ from PyQt6 import sip as _sip # type: ignore
22
+
23
+ _sip_isdeleted = getattr(_sip, "isdeleted", None)
32
24
  except Exception:
33
25
  _sip = None
34
26
  _sip_isdeleted = None
35
27
 
28
+
36
29
  def sip_isdeleted_safe(obj) -> bool:
37
30
  """Return True if sip reports the given wrapper object as deleted.
38
31
 
@@ -40,10 +33,9 @@ def sip_isdeleted_safe(obj) -> bool:
40
33
  occurs while checking, it returns False (i.e. not deleted) so that the
41
34
  caller can continue other lightweight guards (like checking scene()).
42
35
  """
43
- try: # pragma: no cover
36
+ try:
44
37
  if _sip_isdeleted is None:
45
38
  return False
46
39
  return bool(_sip_isdeleted(obj))
47
40
  except Exception:
48
41
  return False
49
-
@@ -10,38 +10,44 @@ 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:
22
22
  from modules.constants import VERSION
23
23
 
24
- class AboutDialog(QDialog): # pragma: no cover
24
+
25
+ class AboutDialog(QDialog): # pragma: no cover
25
26
  def __init__(self, main_window, parent=None):
26
27
  super().__init__(parent)
27
28
  self.main_window = main_window
28
29
  self.setWindowTitle("About MoleditPy")
29
30
  self.setFixedSize(250, 300)
30
31
  self.init_ui()
31
-
32
+
32
33
  def init_ui(self):
33
34
  layout = QVBoxLayout(self)
34
-
35
+
35
36
  # Create a clickable image label
36
37
  self.image_label = QLabel()
37
38
  self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
38
-
39
+
39
40
  # Load the original icon image
40
- icon_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'icon.png')
41
+ icon_path = os.path.join(os.path.dirname(__file__), "..", "assets", "icon.png")
41
42
  if os.path.exists(icon_path):
42
43
  original_pixmap = QPixmap(icon_path)
43
44
  # Scale to 2x size (160x160)
44
- pixmap = original_pixmap.scaled(160, 160, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
45
+ pixmap = original_pixmap.scaled(
46
+ 160,
47
+ 160,
48
+ Qt.AspectRatioMode.KeepAspectRatio,
49
+ Qt.TransformationMode.SmoothTransformation,
50
+ )
45
51
  else:
46
52
  # Fallback: create a simple placeholder if icon.png not found
47
53
  pixmap = QPixmap(160, 160)
@@ -50,35 +56,36 @@ class AboutDialog(QDialog): # pragma: no cover
50
56
  painter.setPen(QPen(Qt.GlobalColor.black, 2))
51
57
  painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "MoleditPy")
52
58
  painter.end()
53
-
59
+
54
60
  self.image_label.setPixmap(pixmap)
55
61
  try:
56
62
  self.image_label.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
57
- except Exception:
58
- pass
63
+ except Exception: # pragma: no cover
64
+ import traceback
65
+ traceback.print_exc()
59
66
 
60
67
  self.image_label.mousePressEvent = self.image_mouse_press_event
61
-
68
+
62
69
  layout.addWidget(self.image_label)
63
-
70
+
64
71
  # Add text information
65
72
  info_text = f"MoleditPy Ver. {VERSION}\nAuthor: Hiromichi Yokoyama\nLicense: GPL-3.0 license"
66
73
  info_label = QLabel(info_text)
67
74
  info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
68
75
  layout.addWidget(info_label)
69
-
76
+
70
77
  # Add OK button
71
78
  ok_button = QPushButton("OK")
72
79
  ok_button.setFixedSize(80, 30) # 小さいサイズに固定
73
80
  ok_button.clicked.connect(self.accept)
74
-
81
+
75
82
  # Center the button
76
83
  button_layout = QHBoxLayout()
77
84
  button_layout.addStretch()
78
85
  button_layout.addWidget(ok_button)
79
86
  button_layout.addStretch()
80
87
  layout.addLayout(button_layout)
81
-
88
+
82
89
  def image_clicked(self, event):
83
90
  """Easter egg: Clear all and load bipyrimidine from SMILES"""
84
91
  # Clear the current scene
@@ -100,5 +107,6 @@ class AboutDialog(QDialog): # pragma: no cover
100
107
  except Exception:
101
108
  try:
102
109
  event.ignore()
103
- except Exception:
104
- pass
110
+ except Exception: # pragma: no cover
111
+ import traceback
112
+ traceback.print_exc()
@@ -10,18 +10,24 @@ 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
21
26
  except Exception:
22
27
  from modules.dialog3_d_picking_mixin import Dialog3DPickingMixin
23
28
 
24
- class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
29
+
30
+ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
25
31
  def __init__(self, mol, main_window, plane, preselected_atoms=None, parent=None):
26
32
  QDialog.__init__(self, parent)
27
33
  Dialog3DPickingMixin.__init__(self)
@@ -29,47 +35,51 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
29
35
  self.main_window = main_window
30
36
  self.plane = plane
31
37
  self.selected_atoms = set()
32
-
38
+
33
39
  # 事前選択された原子を追加
34
40
  if preselected_atoms:
35
41
  self.selected_atoms.update(preselected_atoms)
36
-
42
+
37
43
  self.init_ui()
38
-
44
+
39
45
  # 事前選択された原子にラベルを追加
40
46
  if self.selected_atoms:
41
47
  self.show_atom_labels()
42
48
  self.update_display()
43
-
49
+
44
50
  def init_ui(self):
45
- plane_names = {'xy': 'XY', 'xz': 'XZ', 'yz': 'YZ'}
51
+ plane_names = {"xy": "XY", "xz": "XZ", "yz": "YZ"}
46
52
  self.setWindowTitle(f"Align to {plane_names[self.plane]} Plane")
47
53
  self.setModal(False)
48
54
  layout = QVBoxLayout(self)
49
-
55
+
50
56
  # Instructions
51
- 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.")
57
+ instruction_label = QLabel(
58
+ 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."
59
+ )
52
60
  instruction_label.setWordWrap(True)
53
61
  layout.addWidget(instruction_label)
54
-
62
+
55
63
  # Selected atoms display
56
64
  self.selection_label = QLabel("No atoms selected")
57
65
  layout.addWidget(self.selection_label)
58
-
66
+
59
67
  # Buttons
60
68
  button_layout = QHBoxLayout()
61
69
  self.clear_button = QPushButton("Clear Selection")
62
70
  self.clear_button.clicked.connect(self.clear_selection)
63
71
  button_layout.addWidget(self.clear_button)
64
-
72
+
65
73
  # Select all atoms button
66
74
  self.select_all_button = QPushButton("Select All Atoms")
67
- self.select_all_button.setToolTip("Select all atoms in the molecule for alignment")
75
+ self.select_all_button.setToolTip(
76
+ "Select all atoms in the molecule for alignment"
77
+ )
68
78
  self.select_all_button.clicked.connect(self.select_all_atoms)
69
79
  button_layout.addWidget(self.select_all_button)
70
-
80
+
71
81
  button_layout.addStretch()
72
-
82
+
73
83
  self.apply_button = QPushButton("Apply align")
74
84
  self.apply_button.clicked.connect(self.apply_PlaneAlign)
75
85
  self.apply_button.setEnabled(False)
@@ -78,35 +88,35 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
78
88
  close_button = QPushButton("Close")
79
89
  close_button.clicked.connect(self.reject)
80
90
  button_layout.addWidget(close_button)
81
-
91
+
82
92
  layout.addLayout(button_layout)
83
-
93
+
84
94
  # Connect to main window's picker
85
95
  self.picker_connection = None
86
96
  self.enable_picking()
87
-
97
+
88
98
  def enable_picking(self):
89
99
  """3Dビューでの原子選択を有効にする"""
90
100
  self.main_window.plotter.interactor.installEventFilter(self)
91
101
  self.picking_enabled = True
92
-
102
+
93
103
  def disable_picking(self):
94
104
  """3Dビューでの原子選択を無効にする"""
95
- if hasattr(self, 'picking_enabled') and self.picking_enabled:
105
+ if hasattr(self, "picking_enabled") and self.picking_enabled:
96
106
  self.main_window.plotter.interactor.removeEventFilter(self)
97
107
  self.picking_enabled = False
98
-
108
+
99
109
  def on_atom_picked(self, atom_idx):
100
110
  """原子がピックされたときの処理"""
101
111
  if atom_idx in self.selected_atoms:
102
112
  self.selected_atoms.remove(atom_idx)
103
113
  else:
104
114
  self.selected_atoms.add(atom_idx)
105
-
115
+
106
116
  # 原子ラベルを表示
107
117
  self.show_atom_labels()
108
118
  self.update_display()
109
-
119
+
110
120
  def keyPressEvent(self, event):
111
121
  """キーボードイベントを処理"""
112
122
  if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
@@ -115,28 +125,36 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
115
125
  event.accept()
116
126
  else:
117
127
  super().keyPressEvent(event)
118
-
128
+
119
129
  def clear_selection(self):
120
130
  """選択をクリア"""
121
131
  self.selected_atoms.clear()
122
132
  self.clear_atom_labels()
123
133
  self.update_display()
124
-
134
+
125
135
  def select_all_atoms(self):
126
136
  """Select all atoms in the current molecule and update labels/UI."""
127
137
  try:
128
138
  # Prefer RDKit molecule if available
129
- if hasattr(self, 'mol') and self.mol is not None:
139
+ if hasattr(self, "mol") and self.mol is not None:
130
140
  try:
131
141
  n = self.mol.GetNumAtoms()
132
142
  # create a set of indices [0..n-1]
133
143
  self.selected_atoms = set(range(n))
134
144
  except Exception:
135
145
  # fallback to main_window data map
136
- self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
146
+ self.selected_atoms = (
147
+ set(self.main_window.data.atoms.keys())
148
+ if hasattr(self.main_window, "data")
149
+ else set()
150
+ )
137
151
  else:
138
152
  # fallback to main_window data map
139
- self.selected_atoms = set(self.main_window.data.atoms.keys()) if hasattr(self.main_window, 'data') else set()
153
+ self.selected_atoms = (
154
+ set(self.main_window.data.atoms.keys())
155
+ if hasattr(self.main_window, "data")
156
+ else set()
157
+ )
140
158
 
141
159
  # Update labels and display
142
160
  self.show_atom_labels()
@@ -144,42 +162,49 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
144
162
 
145
163
  except Exception as e:
146
164
  QMessageBox.warning(self, "Warning", f"Failed to select all atoms: {e}")
147
-
165
+
148
166
  def update_display(self):
149
167
  """表示を更新"""
150
168
  count = len(self.selected_atoms)
151
169
  if count == 0:
152
- self.selection_label.setText("Click atoms to select for align (minimum 3 required)")
170
+ self.selection_label.setText(
171
+ "Click atoms to select for align (minimum 3 required)"
172
+ )
153
173
  self.apply_button.setEnabled(False)
154
174
  else:
155
175
  atom_list = sorted(self.selected_atoms)
156
176
  atom_display = []
157
177
  for i, atom_idx in enumerate(atom_list):
158
178
  symbol = self.mol.GetAtomWithIdx(atom_idx).GetSymbol()
159
- atom_display.append(f"#{i+1}: {symbol}({atom_idx})")
160
-
161
- self.selection_label.setText(f"Selected {count} atoms: {', '.join(atom_display)}")
179
+ atom_display.append(f"#{i + 1}: {symbol}({atom_idx})")
180
+
181
+ self.selection_label.setText(
182
+ f"Selected {count} atoms: {', '.join(atom_display)}"
183
+ )
162
184
  self.apply_button.setEnabled(count >= 3)
163
-
185
+
164
186
  def show_atom_labels(self):
165
187
  """選択された原子にラベルを表示"""
166
188
  if self.selected_atoms:
167
189
  sorted_atoms = sorted(self.selected_atoms)
168
- pairs = [(idx, f"#{i+1}") for i, idx in enumerate(sorted_atoms)]
169
- self.show_atom_labels_for(pairs, color='blue')
190
+ pairs = [(idx, f"#{i + 1}") for i, idx in enumerate(sorted_atoms)]
191
+ self.show_atom_labels_for(pairs, color="blue")
170
192
  else:
171
193
  self.clear_atom_labels()
172
-
194
+
173
195
  def apply_PlaneAlign(self):
174
196
  """alignを適用(回転ベース)"""
175
197
  if len(self.selected_atoms) < 3:
176
- QMessageBox.warning(self, "Warning", "Please select at least 3 atoms for align.")
198
+ QMessageBox.warning(
199
+ self, "Warning", "Please select at least 3 atoms for align."
200
+ )
177
201
  return
178
202
  try:
179
-
180
203
  # 選択された原子の位置を取得
181
204
  selected_indices = list(self.selected_atoms)
182
- selected_positions = self.main_window.atom_positions_3d[selected_indices].copy()
205
+ selected_positions = self.main_window.atom_positions_3d[
206
+ selected_indices
207
+ ].copy()
183
208
 
184
209
  # 重心を計算
185
210
  centroid = np.mean(selected_positions, axis=0)
@@ -196,12 +221,14 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
196
221
  normal_vector = eigenvectors[:, 0] # 最小固有値に対応する固有ベクトル
197
222
 
198
223
  # 目標の平面の法線ベクトルを定義
199
- if self.plane == 'xy':
224
+ if self.plane == "xy":
200
225
  target_normal = np.array([0, 0, 1]) # Z軸方向
201
- elif self.plane == 'xz':
226
+ elif self.plane == "xz":
202
227
  target_normal = np.array([0, 1, 0]) # Y軸方向
203
- elif self.plane == 'yz':
228
+ elif self.plane == "yz":
204
229
  target_normal = np.array([1, 0, 0]) # X軸方向
230
+ else:
231
+ target_normal = np.array([0, 0, 1]) # Default to Z-axis (XY plane)
205
232
 
206
233
  # 法線ベクトルの向きを調整(内積が正になるように)
207
234
  if np.dot(normal_vector, target_normal) < 0:
@@ -221,7 +248,11 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
221
248
  def rodrigues_rotation(v, axis, angle):
222
249
  cos_a = np.cos(angle)
223
250
  sin_a = np.sin(angle)
224
- return v * cos_a + np.cross(axis, v) * sin_a + axis * np.dot(axis, v) * (1 - cos_a)
251
+ return (
252
+ v * cos_a
253
+ + np.cross(axis, v) * sin_a
254
+ + axis * np.dot(axis, v) * (1 - cos_a)
255
+ )
225
256
 
226
257
  # 分子全体を回転させる
227
258
  conf = self.mol.GetConformer()
@@ -229,7 +260,9 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
229
260
  current_pos = np.array(conf.GetAtomPosition(i))
230
261
  # 重心基準で回転
231
262
  centered_pos = current_pos - centroid
232
- rotated_pos = rodrigues_rotation(centered_pos, rotation_axis, rotation_angle)
263
+ rotated_pos = rodrigues_rotation(
264
+ centered_pos, rotation_axis, rotation_angle
265
+ )
233
266
  new_pos = rotated_pos + centroid
234
267
  conf.SetAtomPosition(i, new_pos.tolist())
235
268
  self.main_window.atom_positions_3d[i] = new_pos
@@ -245,19 +278,19 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
245
278
 
246
279
  except Exception as e:
247
280
  QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
248
-
281
+
249
282
  def closeEvent(self, event):
250
283
  """ダイアログが閉じられる時の処理"""
251
284
  self.clear_atom_labels()
252
285
  self.disable_picking()
253
286
  super().closeEvent(event)
254
-
287
+
255
288
  def reject(self):
256
289
  """キャンセル時の処理"""
257
290
  self.clear_atom_labels()
258
291
  self.disable_picking()
259
292
  super().reject()
260
-
293
+
261
294
  def accept(self):
262
295
  """OK時の処理"""
263
296
  self.clear_atom_labels()