moleditpy 2.5.2__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.5.2 → moleditpy-2.6.1}/PKG-INFO +1 -1
  2. {moleditpy-2.5.2 → moleditpy-2.6.1}/pyproject.toml +2 -2
  3. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/SOURCES.txt +7 -5
  5. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/main.py +1 -1
  6. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/__init__.py +1 -11
  7. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/about_dialog.py +15 -15
  8. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/align_plane_dialog.py +40 -60
  9. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/alignment_dialog.py +44 -69
  10. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/analysis_window.py +30 -24
  11. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/angle_dialog.py +71 -141
  12. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/atom_item.py +47 -39
  13. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/bond_item.py +67 -58
  14. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/bond_length_dialog.py +66 -135
  15. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/calculation_worker.py +104 -101
  16. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/color_settings_dialog.py +18 -9
  17. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/constants.py +3 -3
  18. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/constrained_optimization_dialog.py +91 -92
  19. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/custom_interactor_style.py +73 -79
  20. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/custom_qt_interactor.py +6 -5
  21. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/dialog3_d_picking_mixin.py +87 -19
  22. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/dihedral_dialog.py +86 -162
  23. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window.py +4 -31
  24. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_app_state.py +48 -74
  25. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_compute.py +53 -89
  26. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_dialog_manager.py +70 -91
  27. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_3d.py +72 -129
  28. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_edit_actions.py +107 -134
  29. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_export.py +111 -134
  30. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_main_init.py +202 -201
  31. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_molecular_parsers.py +74 -87
  32. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_project_io.py +68 -88
  33. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_string_importers.py +20 -41
  34. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_ui_manager.py +42 -61
  35. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_3d.py +130 -153
  36. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/main_window_view_loaders.py +23 -42
  37. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/mirror_dialog.py +37 -26
  38. moleditpy-2.6.1/src/moleditpy/modules/mol_geometry.py +164 -0
  39. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/molecular_data.py +12 -12
  40. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/molecule_scene.py +134 -147
  41. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/move_group_dialog.py +112 -107
  42. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/periodic_table_dialog.py +3 -1
  43. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/planarize_dialog.py +15 -30
  44. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_interface.py +13 -23
  45. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager.py +54 -55
  46. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/plugin_manager_window.py +43 -34
  47. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/settings_dialog.py +102 -238
  48. moleditpy-2.6.1/src/moleditpy/modules/sip_isdeleted_safe.py +16 -0
  49. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_item.py +18 -27
  50. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/template_preview_view.py +9 -9
  51. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/translation_dialog.py +44 -43
  52. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/user_template_dialog.py +115 -103
  53. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/modules/zoomable_view.py +7 -12
  54. {moleditpy-2.5.2 → moleditpy-2.6.1}/LICENSE +0 -0
  55. {moleditpy-2.5.2 → moleditpy-2.6.1}/README.md +0 -0
  56. {moleditpy-2.5.2 → moleditpy-2.6.1}/setup.cfg +0 -0
  57. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  58. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  59. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/requires.txt +0 -0
  60. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/MoleditPy.egg-info/top_level.txt +0 -0
  61. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/__init__.py +0 -0
  62. {moleditpy-2.5.2 → moleditpy-2.6.1}/src/moleditpy/__main__.py +0 -0
  63. {moleditpy-2.5.2/src/moleditpy/modules → moleditpy-2.6.1/src/moleditpy}/assets/file_icon.ico +0 -0
  64. {moleditpy-2.5.2/src/moleditpy/modules → moleditpy-2.6.1/src/moleditpy}/assets/icon.icns +0 -0
  65. {moleditpy-2.5.2/src/moleditpy/modules → moleditpy-2.6.1/src/moleditpy}/assets/icon.ico +0 -0
  66. {moleditpy-2.5.2/src/moleditpy/modules → 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.5.2
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.5.2"
8
+ version = "2.6.1"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -48,4 +48,4 @@ Issues = "https://github.com/HiroYokoyama/python_molecular_editor/issues"
48
48
  moleditpy = "moleditpy.__main__:main"
49
49
 
50
50
  [tool.setuptools.package-data]
51
- "moleditpy" = ["modules/assets/*"]
51
+ "moleditpy" = ["assets/*"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 2.5.2
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
@@ -16,6 +16,10 @@ src/moleditpy.egg-info/dependency_links.txt
16
16
  src/moleditpy.egg-info/entry_points.txt
17
17
  src/moleditpy.egg-info/requires.txt
18
18
  src/moleditpy.egg-info/top_level.txt
19
+ src/moleditpy/assets/file_icon.ico
20
+ src/moleditpy/assets/icon.icns
21
+ src/moleditpy/assets/icon.ico
22
+ src/moleditpy/assets/icon.png
19
23
  src/moleditpy/modules/__init__.py
20
24
  src/moleditpy/modules/about_dialog.py
21
25
  src/moleditpy/modules/align_plane_dialog.py
@@ -48,6 +52,7 @@ src/moleditpy/modules/main_window_ui_manager.py
48
52
  src/moleditpy/modules/main_window_view_3d.py
49
53
  src/moleditpy/modules/main_window_view_loaders.py
50
54
  src/moleditpy/modules/mirror_dialog.py
55
+ src/moleditpy/modules/mol_geometry.py
51
56
  src/moleditpy/modules/molecular_data.py
52
57
  src/moleditpy/modules/molecule_scene.py
53
58
  src/moleditpy/modules/move_group_dialog.py
@@ -57,12 +62,9 @@ src/moleditpy/modules/plugin_interface.py
57
62
  src/moleditpy/modules/plugin_manager.py
58
63
  src/moleditpy/modules/plugin_manager_window.py
59
64
  src/moleditpy/modules/settings_dialog.py
65
+ src/moleditpy/modules/sip_isdeleted_safe.py
60
66
  src/moleditpy/modules/template_preview_item.py
61
67
  src/moleditpy/modules/template_preview_view.py
62
68
  src/moleditpy/modules/translation_dialog.py
63
69
  src/moleditpy/modules/user_template_dialog.py
64
- src/moleditpy/modules/zoomable_view.py
65
- src/moleditpy/modules/assets/file_icon.ico
66
- src/moleditpy/modules/assets/icon.icns
67
- src/moleditpy/modules/assets/icon.ico
68
- src/moleditpy/modules/assets/icon.png
70
+ src/moleditpy/modules/zoomable_view.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,16 +28,16 @@ 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
- icon_path = os.path.join(os.path.dirname(__file__), 'assets', 'icon.png')
40
+ icon_path = os.path.join(os.path.dirname(__file__), '..', 'assets', 'icon.png')
41
41
  if os.path.exists(icon_path):
42
42
  original_pixmap = QPixmap(icon_path)
43
43
  # Scale to 2x size (160x160)
@@ -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,46 +162,19 @@ 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
- # 既存のラベルをクリア
167
- self.clear_atom_labels()
168
-
169
- # 新しいラベルを表示
170
- if not hasattr(self, 'selection_labels'):
171
- self.selection_labels = []
172
-
173
171
  if self.selected_atoms:
174
172
  sorted_atoms = sorted(self.selected_atoms)
175
-
176
- for i, atom_idx in enumerate(sorted_atoms):
177
- pos = self.main_window.atom_positions_3d[atom_idx]
178
- label_text = f"#{i+1}"
179
-
180
- # ラベルを追加
181
- label_actor = self.main_window.plotter.add_point_labels(
182
- [pos], [label_text],
183
- point_size=20,
184
- font_size=12,
185
- text_color='blue',
186
- always_visible=True
187
- )
188
- self.selection_labels.append(label_actor)
189
-
190
- def clear_atom_labels(self):
191
- """原子ラベルをクリア"""
192
- if hasattr(self, 'selection_labels'):
193
- for label_actor in self.selection_labels:
194
- try:
195
- self.main_window.plotter.remove_actor(label_actor)
196
- except Exception:
197
- pass
198
- self.selection_labels = []
199
-
173
+ pairs = [(idx, f"#{i+1}") for i, idx in enumerate(sorted_atoms)]
174
+ self.show_atom_labels_for(pairs, color='blue')
175
+ else:
176
+ self.clear_atom_labels()
177
+
200
178
  def apply_PlaneAlign(self):
201
179
  """alignを適用(回転ベース)"""
202
180
  if len(self.selected_atoms) < 3:
@@ -229,6 +207,8 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
229
207
  target_normal = np.array([0, 1, 0]) # Y軸方向
230
208
  elif self.plane == 'yz':
231
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)
232
212
 
233
213
  # 法線ベクトルの向きを調整(内積が正になるように)
234
214
  if np.dot(normal_vector, target_normal) < 0:
@@ -272,19 +252,19 @@ class AlignPlaneDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
272
252
 
273
253
  except Exception as e:
274
254
  QMessageBox.critical(self, "Error", f"Failed to apply align: {str(e)}")
275
-
255
+
276
256
  def closeEvent(self, event):
277
257
  """ダイアログが閉じられる時の処理"""
278
258
  self.clear_atom_labels()
279
259
  self.disable_picking()
280
260
  super().closeEvent(event)
281
-
261
+
282
262
  def reject(self):
283
263
  """キャンセル時の処理"""
284
264
  self.clear_atom_labels()
285
265
  self.disable_picking()
286
266
  super().reject()
287
-
267
+
288
268
  def accept(self):
289
269
  """OK時の処理"""
290
270
  self.clear_atom_labels()
@@ -10,12 +10,15 @@ 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
- from PyQt6.QtCore import Qt
17
- from PyQt6.QtWidgets import QMessageBox
18
- import numpy as np
19
22
 
20
23
  try:
21
24
  from .dialog3_d_picking_mixin import Dialog3DPickingMixin
@@ -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,31 +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
-
136
- def add_selection_label(self, atom_idx, label_text):
137
- """選択された原子にラベルを追加"""
138
- if not hasattr(self, 'selection_labels'):
139
- self.selection_labels = []
140
-
141
- # 原子の位置を取得
142
- pos = self.main_window.atom_positions_3d[atom_idx]
143
-
144
- # ラベルを追加
145
- label_actor = self.main_window.plotter.add_point_labels(
146
- [pos], [label_text],
147
- point_size=20,
148
- font_size=12,
149
- text_color='yellow',
150
- always_visible=True
151
- )
152
- self.selection_labels.append(label_actor)
153
-
138
+
154
139
  def remove_atom_label(self, atom_idx):
155
140
  """特定の原子のラベルを削除"""
156
141
  # 簡単化のため、全ラベルをクリアして再描画
@@ -158,17 +143,7 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
158
143
  for i, idx in enumerate(sorted(self.selected_atoms), 1):
159
144
  if idx != atom_idx:
160
145
  self.add_selection_label(idx, f"Atom {i}")
161
-
162
- def clear_selection_labels(self):
163
- """選択ラベルをクリア"""
164
- if hasattr(self, 'selection_labels'):
165
- for label_actor in self.selection_labels:
166
- try:
167
- self.main_window.plotter.remove_actor(label_actor)
168
- except Exception:
169
- pass
170
- self.selection_labels = []
171
-
146
+
172
147
  def apply_alignment(self):
173
148
  """アライメントを適用"""
174
149
  if len(self.selected_atoms) != 2:
@@ -202,69 +177,69 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog): # pragma: no cover
202
177
  'z': np.array([0.0, 0.0, 1.0])
203
178
  }
204
179
  target_axis = axis_vectors[self.axis]
205
-
180
+
206
181
  # atom2から原点への方向ベクトル
207
182
  current_vector = pos2_translated
208
183
  current_length = np.linalg.norm(current_vector)
209
-
184
+
210
185
  if current_length > 1e-10: # ゼロベクトルでない場合
211
186
  current_vector_normalized = current_vector / current_length
212
-
187
+
213
188
  # 回転軸と角度を計算
214
189
  rotation_axis = np.cross(current_vector_normalized, target_axis)
215
190
  rotation_axis_length = np.linalg.norm(rotation_axis)
216
-
191
+
217
192
  if rotation_axis_length > 1e-10: # 回転が必要
218
193
  rotation_axis = rotation_axis / rotation_axis_length
219
194
  cos_angle = np.dot(current_vector_normalized, target_axis)
220
195
  cos_angle = np.clip(cos_angle, -1.0, 1.0)
221
196
  rotation_angle = np.arccos(cos_angle)
222
-
197
+
223
198
  # ロドリゲスの回転公式を使用
224
199
  def rodrigues_rotation(v, k, theta):
225
200
  cos_theta = np.cos(theta)
226
201
  sin_theta = np.sin(theta)
227
- return (v * cos_theta +
228
- np.cross(k, v) * sin_theta +
202
+ return (v * cos_theta +
203
+ np.cross(k, v) * sin_theta +
229
204
  k * np.dot(k, v) * (1 - cos_theta))
230
-
205
+
231
206
  # 全ての原子に回転を適用
232
207
  for i in range(self.mol.GetNumAtoms()):
233
208
  current_pos = np.array(conf.GetAtomPosition(i))
234
209
  rotated_pos = rodrigues_rotation(current_pos, rotation_axis, rotation_angle)
235
210
  conf.SetAtomPosition(i, rotated_pos.tolist())
236
-
211
+
237
212
  # 3D座標を更新
238
213
  self.main_window.atom_positions_3d = np.array([
239
214
  list(conf.GetAtomPosition(i)) for i in range(self.mol.GetNumAtoms())
240
215
  ])
241
-
216
+
242
217
  # 3Dビューを更新
243
218
  self.main_window.draw_molecule_3d(self.mol)
244
-
219
+
245
220
  # キラルラベルを更新
246
221
  self.main_window.update_chiral_labels()
247
222
 
248
223
  # Undo状態を保存
249
224
  self.main_window.push_undo_state()
250
-
225
+
251
226
  QMessageBox.information(self, "Success", f"Alignment to {self.axis.upper()}-axis completed.")
252
-
227
+
253
228
  except Exception as e:
254
229
  QMessageBox.critical(self, "Error", f"Failed to apply alignment: {str(e)}")
255
-
230
+
256
231
  def closeEvent(self, event):
257
232
  """ダイアログが閉じられる時の処理"""
258
233
  self.clear_selection_labels()
259
234
  self.disable_picking()
260
235
  super().closeEvent(event)
261
-
236
+
262
237
  def reject(self):
263
238
  """キャンセル時の処理"""
264
239
  self.clear_selection_labels()
265
240
  self.disable_picking()
266
241
  super().reject()
267
-
242
+
268
243
  def accept(self):
269
244
  """OK時の処理"""
270
245
  self.clear_selection_labels()